Comparing version 8.53.0 to 8.55.0
@@ -131,12 +131,24 @@ /** | ||
const cachedResults = fileDescriptor.meta.results; | ||
// Just in case, not sure if this can ever happen. | ||
if (!cachedResults) { | ||
return cachedResults; | ||
} | ||
/* | ||
* Shallow clone the object to ensure that any properties added or modified afterwards | ||
* will not be accidentally stored in the cache file when `reconcile()` is called. | ||
* https://github.com/eslint/eslint/issues/13507 | ||
* All intentional changes to the cache file must be done through `setCachedLintResults()`. | ||
*/ | ||
const results = { ...cachedResults }; | ||
// If source is present but null, need to reread the file from the filesystem. | ||
if ( | ||
fileDescriptor.meta.results && | ||
fileDescriptor.meta.results.source === null | ||
) { | ||
if (results.source === null) { | ||
debug(`Rereading cached result source from filesystem: ${filePath}`); | ||
fileDescriptor.meta.results.source = fs.readFileSync(filePath, "utf-8"); | ||
results.source = fs.readFileSync(filePath, "utf-8"); | ||
} | ||
return fileDescriptor.meta.results; | ||
return results; | ||
} | ||
@@ -143,0 +155,0 @@ |
@@ -104,26 +104,33 @@ /** | ||
} | ||
return { | ||
ForStatement(node) { | ||
if (node.test && node.test.type === "BinaryExpression" && node.test.left.type === "Identifier" && node.update) { | ||
const counter = node.test.left.name; | ||
const operator = node.test.operator; | ||
const update = node.update; | ||
if (node.test && node.test.type === "BinaryExpression" && node.update) { | ||
for (const counterPosition of ["left", "right"]) { | ||
if (node.test[counterPosition].type !== "Identifier") { | ||
continue; | ||
} | ||
let wrongDirection; | ||
const counter = node.test[counterPosition].name; | ||
const operator = node.test.operator; | ||
const update = node.update; | ||
if (operator === "<" || operator === "<=") { | ||
wrongDirection = -1; | ||
} else if (operator === ">" || operator === ">=") { | ||
wrongDirection = 1; | ||
} else { | ||
return; | ||
} | ||
let wrongDirection; | ||
if (update.type === "UpdateExpression") { | ||
if (getUpdateDirection(update, counter) === wrongDirection) { | ||
if (operator === "<" || operator === "<=") { | ||
wrongDirection = counterPosition === "left" ? -1 : 1; | ||
} else if (operator === ">" || operator === ">=") { | ||
wrongDirection = counterPosition === "left" ? 1 : -1; | ||
} else { | ||
return; | ||
} | ||
if (update.type === "UpdateExpression") { | ||
if (getUpdateDirection(update, counter) === wrongDirection) { | ||
report(node); | ||
} | ||
} else if (update.type === "AssignmentExpression" && getAssignmentDirection(update, counter) === wrongDirection) { | ||
report(node); | ||
} | ||
} else if (update.type === "AssignmentExpression" && getAssignmentDirection(update, counter) === wrongDirection) { | ||
report(node); | ||
} | ||
@@ -130,0 +137,0 @@ } |
@@ -9,2 +9,14 @@ /** | ||
//------------------------------------------------------------------------------ | ||
// Requirements | ||
//------------------------------------------------------------------------------ | ||
const { | ||
getVariableByName, | ||
isClosingParenToken, | ||
isOpeningParenToken, | ||
isStartOfExpressionStatement, | ||
needsPrecedingSemicolon | ||
} = require("./utils/ast-utils"); | ||
//------------------------------------------------------------------------------ | ||
// Rule Definition | ||
@@ -24,6 +36,10 @@ //------------------------------------------------------------------------------ | ||
hasSuggestions: true, | ||
schema: [], | ||
messages: { | ||
preferLiteral: "The array literal notation [] is preferable." | ||
preferLiteral: "The array literal notation [] is preferable.", | ||
useLiteral: "Replace with an array literal.", | ||
useLiteralAfterSemicolon: "Replace with an array literal, add preceding semicolon." | ||
} | ||
@@ -34,3 +50,29 @@ }, | ||
const sourceCode = context.sourceCode; | ||
/** | ||
* Gets the text between the calling parentheses of a CallExpression or NewExpression. | ||
* @param {ASTNode} node A CallExpression or NewExpression node. | ||
* @returns {string} The text between the calling parentheses, or an empty string if there are none. | ||
*/ | ||
function getArgumentsText(node) { | ||
const lastToken = sourceCode.getLastToken(node); | ||
if (!isClosingParenToken(lastToken)) { | ||
return ""; | ||
} | ||
let firstToken = node.callee; | ||
do { | ||
firstToken = sourceCode.getTokenAfter(firstToken); | ||
if (!firstToken || firstToken === lastToken) { | ||
return ""; | ||
} | ||
} while (!isOpeningParenToken(firstToken)); | ||
return sourceCode.text.slice(firstToken.range[1], lastToken.range[0]); | ||
} | ||
/** | ||
* Disallow construction of dense arrays using the Array constructor | ||
@@ -43,8 +85,45 @@ * @param {ASTNode} node node to evaluate | ||
if ( | ||
node.arguments.length !== 1 && | ||
node.callee.type === "Identifier" && | ||
node.callee.name === "Array" | ||
) { | ||
context.report({ node, messageId: "preferLiteral" }); | ||
node.callee.type !== "Identifier" || | ||
node.callee.name !== "Array" || | ||
node.arguments.length === 1 && | ||
node.arguments[0].type !== "SpreadElement") { | ||
return; | ||
} | ||
const variable = getVariableByName(sourceCode.getScope(node), "Array"); | ||
/* | ||
* Check if `Array` is a predefined global variable: predefined globals have no declarations, | ||
* meaning that the `identifiers` list of the variable object is empty. | ||
*/ | ||
if (variable && variable.identifiers.length === 0) { | ||
const argsText = getArgumentsText(node); | ||
let fixText; | ||
let messageId; | ||
/* | ||
* Check if the suggested change should include a preceding semicolon or not. | ||
* Due to JavaScript's ASI rules, a missing semicolon may be inserted automatically | ||
* before an expression like `Array()` or `new Array()`, but not when the expression | ||
* is changed into an array literal like `[]`. | ||
*/ | ||
if (isStartOfExpressionStatement(node) && needsPrecedingSemicolon(sourceCode, node)) { | ||
fixText = `;[${argsText}]`; | ||
messageId = "useLiteralAfterSemicolon"; | ||
} else { | ||
fixText = `[${argsText}]`; | ||
messageId = "useLiteral"; | ||
} | ||
context.report({ | ||
node, | ||
messageId: "preferLiteral", | ||
suggest: [ | ||
{ | ||
messageId, | ||
fix: fixer => fixer.replaceText(node, fixText) | ||
} | ||
] | ||
}); | ||
} | ||
} | ||
@@ -51,0 +130,0 @@ |
@@ -46,4 +46,7 @@ /** | ||
hasSuggestions: true, | ||
messages: { | ||
unexpected: "Unexpected console statement." | ||
unexpected: "Unexpected console statement.", | ||
removeConsole: "Remove the console.{{ propertyName }}()." | ||
} | ||
@@ -99,2 +102,60 @@ }, | ||
/** | ||
* Checks if removing the ExpressionStatement node will cause ASI to | ||
* break. | ||
* eg. | ||
* foo() | ||
* console.log(); | ||
* [1, 2, 3].forEach(a => doSomething(a)) | ||
* | ||
* Removing the console.log(); statement should leave two statements, but | ||
* here the two statements will become one because [ causes continuation after | ||
* foo(). | ||
* @param {ASTNode} node The ExpressionStatement node to check. | ||
* @returns {boolean} `true` if ASI will break after removing the ExpressionStatement | ||
* node. | ||
*/ | ||
function maybeAsiHazard(node) { | ||
const SAFE_TOKENS_BEFORE = /^[:;{]$/u; // One of :;{ | ||
const UNSAFE_CHARS_AFTER = /^[-[(/+`]/u; // One of [(/+-` | ||
const tokenBefore = sourceCode.getTokenBefore(node); | ||
const tokenAfter = sourceCode.getTokenAfter(node); | ||
return ( | ||
Boolean(tokenAfter) && | ||
UNSAFE_CHARS_AFTER.test(tokenAfter.value) && | ||
tokenAfter.value !== "++" && | ||
tokenAfter.value !== "--" && | ||
Boolean(tokenBefore) && | ||
!SAFE_TOKENS_BEFORE.test(tokenBefore.value) | ||
); | ||
} | ||
/** | ||
* Checks if the MemberExpression node's parent.parent.parent is a | ||
* Program, BlockStatement, StaticBlock, or SwitchCase node. This check | ||
* is necessary to avoid providing a suggestion that might cause a syntax error. | ||
* | ||
* eg. if (a) console.log(b), removing console.log() here will lead to a | ||
* syntax error. | ||
* if (a) { console.log(b) }, removing console.log() here is acceptable. | ||
* | ||
* Additionally, it checks if the callee of the CallExpression node is | ||
* the node itself. | ||
* | ||
* eg. foo(console.log), cannot provide a suggestion here. | ||
* @param {ASTNode} node The MemberExpression node to check. | ||
* @returns {boolean} `true` if a suggestion can be provided for a node. | ||
*/ | ||
function canProvideSuggestions(node) { | ||
return ( | ||
node.parent.type === "CallExpression" && | ||
node.parent.callee === node && | ||
node.parent.parent.type === "ExpressionStatement" && | ||
astUtils.STATEMENT_LIST_PARENTS.has(node.parent.parent.parent.type) && | ||
!maybeAsiHazard(node.parent.parent) | ||
); | ||
} | ||
/** | ||
* Reports the given reference as a violation. | ||
@@ -107,6 +168,17 @@ * @param {eslint-scope.Reference} reference The reference to report. | ||
const propertyName = astUtils.getStaticPropertyName(node); | ||
context.report({ | ||
node, | ||
loc: node.loc, | ||
messageId: "unexpected" | ||
messageId: "unexpected", | ||
suggest: canProvideSuggestions(node) | ||
? [{ | ||
messageId: "removeConsole", | ||
data: { propertyName }, | ||
fix(fixer) { | ||
return fixer.remove(node.parent.parent); | ||
} | ||
}] | ||
: [] | ||
}); | ||
@@ -113,0 +185,0 @@ } |
@@ -12,65 +12,10 @@ /** | ||
const { getVariableByName, isArrowToken, isClosingBraceToken, isClosingParenToken } = require("./utils/ast-utils"); | ||
const { | ||
getVariableByName, | ||
isArrowToken, | ||
isStartOfExpressionStatement, | ||
needsPrecedingSemicolon | ||
} = require("./utils/ast-utils"); | ||
//------------------------------------------------------------------------------ | ||
// Helpers | ||
//------------------------------------------------------------------------------ | ||
const BREAK_OR_CONTINUE = new Set(["BreakStatement", "ContinueStatement"]); | ||
// Declaration types that must contain a string Literal node at the end. | ||
const DECLARATIONS = new Set(["ExportAllDeclaration", "ExportNamedDeclaration", "ImportDeclaration"]); | ||
const IDENTIFIER_OR_KEYWORD = new Set(["Identifier", "Keyword"]); | ||
// Keywords that can immediately precede an ExpressionStatement node, mapped to the their node types. | ||
const NODE_TYPES_BY_KEYWORD = { | ||
__proto__: null, | ||
break: "BreakStatement", | ||
continue: "ContinueStatement", | ||
debugger: "DebuggerStatement", | ||
do: "DoWhileStatement", | ||
else: "IfStatement", | ||
return: "ReturnStatement", | ||
yield: "YieldExpression" | ||
}; | ||
/* | ||
* Before an opening parenthesis, postfix `++` and `--` always trigger ASI; | ||
* the tokens `:`, `;`, `{` and `=>` don't expect a semicolon, as that would count as an empty statement. | ||
*/ | ||
const PUNCTUATORS = new Set([":", ";", "{", "=>", "++", "--"]); | ||
/* | ||
* Statements that can contain an `ExpressionStatement` after a closing parenthesis. | ||
* DoWhileStatement is an exception in that it always triggers ASI after the closing parenthesis. | ||
*/ | ||
const STATEMENTS = new Set([ | ||
"DoWhileStatement", | ||
"ForInStatement", | ||
"ForOfStatement", | ||
"ForStatement", | ||
"IfStatement", | ||
"WhileStatement", | ||
"WithStatement" | ||
]); | ||
/** | ||
* Tests if a node appears at the beginning of an ancestor ExpressionStatement node. | ||
* @param {ASTNode} node The node to check. | ||
* @returns {boolean} Whether the node appears at the beginning of an ancestor ExpressionStatement node. | ||
*/ | ||
function isStartOfExpressionStatement(node) { | ||
const start = node.range[0]; | ||
let ancestor = node; | ||
while ((ancestor = ancestor.parent) && ancestor.range[0] === start) { | ||
if (ancestor.type === "ExpressionStatement") { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
//------------------------------------------------------------------------------ | ||
// Rule Definition | ||
@@ -125,46 +70,2 @@ //------------------------------------------------------------------------------ | ||
/** | ||
* Determines whether a parenthesized object literal that replaces a specified node needs to be preceded by a semicolon. | ||
* @param {ASTNode} node The node to be replaced. This node should be at the start of an `ExpressionStatement` or at the start of the body of an `ArrowFunctionExpression`. | ||
* @returns {boolean} Whether a semicolon is required before the parenthesized object literal. | ||
*/ | ||
function needsSemicolon(node) { | ||
const prevToken = sourceCode.getTokenBefore(node); | ||
if (!prevToken || prevToken.type === "Punctuator" && PUNCTUATORS.has(prevToken.value)) { | ||
return false; | ||
} | ||
const prevNode = sourceCode.getNodeByRangeIndex(prevToken.range[0]); | ||
if (isClosingParenToken(prevToken)) { | ||
return !STATEMENTS.has(prevNode.type); | ||
} | ||
if (isClosingBraceToken(prevToken)) { | ||
return ( | ||
prevNode.type === "BlockStatement" && prevNode.parent.type === "FunctionExpression" || | ||
prevNode.type === "ClassBody" && prevNode.parent.type === "ClassExpression" || | ||
prevNode.type === "ObjectExpression" | ||
); | ||
} | ||
if (IDENTIFIER_OR_KEYWORD.has(prevToken.type)) { | ||
if (BREAK_OR_CONTINUE.has(prevNode.parent.type)) { | ||
return false; | ||
} | ||
const keyword = prevToken.value; | ||
const nodeType = NODE_TYPES_BY_KEYWORD[keyword]; | ||
return prevNode.type !== nodeType; | ||
} | ||
if (prevToken.type === "String") { | ||
return !DECLARATIONS.has(prevNode.parent.type); | ||
} | ||
return true; | ||
} | ||
/** | ||
* Reports on nodes where the `Object` constructor is called without arguments. | ||
@@ -188,3 +89,3 @@ * @param {ASTNode} node The node to evaluate. | ||
replacement = "({})"; | ||
if (needsSemicolon(node)) { | ||
if (needsPrecedingSemicolon(sourceCode, node)) { | ||
fixText = ";({})"; | ||
@@ -191,0 +92,0 @@ messageId = "useLiteralAfterSemicolon"; |
@@ -77,2 +77,5 @@ /** | ||
}, | ||
importNamePattern: { | ||
type: "string" | ||
}, | ||
message: { | ||
@@ -119,4 +122,8 @@ type: "string", | ||
patternAndEverything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern.", | ||
patternAndEverythingWithRegexImportName: "* import is invalid because import name matching '{{importNames}}' pattern from '{{importSource}}' is restricted from being used.", | ||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period | ||
patternAndEverythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}", | ||
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period | ||
patternAndEverythingWithRegexImportNameAndCustomMessage: "* import is invalid because import name matching '{{importNames}}' pattern from '{{importSource}}' is restricted from being used. {{customMessage}}", | ||
@@ -180,6 +187,7 @@ everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.", | ||
// relative paths are supported for this rule | ||
const restrictedPatternGroups = restrictedPatterns.map(({ group, message, caseSensitive, importNames }) => ({ | ||
const restrictedPatternGroups = restrictedPatterns.map(({ group, message, caseSensitive, importNames, importNamePattern }) => ({ | ||
matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group), | ||
customMessage: message, | ||
importNames | ||
importNames, | ||
importNamePattern | ||
})); | ||
@@ -268,2 +276,3 @@ | ||
const restrictedImportNames = group.importNames; | ||
const restrictedImportNamePattern = group.importNamePattern ? new RegExp(group.importNamePattern, "u") : null; | ||
@@ -274,3 +283,3 @@ /* | ||
*/ | ||
if (!restrictedImportNames) { | ||
if (!restrictedImportNames && !restrictedImportNamePattern) { | ||
context.report({ | ||
@@ -287,36 +296,50 @@ node, | ||
if (importNames.has("*")) { | ||
const specifierData = importNames.get("*")[0]; | ||
importNames.forEach((specifiers, importName) => { | ||
if (importName === "*") { | ||
const [specifier] = specifiers; | ||
context.report({ | ||
node, | ||
messageId: customMessage ? "patternAndEverythingWithCustomMessage" : "patternAndEverything", | ||
loc: specifierData.loc, | ||
data: { | ||
importSource, | ||
importNames: restrictedImportNames, | ||
customMessage | ||
if (restrictedImportNames) { | ||
context.report({ | ||
node, | ||
messageId: customMessage ? "patternAndEverythingWithCustomMessage" : "patternAndEverything", | ||
loc: specifier.loc, | ||
data: { | ||
importSource, | ||
importNames: restrictedImportNames, | ||
customMessage | ||
} | ||
}); | ||
} else { | ||
context.report({ | ||
node, | ||
messageId: customMessage ? "patternAndEverythingWithRegexImportNameAndCustomMessage" : "patternAndEverythingWithRegexImportName", | ||
loc: specifier.loc, | ||
data: { | ||
importSource, | ||
importNames: restrictedImportNamePattern, | ||
customMessage | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
restrictedImportNames.forEach(importName => { | ||
if (!importNames.has(importName)) { | ||
return; | ||
} | ||
const specifiers = importNames.get(importName); | ||
specifiers.forEach(specifier => { | ||
context.report({ | ||
node, | ||
messageId: customMessage ? "patternAndImportNameWithCustomMessage" : "patternAndImportName", | ||
loc: specifier.loc, | ||
data: { | ||
importSource, | ||
customMessage, | ||
importName | ||
} | ||
if ( | ||
(restrictedImportNames && restrictedImportNames.includes(importName)) || | ||
(restrictedImportNamePattern && restrictedImportNamePattern.test(importName)) | ||
) { | ||
specifiers.forEach(specifier => { | ||
context.report({ | ||
node, | ||
messageId: customMessage ? "patternAndImportNameWithCustomMessage" : "patternAndImportName", | ||
loc: specifier.loc, | ||
data: { | ||
importSource, | ||
customMessage, | ||
importName | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
}); | ||
@@ -323,0 +346,0 @@ } |
{ | ||
"name": "eslint", | ||
"version": "8.53.0", | ||
"version": "8.55.0", | ||
"author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>", | ||
@@ -22,2 +22,3 @@ "description": "An AST-based pattern checker for JavaScript.", | ||
"lint:docs:js": "node Makefile.js lintDocsJS", | ||
"lint:docs:rule-examples": "node Makefile.js checkRuleExamples", | ||
"lint:fix": "node Makefile.js lint -- fix", | ||
@@ -45,2 +46,7 @@ "lint:fix:docs:js": "node Makefile.js lintDocsJS -- fix", | ||
], | ||
"docs/src/rules/*.md": [ | ||
"node tools/check-rule-examples.js", | ||
"node tools/fetch-docs-links.js", | ||
"git add docs/src/_data/further_reading_links.json" | ||
], | ||
"docs/**/*.svg": "npx svgo -r --multipass" | ||
@@ -63,4 +69,4 @@ }, | ||
"@eslint-community/regexpp": "^4.6.1", | ||
"@eslint/eslintrc": "^2.1.3", | ||
"@eslint/js": "8.53.0", | ||
"@eslint/eslintrc": "^2.1.4", | ||
"@eslint/js": "8.55.0", | ||
"@humanwhocodes/config-array": "^0.11.13", | ||
@@ -134,6 +140,14 @@ "@humanwhocodes/module-importer": "^1.0.1", | ||
"load-perf": "^0.2.0", | ||
"markdownlint": "^0.25.1", | ||
"markdownlint-cli": "^0.31.1", | ||
"markdown-it": "^12.2.0", | ||
"markdown-it-container": "^3.0.0", | ||
"markdownlint": "^0.31.1", | ||
"markdownlint-cli": "^0.37.0", | ||
"marked": "^4.0.8", | ||
"memfs": "^3.0.1", | ||
"metascraper": "^5.25.7", | ||
"metascraper-description": "^5.25.7", | ||
"metascraper-image": "^5.29.3", | ||
"metascraper-logo": "^5.25.7", | ||
"metascraper-logo-favicon": "^5.25.7", | ||
"metascraper-title": "^5.25.7", | ||
"mocha": "^8.3.2", | ||
@@ -146,4 +160,4 @@ "mocha-junit-reporter": "^2.0.0", | ||
"proxyquire": "^2.0.1", | ||
"recast": "^0.20.4", | ||
"regenerator-runtime": "^0.13.2", | ||
"recast": "^0.23.0", | ||
"regenerator-runtime": "^0.14.0", | ||
"rollup-plugin-node-polyfills": "^0.2.1", | ||
@@ -153,3 +167,3 @@ "semver": "^7.5.3", | ||
"sinon": "^11.0.0", | ||
"vite-plugin-commonjs": "^0.8.2", | ||
"vite-plugin-commonjs": "^0.10.0", | ||
"webdriverio": "^8.14.6", | ||
@@ -156,0 +170,0 @@ "webpack": "^5.23.0", |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3031350
71479
62
+ Added@eslint/js@8.55.0(transitive)
- Removed@eslint/js@8.53.0(transitive)
Updated@eslint/eslintrc@^2.1.4
Updated@eslint/js@8.55.0