Comparing version 8.47.0 to 8.50.0
@@ -510,3 +510,3 @@ /** | ||
exports.flatConfigSchema = { | ||
const flatConfigSchema = { | ||
@@ -537,1 +537,11 @@ // eslintrc-style keys that should always error | ||
}; | ||
//----------------------------------------------------------------------------- | ||
// Exports | ||
//----------------------------------------------------------------------------- | ||
module.exports = { | ||
flatConfigSchema, | ||
assertIsRuleSeverity, | ||
assertIsRuleOptions | ||
}; |
@@ -12,3 +12,4 @@ /** | ||
const ajv = require("../shared/ajv")(); | ||
const ajvImport = require("../shared/ajv"); | ||
const ajv = ajvImport(); | ||
const { | ||
@@ -15,0 +16,0 @@ parseRuleId, |
@@ -195,11 +195,14 @@ /** | ||
if (currentSegment !== headSegment && currentSegment) { | ||
debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`); | ||
if (currentSegment.reachable) { | ||
analyzer.emitter.emit( | ||
"onCodePathSegmentEnd", | ||
currentSegment, | ||
node | ||
); | ||
} | ||
const eventName = currentSegment.reachable | ||
? "onCodePathSegmentEnd" | ||
: "onUnreachableCodePathSegmentEnd"; | ||
debug.dump(`${eventName} ${currentSegment.id}`); | ||
analyzer.emitter.emit( | ||
eventName, | ||
currentSegment, | ||
node | ||
); | ||
} | ||
@@ -217,12 +220,15 @@ } | ||
if (currentSegment !== headSegment && headSegment) { | ||
debug.dump(`onCodePathSegmentStart ${headSegment.id}`); | ||
const eventName = headSegment.reachable | ||
? "onCodePathSegmentStart" | ||
: "onUnreachableCodePathSegmentStart"; | ||
debug.dump(`${eventName} ${headSegment.id}`); | ||
CodePathSegment.markUsed(headSegment); | ||
if (headSegment.reachable) { | ||
analyzer.emitter.emit( | ||
"onCodePathSegmentStart", | ||
headSegment, | ||
node | ||
); | ||
} | ||
analyzer.emitter.emit( | ||
eventName, | ||
headSegment, | ||
node | ||
); | ||
} | ||
@@ -246,11 +252,13 @@ } | ||
const currentSegment = currentSegments[i]; | ||
const eventName = currentSegment.reachable | ||
? "onCodePathSegmentEnd" | ||
: "onUnreachableCodePathSegmentEnd"; | ||
debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`); | ||
if (currentSegment.reachable) { | ||
analyzer.emitter.emit( | ||
"onCodePathSegmentEnd", | ||
currentSegment, | ||
node | ||
); | ||
} | ||
debug.dump(`${eventName} ${currentSegment.id}`); | ||
analyzer.emitter.emit( | ||
eventName, | ||
currentSegment, | ||
node | ||
); | ||
} | ||
@@ -257,0 +265,0 @@ |
/** | ||
* @fileoverview A class of the code path segment. | ||
* @fileoverview The CodePathSegment class. | ||
* @author Toru Nagashima | ||
@@ -33,2 +33,13 @@ */ | ||
* A code path segment. | ||
* | ||
* Each segment is arranged in a series of linked lists (implemented by arrays) | ||
* that keep track of the previous and next segments in a code path. In this way, | ||
* you can navigate between all segments in any code path so long as you have a | ||
* reference to any segment in that code path. | ||
* | ||
* When first created, the segment is in a detached state, meaning that it knows the | ||
* segments that came before it but those segments don't know that this new segment | ||
* follows it. Only when `CodePathSegment#markUsed()` is called on a segment does it | ||
* officially become part of the code path by updating the previous segments to know | ||
* that this new segment follows. | ||
*/ | ||
@@ -38,2 +49,3 @@ class CodePathSegment { | ||
/** | ||
* Creates a new instance. | ||
* @param {string} id An identifier. | ||
@@ -54,3 +66,3 @@ * @param {CodePathSegment[]} allPrevSegments An array of the previous segments. | ||
/** | ||
* An array of the next segments. | ||
* An array of the next reachable segments. | ||
* @type {CodePathSegment[]} | ||
@@ -61,3 +73,3 @@ */ | ||
/** | ||
* An array of the previous segments. | ||
* An array of the previous reachable segments. | ||
* @type {CodePathSegment[]} | ||
@@ -68,4 +80,3 @@ */ | ||
/** | ||
* An array of the next segments. | ||
* This array includes unreachable segments. | ||
* An array of all next segments including reachable and unreachable. | ||
* @type {CodePathSegment[]} | ||
@@ -76,4 +87,3 @@ */ | ||
/** | ||
* An array of the previous segments. | ||
* This array includes unreachable segments. | ||
* An array of all previous segments including reachable and unreachable. | ||
* @type {CodePathSegment[]} | ||
@@ -92,3 +102,7 @@ */ | ||
value: { | ||
// determines if the segment has been attached to the code path | ||
used: false, | ||
// array of previous segments coming from the end of a loop | ||
loopedPrevSegments: [] | ||
@@ -123,5 +137,6 @@ } | ||
/** | ||
* Creates a segment that follows given segments. | ||
* Creates a new segment and appends it after the given segments. | ||
* @param {string} id An identifier. | ||
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments. | ||
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments | ||
* to append to. | ||
* @returns {CodePathSegment} The created segment. | ||
@@ -138,3 +153,3 @@ */ | ||
/** | ||
* Creates an unreachable segment that follows given segments. | ||
* Creates an unreachable segment and appends it after the given segments. | ||
* @param {string} id An identifier. | ||
@@ -149,3 +164,3 @@ * @param {CodePathSegment[]} allPrevSegments An array of the previous segments. | ||
* In `if (a) return a; foo();` case, the unreachable segment preceded by | ||
* the return statement is not used but must not be remove. | ||
* the return statement is not used but must not be removed. | ||
*/ | ||
@@ -170,3 +185,3 @@ CodePathSegment.markUsed(segment); | ||
/** | ||
* Makes a given segment being used. | ||
* Marks a given segment as used. | ||
* | ||
@@ -186,2 +201,9 @@ * And this function registers the segment into the previous segments as a next. | ||
if (segment.reachable) { | ||
/* | ||
* If the segment is reachable, then it's officially part of the | ||
* code path. This loops through all previous segments to update | ||
* their list of next segments. Because the segment is reachable, | ||
* it's added to both `nextSegments` and `allNextSegments`. | ||
*/ | ||
for (i = 0; i < segment.allPrevSegments.length; ++i) { | ||
@@ -194,2 +216,9 @@ const prevSegment = segment.allPrevSegments[i]; | ||
} else { | ||
/* | ||
* If the segment is not reachable, then it's not officially part of the | ||
* code path. This loops through all previous segments to update | ||
* their list of next segments. Because the segment is not reachable, | ||
* it's added only to `allNextSegments`. | ||
*/ | ||
for (i = 0; i < segment.allPrevSegments.length; ++i) { | ||
@@ -212,9 +241,10 @@ segment.allPrevSegments[i].allNextSegments.push(segment); | ||
/** | ||
* Replaces unused segments with the previous segments of each unused segment. | ||
* @param {CodePathSegment[]} segments An array of segments to replace. | ||
* @returns {CodePathSegment[]} The replaced array. | ||
* Creates a new array based on an array of segments. If any segment in the | ||
* array is unused, then it is replaced by all of its previous segments. | ||
* All used segments are returned as-is without replacement. | ||
* @param {CodePathSegment[]} segments The array of segments to flatten. | ||
* @returns {CodePathSegment[]} The flattened array. | ||
*/ | ||
static flattenUnusedSegments(segments) { | ||
const done = Object.create(null); | ||
const retv = []; | ||
const done = new Set(); | ||
@@ -225,3 +255,3 @@ for (let i = 0; i < segments.length; ++i) { | ||
// Ignores duplicated. | ||
if (done[segment.id]) { | ||
if (done.has(segment)) { | ||
continue; | ||
@@ -235,14 +265,12 @@ } | ||
if (!done[prevSegment.id]) { | ||
done[prevSegment.id] = true; | ||
retv.push(prevSegment); | ||
if (!done.has(prevSegment)) { | ||
done.add(prevSegment); | ||
} | ||
} | ||
} else { | ||
done[segment.id] = true; | ||
retv.push(segment); | ||
done.add(segment); | ||
} | ||
} | ||
return retv; | ||
return [...done]; | ||
} | ||
@@ -249,0 +277,0 @@ } |
@@ -120,2 +120,3 @@ /** | ||
* @type {CodePathSegment[]} | ||
* @deprecated | ||
*/ | ||
@@ -122,0 +123,0 @@ get currentSegments() { |
@@ -19,3 +19,5 @@ /** | ||
{ getRuleOptionsSchema } = require("../config/flat-config-helpers"), | ||
{ Linter, SourceCodeFixer, interpolate } = require("../linter"); | ||
{ Linter, SourceCodeFixer, interpolate } = require("../linter"), | ||
CodePath = require("../linter/code-path-analysis/code-path"); | ||
const { FlatConfigArray } = require("../config/flat-config-array"); | ||
@@ -36,2 +38,3 @@ const { defaultConfig } = require("../config/default-config"); | ||
/** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */ | ||
/** @typedef {import("../shared/types").Rule} Rule */ | ||
@@ -135,2 +138,11 @@ | ||
const forbiddenMethods = [ | ||
"applyInlineConfig", | ||
"applyLanguageOptions", | ||
"finalize" | ||
]; | ||
/** @type {Map<string,WeakSet>} */ | ||
const forbiddenMethodCalls = new Map(forbiddenMethods.map(methodName => ([methodName, new WeakSet()]))); | ||
const hasOwnProperty = Function.call.bind(Object.hasOwnProperty); | ||
@@ -279,2 +291,45 @@ | ||
/** | ||
* Emit a deprecation warning if rule uses CodePath#currentSegments. | ||
* @param {string} ruleName Name of the rule. | ||
* @returns {void} | ||
*/ | ||
function emitCodePathCurrentSegmentsWarning(ruleName) { | ||
if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) { | ||
emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true; | ||
process.emitWarning( | ||
`"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`, | ||
"DeprecationWarning" | ||
); | ||
} | ||
} | ||
/** | ||
* Function to replace forbidden `SourceCode` methods. Allows just one call per method. | ||
* @param {string} methodName The name of the method to forbid. | ||
* @param {Function} prototype The prototype with the original method to call. | ||
* @returns {Function} The function that throws the error. | ||
*/ | ||
function throwForbiddenMethodError(methodName, prototype) { | ||
const original = prototype[methodName]; | ||
return function(...args) { | ||
const called = forbiddenMethodCalls.get(methodName); | ||
/* eslint-disable no-invalid-this -- needed to operate as a method. */ | ||
if (!called.has(this)) { | ||
called.add(this); | ||
return original.apply(this, args); | ||
} | ||
/* eslint-enable no-invalid-this -- not needed past this point */ | ||
throw new Error( | ||
`\`SourceCode#${methodName}()\` cannot be called inside a rule.` | ||
); | ||
}; | ||
} | ||
//------------------------------------------------------------------------------ | ||
@@ -453,3 +508,3 @@ // Public Interface | ||
* @param {string} ruleName The name of the rule to run. | ||
* @param {Function} rule The rule to test. | ||
* @param {Function | Rule} rule The rule to test. | ||
* @param {{ | ||
@@ -488,2 +543,3 @@ * valid: (ValidTestCase | string)[], | ||
const baseConfig = [ | ||
{ files: ["**"] }, // Make sure the default config matches for all files | ||
{ | ||
@@ -670,6 +726,2 @@ plugins: { | ||
// Verify the code. | ||
const { getComments } = SourceCode.prototype; | ||
let messages; | ||
// check for validation errors | ||
@@ -684,9 +736,30 @@ try { | ||
// Verify the code. | ||
const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; | ||
const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); | ||
let messages; | ||
try { | ||
SourceCode.prototype.getComments = getCommentsDeprecation; | ||
Object.defineProperty(CodePath.prototype, "currentSegments", { | ||
get() { | ||
emitCodePathCurrentSegmentsWarning(ruleName); | ||
return originalCurrentSegments.get.call(this); | ||
} | ||
}); | ||
forbiddenMethods.forEach(methodName => { | ||
SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName, SourceCode.prototype); | ||
}); | ||
messages = linter.verify(code, configs, filename); | ||
} finally { | ||
SourceCode.prototype.getComments = getComments; | ||
Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); | ||
SourceCode.prototype.applyInlineConfig = applyInlineConfig; | ||
SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; | ||
SourceCode.prototype.finalize = finalize; | ||
} | ||
const fatalErrorMessage = messages.find(m => m.fatal); | ||
@@ -1022,25 +1095,31 @@ | ||
* one of the templates above. | ||
* The test suites for valid/invalid are created conditionally as | ||
* test runners (eg. vitest) fail for empty test suites. | ||
*/ | ||
this.constructor.describe(ruleName, () => { | ||
this.constructor.describe("valid", () => { | ||
test.valid.forEach(valid => { | ||
this.constructor[valid.only ? "itOnly" : "it"]( | ||
sanitize(typeof valid === "object" ? valid.name || valid.code : valid), | ||
() => { | ||
testValidTemplate(valid); | ||
} | ||
); | ||
if (test.valid.length > 0) { | ||
this.constructor.describe("valid", () => { | ||
test.valid.forEach(valid => { | ||
this.constructor[valid.only ? "itOnly" : "it"]( | ||
sanitize(typeof valid === "object" ? valid.name || valid.code : valid), | ||
() => { | ||
testValidTemplate(valid); | ||
} | ||
); | ||
}); | ||
}); | ||
}); | ||
} | ||
this.constructor.describe("invalid", () => { | ||
test.invalid.forEach(invalid => { | ||
this.constructor[invalid.only ? "itOnly" : "it"]( | ||
sanitize(invalid.name || invalid.code), | ||
() => { | ||
testInvalidTemplate(invalid); | ||
} | ||
); | ||
if (test.invalid.length > 0) { | ||
this.constructor.describe("invalid", () => { | ||
test.invalid.forEach(invalid => { | ||
this.constructor[invalid.only ? "itOnly" : "it"]( | ||
sanitize(invalid.name || invalid.code), | ||
() => { | ||
testInvalidTemplate(invalid); | ||
} | ||
); | ||
}); | ||
}); | ||
}); | ||
} | ||
}); | ||
@@ -1047,0 +1126,0 @@ } |
@@ -51,3 +51,4 @@ /** | ||
{ getRuleOptionsSchema, validate } = require("../shared/config-validator"), | ||
{ Linter, SourceCodeFixer, interpolate } = require("../linter"); | ||
{ Linter, SourceCodeFixer, interpolate } = require("../linter"), | ||
CodePath = require("../linter/code-path-analysis/code-path"); | ||
@@ -66,2 +67,3 @@ const ajv = require("../shared/ajv")({ strictDefaults: true }); | ||
/** @typedef {import("../shared/types").Parser} Parser */ | ||
/** @typedef {import("../shared/types").Rule} Rule */ | ||
@@ -166,4 +168,39 @@ | ||
const forbiddenMethods = [ | ||
"applyInlineConfig", | ||
"applyLanguageOptions", | ||
"finalize" | ||
]; | ||
const hasOwnProperty = Function.call.bind(Object.hasOwnProperty); | ||
const DEPRECATED_SOURCECODE_PASSTHROUGHS = { | ||
getSource: "getText", | ||
getSourceLines: "getLines", | ||
getAllComments: "getAllComments", | ||
getNodeByRangeIndex: "getNodeByRangeIndex", | ||
// getComments: "getComments", -- already handled by a separate error | ||
getCommentsBefore: "getCommentsBefore", | ||
getCommentsAfter: "getCommentsAfter", | ||
getCommentsInside: "getCommentsInside", | ||
getJSDocComment: "getJSDocComment", | ||
getFirstToken: "getFirstToken", | ||
getFirstTokens: "getFirstTokens", | ||
getLastToken: "getLastToken", | ||
getLastTokens: "getLastTokens", | ||
getTokenAfter: "getTokenAfter", | ||
getTokenBefore: "getTokenBefore", | ||
getTokenByRangeStart: "getTokenByRangeStart", | ||
getTokens: "getTokens", | ||
getTokensAfter: "getTokensAfter", | ||
getTokensBefore: "getTokensBefore", | ||
getTokensBetween: "getTokensBetween", | ||
getScope: "getScope", | ||
getAncestors: "getAncestors", | ||
getDeclaredVariables: "getDeclaredVariables", | ||
markVariableAsUsed: "markVariableAsUsed" | ||
}; | ||
/** | ||
@@ -311,2 +348,15 @@ * Clones a given value deeply. | ||
/** | ||
* Function to replace forbidden `SourceCode` methods. | ||
* @param {string} methodName The name of the method to forbid. | ||
* @returns {Function} The function that throws the error. | ||
*/ | ||
function throwForbiddenMethodError(methodName) { | ||
return () => { | ||
throw new Error( | ||
`\`SourceCode#${methodName}()\` cannot be called inside a rule.` | ||
); | ||
}; | ||
} | ||
/** | ||
* Emit a deprecation warning if function-style format is being used. | ||
@@ -341,2 +391,49 @@ * @param {string} ruleName Name of the rule. | ||
/** | ||
* Emit a deprecation warning if a rule uses a deprecated `context` method. | ||
* @param {string} ruleName Name of the rule. | ||
* @param {string} methodName The name of the method on `context` that was used. | ||
* @returns {void} | ||
*/ | ||
function emitDeprecatedContextMethodWarning(ruleName, methodName) { | ||
if (!emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`]) { | ||
emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`] = true; | ||
process.emitWarning( | ||
`"${ruleName}" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]}()\` instead.`, | ||
"DeprecationWarning" | ||
); | ||
} | ||
} | ||
/** | ||
* Emit a deprecation warning if rule uses CodePath#currentSegments. | ||
* @param {string} ruleName Name of the rule. | ||
* @returns {void} | ||
*/ | ||
function emitCodePathCurrentSegmentsWarning(ruleName) { | ||
if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) { | ||
emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true; | ||
process.emitWarning( | ||
`"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`, | ||
"DeprecationWarning" | ||
); | ||
} | ||
} | ||
/** | ||
* Emit a deprecation warning if `context.parserServices` is used. | ||
* @param {string} ruleName Name of the rule. | ||
* @returns {void} | ||
*/ | ||
function emitParserServicesWarning(ruleName) { | ||
if (!emitParserServicesWarning[`warned-${ruleName}`]) { | ||
emitParserServicesWarning[`warned-${ruleName}`] = true; | ||
process.emitWarning( | ||
`"${ruleName}" rule is using \`context.parserServices\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.parserServices\` instead.`, | ||
"DeprecationWarning" | ||
); | ||
} | ||
} | ||
//------------------------------------------------------------------------------ | ||
@@ -516,6 +613,9 @@ // Public Interface | ||
* @param {string} name The name of the rule to define. | ||
* @param {Function} rule The rule definition. | ||
* @param {Function | Rule} rule The rule definition. | ||
* @returns {void} | ||
*/ | ||
defineRule(name, rule) { | ||
if (typeof rule === "function") { | ||
emitLegacyRuleAPIWarning(name); | ||
} | ||
this.rules[name] = rule; | ||
@@ -527,3 +627,3 @@ } | ||
* @param {string} ruleName The name of the rule to run. | ||
* @param {Function} rule The rule to test. | ||
* @param {Function | Rule} rule The rule to test. | ||
* @param {{ | ||
@@ -572,3 +672,34 @@ * valid: (ValidTestCase | string)[], | ||
return (typeof rule === "function" ? rule : rule.create)(context); | ||
// wrap all deprecated methods | ||
const newContext = Object.create( | ||
context, | ||
Object.fromEntries(Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).map(methodName => [ | ||
methodName, | ||
{ | ||
value(...args) { | ||
// emit deprecation warning | ||
emitDeprecatedContextMethodWarning(ruleName, methodName); | ||
// call the original method | ||
return context[methodName].call(this, ...args); | ||
}, | ||
enumerable: true | ||
} | ||
])) | ||
); | ||
// emit warning about context.parserServices | ||
const parserServices = context.parserServices; | ||
Object.defineProperty(newContext, "parserServices", { | ||
get() { | ||
emitParserServicesWarning(ruleName); | ||
return parserServices; | ||
} | ||
}); | ||
Object.freeze(newContext); | ||
return (typeof rule === "function" ? rule : rule.create)(newContext); | ||
} | ||
@@ -692,3 +823,4 @@ })); | ||
// Verify the code. | ||
const { getComments } = SourceCode.prototype; | ||
const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; | ||
const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); | ||
let messages; | ||
@@ -698,5 +830,20 @@ | ||
SourceCode.prototype.getComments = getCommentsDeprecation; | ||
Object.defineProperty(CodePath.prototype, "currentSegments", { | ||
get() { | ||
emitCodePathCurrentSegmentsWarning(ruleName); | ||
return originalCurrentSegments.get.call(this); | ||
} | ||
}); | ||
forbiddenMethods.forEach(methodName => { | ||
SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName); | ||
}); | ||
messages = linter.verify(code, config, filename); | ||
} finally { | ||
SourceCode.prototype.getComments = getComments; | ||
Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); | ||
SourceCode.prototype.applyInlineConfig = applyInlineConfig; | ||
SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; | ||
SourceCode.prototype.finalize = finalize; | ||
} | ||
@@ -1034,25 +1181,31 @@ | ||
* one of the templates above. | ||
* The test suites for valid/invalid are created conditionally as | ||
* test runners (eg. vitest) fail for empty test suites. | ||
*/ | ||
this.constructor.describe(ruleName, () => { | ||
this.constructor.describe("valid", () => { | ||
test.valid.forEach(valid => { | ||
this.constructor[valid.only ? "itOnly" : "it"]( | ||
sanitize(typeof valid === "object" ? valid.name || valid.code : valid), | ||
() => { | ||
testValidTemplate(valid); | ||
} | ||
); | ||
if (test.valid.length > 0) { | ||
this.constructor.describe("valid", () => { | ||
test.valid.forEach(valid => { | ||
this.constructor[valid.only ? "itOnly" : "it"]( | ||
sanitize(typeof valid === "object" ? valid.name || valid.code : valid), | ||
() => { | ||
testValidTemplate(valid); | ||
} | ||
); | ||
}); | ||
}); | ||
}); | ||
} | ||
this.constructor.describe("invalid", () => { | ||
test.invalid.forEach(invalid => { | ||
this.constructor[invalid.only ? "itOnly" : "it"]( | ||
sanitize(invalid.name || invalid.code), | ||
() => { | ||
testInvalidTemplate(invalid); | ||
} | ||
); | ||
if (test.invalid.length > 0) { | ||
this.constructor.describe("invalid", () => { | ||
test.invalid.forEach(invalid => { | ||
this.constructor[invalid.only ? "itOnly" : "it"]( | ||
sanitize(invalid.name || invalid.code), | ||
() => { | ||
testInvalidTemplate(invalid); | ||
} | ||
); | ||
}); | ||
}); | ||
}); | ||
} | ||
}); | ||
@@ -1059,0 +1212,0 @@ } |
@@ -22,11 +22,2 @@ /** | ||
/** | ||
* Checks a given code path segment is reachable. | ||
* @param {CodePathSegment} segment A segment to check. | ||
* @returns {boolean} `true` if the segment is reachable. | ||
*/ | ||
function isReachable(segment) { | ||
return segment.reachable; | ||
} | ||
/** | ||
* Checks a given node is a member access which has the specified name's | ||
@@ -43,2 +34,18 @@ * property. | ||
/** | ||
* Checks all segments in a set and returns true if any are reachable. | ||
* @param {Set<CodePathSegment>} segments The segments to check. | ||
* @returns {boolean} True if any segment is reachable; false otherwise. | ||
*/ | ||
function isAnySegmentReachable(segments) { | ||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
/** | ||
* Returns a human-legible description of an array method | ||
@@ -134,2 +141,72 @@ * @param {string} arrayMethodName A method name to fully qualify | ||
/** | ||
* Checks if the given node is a void expression. | ||
* @param {ASTNode} node The node to check. | ||
* @returns {boolean} - `true` if the node is a void expression | ||
*/ | ||
function isExpressionVoid(node) { | ||
return node.type === "UnaryExpression" && node.operator === "void"; | ||
} | ||
/** | ||
* Fixes the linting error by prepending "void " to the given node | ||
* @param {Object} sourceCode context given by context.sourceCode | ||
* @param {ASTNode} node The node to fix. | ||
* @param {Object} fixer The fixer object provided by ESLint. | ||
* @returns {Array<Object>} - An array of fix objects to apply to the node. | ||
*/ | ||
function voidPrependFixer(sourceCode, node, fixer) { | ||
const requiresParens = | ||
// prepending `void ` will fail if the node has a lower precedence than void | ||
astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) && | ||
// check if there are parentheses around the node to avoid redundant parentheses | ||
!astUtils.isParenthesised(sourceCode, node); | ||
// avoid parentheses issues | ||
const returnOrArrowToken = sourceCode.getTokenBefore( | ||
node, | ||
node.parent.type === "ArrowFunctionExpression" | ||
? astUtils.isArrowToken | ||
// isReturnToken | ||
: token => token.type === "Keyword" && token.value === "return" | ||
); | ||
const firstToken = sourceCode.getTokenAfter(returnOrArrowToken); | ||
const prependSpace = | ||
// is return token, as => allows void to be adjacent | ||
returnOrArrowToken.value === "return" && | ||
// If two tokens (return and "(") are adjacent | ||
returnOrArrowToken.range[1] === firstToken.range[0]; | ||
return [ | ||
fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`), | ||
fixer.insertTextAfter(node, requiresParens ? ")" : "") | ||
]; | ||
} | ||
/** | ||
* Fixes the linting error by `wrapping {}` around the given node's body. | ||
* @param {Object} sourceCode context given by context.sourceCode | ||
* @param {ASTNode} node The node to fix. | ||
* @param {Object} fixer The fixer object provided by ESLint. | ||
* @returns {Array<Object>} - An array of fix objects to apply to the node. | ||
*/ | ||
function curlyWrapFixer(sourceCode, node, fixer) { | ||
const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken); | ||
const firstToken = sourceCode.getTokenAfter(arrowToken); | ||
const lastToken = sourceCode.getLastToken(node); | ||
return [ | ||
fixer.insertTextBefore(firstToken, "{"), | ||
fixer.insertTextAfter(lastToken, "}") | ||
]; | ||
} | ||
//------------------------------------------------------------------------------ | ||
@@ -150,2 +227,5 @@ // Rule Definition | ||
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- false positive | ||
hasSuggestions: true, | ||
schema: [ | ||
@@ -162,2 +242,6 @@ { | ||
default: false | ||
}, | ||
allowVoid: { | ||
type: "boolean", | ||
default: false | ||
} | ||
@@ -173,3 +257,5 @@ }, | ||
expectedReturnValue: "{{arrayMethodName}}() expects a return value from {{name}}.", | ||
expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}." | ||
expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}.", | ||
wrapBraces: "Wrap the expression in `{}`.", | ||
prependVoid: "Prepend `void` to the expression." | ||
} | ||
@@ -180,3 +266,3 @@ }, | ||
const options = context.options[0] || { allowImplicit: false, checkForEach: false }; | ||
const options = context.options[0] || { allowImplicit: false, checkForEach: false, allowVoid: false }; | ||
const sourceCode = context.sourceCode; | ||
@@ -208,15 +294,44 @@ | ||
let messageId = null; | ||
const messageAndSuggestions = { messageId: "", suggest: [] }; | ||
if (funcInfo.arrayMethodName === "forEach") { | ||
if (options.checkForEach && node.type === "ArrowFunctionExpression" && node.expression) { | ||
messageId = "expectedNoReturnValue"; | ||
if (options.allowVoid) { | ||
if (isExpressionVoid(node.body)) { | ||
return; | ||
} | ||
messageAndSuggestions.messageId = "expectedNoReturnValue"; | ||
messageAndSuggestions.suggest = [ | ||
{ | ||
messageId: "wrapBraces", | ||
fix(fixer) { | ||
return curlyWrapFixer(sourceCode, node, fixer); | ||
} | ||
}, | ||
{ | ||
messageId: "prependVoid", | ||
fix(fixer) { | ||
return voidPrependFixer(sourceCode, node.body, fixer); | ||
} | ||
} | ||
]; | ||
} else { | ||
messageAndSuggestions.messageId = "expectedNoReturnValue"; | ||
messageAndSuggestions.suggest = [{ | ||
messageId: "wrapBraces", | ||
fix(fixer) { | ||
return curlyWrapFixer(sourceCode, node, fixer); | ||
} | ||
}]; | ||
} | ||
} | ||
} else { | ||
if (node.body.type === "BlockStatement" && funcInfo.codePath.currentSegments.some(isReachable)) { | ||
messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside"; | ||
if (node.body.type === "BlockStatement" && isAnySegmentReachable(funcInfo.currentSegments)) { | ||
messageAndSuggestions.messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside"; | ||
} | ||
} | ||
if (messageId) { | ||
if (messageAndSuggestions.messageId) { | ||
const name = astUtils.getFunctionNameWithKind(node); | ||
@@ -227,4 +342,5 @@ | ||
loc: astUtils.getFunctionHeadLoc(node, sourceCode), | ||
messageId, | ||
data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) } | ||
messageId: messageAndSuggestions.messageId, | ||
data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) }, | ||
suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null | ||
}); | ||
@@ -254,3 +370,4 @@ } | ||
!node.generator, | ||
node | ||
node, | ||
currentSegments: new Set() | ||
}; | ||
@@ -264,2 +381,19 @@ }, | ||
onUnreachableCodePathSegmentStart(segment) { | ||
funcInfo.currentSegments.add(segment); | ||
}, | ||
onUnreachableCodePathSegmentEnd(segment) { | ||
funcInfo.currentSegments.delete(segment); | ||
}, | ||
onCodePathSegmentStart(segment) { | ||
funcInfo.currentSegments.add(segment); | ||
}, | ||
onCodePathSegmentEnd(segment) { | ||
funcInfo.currentSegments.delete(segment); | ||
}, | ||
// Checks the return statement is valid. | ||
@@ -274,3 +408,3 @@ ReturnStatement(node) { | ||
let messageId = null; | ||
const messageAndSuggestions = { messageId: "", suggest: [] }; | ||
@@ -281,3 +415,18 @@ if (funcInfo.arrayMethodName === "forEach") { | ||
if (options.checkForEach && node.argument) { | ||
messageId = "expectedNoReturnValue"; | ||
if (options.allowVoid) { | ||
if (isExpressionVoid(node.argument)) { | ||
return; | ||
} | ||
messageAndSuggestions.messageId = "expectedNoReturnValue"; | ||
messageAndSuggestions.suggest = [{ | ||
messageId: "prependVoid", | ||
fix(fixer) { | ||
return voidPrependFixer(sourceCode, node.argument, fixer); | ||
} | ||
}]; | ||
} else { | ||
messageAndSuggestions.messageId = "expectedNoReturnValue"; | ||
} | ||
} | ||
@@ -288,14 +437,15 @@ } else { | ||
if (!options.allowImplicit && !node.argument) { | ||
messageId = "expectedReturnValue"; | ||
messageAndSuggestions.messageId = "expectedReturnValue"; | ||
} | ||
} | ||
if (messageId) { | ||
if (messageAndSuggestions.messageId) { | ||
context.report({ | ||
node, | ||
messageId, | ||
messageId: messageAndSuggestions.messageId, | ||
data: { | ||
name: astUtils.getFunctionNameWithKind(funcInfo.node), | ||
arrayMethodName: fullMethodName(funcInfo.arrayMethodName) | ||
} | ||
}, | ||
suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null | ||
}); | ||
@@ -302,0 +452,0 @@ } |
@@ -19,8 +19,15 @@ /** | ||
/** | ||
* Checks whether or not a given code path segment is unreachable. | ||
* @param {CodePathSegment} segment A CodePathSegment to check. | ||
* @returns {boolean} `true` if the segment is unreachable. | ||
* Checks all segments in a set and returns true if all are unreachable. | ||
* @param {Set<CodePathSegment>} segments The segments to check. | ||
* @returns {boolean} True if all segments are unreachable; false otherwise. | ||
*/ | ||
function isUnreachable(segment) { | ||
return !segment.reachable; | ||
function areAllSegmentsUnreachable(segments) { | ||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
@@ -92,3 +99,3 @@ | ||
if (!funcInfo.hasReturnValue || | ||
funcInfo.codePath.currentSegments.every(isUnreachable) || | ||
areAllSegmentsUnreachable(funcInfo.currentSegments) || | ||
astUtils.isES5Constructor(node) || | ||
@@ -146,3 +153,4 @@ isClassConstructor(node) | ||
messageId: "", | ||
node | ||
node, | ||
currentSegments: new Set() | ||
}; | ||
@@ -154,2 +162,19 @@ }, | ||
onUnreachableCodePathSegmentStart(segment) { | ||
funcInfo.currentSegments.add(segment); | ||
}, | ||
onUnreachableCodePathSegmentEnd(segment) { | ||
funcInfo.currentSegments.delete(segment); | ||
}, | ||
onCodePathSegmentStart(segment) { | ||
funcInfo.currentSegments.add(segment); | ||
}, | ||
onCodePathSegmentEnd(segment) { | ||
funcInfo.currentSegments.delete(segment); | ||
}, | ||
// Reports a given return statement if it's inconsistent. | ||
@@ -156,0 +181,0 @@ ReturnStatement(node) { |
@@ -13,8 +13,15 @@ /** | ||
/** | ||
* Checks whether a given code path segment is reachable or not. | ||
* @param {CodePathSegment} segment A code path segment to check. | ||
* @returns {boolean} `true` if the segment is reachable. | ||
* Checks all segments in a set and returns true if any are reachable. | ||
* @param {Set<CodePathSegment>} segments The segments to check. | ||
* @returns {boolean} True if any segment is reachable; false otherwise. | ||
*/ | ||
function isReachable(segment) { | ||
return segment.reachable; | ||
function isAnySegmentReachable(segments) { | ||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
@@ -214,3 +221,4 @@ | ||
superIsConstructor: isPossibleConstructor(superClass), | ||
codePath | ||
codePath, | ||
currentSegments: new Set() | ||
}; | ||
@@ -223,3 +231,4 @@ } else { | ||
superIsConstructor: false, | ||
codePath | ||
codePath, | ||
currentSegments: new Set() | ||
}; | ||
@@ -267,2 +276,5 @@ } | ||
onCodePathSegmentStart(segment) { | ||
funcInfo.currentSegments.add(segment); | ||
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { | ||
@@ -288,2 +300,15 @@ return; | ||
onUnreachableCodePathSegmentStart(segment) { | ||
funcInfo.currentSegments.add(segment); | ||
}, | ||
onUnreachableCodePathSegmentEnd(segment) { | ||
funcInfo.currentSegments.delete(segment); | ||
}, | ||
onCodePathSegmentEnd(segment) { | ||
funcInfo.currentSegments.delete(segment); | ||
}, | ||
/** | ||
@@ -352,8 +377,7 @@ * Update information of the code path segment when a code path was | ||
if (funcInfo.hasExtends) { | ||
const segments = funcInfo.codePath.currentSegments; | ||
const segments = funcInfo.currentSegments; | ||
let duplicate = false; | ||
let info = null; | ||
for (let i = 0; i < segments.length; ++i) { | ||
const segment = segments[i]; | ||
for (const segment of segments) { | ||
@@ -383,3 +407,3 @@ if (segment.reachable) { | ||
} | ||
} else if (funcInfo.codePath.currentSegments.some(isReachable)) { | ||
} else if (isAnySegmentReachable(funcInfo.currentSegments)) { | ||
context.report({ | ||
@@ -408,6 +432,5 @@ messageId: "unexpected", | ||
// Returning argument is a substitute of 'super()'. | ||
const segments = funcInfo.codePath.currentSegments; | ||
const segments = funcInfo.currentSegments; | ||
for (let i = 0; i < segments.length; ++i) { | ||
const segment = segments[i]; | ||
for (const segment of segments) { | ||
@@ -414,0 +437,0 @@ if (segment.reachable) { |
@@ -9,2 +9,8 @@ /** | ||
//------------------------------------------------------------------------------ | ||
// Requirements | ||
//------------------------------------------------------------------------------ | ||
const { getStaticValue } = require("@eslint-community/eslint-utils"); | ||
//------------------------------------------------------------------------------ | ||
// Rule Definition | ||
@@ -33,2 +39,3 @@ //------------------------------------------------------------------------------ | ||
create(context) { | ||
const { sourceCode } = context; | ||
@@ -51,13 +58,13 @@ /** | ||
* @param {int} dir expected direction that could either be turned around or invalidated | ||
* @returns {int} return dir, the negated dir or zero if it's not clear for identifiers | ||
* @returns {int} return dir, the negated dir, or zero if the counter does not change or the direction is not clear | ||
*/ | ||
function getRightDirection(update, dir) { | ||
if (update.right.type === "UnaryExpression") { | ||
if (update.right.operator === "-") { | ||
return -dir; | ||
} | ||
} else if (update.right.type === "Identifier") { | ||
return 0; | ||
const staticValue = getStaticValue(update.right, sourceCode.getScope(update)); | ||
if (staticValue && ["bigint", "boolean", "number"].includes(typeof staticValue.value)) { | ||
const sign = Math.sign(Number(staticValue.value)) || 0; // convert NaN to 0 | ||
return dir * sign; | ||
} | ||
return dir; | ||
return 0; | ||
} | ||
@@ -64,0 +71,0 @@ |
@@ -17,11 +17,19 @@ /** | ||
//------------------------------------------------------------------------------ | ||
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u; | ||
/** | ||
* Checks a given code path segment is reachable. | ||
* @param {CodePathSegment} segment A segment to check. | ||
* @returns {boolean} `true` if the segment is reachable. | ||
* Checks all segments in a set and returns true if any are reachable. | ||
* @param {Set<CodePathSegment>} segments The segments to check. | ||
* @returns {boolean} True if any segment is reachable; false otherwise. | ||
*/ | ||
function isReachable(segment) { | ||
return segment.reachable; | ||
function isAnySegmentReachable(segments) { | ||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
@@ -75,3 +83,4 @@ | ||
shouldCheck: false, | ||
node: null | ||
node: null, | ||
currentSegments: [] | ||
}; | ||
@@ -90,3 +99,3 @@ | ||
if (funcInfo.shouldCheck && | ||
funcInfo.codePath.currentSegments.some(isReachable) | ||
isAnySegmentReachable(funcInfo.currentSegments) | ||
) { | ||
@@ -150,3 +159,4 @@ context.report({ | ||
shouldCheck: isGetter(node), | ||
node | ||
node, | ||
currentSegments: new Set() | ||
}; | ||
@@ -159,3 +169,18 @@ }, | ||
}, | ||
onUnreachableCodePathSegmentStart(segment) { | ||
funcInfo.currentSegments.add(segment); | ||
}, | ||
onUnreachableCodePathSegmentEnd(segment) { | ||
funcInfo.currentSegments.delete(segment); | ||
}, | ||
onCodePathSegmentStart(segment) { | ||
funcInfo.currentSegments.add(segment); | ||
}, | ||
onCodePathSegmentEnd(segment) { | ||
funcInfo.currentSegments.delete(segment); | ||
}, | ||
// Checks the return statement is valid. | ||
@@ -162,0 +187,0 @@ ReturnStatement(node) { |
@@ -178,2 +178,3 @@ /** | ||
"no-obj-calls": () => require("./no-obj-calls"), | ||
"no-object-constructor": () => require("./no-object-constructor"), | ||
"no-octal": () => require("./no-octal"), | ||
@@ -180,0 +181,0 @@ "no-octal-escape": () => require("./no-octal-escape"), |
@@ -14,2 +14,17 @@ /** | ||
//------------------------------------------------------------------------------ | ||
// Helpers | ||
//------------------------------------------------------------------------------ | ||
/** | ||
* Types of class members. | ||
* Those have `test` method to check it matches to the given class member. | ||
* @private | ||
*/ | ||
const ClassMemberTypes = { | ||
"*": { test: () => true }, | ||
field: { test: node => node.type === "PropertyDefinition" }, | ||
method: { test: node => node.type === "MethodDefinition" } | ||
}; | ||
//------------------------------------------------------------------------------ | ||
// Rule Definition | ||
@@ -33,3 +48,28 @@ //------------------------------------------------------------------------------ | ||
{ | ||
enum: ["always", "never"] | ||
anyOf: [ | ||
{ | ||
type: "object", | ||
properties: { | ||
enforce: { | ||
type: "array", | ||
items: { | ||
type: "object", | ||
properties: { | ||
blankLine: { enum: ["always", "never"] }, | ||
prev: { enum: ["method", "field", "*"] }, | ||
next: { enum: ["method", "field", "*"] } | ||
}, | ||
additionalProperties: false, | ||
required: ["blankLine", "prev", "next"] | ||
}, | ||
minItems: 1 | ||
} | ||
}, | ||
additionalProperties: false, | ||
required: ["enforce"] | ||
}, | ||
{ | ||
enum: ["always", "never"] | ||
} | ||
] | ||
}, | ||
@@ -60,2 +100,3 @@ { | ||
const configureList = typeof options[0] === "object" ? options[0].enforce : [{ blankLine: options[0], prev: "*", next: "*" }]; | ||
const sourceCode = context.sourceCode; | ||
@@ -150,2 +191,34 @@ | ||
/** | ||
* Checks whether the given node matches the given type. | ||
* @param {ASTNode} node The class member node to check. | ||
* @param {string} type The class member type to check. | ||
* @returns {boolean} `true` if the class member node matched the type. | ||
* @private | ||
*/ | ||
function match(node, type) { | ||
return ClassMemberTypes[type].test(node); | ||
} | ||
/** | ||
* Finds the last matched configuration from the configureList. | ||
* @param {ASTNode} prevNode The previous node to match. | ||
* @param {ASTNode} nextNode The current node to match. | ||
* @returns {string|null} Padding type or `null` if no matches were found. | ||
* @private | ||
*/ | ||
function getPaddingType(prevNode, nextNode) { | ||
for (let i = configureList.length - 1; i >= 0; --i) { | ||
const configure = configureList[i]; | ||
const matched = | ||
match(prevNode, configure.prev) && | ||
match(nextNode, configure.next); | ||
if (matched) { | ||
return configure.blankLine; | ||
} | ||
} | ||
return null; | ||
} | ||
return { | ||
@@ -165,8 +238,9 @@ ClassBody(node) { | ||
const curLineLastToken = findLastConsecutiveTokenAfter(curLast, nextFirst, 0); | ||
const paddingType = getPaddingType(body[i], body[i + 1]); | ||
if ((options[0] === "always" && !skip && !isPadded) || | ||
(options[0] === "never" && isPadded)) { | ||
if (paddingType === "never" && isPadded) { | ||
context.report({ | ||
node: body[i + 1], | ||
messageId: isPadded ? "never" : "always", | ||
messageId: "never", | ||
fix(fixer) { | ||
@@ -176,8 +250,19 @@ if (hasTokenInPadding) { | ||
} | ||
return isPadded | ||
? fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n") | ||
: fixer.insertTextAfter(curLineLastToken, "\n"); | ||
return fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n"); | ||
} | ||
}); | ||
} else if (paddingType === "always" && !skip && !isPadded) { | ||
context.report({ | ||
node: body[i + 1], | ||
messageId: "always", | ||
fix(fixer) { | ||
if (hasTokenInPadding) { | ||
return null; | ||
} | ||
return fixer.insertTextAfter(curLineLastToken, "\n"); | ||
} | ||
}); | ||
} | ||
} | ||
@@ -184,0 +269,0 @@ } |
@@ -20,2 +20,18 @@ /** | ||
/** | ||
* Checks all segments in a set and returns true if any are reachable. | ||
* @param {Set<CodePathSegment>} segments The segments to check. | ||
* @returns {boolean} True if any segment is reachable; false otherwise. | ||
*/ | ||
function isAnySegmentReachable(segments) { | ||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
/** | ||
* Checks whether or not a given comment string is really a fallthrough comment and not an ESLint directive. | ||
@@ -56,11 +72,2 @@ * @param {string} comment The comment string to check. | ||
/** | ||
* Checks whether or not a given code path segment is reachable. | ||
* @param {CodePathSegment} segment A CodePathSegment to check. | ||
* @returns {boolean} `true` if the segment is reachable. | ||
*/ | ||
function isReachable(segment) { | ||
return segment.reachable; | ||
} | ||
/** | ||
* Checks whether a node and a token are separated by blank lines | ||
@@ -114,3 +121,4 @@ * @param {ASTNode} node The node to check | ||
const options = context.options[0] || {}; | ||
let currentCodePath = null; | ||
const codePathSegments = []; | ||
let currentCodePathSegments = new Set(); | ||
const sourceCode = context.sourceCode; | ||
@@ -132,9 +140,29 @@ const allowEmptyCase = options.allowEmptyCase || false; | ||
return { | ||
onCodePathStart(codePath) { | ||
currentCodePath = codePath; | ||
onCodePathStart() { | ||
codePathSegments.push(currentCodePathSegments); | ||
currentCodePathSegments = new Set(); | ||
}, | ||
onCodePathEnd() { | ||
currentCodePath = currentCodePath.upper; | ||
currentCodePathSegments = codePathSegments.pop(); | ||
}, | ||
onUnreachableCodePathSegmentStart(segment) { | ||
currentCodePathSegments.add(segment); | ||
}, | ||
onUnreachableCodePathSegmentEnd(segment) { | ||
currentCodePathSegments.delete(segment); | ||
}, | ||
onCodePathSegmentStart(segment) { | ||
currentCodePathSegments.add(segment); | ||
}, | ||
onCodePathSegmentEnd(segment) { | ||
currentCodePathSegments.delete(segment); | ||
}, | ||
SwitchCase(node) { | ||
@@ -164,3 +192,3 @@ | ||
*/ | ||
if (currentCodePath.currentSegments.some(isReachable) && | ||
if (isAnySegmentReachable(currentCodePathSegments) && | ||
(node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) && | ||
@@ -167,0 +195,0 @@ node.parent.cases[node.parent.cases.length - 1] !== node) { |
@@ -17,2 +17,7 @@ /** | ||
/** | ||
* @typedef {import('@eslint-community/regexpp').AST.Character} Character | ||
* @typedef {import('@eslint-community/regexpp').AST.CharacterClassElement} CharacterClassElement | ||
*/ | ||
/** | ||
* Iterate character sequences of a given nodes. | ||
@@ -22,6 +27,8 @@ * | ||
* so this function reverts CharacterClassRange syntax and restore the sequence. | ||
* @param {import('@eslint-community/regexpp').AST.CharacterClassElement[]} nodes The node list to iterate character sequences. | ||
* @returns {IterableIterator<number[]>} The list of character sequences. | ||
* @param {CharacterClassElement[]} nodes The node list to iterate character sequences. | ||
* @returns {IterableIterator<Character[]>} The list of character sequences. | ||
*/ | ||
function *iterateCharacterSequence(nodes) { | ||
/** @type {Character[]} */ | ||
let seq = []; | ||
@@ -32,9 +39,9 @@ | ||
case "Character": | ||
seq.push(node.value); | ||
seq.push(node); | ||
break; | ||
case "CharacterClassRange": | ||
seq.push(node.min.value); | ||
seq.push(node.min); | ||
yield seq; | ||
seq = [node.max.value]; | ||
seq = [node.max]; | ||
break; | ||
@@ -61,12 +68,54 @@ | ||
/** | ||
* Checks whether the given character node is a Unicode code point escape or not. | ||
* @param {Character} char the character node to check. | ||
* @returns {boolean} `true` if the character node is a Unicode code point escape. | ||
*/ | ||
function isUnicodeCodePointEscape(char) { | ||
return /^\\u\{[\da-f]+\}$/iu.test(char.raw); | ||
} | ||
/** | ||
* Each function returns `true` if it detects that kind of problem. | ||
* @type {Record<string, (chars: Character[]) => boolean>} | ||
*/ | ||
const hasCharacterSequence = { | ||
surrogatePairWithoutUFlag(chars) { | ||
return chars.some((c, i) => i !== 0 && isSurrogatePair(chars[i - 1], c)); | ||
return chars.some((c, i) => { | ||
if (i === 0) { | ||
return false; | ||
} | ||
const c1 = chars[i - 1]; | ||
return ( | ||
isSurrogatePair(c1.value, c.value) && | ||
!isUnicodeCodePointEscape(c1) && | ||
!isUnicodeCodePointEscape(c) | ||
); | ||
}); | ||
}, | ||
surrogatePair(chars) { | ||
return chars.some((c, i) => { | ||
if (i === 0) { | ||
return false; | ||
} | ||
const c1 = chars[i - 1]; | ||
return ( | ||
isSurrogatePair(c1.value, c.value) && | ||
( | ||
isUnicodeCodePointEscape(c1) || | ||
isUnicodeCodePointEscape(c) | ||
) | ||
); | ||
}); | ||
}, | ||
combiningClass(chars) { | ||
return chars.some((c, i) => ( | ||
i !== 0 && | ||
isCombiningCharacter(c) && | ||
!isCombiningCharacter(chars[i - 1]) | ||
isCombiningCharacter(c.value) && | ||
!isCombiningCharacter(chars[i - 1].value) | ||
)); | ||
@@ -78,4 +127,4 @@ }, | ||
i !== 0 && | ||
isEmojiModifier(c) && | ||
!isEmojiModifier(chars[i - 1]) | ||
isEmojiModifier(c.value) && | ||
!isEmojiModifier(chars[i - 1].value) | ||
)); | ||
@@ -87,4 +136,4 @@ }, | ||
i !== 0 && | ||
isRegionalIndicatorSymbol(c) && | ||
isRegionalIndicatorSymbol(chars[i - 1]) | ||
isRegionalIndicatorSymbol(c.value) && | ||
isRegionalIndicatorSymbol(chars[i - 1].value) | ||
)); | ||
@@ -99,5 +148,5 @@ }, | ||
i !== lastIndex && | ||
c === 0x200d && | ||
chars[i - 1] !== 0x200d && | ||
chars[i + 1] !== 0x200d | ||
c.value === 0x200d && | ||
chars[i - 1].value !== 0x200d && | ||
chars[i + 1].value !== 0x200d | ||
)); | ||
@@ -130,2 +179,3 @@ } | ||
surrogatePairWithoutUFlag: "Unexpected surrogate pair in character class. Use 'u' flag.", | ||
surrogatePair: "Unexpected surrogate pair in character class.", | ||
combiningClass: "Unexpected combined character in character class.", | ||
@@ -132,0 +182,0 @@ emojiModifier: "Unexpected modified Emoji in character class.", |
/** | ||
* @fileoverview A rule to disallow calls to the Object constructor | ||
* @author Matt DuVall <http://www.mattduvall.com/> | ||
* @deprecated in ESLint v8.50.0 | ||
*/ | ||
@@ -29,2 +30,8 @@ | ||
deprecated: true, | ||
replacedBy: [ | ||
"no-object-constructor" | ||
], | ||
schema: [], | ||
@@ -31,0 +38,0 @@ |
@@ -13,2 +13,3 @@ /** | ||
const { findVariable } = require("@eslint-community/eslint-utils"); | ||
const astUtils = require("./utils/ast-utils"); | ||
@@ -63,2 +64,74 @@ //------------------------------------------------------------------------------ | ||
/** | ||
* Checks if the given node is a void expression. | ||
* @param {ASTNode} node The node to check. | ||
* @returns {boolean} - `true` if the node is a void expression | ||
*/ | ||
function expressionIsVoid(node) { | ||
return node.type === "UnaryExpression" && node.operator === "void"; | ||
} | ||
/** | ||
* Fixes the linting error by prepending "void " to the given node | ||
* @param {Object} sourceCode context given by context.sourceCode | ||
* @param {ASTNode} node The node to fix. | ||
* @param {Object} fixer The fixer object provided by ESLint. | ||
* @returns {Array<Object>} - An array of fix objects to apply to the node. | ||
*/ | ||
function voidPrependFixer(sourceCode, node, fixer) { | ||
const requiresParens = | ||
// prepending `void ` will fail if the node has a lower precedence than void | ||
astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) && | ||
// check if there are parentheses around the node to avoid redundant parentheses | ||
!astUtils.isParenthesised(sourceCode, node); | ||
// avoid parentheses issues | ||
const returnOrArrowToken = sourceCode.getTokenBefore( | ||
node, | ||
node.parent.type === "ArrowFunctionExpression" | ||
? astUtils.isArrowToken | ||
// isReturnToken | ||
: token => token.type === "Keyword" && token.value === "return" | ||
); | ||
const firstToken = sourceCode.getTokenAfter(returnOrArrowToken); | ||
const prependSpace = | ||
// is return token, as => allows void to be adjacent | ||
returnOrArrowToken.value === "return" && | ||
// If two tokens (return and "(") are adjacent | ||
returnOrArrowToken.range[1] === firstToken.range[0]; | ||
return [ | ||
fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`), | ||
fixer.insertTextAfter(node, requiresParens ? ")" : "") | ||
]; | ||
} | ||
/** | ||
* Fixes the linting error by `wrapping {}` around the given node's body. | ||
* @param {Object} sourceCode context given by context.sourceCode | ||
* @param {ASTNode} node The node to fix. | ||
* @param {Object} fixer The fixer object provided by ESLint. | ||
* @returns {Array<Object>} - An array of fix objects to apply to the node. | ||
*/ | ||
function curlyWrapFixer(sourceCode, node, fixer) { | ||
// https://github.com/eslint/eslint/pull/17282#issuecomment-1592795923 | ||
const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken); | ||
const firstToken = sourceCode.getTokenAfter(arrowToken); | ||
const lastToken = sourceCode.getLastToken(node); | ||
return [ | ||
fixer.insertTextBefore(firstToken, "{"), | ||
fixer.insertTextAfter(lastToken, "}") | ||
]; | ||
} | ||
//------------------------------------------------------------------------------ | ||
@@ -79,6 +152,23 @@ // Rule Definition | ||
schema: [], | ||
hasSuggestions: true, | ||
schema: [{ | ||
type: "object", | ||
properties: { | ||
allowVoid: { | ||
type: "boolean", | ||
default: false | ||
} | ||
}, | ||
additionalProperties: false | ||
}], | ||
messages: { | ||
returnsValue: "Return values from promise executor functions cannot be read." | ||
returnsValue: "Return values from promise executor functions cannot be read.", | ||
// arrow and function suggestions | ||
prependVoid: "Prepend `void` to the expression.", | ||
// only arrow suggestions | ||
wrapBraces: "Wrap the expression in `{}`." | ||
} | ||
@@ -91,12 +181,6 @@ }, | ||
const sourceCode = context.sourceCode; | ||
const { | ||
allowVoid = false | ||
} = context.options[0] || {}; | ||
/** | ||
* Reports the given node. | ||
* @param {ASTNode} node Node to report. | ||
* @returns {void} | ||
*/ | ||
function report(node) { | ||
context.report({ node, messageId: "returnsValue" }); | ||
} | ||
return { | ||
@@ -107,7 +191,39 @@ | ||
upper: funcInfo, | ||
shouldCheck: functionTypesToCheck.has(node.type) && isPromiseExecutor(node, sourceCode.getScope(node)) | ||
shouldCheck: | ||
functionTypesToCheck.has(node.type) && | ||
isPromiseExecutor(node, sourceCode.getScope(node)) | ||
}; | ||
if (funcInfo.shouldCheck && node.type === "ArrowFunctionExpression" && node.expression) { | ||
report(node.body); | ||
if (// Is a Promise executor | ||
funcInfo.shouldCheck && | ||
node.type === "ArrowFunctionExpression" && | ||
node.expression && | ||
// Except void | ||
!(allowVoid && expressionIsVoid(node.body)) | ||
) { | ||
const suggest = []; | ||
// prevent useless refactors | ||
if (allowVoid) { | ||
suggest.push({ | ||
messageId: "prependVoid", | ||
fix(fixer) { | ||
return voidPrependFixer(sourceCode, node.body, fixer); | ||
} | ||
}); | ||
} | ||
suggest.push({ | ||
messageId: "wrapBraces", | ||
fix(fixer) { | ||
return curlyWrapFixer(sourceCode, node, fixer); | ||
} | ||
}); | ||
context.report({ | ||
node: node.body, | ||
messageId: "returnsValue", | ||
suggest | ||
}); | ||
} | ||
@@ -121,5 +237,27 @@ }, | ||
ReturnStatement(node) { | ||
if (funcInfo.shouldCheck && node.argument) { | ||
report(node); | ||
if (!(funcInfo.shouldCheck && node.argument)) { | ||
return; | ||
} | ||
// node is `return <expression>` | ||
if (!allowVoid) { | ||
context.report({ node, messageId: "returnsValue" }); | ||
return; | ||
} | ||
if (expressionIsVoid(node.argument)) { | ||
return; | ||
} | ||
// allowVoid && !expressionIsVoid | ||
context.report({ | ||
node, | ||
messageId: "returnsValue", | ||
suggest: [{ | ||
messageId: "prependVoid", | ||
fix(fixer) { | ||
return voidPrependFixer(sourceCode, node.argument, fixer); | ||
} | ||
}] | ||
}); | ||
} | ||
@@ -126,0 +264,0 @@ }; |
@@ -94,2 +94,17 @@ /** | ||
/** | ||
* Determines if every segment in a set has been called. | ||
* @param {Set<CodePathSegment>} segments The segments to search. | ||
* @returns {boolean} True if every segment has been called; false otherwise. | ||
*/ | ||
function isEverySegmentCalled(segments) { | ||
for (const segment of segments) { | ||
if (!isCalled(segment)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
/** | ||
* Checks whether or not this is before `super()` is called. | ||
@@ -101,3 +116,3 @@ * @returns {boolean} `true` if this is before `super()` is called. | ||
isInConstructorOfDerivedClass() && | ||
!funcInfo.codePath.currentSegments.every(isCalled) | ||
!isEverySegmentCalled(funcInfo.currentSegments) | ||
); | ||
@@ -113,7 +128,5 @@ } | ||
function setInvalid(node) { | ||
const segments = funcInfo.codePath.currentSegments; | ||
const segments = funcInfo.currentSegments; | ||
for (let i = 0; i < segments.length; ++i) { | ||
const segment = segments[i]; | ||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
@@ -130,7 +143,5 @@ segInfoMap[segment.id].invalidNodes.push(node); | ||
function setSuperCalled() { | ||
const segments = funcInfo.codePath.currentSegments; | ||
const segments = funcInfo.currentSegments; | ||
for (let i = 0; i < segments.length; ++i) { | ||
const segment = segments[i]; | ||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
@@ -163,3 +174,4 @@ segInfoMap[segment.id].superCalled = true; | ||
), | ||
codePath | ||
codePath, | ||
currentSegments: new Set() | ||
}; | ||
@@ -171,3 +183,4 @@ } else { | ||
hasExtends: false, | ||
codePath | ||
codePath, | ||
currentSegments: new Set() | ||
}; | ||
@@ -220,2 +233,4 @@ } | ||
onCodePathSegmentStart(segment) { | ||
funcInfo.currentSegments.add(segment); | ||
if (!isInConstructorOfDerivedClass()) { | ||
@@ -235,2 +250,14 @@ return; | ||
onUnreachableCodePathSegmentStart(segment) { | ||
funcInfo.currentSegments.add(segment); | ||
}, | ||
onUnreachableCodePathSegmentEnd(segment) { | ||
funcInfo.currentSegments.delete(segment); | ||
}, | ||
onCodePathSegmentEnd(segment) { | ||
funcInfo.currentSegments.delete(segment); | ||
}, | ||
/** | ||
@@ -237,0 +264,0 @@ * Update information of the code path segment when a code path was |
@@ -15,2 +15,18 @@ /** | ||
/** | ||
* Checks all segments in a set and returns true if any are reachable. | ||
* @param {Set<CodePathSegment>} segments The segments to check. | ||
* @returns {boolean} True if any segment is reachable; false otherwise. | ||
*/ | ||
function isAnySegmentReachable(segments) { | ||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
/** | ||
* Determines whether the given node is the first node in the code path to which a loop statement | ||
@@ -94,25 +110,32 @@ * 'loops' for the next iteration. | ||
let currentCodePath = null; | ||
const codePathSegments = []; | ||
let currentCodePathSegments = new Set(); | ||
return { | ||
onCodePathStart(codePath) { | ||
currentCodePath = codePath; | ||
onCodePathStart() { | ||
codePathSegments.push(currentCodePathSegments); | ||
currentCodePathSegments = new Set(); | ||
}, | ||
onCodePathEnd() { | ||
currentCodePath = currentCodePath.upper; | ||
currentCodePathSegments = codePathSegments.pop(); | ||
}, | ||
[loopSelector](node) { | ||
onUnreachableCodePathSegmentStart(segment) { | ||
currentCodePathSegments.add(segment); | ||
}, | ||
/** | ||
* Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise. | ||
* For unreachable segments, the code path analysis does not raise events required for this implementation. | ||
*/ | ||
if (currentCodePath.currentSegments.some(segment => segment.reachable)) { | ||
loopsToReport.add(node); | ||
} | ||
onUnreachableCodePathSegmentEnd(segment) { | ||
currentCodePathSegments.delete(segment); | ||
}, | ||
onCodePathSegmentEnd(segment) { | ||
currentCodePathSegments.delete(segment); | ||
}, | ||
onCodePathSegmentStart(segment, node) { | ||
currentCodePathSegments.add(segment); | ||
if (isLoopingTarget(node)) { | ||
@@ -145,2 +168,14 @@ const loop = node.parent; | ||
[loopSelector](node) { | ||
/** | ||
* Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise. | ||
* For unreachable segments, the code path analysis does not raise events required for this implementation. | ||
*/ | ||
if (isAnySegmentReachable(currentCodePathSegments)) { | ||
loopsToReport.add(node); | ||
} | ||
}, | ||
"Program:exit"() { | ||
@@ -147,0 +182,0 @@ loopsToReport.forEach( |
@@ -27,8 +27,15 @@ /** | ||
/** | ||
* Checks whether or not a given code path segment is unreachable. | ||
* @param {CodePathSegment} segment A CodePathSegment to check. | ||
* @returns {boolean} `true` if the segment is unreachable. | ||
* Checks all segments in a set and returns true if all are unreachable. | ||
* @param {Set<CodePathSegment>} segments The segments to check. | ||
* @returns {boolean} True if all segments are unreachable; false otherwise. | ||
*/ | ||
function isUnreachable(segment) { | ||
return !segment.reachable; | ||
function areAllSegmentsUnreachable(segments) { | ||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
@@ -128,3 +135,2 @@ | ||
create(context) { | ||
let currentCodePath = null; | ||
@@ -137,2 +143,8 @@ /** @type {ConstructorInfo | null} */ | ||
/** @type {Array<Set<CodePathSegment>>} */ | ||
const codePathSegments = []; | ||
/** @type {Set<CodePathSegment>} */ | ||
let currentCodePathSegments = new Set(); | ||
/** | ||
@@ -146,3 +158,3 @@ * Reports a given node if it's unreachable. | ||
if (node && (node.type === "PropertyDefinition" || currentCodePath.currentSegments.every(isUnreachable))) { | ||
if (node && (node.type === "PropertyDefinition" || areAllSegmentsUnreachable(currentCodePathSegments))) { | ||
@@ -188,10 +200,27 @@ // Store this statement to distinguish consecutive statements. | ||
// Manages the current code path. | ||
onCodePathStart(codePath) { | ||
currentCodePath = codePath; | ||
onCodePathStart() { | ||
codePathSegments.push(currentCodePathSegments); | ||
currentCodePathSegments = new Set(); | ||
}, | ||
onCodePathEnd() { | ||
currentCodePath = currentCodePath.upper; | ||
currentCodePathSegments = codePathSegments.pop(); | ||
}, | ||
onUnreachableCodePathSegmentStart(segment) { | ||
currentCodePathSegments.add(segment); | ||
}, | ||
onUnreachableCodePathSegmentEnd(segment) { | ||
currentCodePathSegments.delete(segment); | ||
}, | ||
onCodePathSegmentEnd(segment) { | ||
currentCodePathSegments.delete(segment); | ||
}, | ||
onCodePathSegmentStart(segment) { | ||
currentCodePathSegments.add(segment); | ||
}, | ||
// Registers for all statement nodes (excludes FunctionDeclaration). | ||
@@ -198,0 +227,0 @@ BlockStatement: reportIfUnreachable, |
@@ -60,2 +60,18 @@ /** | ||
/** | ||
* Checks all segments in a set and returns true if any are reachable. | ||
* @param {Set<CodePathSegment>} segments The segments to check. | ||
* @returns {boolean} True if any segment is reachable; false otherwise. | ||
*/ | ||
function isAnySegmentReachable(segments) { | ||
for (const segment of segments) { | ||
if (segment.reachable) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
//------------------------------------------------------------------------------ | ||
@@ -209,3 +225,2 @@ // Rule Definition | ||
scopeInfo | ||
.codePath | ||
.currentSegments | ||
@@ -227,3 +242,4 @@ .forEach(segment => markReturnStatementsOnSegmentAsUsed(segment, new Set())); | ||
traversedTryBlockStatements: [], | ||
codePath | ||
codePath, | ||
currentSegments: new Set() | ||
}; | ||
@@ -265,2 +281,5 @@ }, | ||
onCodePathSegmentStart(segment) { | ||
scopeInfo.currentSegments.add(segment); | ||
const info = { | ||
@@ -275,2 +294,14 @@ uselessReturns: getUselessReturns([], segment.allPrevSegments), | ||
onUnreachableCodePathSegmentStart(segment) { | ||
scopeInfo.currentSegments.add(segment); | ||
}, | ||
onUnreachableCodePathSegmentEnd(segment) { | ||
scopeInfo.currentSegments.delete(segment); | ||
}, | ||
onCodePathSegmentEnd(segment) { | ||
scopeInfo.currentSegments.delete(segment); | ||
}, | ||
// Adds ReturnStatement node to check whether it's useless or not. | ||
@@ -287,3 +318,3 @@ ReturnStatement(node) { | ||
// Ignore `return` statements in unreachable places (https://github.com/eslint/eslint/issues/11647). | ||
!scopeInfo.codePath.currentSegments.some(s => s.reachable) | ||
!isAnySegmentReachable(scopeInfo.currentSegments) | ||
) { | ||
@@ -293,3 +324,3 @@ return; | ||
for (const segment of scopeInfo.codePath.currentSegments) { | ||
for (const segment of scopeInfo.currentSegments) { | ||
const info = segmentInfoMap.get(segment); | ||
@@ -296,0 +327,0 @@ |
@@ -216,3 +216,4 @@ /** | ||
codePath, | ||
referenceMap: shouldVerify ? createReferenceMap(scope) : null | ||
referenceMap: shouldVerify ? createReferenceMap(scope) : null, | ||
currentSegments: new Set() | ||
}; | ||
@@ -227,7 +228,21 @@ }, | ||
segmentInfo.initialize(segment); | ||
stack.currentSegments.add(segment); | ||
}, | ||
onUnreachableCodePathSegmentStart(segment) { | ||
stack.currentSegments.add(segment); | ||
}, | ||
onUnreachableCodePathSegmentEnd(segment) { | ||
stack.currentSegments.delete(segment); | ||
}, | ||
onCodePathSegmentEnd(segment) { | ||
stack.currentSegments.delete(segment); | ||
}, | ||
// Handle references to prepare verification. | ||
Identifier(node) { | ||
const { codePath, referenceMap } = stack; | ||
const { referenceMap } = stack; | ||
const reference = referenceMap && referenceMap.get(node); | ||
@@ -245,3 +260,3 @@ | ||
if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) { | ||
segmentInfo.markAsRead(codePath.currentSegments, variable); | ||
segmentInfo.markAsRead(stack.currentSegments, variable); | ||
} | ||
@@ -273,6 +288,5 @@ | ||
":expression:exit"(node) { | ||
const { codePath, referenceMap } = stack; | ||
// referenceMap exists if this is in a resumable function scope. | ||
if (!referenceMap) { | ||
if (!stack.referenceMap) { | ||
return; | ||
@@ -283,3 +297,3 @@ } | ||
if (node.type === "AwaitExpression" || node.type === "YieldExpression") { | ||
segmentInfo.makeOutdated(codePath.currentSegments); | ||
segmentInfo.makeOutdated(stack.currentSegments); | ||
} | ||
@@ -296,3 +310,3 @@ | ||
if (segmentInfo.isOutdated(codePath.currentSegments, variable)) { | ||
if (segmentInfo.isOutdated(stack.currentSegments, variable)) { | ||
if (node.parent.left === reference.identifier) { | ||
@@ -299,0 +313,0 @@ context.report({ |
@@ -15,4 +15,12 @@ /** | ||
astUtils = require("../shared/ast-utils"), | ||
Traverser = require("../shared/traverser"); | ||
Traverser = require("../shared/traverser"), | ||
globals = require("../../conf/globals"), | ||
{ | ||
directivesPattern | ||
} = require("../shared/directives"), | ||
/* eslint-disable-next-line n/no-restricted-require -- Too messy to figure out right now. */ | ||
ConfigCommentParser = require("../linter/config-comment-parser"), | ||
eslintScope = require("eslint-scope"); | ||
//------------------------------------------------------------------------------ | ||
@@ -28,2 +36,4 @@ // Type Definitions | ||
const commentParser = new ConfigCommentParser(); | ||
/** | ||
@@ -55,2 +65,25 @@ * Validates that the given AST has the required information. | ||
/** | ||
* Retrieves globals for the given ecmaVersion. | ||
* @param {number} ecmaVersion The version to retrieve globals for. | ||
* @returns {Object} The globals for the given ecmaVersion. | ||
*/ | ||
function getGlobalsForEcmaVersion(ecmaVersion) { | ||
switch (ecmaVersion) { | ||
case 3: | ||
return globals.es3; | ||
case 5: | ||
return globals.es5; | ||
default: | ||
if (ecmaVersion < 2015) { | ||
return globals[`es${ecmaVersion + 2009}`]; | ||
} | ||
return globals[`es${ecmaVersion}`]; | ||
} | ||
} | ||
/** | ||
* Check to see if its a ES6 export declaration. | ||
@@ -90,2 +123,32 @@ * @param {ASTNode} astNode An AST node. | ||
/** | ||
* Normalizes a value for a global in a config | ||
* @param {(boolean|string|null)} configuredValue The value given for a global in configuration or in | ||
* a global directive comment | ||
* @returns {("readable"|"writeable"|"off")} The value normalized as a string | ||
* @throws Error if global value is invalid | ||
*/ | ||
function normalizeConfigGlobal(configuredValue) { | ||
switch (configuredValue) { | ||
case "off": | ||
return "off"; | ||
case true: | ||
case "true": | ||
case "writeable": | ||
case "writable": | ||
return "writable"; | ||
case null: | ||
case false: | ||
case "false": | ||
case "readable": | ||
case "readonly": | ||
return "readonly"; | ||
default: | ||
throw new Error(`'${configuredValue}' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')`); | ||
} | ||
} | ||
/** | ||
* Determines if two nodes or tokens overlap. | ||
@@ -152,2 +215,112 @@ * @param {ASTNode|Token} first The first node or token to check. | ||
//----------------------------------------------------------------------------- | ||
// Directive Comments | ||
//----------------------------------------------------------------------------- | ||
/** | ||
* Extract the directive and the justification from a given directive comment and trim them. | ||
* @param {string} value The comment text to extract. | ||
* @returns {{directivePart: string, justificationPart: string}} The extracted directive and justification. | ||
*/ | ||
function extractDirectiveComment(value) { | ||
const match = /\s-{2,}\s/u.exec(value); | ||
if (!match) { | ||
return { directivePart: value.trim(), justificationPart: "" }; | ||
} | ||
const directive = value.slice(0, match.index).trim(); | ||
const justification = value.slice(match.index + match[0].length).trim(); | ||
return { directivePart: directive, justificationPart: justification }; | ||
} | ||
/** | ||
* Ensures that variables representing built-in properties of the Global Object, | ||
* and any globals declared by special block comments, are present in the global | ||
* scope. | ||
* @param {Scope} globalScope The global scope. | ||
* @param {Object|undefined} configGlobals The globals declared in configuration | ||
* @param {Object|undefined} inlineGlobals The globals declared in the source code | ||
* @returns {void} | ||
*/ | ||
function addDeclaredGlobals(globalScope, configGlobals = {}, inlineGlobals = {}) { | ||
// Define configured global variables. | ||
for (const id of new Set([...Object.keys(configGlobals), ...Object.keys(inlineGlobals)])) { | ||
/* | ||
* `normalizeConfigGlobal` will throw an error if a configured global value is invalid. However, these errors would | ||
* typically be caught when validating a config anyway (validity for inline global comments is checked separately). | ||
*/ | ||
const configValue = configGlobals[id] === void 0 ? void 0 : normalizeConfigGlobal(configGlobals[id]); | ||
const commentValue = inlineGlobals[id] && inlineGlobals[id].value; | ||
const value = commentValue || configValue; | ||
const sourceComments = inlineGlobals[id] && inlineGlobals[id].comments; | ||
if (value === "off") { | ||
continue; | ||
} | ||
let variable = globalScope.set.get(id); | ||
if (!variable) { | ||
variable = new eslintScope.Variable(id, globalScope); | ||
globalScope.variables.push(variable); | ||
globalScope.set.set(id, variable); | ||
} | ||
variable.eslintImplicitGlobalSetting = configValue; | ||
variable.eslintExplicitGlobal = sourceComments !== void 0; | ||
variable.eslintExplicitGlobalComments = sourceComments; | ||
variable.writeable = (value === "writable"); | ||
} | ||
/* | ||
* "through" contains all references which definitions cannot be found. | ||
* Since we augment the global scope using configuration, we need to update | ||
* references and remove the ones that were added by configuration. | ||
*/ | ||
globalScope.through = globalScope.through.filter(reference => { | ||
const name = reference.identifier.name; | ||
const variable = globalScope.set.get(name); | ||
if (variable) { | ||
/* | ||
* Links the variable and the reference. | ||
* And this reference is removed from `Scope#through`. | ||
*/ | ||
reference.resolved = variable; | ||
variable.references.push(reference); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
} | ||
/** | ||
* Sets the given variable names as exported so they won't be triggered by | ||
* the `no-unused-vars` rule. | ||
* @param {eslint.Scope} globalScope The global scope to define exports in. | ||
* @param {Record<string,string>} variables An object whose keys are the variable | ||
* names to export. | ||
* @returns {void} | ||
*/ | ||
function markExportedVariables(globalScope, variables) { | ||
Object.keys(variables).forEach(name => { | ||
const variable = globalScope.set.get(name); | ||
if (variable) { | ||
variable.eslintUsed = true; | ||
variable.eslintExported = true; | ||
} | ||
}); | ||
} | ||
//------------------------------------------------------------------------------ | ||
@@ -195,3 +368,5 @@ // Public Interface | ||
this[caches] = new Map([ | ||
["scopes", new WeakMap()] | ||
["scopes", new WeakMap()], | ||
["vars", new Map()], | ||
["configNodes", void 0] | ||
]); | ||
@@ -275,3 +450,3 @@ | ||
// don't allow modification of this object | ||
// don't allow further modification of this object | ||
Object.freeze(this); | ||
@@ -734,4 +909,176 @@ Object.freeze(this.lines); | ||
/** | ||
* Returns an array of all inline configuration nodes found in the | ||
* source code. | ||
* @returns {Array<Token>} An array of all inline configuration nodes. | ||
*/ | ||
getInlineConfigNodes() { | ||
// check the cache first | ||
let configNodes = this[caches].get("configNodes"); | ||
if (configNodes) { | ||
return configNodes; | ||
} | ||
// calculate fresh config nodes | ||
configNodes = this.ast.comments.filter(comment => { | ||
// shebang comments are never directives | ||
if (comment.type === "Shebang") { | ||
return false; | ||
} | ||
const { directivePart } = extractDirectiveComment(comment.value); | ||
const directiveMatch = directivesPattern.exec(directivePart); | ||
if (!directiveMatch) { | ||
return false; | ||
} | ||
// only certain comment types are supported as line comments | ||
return comment.type !== "Line" || !!/^eslint-disable-(next-)?line$/u.test(directiveMatch[1]); | ||
}); | ||
this[caches].set("configNodes", configNodes); | ||
return configNodes; | ||
} | ||
/** | ||
* Applies language options sent in from the core. | ||
* @param {Object} languageOptions The language options for this run. | ||
* @returns {void} | ||
*/ | ||
applyLanguageOptions(languageOptions) { | ||
/* | ||
* Add configured globals and language globals | ||
* | ||
* Using Object.assign instead of object spread for performance reasons | ||
* https://github.com/eslint/eslint/issues/16302 | ||
*/ | ||
const configGlobals = Object.assign( | ||
{}, | ||
getGlobalsForEcmaVersion(languageOptions.ecmaVersion), | ||
languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0, | ||
languageOptions.globals | ||
); | ||
const varsCache = this[caches].get("vars"); | ||
varsCache.set("configGlobals", configGlobals); | ||
} | ||
/** | ||
* Applies configuration found inside of the source code. This method is only | ||
* called when ESLint is running with inline configuration allowed. | ||
* @returns {{problems:Array<Problem>,configs:{config:FlatConfigArray,node:ASTNode}}} Information | ||
* that ESLint needs to further process the inline configuration. | ||
*/ | ||
applyInlineConfig() { | ||
const problems = []; | ||
const configs = []; | ||
const exportedVariables = {}; | ||
const inlineGlobals = Object.create(null); | ||
this.getInlineConfigNodes().forEach(comment => { | ||
const { directivePart } = extractDirectiveComment(comment.value); | ||
const match = directivesPattern.exec(directivePart); | ||
const directiveText = match[1]; | ||
const directiveValue = directivePart.slice(match.index + directiveText.length); | ||
switch (directiveText) { | ||
case "exported": | ||
Object.assign(exportedVariables, commentParser.parseStringConfig(directiveValue, comment)); | ||
break; | ||
case "globals": | ||
case "global": | ||
for (const [id, { value }] of Object.entries(commentParser.parseStringConfig(directiveValue, comment))) { | ||
let normalizedValue; | ||
try { | ||
normalizedValue = normalizeConfigGlobal(value); | ||
} catch (err) { | ||
problems.push({ | ||
ruleId: null, | ||
loc: comment.loc, | ||
message: err.message | ||
}); | ||
continue; | ||
} | ||
if (inlineGlobals[id]) { | ||
inlineGlobals[id].comments.push(comment); | ||
inlineGlobals[id].value = normalizedValue; | ||
} else { | ||
inlineGlobals[id] = { | ||
comments: [comment], | ||
value: normalizedValue | ||
}; | ||
} | ||
} | ||
break; | ||
case "eslint": { | ||
const parseResult = commentParser.parseJsonConfig(directiveValue, comment.loc); | ||
if (parseResult.success) { | ||
configs.push({ | ||
config: { | ||
rules: parseResult.config | ||
}, | ||
node: comment | ||
}); | ||
} else { | ||
problems.push(parseResult.error); | ||
} | ||
break; | ||
} | ||
// no default | ||
} | ||
}); | ||
// save all the new variables for later | ||
const varsCache = this[caches].get("vars"); | ||
varsCache.set("inlineGlobals", inlineGlobals); | ||
varsCache.set("exportedVariables", exportedVariables); | ||
return { | ||
configs, | ||
problems | ||
}; | ||
} | ||
/** | ||
* Called by ESLint core to indicate that it has finished providing | ||
* information. We now add in all the missing variables and ensure that | ||
* state-changing methods cannot be called by rules. | ||
* @returns {void} | ||
*/ | ||
finalize() { | ||
// Step 1: ensure that all of the necessary variables are up to date | ||
const varsCache = this[caches].get("vars"); | ||
const globalScope = this.scopeManager.scopes[0]; | ||
const configGlobals = varsCache.get("configGlobals"); | ||
const inlineGlobals = varsCache.get("inlineGlobals"); | ||
const exportedVariables = varsCache.get("exportedVariables"); | ||
addDeclaredGlobals(globalScope, configGlobals, inlineGlobals); | ||
if (exportedVariables) { | ||
markExportedVariables(globalScope, exportedVariables); | ||
} | ||
} | ||
} | ||
module.exports = SourceCode; |
@@ -22,3 +22,3 @@ "use strict"; | ||
Please see the following page for more information: | ||
https://eslint.org/docs/latest/use/configure/migration-guide#predefined-configs | ||
https://eslint.org/docs/latest/use/configure/migration-guide#predefined-and-shareable-configs | ||
`, | ||
@@ -25,0 +25,0 @@ |
{ | ||
"name": "eslint", | ||
"version": "8.47.0", | ||
"version": "8.50.0", | ||
"author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>", | ||
@@ -66,4 +66,4 @@ "description": "An AST-based pattern checker for JavaScript.", | ||
"@eslint/eslintrc": "^2.1.2", | ||
"@eslint/js": "^8.47.0", | ||
"@humanwhocodes/config-array": "^0.11.10", | ||
"@eslint/js": "8.50.0", | ||
"@humanwhocodes/config-array": "^0.11.11", | ||
"@humanwhocodes/module-importer": "^1.0.1", | ||
@@ -105,2 +105,7 @@ "@nodelib/fs.walk": "^1.2.8", | ||
"@babel/preset-env": "^7.4.3", | ||
"@wdio/browser-runner": "^8.14.6", | ||
"@wdio/cli": "^8.14.6", | ||
"@wdio/concise-reporter": "^8.14.0", | ||
"@wdio/globals": "^8.14.6", | ||
"@wdio/mocha-framework": "^8.14.0", | ||
"babel-loader": "^8.0.5", | ||
@@ -129,7 +134,2 @@ "c8": "^7.12.0", | ||
"gray-matter": "^4.0.3", | ||
"karma": "^6.1.1", | ||
"karma-chrome-launcher": "^3.1.0", | ||
"karma-mocha": "^2.0.1", | ||
"karma-mocha-reporter": "^2.2.5", | ||
"karma-webpack": "^5.0.0", | ||
"lint-staged": "^11.0.0", | ||
@@ -154,8 +154,10 @@ "load-perf": "^0.2.0", | ||
"proxyquire": "^2.0.1", | ||
"puppeteer": "^13.7.0", | ||
"recast": "^0.20.4", | ||
"regenerator-runtime": "^0.13.2", | ||
"rollup-plugin-node-polyfills": "^0.2.1", | ||
"semver": "^7.5.3", | ||
"shelljs": "^0.8.2", | ||
"sinon": "^11.0.0", | ||
"vite-plugin-commonjs": "^0.8.2", | ||
"webdriverio": "^8.14.6", | ||
"webpack": "^5.23.0", | ||
@@ -162,0 +164,0 @@ "webpack-cli": "^4.5.0", |
@@ -291,4 +291,4 @@ [![npm version](https://img.shields.io/npm/v/eslint.svg)](https://www.npmjs.com/package/eslint) | ||
<p><a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3> | ||
<p><a href="https://sentry.io"><img src="https://avatars.githubusercontent.com/u/1396951?v=4" alt="Sentry" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a></p><h3>Bronze Sponsors</h3> | ||
<p><a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://nx.dev"><img src="https://images.opencollective.com/nx/0efbe42/logo.png" alt="Nx (by Nrwl)" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8: free icons, photos, illustrations, and music" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://github.com/about"><img src="https://avatars.githubusercontent.com/u/9919?v=4" alt="GitHub" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a> <a href="https://quickbookstoolhub.com"><img src="https://avatars.githubusercontent.com/u/95090305?u=e5bc398ef775c9ed19f955c675cdc1fb6abf01df&v=4" alt="QuickBooks Tool hub" height="32"></a></p> | ||
<p><a href="https://sentry.io"><img src="https://avatars.githubusercontent.com/u/1396951?v=4" alt="Sentry" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://opensource.siemens.com"><img src="https://avatars.githubusercontent.com/u/624020?v=4" alt="Siemens" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a></p><h3>Bronze Sponsors</h3> | ||
<p><a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://nx.dev"><img src="https://images.opencollective.com/nx/0efbe42/logo.png" alt="Nx (by Nrwl)" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a> <a href="https://quickbookstoolhub.com"><img src="https://avatars.githubusercontent.com/u/95090305?u=e5bc398ef775c9ed19f955c675cdc1fb6abf01df&v=4" alt="QuickBooks Tool hub" height="32"></a></p> | ||
<!--sponsorsend--> | ||
@@ -295,0 +295,0 @@ |
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
2957015
407
69826
60
+ Added@eslint-community/regexpp@4.11.0(transitive)
+ Added@eslint/js@8.50.0(transitive)
- Removed@eslint-community/regexpp@4.11.1(transitive)
- Removed@eslint/js@8.57.0(transitive)
Updated@eslint/js@8.50.0