Comparing version 8.47.0 to 8.51.0
@@ -96,2 +96,10 @@ #!/usr/bin/env node | ||
/** | ||
* Tracks error messages that are shown to the user so we only ever show the | ||
* same message once. | ||
* @type {Set<string>} | ||
*/ | ||
const displayedErrors = new Set(); | ||
/** | ||
* Catch and report unexpected error. | ||
@@ -105,5 +113,3 @@ * @param {any} error The thrown error object. | ||
const { version } = require("../package.json"); | ||
const message = getErrorMessage(error); | ||
console.error(` | ||
const message = ` | ||
Oops! Something went wrong! :( | ||
@@ -113,3 +119,8 @@ | ||
${message}`); | ||
${getErrorMessage(error)}`; | ||
if (!displayedErrors.has(message)) { | ||
console.error(message); | ||
displayedErrors.add(message); | ||
} | ||
} | ||
@@ -116,0 +127,0 @@ |
@@ -94,3 +94,4 @@ /** | ||
rule, | ||
rulesdir | ||
rulesdir, | ||
warnIgnored | ||
}, configType) { | ||
@@ -186,2 +187,3 @@ | ||
options.ignorePatterns = ignorePattern; | ||
options.warnIgnored = warnIgnored; | ||
} else { | ||
@@ -390,3 +392,5 @@ options.resolvePluginsRelativeTo = resolvePluginsRelativeTo; | ||
filePath: options.stdinFilename, | ||
warnIgnored: true | ||
// flatConfig respects CLI flag and constructor warnIgnored, eslintrc forces true for backwards compatibility | ||
warnIgnored: usingFlatConfig ? void 0 : true | ||
}); | ||
@@ -393,0 +397,0 @@ } else { |
@@ -182,5 +182,3 @@ /** | ||
function assertIsRuleSeverity(ruleId, value) { | ||
const severity = typeof value === "string" | ||
? ruleSeverities.get(value.toLowerCase()) | ||
: ruleSeverities.get(value); | ||
const severity = ruleSeverities.get(value); | ||
@@ -511,3 +509,3 @@ if (typeof severity === "undefined") { | ||
exports.flatConfigSchema = { | ||
const flatConfigSchema = { | ||
@@ -538,1 +536,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, |
@@ -597,5 +597,5 @@ /** | ||
if (isInNodeModules) { | ||
message = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to override."; | ||
message = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."; | ||
} else { | ||
message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."; | ||
message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."; | ||
} | ||
@@ -680,2 +680,3 @@ | ||
reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instead because we cannot configure the `error` severity with that. | ||
warnIgnored = true, | ||
...unknownOptions | ||
@@ -786,2 +787,5 @@ }) { | ||
} | ||
if (typeof warnIgnored !== "boolean") { | ||
errors.push("'warnIgnored' must be a boolean."); | ||
} | ||
if (errors.length > 0) { | ||
@@ -808,3 +812,4 @@ throw new ESLintInvalidOptionsError(errors); | ||
ignorePatterns, | ||
reportUnusedDisableDirectives | ||
reportUnusedDisableDirectives, | ||
warnIgnored | ||
}; | ||
@@ -811,0 +816,0 @@ } |
@@ -87,2 +87,3 @@ /** | ||
* @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives. | ||
* @property {boolean} warnIgnored Show warnings when the file list includes ignored files | ||
*/ | ||
@@ -753,3 +754,4 @@ | ||
globInputPaths, | ||
errorOnUnmatchedPattern | ||
errorOnUnmatchedPattern, | ||
warnIgnored | ||
} = eslintOptions; | ||
@@ -800,3 +802,7 @@ const startTime = Date.now(); | ||
if (ignored) { | ||
return createIgnoreResult(filePath, cwd); | ||
if (warnIgnored) { | ||
return createIgnoreResult(filePath, cwd); | ||
} | ||
return void 0; | ||
} | ||
@@ -914,3 +920,3 @@ | ||
filePath, | ||
warnIgnored = false, | ||
warnIgnored, | ||
...unknownOptions | ||
@@ -929,3 +935,3 @@ } = options || {}; | ||
if (typeof warnIgnored !== "boolean") { | ||
if (typeof warnIgnored !== "boolean" && typeof warnIgnored !== "undefined") { | ||
throw new Error("'options.warnIgnored' must be a boolean or undefined"); | ||
@@ -945,3 +951,4 @@ } | ||
fix, | ||
reportUnusedDisableDirectives | ||
reportUnusedDisableDirectives, | ||
warnIgnored: constructorWarnIgnored | ||
} = eslintOptions; | ||
@@ -954,3 +961,5 @@ const results = []; | ||
if (resolvedFilename && await this.isPathIgnored(resolvedFilename)) { | ||
if (warnIgnored) { | ||
const shouldWarnIgnored = typeof warnIgnored === "boolean" ? warnIgnored : constructorWarnIgnored; | ||
if (shouldWarnIgnored) { | ||
results.push(createIgnoreResult(resolvedFilename, cwd)); | ||
@@ -957,0 +966,0 @@ } |
@@ -90,3 +90,3 @@ /** | ||
const regex = new RegExp(String.raw`(?:^|\s*,\s*)${escapeRegExp(ruleId)}(?:\s*,\s*|$)`, "u"); | ||
const regex = new RegExp(String.raw`(?:^|\s*,\s*)(?<quote>['"]?)${escapeRegExp(ruleId)}\k<quote>(?:\s*,\s*|$)`, "u"); | ||
const match = regex.exec(listText); | ||
@@ -93,0 +93,0 @@ const matchedText = match[0]; |
@@ -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 @@ } |
@@ -83,3 +83,5 @@ /** | ||
/** | ||
* The initial code path segment. | ||
* The initial code path segment. This is the segment that is at the head | ||
* of the code path. | ||
* This is a passthrough to the underlying `CodePathState`. | ||
* @type {CodePathSegment} | ||
@@ -92,4 +94,6 @@ */ | ||
/** | ||
* Final code path segments. | ||
* This array is a mix of `returnedSegments` and `thrownSegments`. | ||
* Final code path segments. These are the terminal (tail) segments in the | ||
* code path, which is the combination of `returnedSegments` and `thrownSegments`. | ||
* All segments in this array are reachable. | ||
* This is a passthrough to the underlying `CodePathState`. | ||
* @type {CodePathSegment[]} | ||
@@ -102,5 +106,10 @@ */ | ||
/** | ||
* Final code path segments which is with `return` statements. | ||
* This array contains the last path segment if it's reachable. | ||
* Since the reachable last path returns `undefined`. | ||
* Final code path segments that represent normal completion of the code path. | ||
* For functions, this means both explicit `return` statements and implicit returns, | ||
* such as the last reachable segment in a function that does not have an | ||
* explicit `return` as this implicitly returns `undefined`. For scripts, | ||
* modules, class field initializers, and class static blocks, this means | ||
* all lines of code have been executed. | ||
* These segments are also present in `finalSegments`. | ||
* This is a passthrough to the underlying `CodePathState`. | ||
* @type {CodePathSegment[]} | ||
@@ -113,3 +122,5 @@ */ | ||
/** | ||
* Final code path segments which is with `throw` statements. | ||
* Final code path segments that represent `throw` statements. | ||
* This is a passthrough to the underlying `CodePathState`. | ||
* These segments are also present in `finalSegments`. | ||
* @type {CodePathSegment[]} | ||
@@ -122,4 +133,10 @@ */ | ||
/** | ||
* Current code path segments. | ||
* Tracks the traversal of the code path through each segment. This array | ||
* starts empty and segments are added or removed as the code path is | ||
* traversed. This array always ends up empty at the end of a code path | ||
* traversal. The `CodePathState` uses this to track its progress through | ||
* the code path. | ||
* This is a passthrough to the underlying `CodePathState`. | ||
* @type {CodePathSegment[]} | ||
* @deprecated | ||
*/ | ||
@@ -133,3 +150,3 @@ get currentSegments() { | ||
* | ||
* codePath.traverseSegments(function(segment, controller) { | ||
* codePath.traverseSegments((segment, controller) => { | ||
* // do something. | ||
@@ -140,36 +157,60 @@ * }); | ||
* | ||
* The `controller` object has two methods. | ||
* The `controller` argument has two methods: | ||
* | ||
* - `controller.skip()` - Skip the following segments in this branch. | ||
* - `controller.break()` - Skip all following segments. | ||
* @param {Object} [options] Omittable. | ||
* @param {CodePathSegment} [options.first] The first segment to traverse. | ||
* @param {CodePathSegment} [options.last] The last segment to traverse. | ||
* - `skip()` - skips the following segments in this branch | ||
* - `break()` - skips all following segments in the traversal | ||
* | ||
* A note on the parameters: the `options` argument is optional. This means | ||
* the first argument might be an options object or the callback function. | ||
* @param {Object} [optionsOrCallback] Optional first and last segments to traverse. | ||
* @param {CodePathSegment} [optionsOrCallback.first] The first segment to traverse. | ||
* @param {CodePathSegment} [optionsOrCallback.last] The last segment to traverse. | ||
* @param {Function} callback A callback function. | ||
* @returns {void} | ||
*/ | ||
traverseSegments(options, callback) { | ||
traverseSegments(optionsOrCallback, callback) { | ||
// normalize the arguments into a callback and options | ||
let resolvedOptions; | ||
let resolvedCallback; | ||
if (typeof options === "function") { | ||
resolvedCallback = options; | ||
if (typeof optionsOrCallback === "function") { | ||
resolvedCallback = optionsOrCallback; | ||
resolvedOptions = {}; | ||
} else { | ||
resolvedOptions = options || {}; | ||
resolvedOptions = optionsOrCallback || {}; | ||
resolvedCallback = callback; | ||
} | ||
// determine where to start traversing from based on the options | ||
const startSegment = resolvedOptions.first || this.internal.initialSegment; | ||
const lastSegment = resolvedOptions.last; | ||
let item = null; | ||
// set up initial location information | ||
let record = null; | ||
let index = 0; | ||
let end = 0; | ||
let segment = null; | ||
const visited = Object.create(null); | ||
// segments that have already been visited during traversal | ||
const visited = new Set(); | ||
// tracks the traversal steps | ||
const stack = [[startSegment, 0]]; | ||
// tracks the last skipped segment during traversal | ||
let skippedSegment = null; | ||
// indicates if we exited early from the traversal | ||
let broken = false; | ||
/** | ||
* Maintains traversal state. | ||
*/ | ||
const controller = { | ||
/** | ||
* Skip the following segments in this branch. | ||
* @returns {void} | ||
*/ | ||
skip() { | ||
@@ -182,2 +223,8 @@ if (stack.length <= 1) { | ||
}, | ||
/** | ||
* Stop traversal completely - do not traverse to any | ||
* other segments. | ||
* @returns {void} | ||
*/ | ||
break() { | ||
@@ -189,3 +236,3 @@ broken = true; | ||
/** | ||
* Checks a given previous segment has been visited. | ||
* Checks if a given previous segment has been visited. | ||
* @param {CodePathSegment} prevSegment A previous segment to check. | ||
@@ -196,3 +243,3 @@ * @returns {boolean} `true` if the segment has been visited. | ||
return ( | ||
visited[prevSegment.id] || | ||
visited.has(prevSegment) || | ||
segment.isLoopedPrevSegment(prevSegment) | ||
@@ -202,11 +249,25 @@ ); | ||
// the traversal | ||
while (stack.length > 0) { | ||
item = stack[stack.length - 1]; | ||
segment = item[0]; | ||
index = item[1]; | ||
/* | ||
* This isn't a pure stack. We use the top record all the time | ||
* but don't always pop it off. The record is popped only if | ||
* one of the following is true: | ||
* | ||
* 1) We have already visited the segment. | ||
* 2) We have not visited *all* of the previous segments. | ||
* 3) We have traversed past the available next segments. | ||
* | ||
* Otherwise, we just read the value and sometimes modify the | ||
* record as we traverse. | ||
*/ | ||
record = stack[stack.length - 1]; | ||
segment = record[0]; | ||
index = record[1]; | ||
if (index === 0) { | ||
// Skip if this segment has been visited already. | ||
if (visited[segment.id]) { | ||
if (visited.has(segment)) { | ||
stack.pop(); | ||
@@ -225,14 +286,25 @@ continue; | ||
// Reset the flag of skipping if all branches have been skipped. | ||
// Reset the skipping flag if all branches have been skipped. | ||
if (skippedSegment && segment.prevSegments.includes(skippedSegment)) { | ||
skippedSegment = null; | ||
} | ||
visited[segment.id] = true; | ||
visited.add(segment); | ||
// Call the callback when the first time. | ||
/* | ||
* If the most recent segment hasn't been skipped, then we call | ||
* the callback, passing in the segment and the controller. | ||
*/ | ||
if (!skippedSegment) { | ||
resolvedCallback.call(this, segment, controller); | ||
// exit if we're at the last segment | ||
if (segment === lastSegment) { | ||
controller.skip(); | ||
} | ||
/* | ||
* If the previous statement was executed, or if the callback | ||
* called a method on the controller, we might need to exit the | ||
* loop, so check for that and break accordingly. | ||
*/ | ||
if (broken) { | ||
@@ -247,8 +319,31 @@ break; | ||
if (index < end) { | ||
item[1] += 1; | ||
/* | ||
* If we haven't yet visited all of the next segments, update | ||
* the current top record on the stack to the next index to visit | ||
* and then push a record for the current segment on top. | ||
* | ||
* Setting the current top record's index lets us know how many | ||
* times we've been here and ensures that the segment won't be | ||
* reprocessed (because we only process segments with an index | ||
* of 0). | ||
*/ | ||
record[1] += 1; | ||
stack.push([segment.nextSegments[index], 0]); | ||
} else if (index === end) { | ||
item[0] = segment.nextSegments[index]; | ||
item[1] = 0; | ||
/* | ||
* If we are at the last next segment, then reset the top record | ||
* in the stack to next segment and set its index to 0 so it will | ||
* be processed next. | ||
*/ | ||
record[0] = segment.nextSegments[index]; | ||
record[1] = 0; | ||
} else { | ||
/* | ||
* If index > end, that means we have no more segments that need | ||
* processing. So, we pop that record off of the stack in order to | ||
* continue traversing at the next level up. | ||
*/ | ||
stack.pop(); | ||
@@ -255,0 +350,0 @@ } |
@@ -24,4 +24,4 @@ /** | ||
/** | ||
* Gets whether or not a given segment is reachable. | ||
* @param {CodePathSegment} segment A segment to get. | ||
* Determines whether or not a given segment is reachable. | ||
* @param {CodePathSegment} segment The segment to check. | ||
* @returns {boolean} `true` if the segment is reachable. | ||
@@ -34,22 +34,53 @@ */ | ||
/** | ||
* Creates new segments from the specific range of `context.segmentsList`. | ||
* Creates a new segment for each fork in the given context and appends it | ||
* to the end of the specified range of segments. Ultimately, this ends up calling | ||
* `new CodePathSegment()` for each of the forks using the `create` argument | ||
* as a wrapper around special behavior. | ||
* | ||
* The `startIndex` and `endIndex` arguments specify a range of segments in | ||
* `context` that should become `allPrevSegments` for the newly created | ||
* `CodePathSegment` objects. | ||
* | ||
* When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and | ||
* `end` is `-1`, this creates `[g, h]`. This `g` is from `a`, `c`, and `e`. | ||
* This `h` is from `b`, `d`, and `f`. | ||
* @param {ForkContext} context An instance. | ||
* @param {number} begin The first index of the previous segments. | ||
* @param {number} end The last index of the previous segments. | ||
* @param {Function} create A factory function of new segments. | ||
* @returns {CodePathSegment[]} New segments. | ||
* `end` is `-1`, this creates two new segments, `[g, h]`. This `g` is appended to | ||
* the end of the path from `a`, `c`, and `e`. This `h` is appended to the end of | ||
* `b`, `d`, and `f`. | ||
* @param {ForkContext} context An instance from which the previous segments | ||
* will be obtained. | ||
* @param {number} startIndex The index of the first segment in the context | ||
* that should be specified as previous segments for the newly created segments. | ||
* @param {number} endIndex The index of the last segment in the context | ||
* that should be specified as previous segments for the newly created segments. | ||
* @param {Function} create A function that creates new `CodePathSegment` | ||
* instances in a particular way. See the `CodePathSegment.new*` methods. | ||
* @returns {Array<CodePathSegment>} An array of the newly-created segments. | ||
*/ | ||
function makeSegments(context, begin, end, create) { | ||
function createSegments(context, startIndex, endIndex, create) { | ||
/** @type {Array<Array<CodePathSegment>>} */ | ||
const list = context.segmentsList; | ||
const normalizedBegin = begin >= 0 ? begin : list.length + begin; | ||
const normalizedEnd = end >= 0 ? end : list.length + end; | ||
/* | ||
* Both `startIndex` and `endIndex` work the same way: if the number is zero | ||
* or more, then the number is used as-is. If the number is negative, | ||
* then that number is added to the length of the segments list to | ||
* determine the index to use. That means -1 for either argument | ||
* is the last element, -2 is the second to last, and so on. | ||
* | ||
* So if `startIndex` is 0, `endIndex` is -1, and `list.length` is 3, the | ||
* effective `startIndex` is 0 and the effective `endIndex` is 2, so this function | ||
* will include items at indices 0, 1, and 2. | ||
* | ||
* Therefore, if `startIndex` is -1 and `endIndex` is -1, that means we'll only | ||
* be using the last segment in `list`. | ||
*/ | ||
const normalizedBegin = startIndex >= 0 ? startIndex : list.length + startIndex; | ||
const normalizedEnd = endIndex >= 0 ? endIndex : list.length + endIndex; | ||
/** @type {Array<CodePathSegment>} */ | ||
const segments = []; | ||
for (let i = 0; i < context.count; ++i) { | ||
// this is passed into `new CodePathSegment` to add to code path. | ||
const allPrevSegments = []; | ||
@@ -61,2 +92,3 @@ | ||
// note: `create` is just a wrapper that augments `new CodePathSegment`. | ||
segments.push(create(context.idGenerator.next(), allPrevSegments)); | ||
@@ -69,9 +101,8 @@ } | ||
/** | ||
* `segments` becomes doubly in a `finally` block. Then if a code path exits by a | ||
* control statement (such as `break`, `continue`) from the `finally` block, the | ||
* destination's segments may be half of the source segments. In that case, this | ||
* merges segments. | ||
* @param {ForkContext} context An instance. | ||
* @param {CodePathSegment[]} segments Segments to merge. | ||
* @returns {CodePathSegment[]} The merged segments. | ||
* Inside of a `finally` block we end up with two parallel paths. If the code path | ||
* exits by a control statement (such as `break` or `continue`) from the `finally` | ||
* block, then we need to merge the remaining parallel paths back into one. | ||
* @param {ForkContext} context The fork context to work on. | ||
* @param {Array<CodePathSegment>} segments Segments to merge. | ||
* @returns {Array<CodePathSegment>} The merged segments. | ||
*/ | ||
@@ -81,6 +112,29 @@ function mergeExtraSegments(context, segments) { | ||
/* | ||
* We need to ensure that the array returned from this function contains no more | ||
* than the number of segments that the context allows. `context.count` indicates | ||
* how many items should be in the returned array to ensure that the new segment | ||
* entries will line up with the already existing segment entries. | ||
*/ | ||
while (currentSegments.length > context.count) { | ||
const merged = []; | ||
for (let i = 0, length = currentSegments.length / 2 | 0; i < length; ++i) { | ||
/* | ||
* Because `context.count` is a factor of 2 inside of a `finally` block, | ||
* we can divide the segment count by 2 to merge the paths together. | ||
* This loops through each segment in the list and creates a new `CodePathSegment` | ||
* that has the segment and the segment two slots away as previous segments. | ||
* | ||
* If `currentSegments` is [a,b,c,d], this will create new segments e and f, such | ||
* that: | ||
* | ||
* When `i` is 0: | ||
* a->e | ||
* c->e | ||
* | ||
* When `i` is 1: | ||
* b->f | ||
* d->f | ||
*/ | ||
for (let i = 0, length = Math.floor(currentSegments.length / 2); i < length; ++i) { | ||
merged.push(CodePathSegment.newNext( | ||
@@ -91,4 +145,11 @@ context.idGenerator.next(), | ||
} | ||
/* | ||
* Go through the loop condition one more time to see if we have the | ||
* number of segments for the context. If not, we'll keep merging paths | ||
* of the merged segments until we get there. | ||
*/ | ||
currentSegments = merged; | ||
} | ||
return currentSegments; | ||
@@ -102,3 +163,3 @@ } | ||
/** | ||
* A class to manage forking. | ||
* Manages the forking of code paths. | ||
*/ | ||
@@ -108,10 +169,40 @@ class ForkContext { | ||
/** | ||
* Creates a new instance. | ||
* @param {IdGenerator} idGenerator An identifier generator for segments. | ||
* @param {ForkContext|null} upper An upper fork context. | ||
* @param {number} count A number of parallel segments. | ||
* @param {ForkContext|null} upper The preceding fork context. | ||
* @param {number} count The number of parallel segments in each element | ||
* of `segmentsList`. | ||
*/ | ||
constructor(idGenerator, upper, count) { | ||
/** | ||
* The ID generator that will generate segment IDs for any new | ||
* segments that are created. | ||
* @type {IdGenerator} | ||
*/ | ||
this.idGenerator = idGenerator; | ||
/** | ||
* The preceding fork context. | ||
* @type {ForkContext|null} | ||
*/ | ||
this.upper = upper; | ||
/** | ||
* The number of elements in each element of `segmentsList`. In most | ||
* cases, this is 1 but can be 2 when there is a `finally` present, | ||
* which forks the code path outside of normal flow. In the case of nested | ||
* `finally` blocks, this can be a multiple of 2. | ||
* @type {number} | ||
*/ | ||
this.count = count; | ||
/** | ||
* The segments within this context. Each element in this array has | ||
* `count` elements that represent one step in each fork. For example, | ||
* when `segmentsList` is `[[a, b], [c, d], [e, f]]`, there is one path | ||
* a->c->e and one path b->d->f, and `count` is 2 because each element | ||
* is an array with two elements. | ||
* @type {Array<Array<CodePathSegment>>} | ||
*/ | ||
this.segmentsList = []; | ||
@@ -121,4 +212,4 @@ } | ||
/** | ||
* The head segments. | ||
* @type {CodePathSegment[]} | ||
* The segments that begin this fork context. | ||
* @type {Array<CodePathSegment>} | ||
*/ | ||
@@ -132,3 +223,3 @@ get head() { | ||
/** | ||
* A flag which shows empty. | ||
* Indicates if the context contains no segments. | ||
* @type {boolean} | ||
@@ -141,3 +232,3 @@ */ | ||
/** | ||
* A flag which shows reachable. | ||
* Indicates if there are any segments that are reachable. | ||
* @type {boolean} | ||
@@ -152,38 +243,49 @@ */ | ||
/** | ||
* Creates new segments from this context. | ||
* @param {number} begin The first index of previous segments. | ||
* @param {number} end The last index of previous segments. | ||
* @returns {CodePathSegment[]} New segments. | ||
* Creates new segments in this context and appends them to the end of the | ||
* already existing `CodePathSegment`s specified by `startIndex` and | ||
* `endIndex`. | ||
* @param {number} startIndex The index of the first segment in the context | ||
* that should be specified as previous segments for the newly created segments. | ||
* @param {number} endIndex The index of the last segment in the context | ||
* that should be specified as previous segments for the newly created segments. | ||
* @returns {Array<CodePathSegment>} An array of the newly created segments. | ||
*/ | ||
makeNext(begin, end) { | ||
return makeSegments(this, begin, end, CodePathSegment.newNext); | ||
makeNext(startIndex, endIndex) { | ||
return createSegments(this, startIndex, endIndex, CodePathSegment.newNext); | ||
} | ||
/** | ||
* Creates new segments from this context. | ||
* The new segments is always unreachable. | ||
* @param {number} begin The first index of previous segments. | ||
* @param {number} end The last index of previous segments. | ||
* @returns {CodePathSegment[]} New segments. | ||
* Creates new unreachable segments in this context and appends them to the end of the | ||
* already existing `CodePathSegment`s specified by `startIndex` and | ||
* `endIndex`. | ||
* @param {number} startIndex The index of the first segment in the context | ||
* that should be specified as previous segments for the newly created segments. | ||
* @param {number} endIndex The index of the last segment in the context | ||
* that should be specified as previous segments for the newly created segments. | ||
* @returns {Array<CodePathSegment>} An array of the newly created segments. | ||
*/ | ||
makeUnreachable(begin, end) { | ||
return makeSegments(this, begin, end, CodePathSegment.newUnreachable); | ||
makeUnreachable(startIndex, endIndex) { | ||
return createSegments(this, startIndex, endIndex, CodePathSegment.newUnreachable); | ||
} | ||
/** | ||
* Creates new segments from this context. | ||
* The new segments don't have connections for previous segments. | ||
* But these inherit the reachable flag from this context. | ||
* @param {number} begin The first index of previous segments. | ||
* @param {number} end The last index of previous segments. | ||
* @returns {CodePathSegment[]} New segments. | ||
* Creates new segments in this context and does not append them to the end | ||
* of the already existing `CodePathSegment`s specified by `startIndex` and | ||
* `endIndex`. The `startIndex` and `endIndex` are only used to determine if | ||
* the new segments should be reachable. If any of the segments in this range | ||
* are reachable then the new segments are also reachable; otherwise, the new | ||
* segments are unreachable. | ||
* @param {number} startIndex The index of the first segment in the context | ||
* that should be considered for reachability. | ||
* @param {number} endIndex The index of the last segment in the context | ||
* that should be considered for reachability. | ||
* @returns {Array<CodePathSegment>} An array of the newly created segments. | ||
*/ | ||
makeDisconnected(begin, end) { | ||
return makeSegments(this, begin, end, CodePathSegment.newDisconnected); | ||
makeDisconnected(startIndex, endIndex) { | ||
return createSegments(this, startIndex, endIndex, CodePathSegment.newDisconnected); | ||
} | ||
/** | ||
* Adds segments into this context. | ||
* The added segments become the head. | ||
* @param {CodePathSegment[]} segments Segments to add. | ||
* Adds segments to the head of this context. | ||
* @param {Array<CodePathSegment>} segments The segments to add. | ||
* @returns {void} | ||
@@ -193,3 +295,2 @@ */ | ||
assert(segments.length >= this.count, `${segments.length} >= ${this.count}`); | ||
this.segmentsList.push(mergeExtraSegments(this, segments)); | ||
@@ -199,11 +300,13 @@ } | ||
/** | ||
* Replaces the head segments with given segments. | ||
* Replaces the head segments with the given segments. | ||
* The current head segments are removed. | ||
* @param {CodePathSegment[]} segments Segments to add. | ||
* @param {Array<CodePathSegment>} replacementHeadSegments The new head segments. | ||
* @returns {void} | ||
*/ | ||
replaceHead(segments) { | ||
assert(segments.length >= this.count, `${segments.length} >= ${this.count}`); | ||
this.segmentsList.splice(-1, 1, mergeExtraSegments(this, segments)); | ||
replaceHead(replacementHeadSegments) { | ||
assert( | ||
replacementHeadSegments.length >= this.count, | ||
`${replacementHeadSegments.length} >= ${this.count}` | ||
); | ||
this.segmentsList.splice(-1, 1, mergeExtraSegments(this, replacementHeadSegments)); | ||
} | ||
@@ -213,13 +316,8 @@ | ||
* Adds all segments of a given fork context into this context. | ||
* @param {ForkContext} context A fork context to add. | ||
* @param {ForkContext} otherForkContext The fork context to add from. | ||
* @returns {void} | ||
*/ | ||
addAll(context) { | ||
assert(context.count === this.count); | ||
const source = context.segmentsList; | ||
for (let i = 0; i < source.length; ++i) { | ||
this.segmentsList.push(source[i]); | ||
} | ||
addAll(otherForkContext) { | ||
assert(otherForkContext.count === this.count); | ||
this.segmentsList.push(...otherForkContext.segmentsList); | ||
} | ||
@@ -236,3 +334,4 @@ | ||
/** | ||
* Creates the root fork context. | ||
* Creates a new root context, meaning that there are no parent | ||
* fork contexts. | ||
* @param {IdGenerator} idGenerator An identifier generator for segments. | ||
@@ -252,10 +351,12 @@ * @returns {ForkContext} New fork context. | ||
* @param {ForkContext} parentContext The parent fork context. | ||
* @param {boolean} forkLeavingPath A flag which shows inside of `finally` block. | ||
* @param {boolean} shouldForkLeavingPath Indicates that we are inside of | ||
* a `finally` block and should therefore fork the path that leaves | ||
* `finally`. | ||
* @returns {ForkContext} New fork context. | ||
*/ | ||
static newEmpty(parentContext, forkLeavingPath) { | ||
static newEmpty(parentContext, shouldForkLeavingPath) { | ||
return new ForkContext( | ||
parentContext.idGenerator, | ||
parentContext, | ||
(forkLeavingPath ? 2 : 1) * parentContext.count | ||
(shouldForkLeavingPath ? 2 : 1) * parentContext.count | ||
); | ||
@@ -262,0 +363,0 @@ } |
@@ -142,3 +142,3 @@ /** | ||
string.split(",").forEach(name => { | ||
const trimmedName = name.trim(); | ||
const trimmedName = name.trim().replace(/^(?<quote>['"]?)(?<ruleId>.*)\k<quote>$/us, "$<ruleId>"); | ||
@@ -145,0 +145,0 @@ if (trimmedName) { |
@@ -58,2 +58,3 @@ /** | ||
* @property {boolean} [version] Output the version number | ||
* @property {boolean} warnIgnored Show warnings when the file list includes ignored files | ||
* @property {string[]} _ Positional filenames or patterns | ||
@@ -143,2 +144,13 @@ */ | ||
let warnIgnoredFlag; | ||
if (usingFlatConfig) { | ||
warnIgnoredFlag = { | ||
option: "warn-ignored", | ||
type: "Boolean", | ||
default: "true", | ||
description: "Suppress warnings when the file list includes ignored files" | ||
}; | ||
} | ||
return optionator({ | ||
@@ -354,2 +366,3 @@ prepend: "eslint [options] file.js [file.js] [dir]", | ||
}, | ||
warnIgnoredFlag, | ||
{ | ||
@@ -356,0 +369,0 @@ option: "debug", |
@@ -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 @@ } |
@@ -153,2 +153,27 @@ /** | ||
/** | ||
* Gets the leftmost operand of a consecutive logical expression. | ||
* @param {SourceCode} sourceCode The ESLint source code object | ||
* @param {LogicalExpression} node LogicalExpression | ||
* @returns {Expression} Leftmost operand | ||
*/ | ||
function getLeftmostOperand(sourceCode, node) { | ||
let left = node.left; | ||
while (left.type === "LogicalExpression" && left.operator === node.operator) { | ||
if (astUtils.isParenthesised(sourceCode, left)) { | ||
/* | ||
* It should have associativity, | ||
* but ignore it if use parentheses to make the evaluation order clear. | ||
*/ | ||
return left; | ||
} | ||
left = left.left; | ||
} | ||
return left; | ||
} | ||
//------------------------------------------------------------------------------ | ||
@@ -322,3 +347,6 @@ // Rule Definition | ||
"AssignmentExpression[operator='='][right.type='LogicalExpression']"(assignment) { | ||
if (!astUtils.isSameReference(assignment.left, assignment.right.left)) { | ||
const leftOperand = getLeftmostOperand(sourceCode, assignment.right); | ||
if (!astUtils.isSameReference(assignment.left, leftOperand) | ||
) { | ||
return; | ||
@@ -347,6 +375,6 @@ } | ||
// -> foo ||= bar | ||
const logicalOperatorToken = getOperatorToken(assignment.right); | ||
const logicalOperatorToken = getOperatorToken(leftOperand.parent); | ||
const firstRightOperandToken = sourceCode.getTokenAfter(logicalOperatorToken); | ||
yield ruleFixer.removeRange([assignment.right.range[0], firstRightOperandToken.range[0]]); | ||
yield ruleFixer.removeRange([leftOperand.parent.range[0], firstRightOperandToken.range[0]]); | ||
} | ||
@@ -353,0 +381,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.51.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.51.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://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://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
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
3003538
407
70814
7
1
60
+ Added@eslint/js@8.51.0(transitive)
- Removed@eslint/js@8.57.1(transitive)
Updated@eslint/js@8.51.0