Ajout de promotion et de commande
This commit is contained in:
+584
File diff suppressed because it is too large
Load Diff
+828
File diff suppressed because it is too large
Load Diff
+262
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* @fileoverview The CodePathSegment class.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const debug = require("./debug-helpers");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given segment is reachable.
|
||||
* @param {CodePathSegment} segment A segment to check.
|
||||
* @returns {boolean} `true` if the segment is reachable.
|
||||
*/
|
||||
function isReachable(segment) {
|
||||
return segment.reachable;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class CodePathSegment {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {string} id An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
|
||||
* This array includes unreachable segments.
|
||||
* @param {boolean} reachable A flag which shows this is reachable.
|
||||
*/
|
||||
constructor(id, allPrevSegments, reachable) {
|
||||
/**
|
||||
* The identifier of this code path.
|
||||
* Rules use it to store additional information of each rule.
|
||||
* @type {string}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* An array of the next reachable segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.nextSegments = [];
|
||||
|
||||
/**
|
||||
* An array of the previous reachable segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.prevSegments = allPrevSegments.filter(isReachable);
|
||||
|
||||
/**
|
||||
* An array of all next segments including reachable and unreachable.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.allNextSegments = [];
|
||||
|
||||
/**
|
||||
* An array of all previous segments including reachable and unreachable.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.allPrevSegments = allPrevSegments;
|
||||
|
||||
/**
|
||||
* A flag which shows this is reachable.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.reachable = reachable;
|
||||
|
||||
// Internal data.
|
||||
Object.defineProperty(this, "internal", {
|
||||
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: [],
|
||||
},
|
||||
});
|
||||
|
||||
/* c8 ignore start */
|
||||
if (debug.enabled) {
|
||||
this.internal.nodes = [];
|
||||
} /* c8 ignore stop */
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a given previous segment is coming from the end of a loop.
|
||||
* @param {CodePathSegment} segment A previous segment to check.
|
||||
* @returns {boolean} `true` if the segment is coming from the end of a loop.
|
||||
*/
|
||||
isLoopedPrevSegment(segment) {
|
||||
return this.internal.loopedPrevSegments.includes(segment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the root segment.
|
||||
* @param {string} id An identifier.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newRoot(id) {
|
||||
return new CodePathSegment(id, [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* to append to.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newNext(id, allPrevSegments) {
|
||||
return new CodePathSegment(
|
||||
id,
|
||||
CodePathSegment.flattenUnusedSegments(allPrevSegments),
|
||||
allPrevSegments.some(isReachable),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unreachable segment and appends it after the given segments.
|
||||
* @param {string} id An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newUnreachable(id, allPrevSegments) {
|
||||
const segment = new CodePathSegment(
|
||||
id,
|
||||
CodePathSegment.flattenUnusedSegments(allPrevSegments),
|
||||
false,
|
||||
);
|
||||
|
||||
/*
|
||||
* In `if (a) return a; foo();` case, the unreachable segment preceded by
|
||||
* the return statement is not used but must not be removed.
|
||||
*/
|
||||
CodePathSegment.markUsed(segment);
|
||||
|
||||
return segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a segment that follows given segments.
|
||||
* This factory method does not connect with `allPrevSegments`.
|
||||
* But this inherits `reachable` flag.
|
||||
* @param {string} id An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newDisconnected(id, allPrevSegments) {
|
||||
return new CodePathSegment(id, [], allPrevSegments.some(isReachable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a given segment as used.
|
||||
*
|
||||
* And this function registers the segment into the previous segments as a next.
|
||||
* @param {CodePathSegment} segment A segment to mark.
|
||||
* @returns {void}
|
||||
*/
|
||||
static markUsed(segment) {
|
||||
if (segment.internal.used) {
|
||||
return;
|
||||
}
|
||||
segment.internal.used = true;
|
||||
|
||||
let i;
|
||||
|
||||
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) {
|
||||
const prevSegment = segment.allPrevSegments[i];
|
||||
|
||||
prevSegment.allNextSegments.push(segment);
|
||||
prevSegment.nextSegments.push(segment);
|
||||
}
|
||||
} 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) {
|
||||
segment.allPrevSegments[i].allNextSegments.push(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a previous segment as looped.
|
||||
* @param {CodePathSegment} segment A segment.
|
||||
* @param {CodePathSegment} prevSegment A previous segment to mark.
|
||||
* @returns {void}
|
||||
*/
|
||||
static markPrevSegmentAsLooped(segment, prevSegment) {
|
||||
segment.internal.loopedPrevSegments.push(prevSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = new Set();
|
||||
|
||||
for (let i = 0; i < segments.length; ++i) {
|
||||
const segment = segments[i];
|
||||
|
||||
// Ignores duplicated.
|
||||
if (done.has(segment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use previous segments if unused.
|
||||
if (!segment.internal.used) {
|
||||
for (let j = 0; j < segment.allPrevSegments.length; ++j) {
|
||||
const prevSegment = segment.allPrevSegments[j];
|
||||
|
||||
if (!done.has(prevSegment)) {
|
||||
done.add(prevSegment);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
done.add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
return [...done];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodePathSegment;
|
||||
+2370
File diff suppressed because it is too large
Load Diff
+332
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* @fileoverview A class of the code path.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const CodePathState = require("./code-path-state");
|
||||
const IdGenerator = require("./id-generator");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A code path.
|
||||
*/
|
||||
class CodePath {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options Options for the function (see below).
|
||||
* @param {string} options.id An identifier.
|
||||
* @param {string} options.origin The type of code path origin.
|
||||
* @param {CodePath|null} options.upper The code path of the upper function scope.
|
||||
* @param {Function} options.onLooped A callback function to notify looping.
|
||||
*/
|
||||
constructor({ id, origin, upper, onLooped }) {
|
||||
/**
|
||||
* The identifier of this code path.
|
||||
* Rules use it to store additional information of each rule.
|
||||
* @type {string}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* The reason that this code path was started. May be "program",
|
||||
* "function", "class-field-initializer", or "class-static-block".
|
||||
* @type {string}
|
||||
*/
|
||||
this.origin = origin;
|
||||
|
||||
/**
|
||||
* The code path of the upper function scope.
|
||||
* @type {CodePath|null}
|
||||
*/
|
||||
this.upper = upper;
|
||||
|
||||
/**
|
||||
* The code paths of nested function scopes.
|
||||
* @type {CodePath[]}
|
||||
*/
|
||||
this.childCodePaths = [];
|
||||
|
||||
// Initializes internal state.
|
||||
Object.defineProperty(this, "internal", {
|
||||
value: new CodePathState(new IdGenerator(`${id}_`), onLooped),
|
||||
});
|
||||
|
||||
// Adds this into `childCodePaths` of `upper`.
|
||||
if (upper) {
|
||||
upper.childCodePaths.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the state of a given code path.
|
||||
* @param {CodePath} codePath A code path to get.
|
||||
* @returns {CodePathState} The state of the code path.
|
||||
*/
|
||||
static getState(codePath) {
|
||||
return codePath.internal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
get initialSegment() {
|
||||
return this.internal.initialSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[]}
|
||||
*/
|
||||
get finalSegments() {
|
||||
return this.internal.finalSegments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[]}
|
||||
*/
|
||||
get returnedSegments() {
|
||||
return this.internal.returnedForkContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[]}
|
||||
*/
|
||||
get thrownSegments() {
|
||||
return this.internal.thrownForkContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses all segments in this code path.
|
||||
*
|
||||
* codePath.traverseSegments((segment, controller) => {
|
||||
* // do something.
|
||||
* });
|
||||
*
|
||||
* This method enumerates segments in order from the head.
|
||||
*
|
||||
* The `controller` argument has two methods:
|
||||
*
|
||||
* - `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(optionsOrCallback, callback) {
|
||||
// normalize the arguments into a callback and options
|
||||
let resolvedOptions;
|
||||
let resolvedCallback;
|
||||
|
||||
if (typeof optionsOrCallback === "function") {
|
||||
resolvedCallback = optionsOrCallback;
|
||||
resolvedOptions = {};
|
||||
} else {
|
||||
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;
|
||||
|
||||
// set up initial location information
|
||||
let record;
|
||||
let index;
|
||||
let end;
|
||||
let segment = null;
|
||||
|
||||
// segments that have already been visited during traversal
|
||||
const visited = new Set();
|
||||
|
||||
// tracks the traversal steps
|
||||
const stack = [[startSegment, 0]];
|
||||
|
||||
// segments that have been skipped during traversal
|
||||
const skipped = new Set();
|
||||
|
||||
// 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() {
|
||||
skipped.add(segment);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop traversal completely - do not traverse to any
|
||||
* other segments.
|
||||
* @returns {void}
|
||||
*/
|
||||
break() {
|
||||
broken = true;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a given previous segment has been visited.
|
||||
* @param {CodePathSegment} prevSegment A previous segment to check.
|
||||
* @returns {boolean} `true` if the segment has been visited.
|
||||
*/
|
||||
function isVisited(prevSegment) {
|
||||
return (
|
||||
visited.has(prevSegment) ||
|
||||
segment.isLoopedPrevSegment(prevSegment)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given previous segment has been skipped.
|
||||
* @param {CodePathSegment} prevSegment A previous segment to check.
|
||||
* @returns {boolean} `true` if the segment has been skipped.
|
||||
*/
|
||||
function isSkipped(prevSegment) {
|
||||
return (
|
||||
skipped.has(prevSegment) ||
|
||||
segment.isLoopedPrevSegment(prevSegment)
|
||||
);
|
||||
}
|
||||
|
||||
// the traversal
|
||||
while (stack.length > 0) {
|
||||
/*
|
||||
* 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.at(-1);
|
||||
segment = record[0];
|
||||
index = record[1];
|
||||
|
||||
if (index === 0) {
|
||||
// Skip if this segment has been visited already.
|
||||
if (visited.has(segment)) {
|
||||
stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if all previous segments have not been visited.
|
||||
if (
|
||||
segment !== startSegment &&
|
||||
segment.prevSegments.length > 0 &&
|
||||
!segment.prevSegments.every(isVisited)
|
||||
) {
|
||||
stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
visited.add(segment);
|
||||
|
||||
// Skips the segment if all previous segments have been skipped.
|
||||
const shouldSkip =
|
||||
skipped.size > 0 &&
|
||||
segment.prevSegments.length > 0 &&
|
||||
segment.prevSegments.every(isSkipped);
|
||||
|
||||
/*
|
||||
* If the most recent segment hasn't been skipped, then we call
|
||||
* the callback, passing in the segment and the controller.
|
||||
*/
|
||||
if (!shouldSkip) {
|
||||
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) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// If the most recent segment has been skipped, then mark it as skipped.
|
||||
skipped.add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the stack.
|
||||
end = segment.nextSegments.length - 1;
|
||||
if (index < end) {
|
||||
/*
|
||||
* 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) {
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodePath;
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* @fileoverview Helpers to debug for code path analysis.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const debug = require("debug")("eslint:code-path");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets id of a given segment.
|
||||
* @param {CodePathSegment} segment A segment to get.
|
||||
* @returns {string} Id of the segment.
|
||||
*/
|
||||
/* c8 ignore next */
|
||||
// eslint-disable-next-line jsdoc/require-jsdoc -- Ignoring
|
||||
function getId(segment) {
|
||||
return segment.id + (segment.reachable ? "" : "!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get string for the given node and operation.
|
||||
* @param {ASTNode} node The node to convert.
|
||||
* @param {"enter" | "exit" | undefined} label The operation label.
|
||||
* @returns {string} The string representation.
|
||||
*/
|
||||
function nodeToString(node, label) {
|
||||
const suffix = label ? `:${label}` : "";
|
||||
|
||||
switch (node.type) {
|
||||
case "Identifier":
|
||||
return `${node.type}${suffix} (${node.name})`;
|
||||
case "Literal":
|
||||
return `${node.type}${suffix} (${node.value})`;
|
||||
default:
|
||||
return `${node.type}${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* A flag that debug dumping is enabled or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
enabled: debug.enabled,
|
||||
|
||||
/**
|
||||
* Dumps given objects.
|
||||
* @param {...any} args objects to dump.
|
||||
* @returns {void}
|
||||
*/
|
||||
dump: debug,
|
||||
|
||||
/**
|
||||
* Dumps the current analyzing state.
|
||||
* @param {ASTNode} node A node to dump.
|
||||
* @param {CodePathState} state A state to dump.
|
||||
* @param {boolean} leaving A flag whether or not it's leaving
|
||||
* @returns {void}
|
||||
*/
|
||||
dumpState: !debug.enabled
|
||||
? debug
|
||||
: /* c8 ignore next */ function (node, state, leaving) {
|
||||
for (let i = 0; i < state.currentSegments.length; ++i) {
|
||||
const segInternal = state.currentSegments[i].internal;
|
||||
|
||||
if (leaving) {
|
||||
const last = segInternal.nodes.length - 1;
|
||||
|
||||
if (
|
||||
last >= 0 &&
|
||||
segInternal.nodes[last] ===
|
||||
nodeToString(node, "enter")
|
||||
) {
|
||||
segInternal.nodes[last] = nodeToString(
|
||||
node,
|
||||
void 0,
|
||||
);
|
||||
} else {
|
||||
segInternal.nodes.push(nodeToString(node, "exit"));
|
||||
}
|
||||
} else {
|
||||
segInternal.nodes.push(nodeToString(node, "enter"));
|
||||
}
|
||||
}
|
||||
|
||||
debug(
|
||||
[
|
||||
`${state.currentSegments.map(getId).join(",")})`,
|
||||
`${node.type}${leaving ? ":exit" : ""}`,
|
||||
].join(" "),
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Dumps a DOT code of a given code path.
|
||||
* The DOT code can be visualized with Graphvis.
|
||||
* @param {CodePath} codePath A code path to dump.
|
||||
* @returns {void}
|
||||
* @see http://www.graphviz.org
|
||||
* @see http://www.webgraphviz.com
|
||||
*/
|
||||
dumpDot: !debug.enabled
|
||||
? debug
|
||||
: /* c8 ignore next */ function (codePath) {
|
||||
let text =
|
||||
"\n" +
|
||||
"digraph {\n" +
|
||||
'node[shape=box,style="rounded,filled",fillcolor=white];\n' +
|
||||
'initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];\n';
|
||||
|
||||
if (codePath.returnedSegments.length > 0) {
|
||||
text +=
|
||||
'final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];\n';
|
||||
}
|
||||
if (codePath.thrownSegments.length > 0) {
|
||||
text +=
|
||||
'thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true];\n';
|
||||
}
|
||||
|
||||
const traceMap = Object.create(null);
|
||||
const arrows = this.makeDotArrows(codePath, traceMap);
|
||||
|
||||
// eslint-disable-next-line guard-for-in -- Want ability to traverse prototype
|
||||
for (const id in traceMap) {
|
||||
const segment = traceMap[id];
|
||||
|
||||
text += `${id}[`;
|
||||
|
||||
if (segment.reachable) {
|
||||
text += 'label="';
|
||||
} else {
|
||||
text +=
|
||||
'style="rounded,dashed,filled",fillcolor="#FF9800",label="<<unreachable>>\\n';
|
||||
}
|
||||
|
||||
if (segment.internal.nodes.length > 0) {
|
||||
text += segment.internal.nodes.join("\\n");
|
||||
} else {
|
||||
text += "????";
|
||||
}
|
||||
|
||||
text += '"];\n';
|
||||
}
|
||||
|
||||
text += `${arrows}\n`;
|
||||
text += "}";
|
||||
debug("DOT", text);
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes a DOT code of a given code path.
|
||||
* The DOT code can be visualized with Graphvis.
|
||||
* @param {CodePath} codePath A code path to make DOT.
|
||||
* @param {Object} traceMap Optional. A map to check whether or not segments had been done.
|
||||
* @returns {string} A DOT code of the code path.
|
||||
*/
|
||||
makeDotArrows(codePath, traceMap) {
|
||||
const stack = [[codePath.initialSegment, 0]];
|
||||
const done = traceMap || Object.create(null);
|
||||
let lastId = codePath.initialSegment.id;
|
||||
let text = `initial->${codePath.initialSegment.id}`;
|
||||
|
||||
while (stack.length > 0) {
|
||||
const item = stack.pop();
|
||||
const segment = item[0];
|
||||
const index = item[1];
|
||||
|
||||
if (done[segment.id] && index === 0) {
|
||||
continue;
|
||||
}
|
||||
done[segment.id] = segment;
|
||||
|
||||
const nextSegment = segment.allNextSegments[index];
|
||||
|
||||
if (!nextSegment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastId === segment.id) {
|
||||
text += `->${nextSegment.id}`;
|
||||
} else {
|
||||
text += `;\n${segment.id}->${nextSegment.id}`;
|
||||
}
|
||||
lastId = nextSegment.id;
|
||||
|
||||
stack.unshift([segment, 1 + index]);
|
||||
stack.push([nextSegment, 0]);
|
||||
}
|
||||
|
||||
codePath.returnedSegments.forEach(finalSegment => {
|
||||
if (lastId === finalSegment.id) {
|
||||
text += "->final";
|
||||
} else {
|
||||
text += `;\n${finalSegment.id}->final`;
|
||||
}
|
||||
lastId = null;
|
||||
});
|
||||
|
||||
codePath.thrownSegments.forEach(finalSegment => {
|
||||
if (lastId === finalSegment.id) {
|
||||
text += "->thrown";
|
||||
} else {
|
||||
text += `;\n${finalSegment.id}->thrown`;
|
||||
}
|
||||
lastId = null;
|
||||
});
|
||||
|
||||
return `${text};`;
|
||||
},
|
||||
};
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
/**
|
||||
* @fileoverview A class to operate forking.
|
||||
*
|
||||
* This is state of forking.
|
||||
* This has a fork list and manages it.
|
||||
*
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const assert = require("../../shared/assert"),
|
||||
CodePathSegment = require("./code-path-segment");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines whether or not a given segment is reachable.
|
||||
* @param {CodePathSegment} segment The segment to check.
|
||||
* @returns {boolean} `true` if the segment is reachable.
|
||||
*/
|
||||
function isReachable(segment) {
|
||||
return segment.reachable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 createSegments(context, startIndex, endIndex, create) {
|
||||
/** @type {Array<Array<CodePathSegment>>} */
|
||||
const list = context.segmentsList;
|
||||
|
||||
/*
|
||||
* 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 = [];
|
||||
|
||||
for (let j = normalizedBegin; j <= normalizedEnd; ++j) {
|
||||
allPrevSegments.push(list[j][i]);
|
||||
}
|
||||
|
||||
// note: `create` is just a wrapper that augments `new CodePathSegment`.
|
||||
segments.push(create(context.idGenerator.next(), allPrevSegments));
|
||||
}
|
||||
|
||||
return 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.
|
||||
*/
|
||||
function mergeExtraSegments(context, segments) {
|
||||
let currentSegments = 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 = [];
|
||||
|
||||
/*
|
||||
* 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(context.idGenerator.next(), [
|
||||
currentSegments[i],
|
||||
currentSegments[i + length],
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Manages the forking of code paths.
|
||||
*/
|
||||
class ForkContext {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {IdGenerator} idGenerator An identifier generator for 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 = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The segments that begin this fork context.
|
||||
* @type {Array<CodePathSegment>}
|
||||
*/
|
||||
get head() {
|
||||
const list = this.segmentsList;
|
||||
|
||||
return list.length === 0 ? [] : list.at(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the context contains no segments.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get empty() {
|
||||
return this.segmentsList.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if there are any segments that are reachable.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get reachable() {
|
||||
const segments = this.head;
|
||||
|
||||
return segments.length > 0 && segments.some(isReachable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(startIndex, endIndex) {
|
||||
return createSegments(
|
||||
this,
|
||||
startIndex,
|
||||
endIndex,
|
||||
CodePathSegment.newNext,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(startIndex, endIndex) {
|
||||
return createSegments(
|
||||
this,
|
||||
startIndex,
|
||||
endIndex,
|
||||
CodePathSegment.newUnreachable,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(startIndex, endIndex) {
|
||||
return createSegments(
|
||||
this,
|
||||
startIndex,
|
||||
endIndex,
|
||||
CodePathSegment.newDisconnected,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds segments to the head of this context.
|
||||
* @param {Array<CodePathSegment>} segments The segments to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
add(segments) {
|
||||
assert(
|
||||
segments.length >= this.count,
|
||||
`${segments.length} >= ${this.count}`,
|
||||
);
|
||||
this.segmentsList.push(mergeExtraSegments(this, segments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the head segments with the given segments.
|
||||
* The current head segments are removed.
|
||||
* @param {Array<CodePathSegment>} replacementHeadSegments The new head segments.
|
||||
* @returns {void}
|
||||
*/
|
||||
replaceHead(replacementHeadSegments) {
|
||||
assert(
|
||||
replacementHeadSegments.length >= this.count,
|
||||
`${replacementHeadSegments.length} >= ${this.count}`,
|
||||
);
|
||||
this.segmentsList.splice(
|
||||
-1,
|
||||
1,
|
||||
mergeExtraSegments(this, replacementHeadSegments),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all segments of a given fork context into this context.
|
||||
* @param {ForkContext} otherForkContext The fork context to add from.
|
||||
* @returns {void}
|
||||
*/
|
||||
addAll(otherForkContext) {
|
||||
assert(otherForkContext.count === this.count);
|
||||
this.segmentsList.push(...otherForkContext.segmentsList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all segments in this context.
|
||||
* @returns {void}
|
||||
*/
|
||||
clear() {
|
||||
this.segmentsList = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new root context, meaning that there are no parent
|
||||
* fork contexts.
|
||||
* @param {IdGenerator} idGenerator An identifier generator for segments.
|
||||
* @returns {ForkContext} New fork context.
|
||||
*/
|
||||
static newRoot(idGenerator) {
|
||||
const context = new ForkContext(idGenerator, null, 1);
|
||||
|
||||
context.add([CodePathSegment.newRoot(idGenerator.next())]);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty fork context preceded by a given context.
|
||||
* @param {ForkContext} parentContext The parent fork context.
|
||||
* @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, shouldForkLeavingPath) {
|
||||
return new ForkContext(
|
||||
parentContext.idGenerator,
|
||||
parentContext,
|
||||
(shouldForkLeavingPath ? 2 : 1) * parentContext.count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ForkContext;
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @fileoverview A class of identifiers generator for code path segments.
|
||||
*
|
||||
* Each rule uses the identifier of code path segments to store additional
|
||||
* information of the code path.
|
||||
*
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A generator for unique ids.
|
||||
*/
|
||||
class IdGenerator {
|
||||
/**
|
||||
* @param {string} prefix Optional. A prefix of generated ids.
|
||||
*/
|
||||
constructor(prefix) {
|
||||
this.prefix = String(prefix);
|
||||
this.n = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates id.
|
||||
* @returns {string} A generated id.
|
||||
*/
|
||||
next() {
|
||||
this.n = (1 + this.n) | 0;
|
||||
|
||||
/* c8 ignore start */
|
||||
if (this.n < 0) {
|
||||
this.n = 1;
|
||||
} /* c8 ignore stop */
|
||||
|
||||
return this.prefix + this.n;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IdGenerator;
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* @fileoverview ESQuery wrapper for ESLint.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const esquery = require("esquery");
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @typedef {import("esquery").Selector} ESQuerySelector
|
||||
* @typedef {import("esquery").ESQueryOptions} ESQueryOptions
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Classes
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The result of parsing and analyzing an ESQuery selector.
|
||||
*/
|
||||
class ESQueryParsedSelector {
|
||||
/**
|
||||
* The raw selector string that was parsed
|
||||
* @type {string}
|
||||
*/
|
||||
source;
|
||||
|
||||
/**
|
||||
* Whether this selector is an exit selector
|
||||
* @type {boolean}
|
||||
*/
|
||||
isExit;
|
||||
|
||||
/**
|
||||
* An object (from esquery) describing the matching behavior of the selector
|
||||
* @type {ESQuerySelector}
|
||||
*/
|
||||
root;
|
||||
|
||||
/**
|
||||
* The node types that could possibly trigger this selector, or `null` if all node types could trigger it
|
||||
* @type {string[]|null}
|
||||
*/
|
||||
nodeTypes;
|
||||
|
||||
/**
|
||||
* The number of class, pseudo-class, and attribute queries in this selector
|
||||
* @type {number}
|
||||
*/
|
||||
attributeCount;
|
||||
|
||||
/**
|
||||
* The number of identifier queries in this selector
|
||||
* @type {number}
|
||||
*/
|
||||
identifierCount;
|
||||
|
||||
/**
|
||||
* Creates a new parsed selector.
|
||||
* @param {string} source The raw selector string that was parsed
|
||||
* @param {boolean} isExit Whether this selector is an exit selector
|
||||
* @param {ESQuerySelector} root An object (from esquery) describing the matching behavior of the selector
|
||||
* @param {string[]|null} nodeTypes The node types that could possibly trigger this selector, or `null` if all node types could trigger it
|
||||
* @param {number} attributeCount The number of class, pseudo-class, and attribute queries in this selector
|
||||
* @param {number} identifierCount The number of identifier queries in this selector
|
||||
*/
|
||||
constructor(
|
||||
source,
|
||||
isExit,
|
||||
root,
|
||||
nodeTypes,
|
||||
attributeCount,
|
||||
identifierCount,
|
||||
) {
|
||||
this.source = source;
|
||||
this.isExit = isExit;
|
||||
this.root = root;
|
||||
this.nodeTypes = nodeTypes;
|
||||
this.attributeCount = attributeCount;
|
||||
this.identifierCount = identifierCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this selector's specificity to another selector for sorting purposes.
|
||||
* @param {ESQueryParsedSelector} otherSelector The selector to compare against
|
||||
* @returns {number}
|
||||
* a value less than 0 if this selector is less specific than otherSelector
|
||||
* a value greater than 0 if this selector is more specific than otherSelector
|
||||
* a value less than 0 if this selector and otherSelector have the same specificity, and this selector <= otherSelector alphabetically
|
||||
* a value greater than 0 if this selector and otherSelector have the same specificity, and this selector > otherSelector alphabetically
|
||||
*/
|
||||
compare(otherSelector) {
|
||||
return (
|
||||
this.attributeCount - otherSelector.attributeCount ||
|
||||
this.identifierCount - otherSelector.identifierCount ||
|
||||
(this.source <= otherSelector.source ? -1 : 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const selectorCache = new Map();
|
||||
|
||||
/**
|
||||
* Computes the union of one or more arrays
|
||||
* @param {...any[]} arrays One or more arrays to union
|
||||
* @returns {any[]} The union of the input arrays
|
||||
*/
|
||||
function union(...arrays) {
|
||||
return [...new Set(arrays.flat())];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the intersection of one or more arrays
|
||||
* @param {...any[]} arrays One or more arrays to intersect
|
||||
* @returns {any[]} The intersection of the input arrays
|
||||
*/
|
||||
function intersection(...arrays) {
|
||||
if (arrays.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let result = [...new Set(arrays[0])];
|
||||
|
||||
for (const array of arrays.slice(1)) {
|
||||
result = result.filter(x => array.includes(x));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes a parsed selector and returns combined data about it
|
||||
* @param {ESQuerySelector} parsedSelector An object (from esquery) describing the matching behavior of the selector
|
||||
* @returns {{nodeTypes:string[]|null, attributeCount:number, identifierCount:number}} Object containing selector data.
|
||||
*/
|
||||
function analyzeParsedSelector(parsedSelector) {
|
||||
let attributeCount = 0;
|
||||
let identifierCount = 0;
|
||||
|
||||
/**
|
||||
* Analyzes a selector and returns the node types that could possibly trigger it.
|
||||
* @param {ESQuerySelector} selector The selector to analyze.
|
||||
* @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
|
||||
*/
|
||||
function analyzeSelector(selector) {
|
||||
switch (selector.type) {
|
||||
case "identifier":
|
||||
identifierCount++;
|
||||
return [selector.value];
|
||||
|
||||
case "not":
|
||||
selector.selectors.map(analyzeSelector);
|
||||
return null;
|
||||
|
||||
case "matches": {
|
||||
const typesForComponents =
|
||||
selector.selectors.map(analyzeSelector);
|
||||
|
||||
if (typesForComponents.every(Boolean)) {
|
||||
return union(...typesForComponents);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
case "compound": {
|
||||
const typesForComponents = selector.selectors
|
||||
.map(analyzeSelector)
|
||||
.filter(typesForComponent => typesForComponent);
|
||||
|
||||
// If all of the components could match any type, then the compound could also match any type.
|
||||
if (!typesForComponents.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* If at least one of the components could only match a particular type, the compound could only match
|
||||
* the intersection of those types.
|
||||
*/
|
||||
return intersection(...typesForComponents);
|
||||
}
|
||||
|
||||
case "attribute":
|
||||
case "field":
|
||||
case "nth-child":
|
||||
case "nth-last-child":
|
||||
attributeCount++;
|
||||
return null;
|
||||
|
||||
case "child":
|
||||
case "descendant":
|
||||
case "sibling":
|
||||
case "adjacent":
|
||||
analyzeSelector(selector.left);
|
||||
return analyzeSelector(selector.right);
|
||||
|
||||
case "class":
|
||||
// TODO: abstract into JSLanguage somehow
|
||||
if (selector.name === "function") {
|
||||
return [
|
||||
"FunctionDeclaration",
|
||||
"FunctionExpression",
|
||||
"ArrowFunctionExpression",
|
||||
];
|
||||
}
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const nodeTypes = analyzeSelector(parsedSelector);
|
||||
|
||||
return {
|
||||
nodeTypes,
|
||||
attributeCount,
|
||||
identifierCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse a simple selector string, such as a single identifier or wildcard.
|
||||
* This saves time by avoiding the overhead of esquery parsing for simple cases.
|
||||
* @param {string} selector The selector string to parse.
|
||||
* @returns {Object|null} An object describing the selector if it is simple, or `null` if it is not.
|
||||
*/
|
||||
function trySimpleParseSelector(selector) {
|
||||
if (selector === "*") {
|
||||
return {
|
||||
type: "wildcard",
|
||||
value: "*",
|
||||
};
|
||||
}
|
||||
|
||||
if (/^[a-z]+$/iu.test(selector)) {
|
||||
return {
|
||||
type: "identifier",
|
||||
value: selector,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a raw selector string, and throws a useful error if parsing fails.
|
||||
* @param {string} selector The selector string to parse.
|
||||
* @returns {Object} An object (from esquery) describing the matching behavior of this selector
|
||||
* @throws {Error} An error if the selector is invalid
|
||||
*/
|
||||
function tryParseSelector(selector) {
|
||||
try {
|
||||
return esquery.parse(selector);
|
||||
} catch (err) {
|
||||
if (
|
||||
err.location &&
|
||||
err.location.start &&
|
||||
typeof err.location.start.offset === "number"
|
||||
) {
|
||||
throw new SyntaxError(
|
||||
`Syntax error in selector "${selector}" at position ${err.location.start.offset}: ${err.message}`,
|
||||
{
|
||||
cause: err,
|
||||
},
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a raw selector string, and returns the parsed selector along with specificity and type information.
|
||||
* @param {string} source A raw AST selector
|
||||
* @returns {ESQueryParsedSelector} A selector descriptor
|
||||
*/
|
||||
function parse(source) {
|
||||
if (selectorCache.has(source)) {
|
||||
return selectorCache.get(source);
|
||||
}
|
||||
|
||||
const cleanSource = source.replace(/:exit$/u, "");
|
||||
const parsedSelector =
|
||||
trySimpleParseSelector(cleanSource) ?? tryParseSelector(cleanSource);
|
||||
const { nodeTypes, attributeCount, identifierCount } =
|
||||
analyzeParsedSelector(parsedSelector);
|
||||
|
||||
const result = new ESQueryParsedSelector(
|
||||
source,
|
||||
source.endsWith(":exit"),
|
||||
parsedSelector,
|
||||
nodeTypes,
|
||||
attributeCount,
|
||||
identifierCount,
|
||||
);
|
||||
|
||||
selectorCache.set(source, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node matches a given selector.
|
||||
* @param {Object} node The node to check against the selector.
|
||||
* @param {ESQuerySelector} root The root of the selector to match against.
|
||||
* @param {Object[]} ancestry The ancestry of the node being checked, which is an array of nodes from the current node to the root.
|
||||
* @param {ESQueryOptions} options The options to use for matching.
|
||||
* @returns {boolean} `true` if the node matches the selector, `false` otherwise.
|
||||
*/
|
||||
function matches(node, root, ancestry, options) {
|
||||
return esquery.matches(node, root, ancestry, options);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
parse,
|
||||
matches,
|
||||
ESQueryParsedSelector,
|
||||
};
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @fileoverview The FileContext class.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Represents a file context that the linter can use to lint a file.
|
||||
*/
|
||||
class FileContext {
|
||||
/**
|
||||
* The current working directory.
|
||||
* @type {string}
|
||||
*/
|
||||
cwd;
|
||||
|
||||
/**
|
||||
* The filename of the file being linted.
|
||||
* @type {string}
|
||||
*/
|
||||
filename;
|
||||
|
||||
/**
|
||||
* The physical filename of the file being linted.
|
||||
* @type {string}
|
||||
*/
|
||||
physicalFilename;
|
||||
|
||||
/**
|
||||
* The source code of the file being linted.
|
||||
* @type {SourceCode}
|
||||
*/
|
||||
sourceCode;
|
||||
|
||||
/**
|
||||
* The parser options for the file being linted.
|
||||
* @type {Record<string, unknown>}
|
||||
* @deprecated Use `languageOptions` instead.
|
||||
*/
|
||||
parserOptions;
|
||||
|
||||
/**
|
||||
* The path to the parser used to parse this file.
|
||||
* @type {string}
|
||||
* @deprecated No longer supported.
|
||||
*/
|
||||
parserPath;
|
||||
|
||||
/**
|
||||
* The language options used when parsing this file.
|
||||
* @type {Record<string, unknown>}
|
||||
*/
|
||||
languageOptions;
|
||||
|
||||
/**
|
||||
* The settings for the file being linted.
|
||||
* @type {Record<string, unknown>}
|
||||
*/
|
||||
settings;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} config The configuration object for the file context.
|
||||
* @param {string} config.cwd The current working directory.
|
||||
* @param {string} config.filename The filename of the file being linted.
|
||||
* @param {string} config.physicalFilename The physical filename of the file being linted.
|
||||
* @param {SourceCode} config.sourceCode The source code of the file being linted.
|
||||
* @param {Record<string, unknown>} config.parserOptions The parser options for the file being linted.
|
||||
* @param {string} config.parserPath The path to the parser used to parse this file.
|
||||
* @param {Record<string, unknown>} config.languageOptions The language options used when parsing this file.
|
||||
* @param {Record<string, unknown>} config.settings The settings for the file being linted.
|
||||
*/
|
||||
constructor({
|
||||
cwd,
|
||||
filename,
|
||||
physicalFilename,
|
||||
sourceCode,
|
||||
parserOptions,
|
||||
parserPath,
|
||||
languageOptions,
|
||||
settings,
|
||||
}) {
|
||||
this.cwd = cwd;
|
||||
this.filename = filename;
|
||||
this.physicalFilename = physicalFilename;
|
||||
this.sourceCode = sourceCode;
|
||||
this.parserOptions = parserOptions;
|
||||
this.parserPath = parserPath;
|
||||
this.languageOptions = languageOptions;
|
||||
this.settings = settings;
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current working directory.
|
||||
* @returns {string} The current working directory.
|
||||
* @deprecated Use `cwd` instead.
|
||||
*/
|
||||
getCwd() {
|
||||
return this.cwd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filename of the file being linted.
|
||||
* @returns {string} The filename of the file being linted.
|
||||
* @deprecated Use `filename` instead.
|
||||
*/
|
||||
getFilename() {
|
||||
return this.filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the physical filename of the file being linted.
|
||||
* @returns {string} The physical filename of the file being linted.
|
||||
* @deprecated Use `physicalFilename` instead.
|
||||
*/
|
||||
getPhysicalFilename() {
|
||||
return this.physicalFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the source code of the file being linted.
|
||||
* @returns {SourceCode} The source code of the file being linted.
|
||||
* @deprecated Use `sourceCode` instead.
|
||||
*/
|
||||
getSourceCode() {
|
||||
return this.sourceCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new object with the current object as the prototype and
|
||||
* the specified properties as its own properties.
|
||||
* @param {Object} extension The properties to add to the new object.
|
||||
* @returns {FileContext} A new object with the current object as the prototype
|
||||
* and the specified properties as its own properties.
|
||||
*/
|
||||
extend(extension) {
|
||||
return Object.freeze(Object.assign(Object.create(this), extension));
|
||||
}
|
||||
}
|
||||
|
||||
exports.FileContext = FileContext;
|
||||
+608
File diff suppressed because it is too large
Load Diff
+11
@@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
const { Linter } = require("./linter");
|
||||
const SourceCodeFixer = require("./source-code-fixer");
|
||||
|
||||
module.exports = {
|
||||
Linter,
|
||||
|
||||
// For testers.
|
||||
SourceCodeFixer,
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @fileoverview Interpolate keys from an object into a string with {{ }} markers.
|
||||
* @author Jed Fox
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns a global expression matching placeholders in messages.
|
||||
* @returns {RegExp} Global regular expression matching placeholders
|
||||
*/
|
||||
function getPlaceholderMatcher() {
|
||||
return /\{\{([^{}]+)\}\}/gu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces {{ placeholders }} in the message with the provided data.
|
||||
* Does not replace placeholders not available in the data.
|
||||
* @param {string} text Original message with potential placeholders
|
||||
* @param {Record<string, string>} data Map of placeholder name to its value
|
||||
* @returns {string} Message with replaced placeholders
|
||||
*/
|
||||
function interpolate(text, data) {
|
||||
if (!data) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const matcher = getPlaceholderMatcher();
|
||||
|
||||
// Substitution content for any {{ }} markers.
|
||||
return text.replace(matcher, (fullMatch, termWithWhitespace) => {
|
||||
const term = termWithWhitespace.trim();
|
||||
|
||||
if (term in data) {
|
||||
return data[term];
|
||||
}
|
||||
|
||||
// Preserve old behavior: If parameter name not provided, don't replace it.
|
||||
return fullMatch;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPlaceholderMatcher,
|
||||
interpolate,
|
||||
};
|
||||
+2676
File diff suppressed because it is too large
Load Diff
+169
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @fileoverview An object that creates fix commands for rules.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @import { SourceRange } from "@eslint/core";
|
||||
*/
|
||||
|
||||
/* eslint class-methods-use-this: off -- Methods desired on instance */
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// none!
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a fix command that inserts text at the specified index in the source text.
|
||||
* @param {number} index The 0-based index at which to insert the new text.
|
||||
* @param {string} text The text to insert.
|
||||
* @returns {Object} The fix command.
|
||||
* @private
|
||||
*/
|
||||
function insertTextAt(index, text) {
|
||||
return {
|
||||
range: [index, index],
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates code fixing commands for rules.
|
||||
*/
|
||||
class RuleFixer {
|
||||
/**
|
||||
* The source code object representing the text to be fixed.
|
||||
* @type {SourceCode}
|
||||
*/
|
||||
#sourceCode;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the fixer.
|
||||
* @param {SourceCode} options.sourceCode The source code object representing the text to be fixed.
|
||||
*/
|
||||
constructor({ sourceCode }) {
|
||||
this.#sourceCode = sourceCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fix command that inserts text after the given node or token.
|
||||
* The fix is not applied until applyFixes() is called.
|
||||
* @param {ASTNode|Token} nodeOrToken The node or token to insert after.
|
||||
* @param {string} text The text to insert.
|
||||
* @returns {Object} The fix command.
|
||||
*/
|
||||
insertTextAfter(nodeOrToken, text) {
|
||||
const range = this.#sourceCode.getRange(nodeOrToken);
|
||||
|
||||
return this.insertTextAfterRange(range, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fix command that inserts text after the specified range in the source text.
|
||||
* The fix is not applied until applyFixes() is called.
|
||||
* @param {SourceRange} range The range to replace, first item is start of range, second
|
||||
* is end of range.
|
||||
* @param {string} text The text to insert.
|
||||
* @returns {Object} The fix command.
|
||||
*/
|
||||
insertTextAfterRange(range, text) {
|
||||
return insertTextAt(range[1], text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fix command that inserts text before the given node or token.
|
||||
* The fix is not applied until applyFixes() is called.
|
||||
* @param {ASTNode|Token} nodeOrToken The node or token to insert before.
|
||||
* @param {string} text The text to insert.
|
||||
* @returns {Object} The fix command.
|
||||
*/
|
||||
insertTextBefore(nodeOrToken, text) {
|
||||
const range = this.#sourceCode.getRange(nodeOrToken);
|
||||
|
||||
return this.insertTextBeforeRange(range, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fix command that inserts text before the specified range in the source text.
|
||||
* The fix is not applied until applyFixes() is called.
|
||||
* @param {SourceRange} range The range to replace, first item is start of range, second
|
||||
* is end of range.
|
||||
* @param {string} text The text to insert.
|
||||
* @returns {Object} The fix command.
|
||||
*/
|
||||
insertTextBeforeRange(range, text) {
|
||||
return insertTextAt(range[0], text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fix command that replaces text at the node or token.
|
||||
* The fix is not applied until applyFixes() is called.
|
||||
* @param {ASTNode|Token} nodeOrToken The node or token to remove.
|
||||
* @param {string} text The text to insert.
|
||||
* @returns {Object} The fix command.
|
||||
*/
|
||||
replaceText(nodeOrToken, text) {
|
||||
const range = this.#sourceCode.getRange(nodeOrToken);
|
||||
|
||||
return this.replaceTextRange(range, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fix command that replaces text at the specified range in the source text.
|
||||
* The fix is not applied until applyFixes() is called.
|
||||
* @param {SourceRange} range The range to replace, first item is start of range, second
|
||||
* is end of range.
|
||||
* @param {string} text The text to insert.
|
||||
* @returns {Object} The fix command.
|
||||
*/
|
||||
replaceTextRange(range, text) {
|
||||
return {
|
||||
range,
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fix command that removes the node or token from the source.
|
||||
* The fix is not applied until applyFixes() is called.
|
||||
* @param {ASTNode|Token} nodeOrToken The node or token to remove.
|
||||
* @returns {Object} The fix command.
|
||||
*/
|
||||
remove(nodeOrToken) {
|
||||
const range = this.#sourceCode.getRange(nodeOrToken);
|
||||
|
||||
return this.removeRange(range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fix command that removes the specified range of text from the source.
|
||||
* The fix is not applied until applyFixes() is called.
|
||||
* @param {SourceRange} range The range to remove, first item is start of range, second
|
||||
* is end of range.
|
||||
* @returns {Object} The fix command.
|
||||
*/
|
||||
removeRange(range) {
|
||||
return {
|
||||
range,
|
||||
text: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RuleFixer };
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @fileoverview Defines a storage for rules.
|
||||
* @author Nicholas C. Zakas
|
||||
* @author aladdin-add
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const builtInRules = require("../rules");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @typedef {import("../types").Rule.RuleModule} Rule */
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A storage for rules.
|
||||
*/
|
||||
class Rules {
|
||||
constructor() {
|
||||
this._rules = Object.create(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a rule module for rule id in storage.
|
||||
* @param {string} ruleId Rule id (file name).
|
||||
* @param {Rule} rule Rule object.
|
||||
* @returns {void}
|
||||
*/
|
||||
define(ruleId, rule) {
|
||||
this._rules[ruleId] = rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access rule handler by id (file name).
|
||||
* @param {string} ruleId Rule id (file name).
|
||||
* @returns {Rule} Rule object.
|
||||
*/
|
||||
get(ruleId) {
|
||||
if (typeof this._rules[ruleId] === "string") {
|
||||
this.define(ruleId, require(this._rules[ruleId]));
|
||||
}
|
||||
if (this._rules[ruleId]) {
|
||||
return this._rules[ruleId];
|
||||
}
|
||||
if (builtInRules.has(ruleId)) {
|
||||
return builtInRules.get(ruleId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
yield* builtInRules;
|
||||
|
||||
for (const ruleId of Object.keys(this._rules)) {
|
||||
yield [ruleId, this.get(ruleId)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Rules;
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @fileoverview An object that caches and applies source code fixes.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const debug = require("debug")("eslint:source-code-fixer");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const BOM = "\uFEFF";
|
||||
|
||||
/**
|
||||
* Compares items in a messages array by range.
|
||||
* @param {Message} a The first message.
|
||||
* @param {Message} b The second message.
|
||||
* @returns {number} -1 if a comes before b, 1 if a comes after b, 0 if equal.
|
||||
* @private
|
||||
*/
|
||||
function compareMessagesByFixRange(a, b) {
|
||||
return a.fix.range[0] - b.fix.range[0] || a.fix.range[1] - b.fix.range[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares items in a messages array by line and column.
|
||||
* @param {Message} a The first message.
|
||||
* @param {Message} b The second message.
|
||||
* @returns {number} -1 if a comes before b, 1 if a comes after b, 0 if equal.
|
||||
* @private
|
||||
*/
|
||||
function compareMessagesByLocation(a, b) {
|
||||
return a.line - b.line || a.column - b.column;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Utility for apply fixes to source code.
|
||||
* @constructor
|
||||
*/
|
||||
function SourceCodeFixer() {
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the fixes specified by the messages to the given text. Tries to be
|
||||
* smart about the fixes and won't apply fixes over the same area in the text.
|
||||
* @param {string} sourceText The text to apply the changes to.
|
||||
* @param {Message[]} messages The array of messages reported by ESLint.
|
||||
* @param {boolean|Function} [shouldFix=true] Determines whether each message should be fixed
|
||||
* @returns {Object} An object containing the fixed text and any unfixed messages.
|
||||
*/
|
||||
SourceCodeFixer.applyFixes = function (sourceText, messages, shouldFix) {
|
||||
debug("Applying fixes");
|
||||
|
||||
if (shouldFix === false) {
|
||||
debug("shouldFix parameter was false, not attempting fixes");
|
||||
return {
|
||||
fixed: false,
|
||||
messages,
|
||||
output: sourceText,
|
||||
};
|
||||
}
|
||||
|
||||
// clone the array
|
||||
const remainingMessages = [],
|
||||
fixes = [],
|
||||
bom = sourceText.startsWith(BOM) ? BOM : "",
|
||||
text = bom ? sourceText.slice(1) : sourceText;
|
||||
let lastPos = Number.NEGATIVE_INFINITY,
|
||||
output = bom;
|
||||
|
||||
/**
|
||||
* Try to use the 'fix' from a problem.
|
||||
* @param {Message} problem The message object to apply fixes from
|
||||
* @returns {boolean} Whether fix was successfully applied
|
||||
*/
|
||||
function attemptFix(problem) {
|
||||
const fix = problem.fix;
|
||||
const start = fix.range[0];
|
||||
const end = fix.range[1];
|
||||
|
||||
// Remain it as a problem if it's overlapped or it's a negative range
|
||||
if (lastPos >= start || start > end) {
|
||||
remainingMessages.push(problem);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove BOM.
|
||||
if (
|
||||
(start < 0 && end >= 0) ||
|
||||
(start === 0 && fix.text.startsWith(BOM))
|
||||
) {
|
||||
output = "";
|
||||
}
|
||||
|
||||
// Make output to this fix.
|
||||
output += text.slice(Math.max(0, lastPos), Math.max(0, start));
|
||||
output += fix.text;
|
||||
lastPos = end;
|
||||
return true;
|
||||
}
|
||||
|
||||
messages.forEach(problem => {
|
||||
if (Object.hasOwn(problem, "fix") && problem.fix) {
|
||||
fixes.push(problem);
|
||||
} else {
|
||||
remainingMessages.push(problem);
|
||||
}
|
||||
});
|
||||
|
||||
if (fixes.length) {
|
||||
debug("Found fixes to apply");
|
||||
let fixesWereApplied = false;
|
||||
|
||||
for (const problem of fixes.sort(compareMessagesByFixRange)) {
|
||||
if (typeof shouldFix !== "function" || shouldFix(problem)) {
|
||||
attemptFix(problem);
|
||||
|
||||
/*
|
||||
* The only time attemptFix will fail is if a previous fix was
|
||||
* applied which conflicts with it. So we can mark this as true.
|
||||
*/
|
||||
fixesWereApplied = true;
|
||||
} else {
|
||||
remainingMessages.push(problem);
|
||||
}
|
||||
}
|
||||
output += text.slice(Math.max(0, lastPos));
|
||||
|
||||
return {
|
||||
fixed: fixesWereApplied,
|
||||
messages: remainingMessages.sort(compareMessagesByLocation),
|
||||
output,
|
||||
};
|
||||
}
|
||||
|
||||
debug("No fixes to apply");
|
||||
return {
|
||||
fixed: false,
|
||||
messages,
|
||||
output: bom + text,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = SourceCodeFixer;
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* @fileoverview Traverser for SourceCode objects.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const { parse, matches } = require("./esquery");
|
||||
const vk = require("eslint-visitor-keys");
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @import { Language, SourceCode } from "@eslint/core";
|
||||
* @import { ESQueryOptions } from "esquery";
|
||||
* @import { ESQueryParsedSelector } from "./esquery.js";
|
||||
* @import { SourceCodeVisitor } from "./source-code-visitor.js";
|
||||
*/
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const STEP_KIND_VISIT = 1;
|
||||
const STEP_KIND_CALL = 2;
|
||||
|
||||
/**
|
||||
* Compares two ESQuery selectors by specificity.
|
||||
* @param {ESQueryParsedSelector} a The first selector to compare.
|
||||
* @param {ESQueryParsedSelector} b The second selector to compare.
|
||||
* @returns {number} A negative number if `a` is less specific than `b` or they are equally specific and `a` <= `b` alphabetically, a positive number if `a` is more specific than `b`.
|
||||
*/
|
||||
function compareSpecificity(a, b) {
|
||||
return a.compare(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to wrap ESQuery operations.
|
||||
*/
|
||||
class ESQueryHelper {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {SourceCodeVisitor} visitor The visitor containing the functions to call.
|
||||
* @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
|
||||
*/
|
||||
constructor(visitor, esqueryOptions) {
|
||||
/**
|
||||
* The visitor to use during traversal.
|
||||
* @type {SourceCodeVisitor}
|
||||
*/
|
||||
this.visitor = visitor;
|
||||
|
||||
/**
|
||||
* The options for `esquery` to use during matching.
|
||||
* @type {ESQueryOptions}
|
||||
*/
|
||||
this.esqueryOptions = esqueryOptions;
|
||||
|
||||
/**
|
||||
* A map of node type to selectors targeting that node type on the
|
||||
* enter phase of traversal.
|
||||
* @type {Map<string, ESQueryParsedSelector[]>}
|
||||
*/
|
||||
this.enterSelectorsByNodeType = new Map();
|
||||
|
||||
/**
|
||||
* A map of node type to selectors targeting that node type on the
|
||||
* exit phase of traversal.
|
||||
* @type {Map<string, ESQueryParsedSelector[]>}
|
||||
*/
|
||||
this.exitSelectorsByNodeType = new Map();
|
||||
|
||||
/**
|
||||
* An array of selectors that match any node type on the
|
||||
* enter phase of traversal.
|
||||
* @type {ESQueryParsedSelector[]}
|
||||
*/
|
||||
this.anyTypeEnterSelectors = [];
|
||||
|
||||
/**
|
||||
* An array of selectors that match any node type on the
|
||||
* exit phase of traversal.
|
||||
* @type {ESQueryParsedSelector[]}
|
||||
*/
|
||||
this.anyTypeExitSelectors = [];
|
||||
|
||||
visitor.forEachName(rawSelector => {
|
||||
const selector = parse(rawSelector);
|
||||
|
||||
/*
|
||||
* If this selector has identified specific node types,
|
||||
* add it to the map for these node types for faster lookup.
|
||||
*/
|
||||
if (selector.nodeTypes) {
|
||||
const typeMap = selector.isExit
|
||||
? this.exitSelectorsByNodeType
|
||||
: this.enterSelectorsByNodeType;
|
||||
|
||||
selector.nodeTypes.forEach(nodeType => {
|
||||
if (!typeMap.has(nodeType)) {
|
||||
typeMap.set(nodeType, []);
|
||||
}
|
||||
typeMap.get(nodeType).push(selector);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remaining selectors are added to the "any type" selectors
|
||||
* list for the appropriate phase of traversal. This ensures
|
||||
* that all selectors will still be applied even if no
|
||||
* specific node type is matched.
|
||||
*/
|
||||
const selectors = selector.isExit
|
||||
? this.anyTypeExitSelectors
|
||||
: this.anyTypeEnterSelectors;
|
||||
|
||||
selectors.push(selector);
|
||||
});
|
||||
|
||||
// sort all selectors by specificity for prioritizing call order
|
||||
this.anyTypeEnterSelectors.sort(compareSpecificity);
|
||||
this.anyTypeExitSelectors.sort(compareSpecificity);
|
||||
this.enterSelectorsByNodeType.forEach(selectorList =>
|
||||
selectorList.sort(compareSpecificity),
|
||||
);
|
||||
this.exitSelectorsByNodeType.forEach(selectorList =>
|
||||
selectorList.sort(compareSpecificity),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node matches a given selector.
|
||||
* @param {ASTNode} node The node to check
|
||||
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
|
||||
* @param {ESQueryParsedSelector} selector An AST selector descriptor
|
||||
* @returns {boolean} `true` if the selector matches the node, `false` otherwise
|
||||
*/
|
||||
matches(node, ancestry, selector) {
|
||||
return matches(node, selector.root, ancestry, this.esqueryOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates all appropriate selectors to a node, in specificity order
|
||||
* @param {ASTNode} node The node to check
|
||||
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
|
||||
* @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
|
||||
* @returns {string[]} An array of selectors that match the node.
|
||||
*/
|
||||
calculateSelectors(node, ancestry, isExit) {
|
||||
const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type";
|
||||
const selectors = [];
|
||||
|
||||
/*
|
||||
* Get the selectors that may match this node. First, check
|
||||
* to see if the node type has specific selectors,
|
||||
* then gather the "any type" selectors.
|
||||
*/
|
||||
const selectorsByNodeType =
|
||||
(isExit
|
||||
? this.exitSelectorsByNodeType
|
||||
: this.enterSelectorsByNodeType
|
||||
).get(node[nodeTypeKey]) || [];
|
||||
const anyTypeSelectors = isExit
|
||||
? this.anyTypeExitSelectors
|
||||
: this.anyTypeEnterSelectors;
|
||||
|
||||
/*
|
||||
* selectorsByNodeType and anyTypeSelectors were already sorted by specificity in the constructor.
|
||||
* Iterate through each of them, applying selectors in the right order.
|
||||
*/
|
||||
let selectorsByNodeTypeIndex = 0;
|
||||
let anyTypeSelectorsIndex = 0;
|
||||
|
||||
while (
|
||||
selectorsByNodeTypeIndex < selectorsByNodeType.length ||
|
||||
anyTypeSelectorsIndex < anyTypeSelectors.length
|
||||
) {
|
||||
/*
|
||||
* If we've already exhausted the selectors for this node type,
|
||||
* or if the next any type selector is more specific than the
|
||||
* next selector for this node type, apply the any type selector.
|
||||
*/
|
||||
const hasMoreNodeTypeSelectors =
|
||||
selectorsByNodeTypeIndex < selectorsByNodeType.length;
|
||||
const hasMoreAnyTypeSelectors =
|
||||
anyTypeSelectorsIndex < anyTypeSelectors.length;
|
||||
const anyTypeSelector = anyTypeSelectors[anyTypeSelectorsIndex];
|
||||
const nodeTypeSelector =
|
||||
selectorsByNodeType[selectorsByNodeTypeIndex];
|
||||
|
||||
// Only compare specificity if both selectors exist
|
||||
const isAnyTypeSelectorLessSpecific =
|
||||
hasMoreAnyTypeSelectors &&
|
||||
hasMoreNodeTypeSelectors &&
|
||||
anyTypeSelector.compare(nodeTypeSelector) < 0;
|
||||
|
||||
if (!hasMoreNodeTypeSelectors || isAnyTypeSelectorLessSpecific) {
|
||||
anyTypeSelectorsIndex++;
|
||||
|
||||
if (this.matches(node, ancestry, anyTypeSelector)) {
|
||||
selectors.push(anyTypeSelector.source);
|
||||
}
|
||||
} else {
|
||||
selectorsByNodeTypeIndex++;
|
||||
|
||||
if (this.matches(node, ancestry, nodeTypeSelector)) {
|
||||
selectors.push(nodeTypeSelector.source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectors;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Traverses source code and ensures that visitor methods are called when
|
||||
* entering and leaving each node.
|
||||
*/
|
||||
class SourceCodeTraverser {
|
||||
/**
|
||||
* The language of the source code being traversed.
|
||||
* @type {Language}
|
||||
*/
|
||||
#language;
|
||||
|
||||
/**
|
||||
* Map of languages to instances of this class.
|
||||
* @type {WeakMap<Language, SourceCodeTraverser>}
|
||||
*/
|
||||
static instances = new WeakMap();
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Language} language The language of the source code being traversed.
|
||||
*/
|
||||
constructor(language) {
|
||||
this.#language = language;
|
||||
}
|
||||
|
||||
static getInstance(language) {
|
||||
if (!this.instances.has(language)) {
|
||||
this.instances.set(language, new this(language));
|
||||
}
|
||||
|
||||
return this.instances.get(language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses the given source code synchronously.
|
||||
* @param {SourceCode} sourceCode The source code to traverse.
|
||||
* @param {SourceCodeVisitor} visitor The emitter to use for events.
|
||||
* @param {Object} options Options for traversal.
|
||||
* @param {ReturnType<SourceCode["traverse"]>} options.steps The steps to take during traversal.
|
||||
* @returns {void}
|
||||
* @throws {Error} If an error occurs during traversal.
|
||||
*/
|
||||
traverseSync(sourceCode, visitor, { steps } = {}) {
|
||||
const esquery = new ESQueryHelper(visitor, {
|
||||
visitorKeys: sourceCode.visitorKeys ?? this.#language.visitorKeys,
|
||||
fallback: vk.getKeys,
|
||||
matchClass: this.#language.matchesSelectorClass ?? (() => false),
|
||||
nodeTypeKey: this.#language.nodeTypeKey,
|
||||
});
|
||||
|
||||
const currentAncestry = [];
|
||||
|
||||
for (const step of steps ?? sourceCode.traverse()) {
|
||||
switch (step.kind) {
|
||||
case STEP_KIND_VISIT: {
|
||||
try {
|
||||
if (step.phase === 1) {
|
||||
esquery
|
||||
.calculateSelectors(
|
||||
step.target,
|
||||
currentAncestry,
|
||||
false,
|
||||
)
|
||||
.forEach(selector => {
|
||||
visitor.callSync(
|
||||
selector,
|
||||
...(step.args ?? [step.target]),
|
||||
);
|
||||
});
|
||||
currentAncestry.unshift(step.target);
|
||||
} else {
|
||||
currentAncestry.shift();
|
||||
esquery
|
||||
.calculateSelectors(
|
||||
step.target,
|
||||
currentAncestry,
|
||||
true,
|
||||
)
|
||||
.forEach(selector => {
|
||||
visitor.callSync(
|
||||
selector,
|
||||
...(step.args ?? [step.target]),
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
err.currentNode = step.target;
|
||||
throw err;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case STEP_KIND_CALL: {
|
||||
visitor.callSync(step.target, ...step.args);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid traversal step found: "${step.kind}".`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SourceCodeTraverser };
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @fileoverview SourceCodeVisitor class
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const emptyArray = Object.freeze([]);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Exports
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A structure to hold a list of functions to call for a given name.
|
||||
* This is used to allow multiple rules to register functions for a given name
|
||||
* without having to know about each other.
|
||||
*/
|
||||
class SourceCodeVisitor {
|
||||
/**
|
||||
* The functions to call for a given name.
|
||||
* @type {Map<string, Function[]>}
|
||||
*/
|
||||
#functions = new Map();
|
||||
|
||||
/**
|
||||
* Adds a function to the list of functions to call for a given name.
|
||||
* @param {string} name The name of the function to call.
|
||||
* @param {Function} func The function to call.
|
||||
* @returns {void}
|
||||
*/
|
||||
add(name, func) {
|
||||
if (this.#functions.has(name)) {
|
||||
this.#functions.get(name).push(func);
|
||||
} else {
|
||||
this.#functions.set(name, [func]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of functions to call for a given name.
|
||||
* @param {string} name The name of the function to call.
|
||||
* @returns {Function[]} The list of functions to call.
|
||||
*/
|
||||
get(name) {
|
||||
if (this.#functions.has(name)) {
|
||||
return this.#functions.get(name);
|
||||
}
|
||||
|
||||
return emptyArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all names and calls the callback with the name.
|
||||
* @param {(name:string) => void} callback The callback to call for each name.
|
||||
* @returns {void}
|
||||
*/
|
||||
forEachName(callback) {
|
||||
this.#functions.forEach((funcs, name) => {
|
||||
callback(name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the functions for a given name with the given arguments.
|
||||
* @param {string} name The name of the function to call.
|
||||
* @param {any[]} args The arguments to pass to the function.
|
||||
* @returns {void}
|
||||
*/
|
||||
callSync(name, ...args) {
|
||||
if (this.#functions.has(name)) {
|
||||
this.#functions.get(name).forEach(func => func(...args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SourceCodeVisitor };
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* @fileoverview Tracks performance of individual rules.
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { startTime, endTime } = require("../shared/stats");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/* c8 ignore next */
|
||||
/**
|
||||
* Align the string to left
|
||||
* @param {string} str string to evaluate
|
||||
* @param {number} len length of the string
|
||||
* @param {string} ch delimiter character
|
||||
* @returns {string} modified string
|
||||
* @private
|
||||
*/
|
||||
function alignLeft(str, len, ch) {
|
||||
return str + new Array(len - str.length + 1).join(ch || " ");
|
||||
}
|
||||
|
||||
/* c8 ignore next */
|
||||
/**
|
||||
* Align the string to right
|
||||
* @param {string} str string to evaluate
|
||||
* @param {number} len length of the string
|
||||
* @param {string} ch delimiter character
|
||||
* @returns {string} modified string
|
||||
* @private
|
||||
*/
|
||||
function alignRight(str, len, ch) {
|
||||
return new Array(len - str.length + 1).join(ch || " ") + str;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Module definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const enabled = !!process.env.TIMING;
|
||||
|
||||
const HEADERS = ["Rule", "Time (ms)", "Relative"];
|
||||
const ALIGN = [alignLeft, alignRight, alignRight];
|
||||
|
||||
/**
|
||||
* Decide how many rules to show in the output list.
|
||||
* @returns {number} the number of rules to show
|
||||
*/
|
||||
function getListSize() {
|
||||
const MINIMUM_SIZE = 10;
|
||||
|
||||
if (typeof process.env.TIMING !== "string") {
|
||||
return MINIMUM_SIZE;
|
||||
}
|
||||
|
||||
if (process.env.TIMING.toLowerCase() === "all") {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
const TIMING_ENV_VAR_AS_INTEGER = Number.parseInt(process.env.TIMING, 10);
|
||||
|
||||
return TIMING_ENV_VAR_AS_INTEGER > 10
|
||||
? TIMING_ENV_VAR_AS_INTEGER
|
||||
: MINIMUM_SIZE;
|
||||
}
|
||||
|
||||
/* c8 ignore next */
|
||||
/**
|
||||
* display the data
|
||||
* @param {Object} data Data object to be displayed
|
||||
* @returns {void} prints modified string with console.log
|
||||
* @private
|
||||
*/
|
||||
function display(data) {
|
||||
let total = 0;
|
||||
const rows = Object.keys(data)
|
||||
.map(key => {
|
||||
const time = data[key];
|
||||
|
||||
total += time;
|
||||
return [key, time];
|
||||
})
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, getListSize());
|
||||
|
||||
rows.forEach(row => {
|
||||
row.push(`${((row[1] * 100) / total).toFixed(1)}%`);
|
||||
row[1] = row[1].toFixed(3);
|
||||
});
|
||||
|
||||
rows.unshift(HEADERS);
|
||||
|
||||
const widths = [];
|
||||
|
||||
rows.forEach(row => {
|
||||
const len = row.length;
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const n = row[i].length;
|
||||
|
||||
if (!widths[i] || n > widths[i]) {
|
||||
widths[i] = n;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const table = rows.map(row =>
|
||||
row.map((cell, index) => ALIGN[index](cell, widths[index])).join(" | "),
|
||||
);
|
||||
|
||||
table.splice(
|
||||
1,
|
||||
0,
|
||||
widths
|
||||
.map((width, index) => {
|
||||
const extraAlignment =
|
||||
index !== 0 && index !== widths.length - 1 ? 2 : 1;
|
||||
|
||||
return ALIGN[index](":", width + extraAlignment, "-");
|
||||
})
|
||||
.join("|"),
|
||||
);
|
||||
|
||||
console.log(table.join("\n")); // eslint-disable-line no-console -- Debugging function
|
||||
}
|
||||
|
||||
/* c8 ignore next */
|
||||
module.exports = (function () {
|
||||
const data = Object.create(null);
|
||||
let displayEnabled = true;
|
||||
|
||||
/**
|
||||
* Time the run
|
||||
* @param {any} key key from the data object
|
||||
* @param {Function} fn function to be called
|
||||
* @param {boolean} stats if 'stats' is true, return the result and the time difference
|
||||
* @returns {Function} function to be executed
|
||||
* @private
|
||||
*/
|
||||
function time(key, fn, stats) {
|
||||
return function (...args) {
|
||||
const t = startTime();
|
||||
const result = fn(...args);
|
||||
const tdiff = endTime(t);
|
||||
|
||||
if (enabled) {
|
||||
if (typeof data[key] === "undefined") {
|
||||
data[key] = 0;
|
||||
}
|
||||
|
||||
data[key] += tdiff;
|
||||
}
|
||||
|
||||
return stats ? { result, tdiff } : result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a shallow copy of the collected timings data.
|
||||
* @returns {Record<string, number>} mapping of ruleId to total time in ms
|
||||
*/
|
||||
function getData() {
|
||||
return { ...data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges rule timing totals collected elsewhere into this process' totals.
|
||||
* @param {Record<string, number>} dataToMerge mapping of ruleId to total time in ms
|
||||
* @returns {void}
|
||||
*/
|
||||
function mergeData(dataToMerge) {
|
||||
for (const [key, value] of Object.entries(dataToMerge)) {
|
||||
if (typeof data[key] === "undefined") {
|
||||
data[key] = 0;
|
||||
}
|
||||
data[key] += value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables printing of timing data on process exit.
|
||||
* Intended for worker threads or non-main contexts.
|
||||
* @returns {void}
|
||||
*/
|
||||
function disableDisplay() {
|
||||
displayEnabled = false;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
process.on("exit", () => {
|
||||
if (displayEnabled && Object.keys(data).length > 0) {
|
||||
display(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
time,
|
||||
enabled,
|
||||
getListSize,
|
||||
getData,
|
||||
mergeData,
|
||||
disableDisplay,
|
||||
};
|
||||
})();
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @fileoverview Virtual file
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/** @typedef {import("@eslint/core").File} File */
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines if a given value has a byte order mark (BOM).
|
||||
* @param {string|Uint8Array} value The value to check.
|
||||
* @returns {boolean} `true` if the value has a BOM, `false` otherwise.
|
||||
*/
|
||||
function hasUnicodeBOM(value) {
|
||||
return typeof value === "string"
|
||||
? value.charCodeAt(0) === 0xfeff
|
||||
: value[0] === 0xef && value[1] === 0xbb && value[2] === 0xbf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips Unicode BOM from the given value.
|
||||
* @param {string|Uint8Array} value The value to remove the BOM from.
|
||||
* @returns {string|Uint8Array} The stripped value.
|
||||
*/
|
||||
function stripUnicodeBOM(value) {
|
||||
if (!hasUnicodeBOM(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
/*
|
||||
* Check Unicode BOM.
|
||||
* In JavaScript, string data is stored as UTF-16, so BOM is 0xFEFF.
|
||||
* http://www.ecma-international.org/ecma-262/6.0/#sec-unicode-format-control-characters
|
||||
*/
|
||||
return value.slice(1);
|
||||
}
|
||||
|
||||
/*
|
||||
* In a Uint8Array, the BOM is represented by three bytes: 0xEF, 0xBB, and 0xBF,
|
||||
* so we can just remove the first three bytes.
|
||||
*/
|
||||
return value.slice(3);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Exports
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Represents a virtual file inside of ESLint.
|
||||
* @implements {File}
|
||||
*/
|
||||
class VFile {
|
||||
/**
|
||||
* The file path including any processor-created virtual path.
|
||||
* @type {string}
|
||||
* @readonly
|
||||
*/
|
||||
path;
|
||||
|
||||
/**
|
||||
* The file path on disk.
|
||||
* @type {string}
|
||||
* @readonly
|
||||
*/
|
||||
physicalPath;
|
||||
|
||||
/**
|
||||
* The file contents.
|
||||
* @type {string|Uint8Array}
|
||||
* @readonly
|
||||
*/
|
||||
body;
|
||||
|
||||
/**
|
||||
* The raw body of the file, including a BOM if present.
|
||||
* @type {string|Uint8Array}
|
||||
* @readonly
|
||||
*/
|
||||
rawBody;
|
||||
|
||||
/**
|
||||
* Indicates whether the file has a byte order mark (BOM).
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
bom;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {string} path The file path.
|
||||
* @param {string|Uint8Array} body The file contents.
|
||||
* @param {Object} [options] Additional options.
|
||||
* @param {string} [options.physicalPath] The file path on disk.
|
||||
*/
|
||||
constructor(path, body, { physicalPath } = {}) {
|
||||
this.path = path;
|
||||
this.physicalPath = physicalPath ?? path;
|
||||
this.bom = hasUnicodeBOM(body);
|
||||
this.body = stripUnicodeBOM(body);
|
||||
this.rawBody = body;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { VFile };
|
||||
Reference in New Issue
Block a user