first
This commit is contained in:
465
node_modules/eslint/lib/linter/apply-disable-directives.js
generated
vendored
Normal file
465
node_modules/eslint/lib/linter/apply-disable-directives.js
generated
vendored
Normal file
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* @fileoverview A module that filters reported problems based on `eslint-disable` and `eslint-enable` comments
|
||||
* @author Teddy Katz
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @typedef {import("../shared/types").LintMessage} LintMessage */
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Module Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const escapeRegExp = require("escape-string-regexp");
|
||||
|
||||
/**
|
||||
* Compares the locations of two objects in a source file
|
||||
* @param {{line: number, column: number}} itemA The first object
|
||||
* @param {{line: number, column: number}} itemB The second object
|
||||
* @returns {number} A value less than 1 if itemA appears before itemB in the source file, greater than 1 if
|
||||
* itemA appears after itemB in the source file, or 0 if itemA and itemB have the same location.
|
||||
*/
|
||||
function compareLocations(itemA, itemB) {
|
||||
return itemA.line - itemB.line || itemA.column - itemB.column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups a set of directives into sub-arrays by their parent comment.
|
||||
* @param {Iterable<Directive>} directives Unused directives to be removed.
|
||||
* @returns {Directive[][]} Directives grouped by their parent comment.
|
||||
*/
|
||||
function groupByParentComment(directives) {
|
||||
const groups = new Map();
|
||||
|
||||
for (const directive of directives) {
|
||||
const { unprocessedDirective: { parentComment } } = directive;
|
||||
|
||||
if (groups.has(parentComment)) {
|
||||
groups.get(parentComment).push(directive);
|
||||
} else {
|
||||
groups.set(parentComment, [directive]);
|
||||
}
|
||||
}
|
||||
|
||||
return [...groups.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates removal details for a set of directives within the same comment.
|
||||
* @param {Directive[]} directives Unused directives to be removed.
|
||||
* @param {Token} commentToken The backing Comment token.
|
||||
* @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
|
||||
*/
|
||||
function createIndividualDirectivesRemoval(directives, commentToken) {
|
||||
|
||||
/*
|
||||
* `commentToken.value` starts right after `//` or `/*`.
|
||||
* All calculated offsets will be relative to this index.
|
||||
*/
|
||||
const commentValueStart = commentToken.range[0] + "//".length;
|
||||
|
||||
// Find where the list of rules starts. `\S+` matches with the directive name (e.g. `eslint-disable-line`)
|
||||
const listStartOffset = /^\s*\S+\s+/u.exec(commentToken.value)[0].length;
|
||||
|
||||
/*
|
||||
* Get the list text without any surrounding whitespace. In order to preserve the original
|
||||
* formatting, we don't want to change that whitespace.
|
||||
*
|
||||
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
|
||||
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
*/
|
||||
const listText = commentToken.value
|
||||
.slice(listStartOffset) // remove directive name and all whitespace before the list
|
||||
.split(/\s-{2,}\s/u)[0] // remove `-- comment`, if it exists
|
||||
.trimEnd(); // remove all whitespace after the list
|
||||
|
||||
/*
|
||||
* We can assume that `listText` contains multiple elements.
|
||||
* Otherwise, this function wouldn't be called - if there is
|
||||
* only one rule in the list, then the whole comment must be removed.
|
||||
*/
|
||||
|
||||
return directives.map(directive => {
|
||||
const { ruleId } = directive;
|
||||
|
||||
const regex = new RegExp(String.raw`(?:^|\s*,\s*)(?<quote>['"]?)${escapeRegExp(ruleId)}\k<quote>(?:\s*,\s*|$)`, "u");
|
||||
const match = regex.exec(listText);
|
||||
const matchedText = match[0];
|
||||
const matchStartOffset = listStartOffset + match.index;
|
||||
const matchEndOffset = matchStartOffset + matchedText.length;
|
||||
|
||||
const firstIndexOfComma = matchedText.indexOf(",");
|
||||
const lastIndexOfComma = matchedText.lastIndexOf(",");
|
||||
|
||||
let removalStartOffset, removalEndOffset;
|
||||
|
||||
if (firstIndexOfComma !== lastIndexOfComma) {
|
||||
|
||||
/*
|
||||
* Since there are two commas, this must one of the elements in the middle of the list.
|
||||
* Matched range starts where the previous rule name ends, and ends where the next rule name starts.
|
||||
*
|
||||
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
|
||||
* ^^^^^^^^^^^^^^
|
||||
*
|
||||
* We want to remove only the content between the two commas, and also one of the commas.
|
||||
*
|
||||
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
|
||||
* ^^^^^^^^^^^
|
||||
*/
|
||||
removalStartOffset = matchStartOffset + firstIndexOfComma;
|
||||
removalEndOffset = matchStartOffset + lastIndexOfComma;
|
||||
|
||||
} else {
|
||||
|
||||
/*
|
||||
* This is either the first element or the last element.
|
||||
*
|
||||
* If this is the first element, matched range starts where the first rule name starts
|
||||
* and ends where the second rule name starts. This is exactly the range we want
|
||||
* to remove so that the second rule name will start where the first one was starting
|
||||
* and thus preserve the original formatting.
|
||||
*
|
||||
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
|
||||
* ^^^^^^^^^^^
|
||||
*
|
||||
* Similarly, if this is the last element, we've already matched the range we want to
|
||||
* remove. The previous rule name will end where the last one was ending, relative
|
||||
* to the content on the right side.
|
||||
*
|
||||
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
|
||||
* ^^^^^^^^^^^^^
|
||||
*/
|
||||
removalStartOffset = matchStartOffset;
|
||||
removalEndOffset = matchEndOffset;
|
||||
}
|
||||
|
||||
return {
|
||||
description: `'${ruleId}'`,
|
||||
fix: {
|
||||
range: [
|
||||
commentValueStart + removalStartOffset,
|
||||
commentValueStart + removalEndOffset
|
||||
],
|
||||
text: ""
|
||||
},
|
||||
unprocessedDirective: directive.unprocessedDirective
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a description of deleting an entire unused disable comment.
|
||||
* @param {Directive[]} directives Unused directives to be removed.
|
||||
* @param {Token} commentToken The backing Comment token.
|
||||
* @returns {{ description, fix, unprocessedDirective }} Details for later creation of an output Problem.
|
||||
*/
|
||||
function createCommentRemoval(directives, commentToken) {
|
||||
const { range } = commentToken;
|
||||
const ruleIds = directives.filter(directive => directive.ruleId).map(directive => `'${directive.ruleId}'`);
|
||||
|
||||
return {
|
||||
description: ruleIds.length <= 2
|
||||
? ruleIds.join(" or ")
|
||||
: `${ruleIds.slice(0, ruleIds.length - 1).join(", ")}, or ${ruleIds[ruleIds.length - 1]}`,
|
||||
fix: {
|
||||
range,
|
||||
text: " "
|
||||
},
|
||||
unprocessedDirective: directives[0].unprocessedDirective
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses details from directives to create output Problems.
|
||||
* @param {Iterable<Directive>} allDirectives Unused directives to be removed.
|
||||
* @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
|
||||
*/
|
||||
function processUnusedDirectives(allDirectives) {
|
||||
const directiveGroups = groupByParentComment(allDirectives);
|
||||
|
||||
return directiveGroups.flatMap(
|
||||
directives => {
|
||||
const { parentComment } = directives[0].unprocessedDirective;
|
||||
const remainingRuleIds = new Set(parentComment.ruleIds);
|
||||
|
||||
for (const directive of directives) {
|
||||
remainingRuleIds.delete(directive.ruleId);
|
||||
}
|
||||
|
||||
return remainingRuleIds.size
|
||||
? createIndividualDirectivesRemoval(directives, parentComment.commentToken)
|
||||
: [createCommentRemoval(directives, parentComment.commentToken)];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect eslint-enable comments that are removing suppressions by eslint-disable comments.
|
||||
* @param {Directive[]} directives The directives to check.
|
||||
* @returns {Set<Directive>} The used eslint-enable comments
|
||||
*/
|
||||
function collectUsedEnableDirectives(directives) {
|
||||
|
||||
/**
|
||||
* A Map of `eslint-enable` keyed by ruleIds that may be marked as used.
|
||||
* If `eslint-enable` does not have a ruleId, the key will be `null`.
|
||||
* @type {Map<string|null, Directive>}
|
||||
*/
|
||||
const enabledRules = new Map();
|
||||
|
||||
/**
|
||||
* A Set of `eslint-enable` marked as used.
|
||||
* It is also the return value of `collectUsedEnableDirectives` function.
|
||||
* @type {Set<Directive>}
|
||||
*/
|
||||
const usedEnableDirectives = new Set();
|
||||
|
||||
/*
|
||||
* Checks the directives backwards to see if the encountered `eslint-enable` is used by the previous `eslint-disable`,
|
||||
* and if so, stores the `eslint-enable` in `usedEnableDirectives`.
|
||||
*/
|
||||
for (let index = directives.length - 1; index >= 0; index--) {
|
||||
const directive = directives[index];
|
||||
|
||||
if (directive.type === "disable") {
|
||||
if (enabledRules.size === 0) {
|
||||
continue;
|
||||
}
|
||||
if (directive.ruleId === null) {
|
||||
|
||||
// If encounter `eslint-disable` without ruleId,
|
||||
// mark all `eslint-enable` currently held in enabledRules as used.
|
||||
// e.g.
|
||||
// /* eslint-disable */ <- current directive
|
||||
// /* eslint-enable rule-id1 */ <- used
|
||||
// /* eslint-enable rule-id2 */ <- used
|
||||
// /* eslint-enable */ <- used
|
||||
for (const enableDirective of enabledRules.values()) {
|
||||
usedEnableDirectives.add(enableDirective);
|
||||
}
|
||||
enabledRules.clear();
|
||||
} else {
|
||||
const enableDirective = enabledRules.get(directive.ruleId);
|
||||
|
||||
if (enableDirective) {
|
||||
|
||||
// If encounter `eslint-disable` with ruleId, and there is an `eslint-enable` with the same ruleId in enabledRules,
|
||||
// mark `eslint-enable` with ruleId as used.
|
||||
// e.g.
|
||||
// /* eslint-disable rule-id */ <- current directive
|
||||
// /* eslint-enable rule-id */ <- used
|
||||
usedEnableDirectives.add(enableDirective);
|
||||
} else {
|
||||
const enabledDirectiveWithoutRuleId = enabledRules.get(null);
|
||||
|
||||
if (enabledDirectiveWithoutRuleId) {
|
||||
|
||||
// If encounter `eslint-disable` with ruleId, and there is no `eslint-enable` with the same ruleId in enabledRules,
|
||||
// mark `eslint-enable` without ruleId as used.
|
||||
// e.g.
|
||||
// /* eslint-disable rule-id */ <- current directive
|
||||
// /* eslint-enable */ <- used
|
||||
usedEnableDirectives.add(enabledDirectiveWithoutRuleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (directive.type === "enable") {
|
||||
if (directive.ruleId === null) {
|
||||
|
||||
// If encounter `eslint-enable` without ruleId, the `eslint-enable` that follows it are unused.
|
||||
// So clear enabledRules.
|
||||
// e.g.
|
||||
// /* eslint-enable */ <- current directive
|
||||
// /* eslint-enable rule-id *// <- unused
|
||||
// /* eslint-enable */ <- unused
|
||||
enabledRules.clear();
|
||||
enabledRules.set(null, directive);
|
||||
} else {
|
||||
enabledRules.set(directive.ruleId, directive);
|
||||
}
|
||||
}
|
||||
}
|
||||
return usedEnableDirectives;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the same as the exported function, except that it
|
||||
* doesn't handle disable-line and disable-next-line directives, and it always reports unused
|
||||
* disable directives.
|
||||
* @param {Object} options options for applying directives. This is the same as the options
|
||||
* for the exported function, except that `reportUnusedDisableDirectives` is not supported
|
||||
* (this function always reports unused disable directives).
|
||||
* @returns {{problems: LintMessage[], unusedDirectives: LintMessage[]}} An object with a list
|
||||
* of problems (including suppressed ones) and unused eslint-disable directives
|
||||
*/
|
||||
function applyDirectives(options) {
|
||||
const problems = [];
|
||||
const usedDisableDirectives = new Set();
|
||||
|
||||
for (const problem of options.problems) {
|
||||
let disableDirectivesForProblem = [];
|
||||
let nextDirectiveIndex = 0;
|
||||
|
||||
while (
|
||||
nextDirectiveIndex < options.directives.length &&
|
||||
compareLocations(options.directives[nextDirectiveIndex], problem) <= 0
|
||||
) {
|
||||
const directive = options.directives[nextDirectiveIndex++];
|
||||
|
||||
if (directive.ruleId === null || directive.ruleId === problem.ruleId) {
|
||||
switch (directive.type) {
|
||||
case "disable":
|
||||
disableDirectivesForProblem.push(directive);
|
||||
break;
|
||||
|
||||
case "enable":
|
||||
disableDirectivesForProblem = [];
|
||||
break;
|
||||
|
||||
// no default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (disableDirectivesForProblem.length > 0) {
|
||||
const suppressions = disableDirectivesForProblem.map(directive => ({
|
||||
kind: "directive",
|
||||
justification: directive.unprocessedDirective.justification
|
||||
}));
|
||||
|
||||
if (problem.suppressions) {
|
||||
problem.suppressions = problem.suppressions.concat(suppressions);
|
||||
} else {
|
||||
problem.suppressions = suppressions;
|
||||
usedDisableDirectives.add(disableDirectivesForProblem[disableDirectivesForProblem.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
problems.push(problem);
|
||||
}
|
||||
|
||||
const unusedDisableDirectivesToReport = options.directives
|
||||
.filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive));
|
||||
|
||||
|
||||
const unusedEnableDirectivesToReport = new Set(
|
||||
options.directives.filter(directive => directive.unprocessedDirective.type === "enable")
|
||||
);
|
||||
|
||||
/*
|
||||
* If directives has the eslint-enable directive,
|
||||
* check whether the eslint-enable comment is used.
|
||||
*/
|
||||
if (unusedEnableDirectivesToReport.size > 0) {
|
||||
for (const directive of collectUsedEnableDirectives(options.directives)) {
|
||||
unusedEnableDirectivesToReport.delete(directive);
|
||||
}
|
||||
}
|
||||
|
||||
const processed = processUnusedDirectives(unusedDisableDirectivesToReport)
|
||||
.concat(processUnusedDirectives(unusedEnableDirectivesToReport));
|
||||
|
||||
const unusedDirectives = processed
|
||||
.map(({ description, fix, unprocessedDirective }) => {
|
||||
const { parentComment, type, line, column } = unprocessedDirective;
|
||||
|
||||
let message;
|
||||
|
||||
if (type === "enable") {
|
||||
message = description
|
||||
? `Unused eslint-enable directive (no matching eslint-disable directives were found for ${description}).`
|
||||
: "Unused eslint-enable directive (no matching eslint-disable directives were found).";
|
||||
} else {
|
||||
message = description
|
||||
? `Unused eslint-disable directive (no problems were reported from ${description}).`
|
||||
: "Unused eslint-disable directive (no problems were reported).";
|
||||
}
|
||||
return {
|
||||
ruleId: null,
|
||||
message,
|
||||
line: type === "disable-next-line" ? parentComment.commentToken.loc.start.line : line,
|
||||
column: type === "disable-next-line" ? parentComment.commentToken.loc.start.column + 1 : column,
|
||||
severity: options.reportUnusedDisableDirectives === "warn" ? 1 : 2,
|
||||
nodeType: null,
|
||||
...options.disableFixes ? {} : { fix }
|
||||
};
|
||||
});
|
||||
|
||||
return { problems, unusedDirectives };
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of directive comments (i.e. metadata about eslint-disable and eslint-enable comments) and a list
|
||||
* of reported problems, adds the suppression information to the problems.
|
||||
* @param {Object} options Information about directives and problems
|
||||
* @param {{
|
||||
* type: ("disable"|"enable"|"disable-line"|"disable-next-line"),
|
||||
* ruleId: (string|null),
|
||||
* line: number,
|
||||
* column: number,
|
||||
* justification: string
|
||||
* }} options.directives Directive comments found in the file, with one-based columns.
|
||||
* Two directive comments can only have the same location if they also have the same type (e.g. a single eslint-disable
|
||||
* comment for two different rules is represented as two directives).
|
||||
* @param {{ruleId: (string|null), line: number, column: number}[]} options.problems
|
||||
* A list of problems reported by rules, sorted by increasing location in the file, with one-based columns.
|
||||
* @param {"off" | "warn" | "error"} options.reportUnusedDisableDirectives If `"warn"` or `"error"`, adds additional problems for unused directives
|
||||
* @param {boolean} options.disableFixes If true, it doesn't make `fix` properties.
|
||||
* @returns {{ruleId: (string|null), line: number, column: number, suppressions?: {kind: string, justification: string}}[]}
|
||||
* An object with a list of reported problems, the suppressed of which contain the suppression information.
|
||||
*/
|
||||
module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirectives = "off" }) => {
|
||||
const blockDirectives = directives
|
||||
.filter(directive => directive.type === "disable" || directive.type === "enable")
|
||||
.map(directive => Object.assign({}, directive, { unprocessedDirective: directive }))
|
||||
.sort(compareLocations);
|
||||
|
||||
const lineDirectives = directives.flatMap(directive => {
|
||||
switch (directive.type) {
|
||||
case "disable":
|
||||
case "enable":
|
||||
return [];
|
||||
|
||||
case "disable-line":
|
||||
return [
|
||||
{ type: "disable", line: directive.line, column: 1, ruleId: directive.ruleId, unprocessedDirective: directive },
|
||||
{ type: "enable", line: directive.line + 1, column: 0, ruleId: directive.ruleId, unprocessedDirective: directive }
|
||||
];
|
||||
|
||||
case "disable-next-line":
|
||||
return [
|
||||
{ type: "disable", line: directive.line + 1, column: 1, ruleId: directive.ruleId, unprocessedDirective: directive },
|
||||
{ type: "enable", line: directive.line + 2, column: 0, ruleId: directive.ruleId, unprocessedDirective: directive }
|
||||
];
|
||||
|
||||
default:
|
||||
throw new TypeError(`Unrecognized directive type '${directive.type}'`);
|
||||
}
|
||||
}).sort(compareLocations);
|
||||
|
||||
const blockDirectivesResult = applyDirectives({
|
||||
problems,
|
||||
directives: blockDirectives,
|
||||
disableFixes,
|
||||
reportUnusedDisableDirectives
|
||||
});
|
||||
const lineDirectivesResult = applyDirectives({
|
||||
problems: blockDirectivesResult.problems,
|
||||
directives: lineDirectives,
|
||||
disableFixes,
|
||||
reportUnusedDisableDirectives
|
||||
});
|
||||
|
||||
return reportUnusedDisableDirectives !== "off"
|
||||
? lineDirectivesResult.problems
|
||||
.concat(blockDirectivesResult.unusedDirectives)
|
||||
.concat(lineDirectivesResult.unusedDirectives)
|
||||
.sort(compareLocations)
|
||||
: lineDirectivesResult.problems;
|
||||
};
|
||||
852
node_modules/eslint/lib/linter/code-path-analysis/code-path-analyzer.js
generated
vendored
Normal file
852
node_modules/eslint/lib/linter/code-path-analysis/code-path-analyzer.js
generated
vendored
Normal file
@@ -0,0 +1,852 @@
|
||||
/**
|
||||
* @fileoverview A class of the code path analyzer.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const assert = require("assert"),
|
||||
{ breakableTypePattern } = require("../../shared/ast-utils"),
|
||||
CodePath = require("./code-path"),
|
||||
CodePathSegment = require("./code-path-segment"),
|
||||
IdGenerator = require("./id-generator"),
|
||||
debug = require("./debug-helpers");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is a `case` node (not `default` node).
|
||||
* @param {ASTNode} node A `SwitchCase` node to check.
|
||||
* @returns {boolean} `true` if the node is a `case` node (not `default` node).
|
||||
*/
|
||||
function isCaseNode(node) {
|
||||
return Boolean(node.test);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given node appears as the value of a PropertyDefinition node.
|
||||
* @param {ASTNode} node THe node to check.
|
||||
* @returns {boolean} `true` if the node is a PropertyDefinition value,
|
||||
* false if not.
|
||||
*/
|
||||
function isPropertyDefinitionValue(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return parent && parent.type === "PropertyDefinition" && parent.value === node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given logical operator is taken into account for the code
|
||||
* path analysis.
|
||||
* @param {string} operator The operator found in the LogicalExpression node
|
||||
* @returns {boolean} `true` if the operator is "&&" or "||" or "??"
|
||||
*/
|
||||
function isHandledLogicalOperator(operator) {
|
||||
return operator === "&&" || operator === "||" || operator === "??";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given assignment operator is a logical assignment operator.
|
||||
* Logical assignments are taken into account for the code path analysis
|
||||
* because of their short-circuiting semantics.
|
||||
* @param {string} operator The operator found in the AssignmentExpression node
|
||||
* @returns {boolean} `true` if the operator is "&&=" or "||=" or "??="
|
||||
*/
|
||||
function isLogicalAssignmentOperator(operator) {
|
||||
return operator === "&&=" || operator === "||=" || operator === "??=";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the label if the parent node of a given node is a LabeledStatement.
|
||||
* @param {ASTNode} node A node to get.
|
||||
* @returns {string|null} The label or `null`.
|
||||
*/
|
||||
function getLabel(node) {
|
||||
if (node.parent.type === "LabeledStatement") {
|
||||
return node.parent.label.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given logical expression node goes different path
|
||||
* between the `true` case and the `false` case.
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {boolean} `true` if the node is a test of a choice statement.
|
||||
*/
|
||||
function isForkingByTrueOrFalse(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case "ConditionalExpression":
|
||||
case "IfStatement":
|
||||
case "WhileStatement":
|
||||
case "DoWhileStatement":
|
||||
case "ForStatement":
|
||||
return parent.test === node;
|
||||
|
||||
case "LogicalExpression":
|
||||
return isHandledLogicalOperator(parent.operator);
|
||||
|
||||
case "AssignmentExpression":
|
||||
return isLogicalAssignmentOperator(parent.operator);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the boolean value of a given literal node.
|
||||
*
|
||||
* This is used to detect infinity loops (e.g. `while (true) {}`).
|
||||
* Statements preceded by an infinity loop are unreachable if the loop didn't
|
||||
* have any `break` statement.
|
||||
* @param {ASTNode} node A node to get.
|
||||
* @returns {boolean|undefined} a boolean value if the node is a Literal node,
|
||||
* otherwise `undefined`.
|
||||
*/
|
||||
function getBooleanValueIfSimpleConstant(node) {
|
||||
if (node.type === "Literal") {
|
||||
return Boolean(node.value);
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a given identifier node is a reference or not.
|
||||
*
|
||||
* This is used to detect the first throwable node in a `try` block.
|
||||
* @param {ASTNode} node An Identifier node to check.
|
||||
* @returns {boolean} `true` if the node is a reference.
|
||||
*/
|
||||
function isIdentifierReference(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case "LabeledStatement":
|
||||
case "BreakStatement":
|
||||
case "ContinueStatement":
|
||||
case "ArrayPattern":
|
||||
case "RestElement":
|
||||
case "ImportSpecifier":
|
||||
case "ImportDefaultSpecifier":
|
||||
case "ImportNamespaceSpecifier":
|
||||
case "CatchClause":
|
||||
return false;
|
||||
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
case "ClassDeclaration":
|
||||
case "ClassExpression":
|
||||
case "VariableDeclarator":
|
||||
return parent.id !== node;
|
||||
|
||||
case "Property":
|
||||
case "PropertyDefinition":
|
||||
case "MethodDefinition":
|
||||
return (
|
||||
parent.key !== node ||
|
||||
parent.computed ||
|
||||
parent.shorthand
|
||||
);
|
||||
|
||||
case "AssignmentPattern":
|
||||
return parent.key !== node;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current segment with the head segment.
|
||||
* This is similar to local branches and tracking branches of git.
|
||||
*
|
||||
* To separate the current and the head is in order to not make useless segments.
|
||||
*
|
||||
* In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
|
||||
* events are fired.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function forwardCurrentToHead(analyzer, node) {
|
||||
const codePath = analyzer.codePath;
|
||||
const state = CodePath.getState(codePath);
|
||||
const currentSegments = state.currentSegments;
|
||||
const headSegments = state.headSegments;
|
||||
const end = Math.max(currentSegments.length, headSegments.length);
|
||||
let i, currentSegment, headSegment;
|
||||
|
||||
// Fires leaving events.
|
||||
for (i = 0; i < end; ++i) {
|
||||
currentSegment = currentSegments[i];
|
||||
headSegment = headSegments[i];
|
||||
|
||||
if (currentSegment !== headSegment && currentSegment) {
|
||||
|
||||
const eventName = currentSegment.reachable
|
||||
? "onCodePathSegmentEnd"
|
||||
: "onUnreachableCodePathSegmentEnd";
|
||||
|
||||
debug.dump(`${eventName} ${currentSegment.id}`);
|
||||
|
||||
analyzer.emitter.emit(
|
||||
eventName,
|
||||
currentSegment,
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update state.
|
||||
state.currentSegments = headSegments;
|
||||
|
||||
// Fires entering events.
|
||||
for (i = 0; i < end; ++i) {
|
||||
currentSegment = currentSegments[i];
|
||||
headSegment = headSegments[i];
|
||||
|
||||
if (currentSegment !== headSegment && headSegment) {
|
||||
|
||||
const eventName = headSegment.reachable
|
||||
? "onCodePathSegmentStart"
|
||||
: "onUnreachableCodePathSegmentStart";
|
||||
|
||||
debug.dump(`${eventName} ${headSegment.id}`);
|
||||
|
||||
CodePathSegment.markUsed(headSegment);
|
||||
analyzer.emitter.emit(
|
||||
eventName,
|
||||
headSegment,
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current segment with empty.
|
||||
* This is called at the last of functions or the program.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function leaveFromCurrentSegment(analyzer, node) {
|
||||
const state = CodePath.getState(analyzer.codePath);
|
||||
const currentSegments = state.currentSegments;
|
||||
|
||||
for (let i = 0; i < currentSegments.length; ++i) {
|
||||
const currentSegment = currentSegments[i];
|
||||
const eventName = currentSegment.reachable
|
||||
? "onCodePathSegmentEnd"
|
||||
: "onUnreachableCodePathSegmentEnd";
|
||||
|
||||
debug.dump(`${eventName} ${currentSegment.id}`);
|
||||
|
||||
analyzer.emitter.emit(
|
||||
eventName,
|
||||
currentSegment,
|
||||
node
|
||||
);
|
||||
}
|
||||
|
||||
state.currentSegments = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path due to the position of a given node in the parent node
|
||||
* thereof.
|
||||
*
|
||||
* For example, if the node is `parent.consequent`, this creates a fork from the
|
||||
* current path.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function preprocess(analyzer, node) {
|
||||
const codePath = analyzer.codePath;
|
||||
const state = CodePath.getState(codePath);
|
||||
const parent = node.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
|
||||
// The `arguments.length == 0` case is in `postprocess` function.
|
||||
case "CallExpression":
|
||||
if (parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node) {
|
||||
state.makeOptionalRight();
|
||||
}
|
||||
break;
|
||||
case "MemberExpression":
|
||||
if (parent.optional === true && parent.property === node) {
|
||||
state.makeOptionalRight();
|
||||
}
|
||||
break;
|
||||
|
||||
case "LogicalExpression":
|
||||
if (
|
||||
parent.right === node &&
|
||||
isHandledLogicalOperator(parent.operator)
|
||||
) {
|
||||
state.makeLogicalRight();
|
||||
}
|
||||
break;
|
||||
|
||||
case "AssignmentExpression":
|
||||
if (
|
||||
parent.right === node &&
|
||||
isLogicalAssignmentOperator(parent.operator)
|
||||
) {
|
||||
state.makeLogicalRight();
|
||||
}
|
||||
break;
|
||||
|
||||
case "ConditionalExpression":
|
||||
case "IfStatement":
|
||||
|
||||
/*
|
||||
* Fork if this node is at `consequent`/`alternate`.
|
||||
* `popForkContext()` exists at `IfStatement:exit` and
|
||||
* `ConditionalExpression:exit`.
|
||||
*/
|
||||
if (parent.consequent === node) {
|
||||
state.makeIfConsequent();
|
||||
} else if (parent.alternate === node) {
|
||||
state.makeIfAlternate();
|
||||
}
|
||||
break;
|
||||
|
||||
case "SwitchCase":
|
||||
if (parent.consequent[0] === node) {
|
||||
state.makeSwitchCaseBody(false, !parent.test);
|
||||
}
|
||||
break;
|
||||
|
||||
case "TryStatement":
|
||||
if (parent.handler === node) {
|
||||
state.makeCatchBlock();
|
||||
} else if (parent.finalizer === node) {
|
||||
state.makeFinallyBlock();
|
||||
}
|
||||
break;
|
||||
|
||||
case "WhileStatement":
|
||||
if (parent.test === node) {
|
||||
state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
|
||||
} else {
|
||||
assert(parent.body === node);
|
||||
state.makeWhileBody();
|
||||
}
|
||||
break;
|
||||
|
||||
case "DoWhileStatement":
|
||||
if (parent.body === node) {
|
||||
state.makeDoWhileBody();
|
||||
} else {
|
||||
assert(parent.test === node);
|
||||
state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
|
||||
}
|
||||
break;
|
||||
|
||||
case "ForStatement":
|
||||
if (parent.test === node) {
|
||||
state.makeForTest(getBooleanValueIfSimpleConstant(node));
|
||||
} else if (parent.update === node) {
|
||||
state.makeForUpdate();
|
||||
} else if (parent.body === node) {
|
||||
state.makeForBody();
|
||||
}
|
||||
break;
|
||||
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
if (parent.left === node) {
|
||||
state.makeForInOfLeft();
|
||||
} else if (parent.right === node) {
|
||||
state.makeForInOfRight();
|
||||
} else {
|
||||
assert(parent.body === node);
|
||||
state.makeForInOfBody();
|
||||
}
|
||||
break;
|
||||
|
||||
case "AssignmentPattern":
|
||||
|
||||
/*
|
||||
* Fork if this node is at `right`.
|
||||
* `left` is executed always, so it uses the current path.
|
||||
* `popForkContext()` exists at `AssignmentPattern:exit`.
|
||||
*/
|
||||
if (parent.right === node) {
|
||||
state.pushForkContext();
|
||||
state.forkBypassPath();
|
||||
state.forkPath();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path due to the type of a given node in entering.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processCodePathToEnter(analyzer, node) {
|
||||
let codePath = analyzer.codePath;
|
||||
let state = codePath && CodePath.getState(codePath);
|
||||
const parent = node.parent;
|
||||
|
||||
/**
|
||||
* Creates a new code path and trigger the onCodePathStart event
|
||||
* based on the currently selected node.
|
||||
* @param {string} origin The reason the code path was started.
|
||||
* @returns {void}
|
||||
*/
|
||||
function startCodePath(origin) {
|
||||
if (codePath) {
|
||||
|
||||
// Emits onCodePathSegmentStart events if updated.
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
debug.dumpState(node, state, false);
|
||||
}
|
||||
|
||||
// Create the code path of this scope.
|
||||
codePath = analyzer.codePath = new CodePath({
|
||||
id: analyzer.idGenerator.next(),
|
||||
origin,
|
||||
upper: codePath,
|
||||
onLooped: analyzer.onLooped
|
||||
});
|
||||
state = CodePath.getState(codePath);
|
||||
|
||||
// Emits onCodePathStart events.
|
||||
debug.dump(`onCodePathStart ${codePath.id}`);
|
||||
analyzer.emitter.emit("onCodePathStart", codePath, node);
|
||||
}
|
||||
|
||||
/*
|
||||
* Special case: The right side of class field initializer is considered
|
||||
* to be its own function, so we need to start a new code path in this
|
||||
* case.
|
||||
*/
|
||||
if (isPropertyDefinitionValue(node)) {
|
||||
startCodePath("class-field-initializer");
|
||||
|
||||
/*
|
||||
* Intentional fall through because `node` needs to also be
|
||||
* processed by the code below. For example, if we have:
|
||||
*
|
||||
* class Foo {
|
||||
* a = () => {}
|
||||
* }
|
||||
*
|
||||
* In this case, we also need start a second code path.
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case "Program":
|
||||
startCodePath("program");
|
||||
break;
|
||||
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
startCodePath("function");
|
||||
break;
|
||||
|
||||
case "StaticBlock":
|
||||
startCodePath("class-static-block");
|
||||
break;
|
||||
|
||||
case "ChainExpression":
|
||||
state.pushChainContext();
|
||||
break;
|
||||
case "CallExpression":
|
||||
if (node.optional === true) {
|
||||
state.makeOptionalNode();
|
||||
}
|
||||
break;
|
||||
case "MemberExpression":
|
||||
if (node.optional === true) {
|
||||
state.makeOptionalNode();
|
||||
}
|
||||
break;
|
||||
|
||||
case "LogicalExpression":
|
||||
if (isHandledLogicalOperator(node.operator)) {
|
||||
state.pushChoiceContext(
|
||||
node.operator,
|
||||
isForkingByTrueOrFalse(node)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "AssignmentExpression":
|
||||
if (isLogicalAssignmentOperator(node.operator)) {
|
||||
state.pushChoiceContext(
|
||||
node.operator.slice(0, -1), // removes `=` from the end
|
||||
isForkingByTrueOrFalse(node)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "ConditionalExpression":
|
||||
case "IfStatement":
|
||||
state.pushChoiceContext("test", false);
|
||||
break;
|
||||
|
||||
case "SwitchStatement":
|
||||
state.pushSwitchContext(
|
||||
node.cases.some(isCaseNode),
|
||||
getLabel(node)
|
||||
);
|
||||
break;
|
||||
|
||||
case "TryStatement":
|
||||
state.pushTryContext(Boolean(node.finalizer));
|
||||
break;
|
||||
|
||||
case "SwitchCase":
|
||||
|
||||
/*
|
||||
* Fork if this node is after the 2st node in `cases`.
|
||||
* It's similar to `else` blocks.
|
||||
* The next `test` node is processed in this path.
|
||||
*/
|
||||
if (parent.discriminant !== node && parent.cases[0] !== node) {
|
||||
state.forkPath();
|
||||
}
|
||||
break;
|
||||
|
||||
case "WhileStatement":
|
||||
case "DoWhileStatement":
|
||||
case "ForStatement":
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
state.pushLoopContext(node.type, getLabel(node));
|
||||
break;
|
||||
|
||||
case "LabeledStatement":
|
||||
if (!breakableTypePattern.test(node.body.type)) {
|
||||
state.pushBreakContext(false, node.label.name);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Emits onCodePathSegmentStart events if updated.
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
debug.dumpState(node, state, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path due to the type of a given node in leaving.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processCodePathToExit(analyzer, node) {
|
||||
|
||||
const codePath = analyzer.codePath;
|
||||
const state = CodePath.getState(codePath);
|
||||
let dontForward = false;
|
||||
|
||||
switch (node.type) {
|
||||
case "ChainExpression":
|
||||
state.popChainContext();
|
||||
break;
|
||||
|
||||
case "IfStatement":
|
||||
case "ConditionalExpression":
|
||||
state.popChoiceContext();
|
||||
break;
|
||||
|
||||
case "LogicalExpression":
|
||||
if (isHandledLogicalOperator(node.operator)) {
|
||||
state.popChoiceContext();
|
||||
}
|
||||
break;
|
||||
|
||||
case "AssignmentExpression":
|
||||
if (isLogicalAssignmentOperator(node.operator)) {
|
||||
state.popChoiceContext();
|
||||
}
|
||||
break;
|
||||
|
||||
case "SwitchStatement":
|
||||
state.popSwitchContext();
|
||||
break;
|
||||
|
||||
case "SwitchCase":
|
||||
|
||||
/*
|
||||
* This is the same as the process at the 1st `consequent` node in
|
||||
* `preprocess` function.
|
||||
* Must do if this `consequent` is empty.
|
||||
*/
|
||||
if (node.consequent.length === 0) {
|
||||
state.makeSwitchCaseBody(true, !node.test);
|
||||
}
|
||||
if (state.forkContext.reachable) {
|
||||
dontForward = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "TryStatement":
|
||||
state.popTryContext();
|
||||
break;
|
||||
|
||||
case "BreakStatement":
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeBreak(node.label && node.label.name);
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case "ContinueStatement":
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeContinue(node.label && node.label.name);
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case "ReturnStatement":
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeReturn();
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case "ThrowStatement":
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeThrow();
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case "Identifier":
|
||||
if (isIdentifierReference(node)) {
|
||||
state.makeFirstThrowablePathInTryBlock();
|
||||
dontForward = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "CallExpression":
|
||||
case "ImportExpression":
|
||||
case "MemberExpression":
|
||||
case "NewExpression":
|
||||
case "YieldExpression":
|
||||
state.makeFirstThrowablePathInTryBlock();
|
||||
break;
|
||||
|
||||
case "WhileStatement":
|
||||
case "DoWhileStatement":
|
||||
case "ForStatement":
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
state.popLoopContext();
|
||||
break;
|
||||
|
||||
case "AssignmentPattern":
|
||||
state.popForkContext();
|
||||
break;
|
||||
|
||||
case "LabeledStatement":
|
||||
if (!breakableTypePattern.test(node.body.type)) {
|
||||
state.popBreakContext();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Emits onCodePathSegmentStart events if updated.
|
||||
if (!dontForward) {
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
}
|
||||
debug.dumpState(node, state, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path to finalize the current code path.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function postprocess(analyzer, node) {
|
||||
|
||||
/**
|
||||
* Ends the code path for the current node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function endCodePath() {
|
||||
let codePath = analyzer.codePath;
|
||||
|
||||
// Mark the current path as the final node.
|
||||
CodePath.getState(codePath).makeFinal();
|
||||
|
||||
// Emits onCodePathSegmentEnd event of the current segments.
|
||||
leaveFromCurrentSegment(analyzer, node);
|
||||
|
||||
// Emits onCodePathEnd event of this code path.
|
||||
debug.dump(`onCodePathEnd ${codePath.id}`);
|
||||
analyzer.emitter.emit("onCodePathEnd", codePath, node);
|
||||
debug.dumpDot(codePath);
|
||||
|
||||
codePath = analyzer.codePath = analyzer.codePath.upper;
|
||||
if (codePath) {
|
||||
debug.dumpState(node, CodePath.getState(codePath), true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case "Program":
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
case "StaticBlock": {
|
||||
endCodePath();
|
||||
break;
|
||||
}
|
||||
|
||||
// The `arguments.length >= 1` case is in `preprocess` function.
|
||||
case "CallExpression":
|
||||
if (node.optional === true && node.arguments.length === 0) {
|
||||
CodePath.getState(analyzer.codePath).makeOptionalRight();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* Special case: The right side of class field initializer is considered
|
||||
* to be its own function, so we need to end a code path in this
|
||||
* case.
|
||||
*
|
||||
* We need to check after the other checks in order to close the
|
||||
* code paths in the correct order for code like this:
|
||||
*
|
||||
*
|
||||
* class Foo {
|
||||
* a = () => {}
|
||||
* }
|
||||
*
|
||||
* In this case, The ArrowFunctionExpression code path is closed first
|
||||
* and then we need to close the code path for the PropertyDefinition
|
||||
* value.
|
||||
*/
|
||||
if (isPropertyDefinitionValue(node)) {
|
||||
endCodePath();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The class to analyze code paths.
|
||||
* This class implements the EventGenerator interface.
|
||||
*/
|
||||
class CodePathAnalyzer {
|
||||
|
||||
/**
|
||||
* @param {EventGenerator} eventGenerator An event generator to wrap.
|
||||
*/
|
||||
constructor(eventGenerator) {
|
||||
this.original = eventGenerator;
|
||||
this.emitter = eventGenerator.emitter;
|
||||
this.codePath = null;
|
||||
this.idGenerator = new IdGenerator("s");
|
||||
this.currentNode = null;
|
||||
this.onLooped = this.onLooped.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the process to enter a given AST node.
|
||||
* This updates state of analysis and calls `enterNode` of the wrapped.
|
||||
* @param {ASTNode} node A node which is entering.
|
||||
* @returns {void}
|
||||
*/
|
||||
enterNode(node) {
|
||||
this.currentNode = node;
|
||||
|
||||
// Updates the code path due to node's position in its parent node.
|
||||
if (node.parent) {
|
||||
preprocess(this, node);
|
||||
}
|
||||
|
||||
/*
|
||||
* Updates the code path.
|
||||
* And emits onCodePathStart/onCodePathSegmentStart events.
|
||||
*/
|
||||
processCodePathToEnter(this, node);
|
||||
|
||||
// Emits node events.
|
||||
this.original.enterNode(node);
|
||||
|
||||
this.currentNode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the process to leave a given AST node.
|
||||
* This updates state of analysis and calls `leaveNode` of the wrapped.
|
||||
* @param {ASTNode} node A node which is leaving.
|
||||
* @returns {void}
|
||||
*/
|
||||
leaveNode(node) {
|
||||
this.currentNode = node;
|
||||
|
||||
/*
|
||||
* Updates the code path.
|
||||
* And emits onCodePathStart/onCodePathSegmentStart events.
|
||||
*/
|
||||
processCodePathToExit(this, node);
|
||||
|
||||
// Emits node events.
|
||||
this.original.leaveNode(node);
|
||||
|
||||
// Emits the last onCodePathStart/onCodePathSegmentStart events.
|
||||
postprocess(this, node);
|
||||
|
||||
this.currentNode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called on a code path looped.
|
||||
* Then this raises a looped event.
|
||||
* @param {CodePathSegment} fromSegment A segment of prev.
|
||||
* @param {CodePathSegment} toSegment A segment of next.
|
||||
* @returns {void}
|
||||
*/
|
||||
onLooped(fromSegment, toSegment) {
|
||||
if (fromSegment.reachable && toSegment.reachable) {
|
||||
debug.dump(`onCodePathSegmentLoop ${fromSegment.id} -> ${toSegment.id}`);
|
||||
this.emitter.emit(
|
||||
"onCodePathSegmentLoop",
|
||||
fromSegment,
|
||||
toSegment,
|
||||
this.currentNode
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodePathAnalyzer;
|
||||
263
node_modules/eslint/lib/linter/code-path-analysis/code-path-segment.js
generated
vendored
Normal file
263
node_modules/eslint/lib/linter/code-path-analysis/code-path-segment.js
generated
vendored
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* @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;
|
||||
2348
node_modules/eslint/lib/linter/code-path-analysis/code-path-state.js
generated
vendored
Normal file
2348
node_modules/eslint/lib/linter/code-path-analysis/code-path-state.js
generated
vendored
Normal file
@@ -0,0 +1,2348 @@
|
||||
/**
|
||||
* @fileoverview A class to manage state of generating a code path.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const CodePathSegment = require("./code-path-segment"),
|
||||
ForkContext = require("./fork-context");
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Contexts
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Represents the context in which a `break` statement can be used.
|
||||
*
|
||||
* A `break` statement without a label is only valid in a few places in
|
||||
* JavaScript: any type of loop or a `switch` statement. Otherwise, `break`
|
||||
* without a label causes a syntax error. For these contexts, `breakable` is
|
||||
* set to `true` to indicate that a `break` without a label is valid.
|
||||
*
|
||||
* However, a `break` statement with a label is also valid inside of a labeled
|
||||
* statement. For example, this is valid:
|
||||
*
|
||||
* a : {
|
||||
* break a;
|
||||
* }
|
||||
*
|
||||
* The `breakable` property is set false for labeled statements to indicate
|
||||
* that `break` without a label is invalid.
|
||||
*/
|
||||
class BreakContext {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {BreakContext} upperContext The previous `BreakContext`.
|
||||
* @param {boolean} breakable Indicates if we are inside a statement where
|
||||
* `break` without a label will exit the statement.
|
||||
* @param {string|null} label The label for the statement.
|
||||
* @param {ForkContext} forkContext The current fork context.
|
||||
*/
|
||||
constructor(upperContext, breakable, label, forkContext) {
|
||||
|
||||
/**
|
||||
* The previous `BreakContext`
|
||||
* @type {BreakContext}
|
||||
*/
|
||||
this.upper = upperContext;
|
||||
|
||||
/**
|
||||
* Indicates if we are inside a statement where `break` without a label
|
||||
* will exit the statement.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.breakable = breakable;
|
||||
|
||||
/**
|
||||
* The label associated with the statement.
|
||||
* @type {string|null}
|
||||
*/
|
||||
this.label = label;
|
||||
|
||||
/**
|
||||
* The fork context for the `break`.
|
||||
* @type {ForkContext}
|
||||
*/
|
||||
this.brokenForkContext = ForkContext.newEmpty(forkContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the context for `ChainExpression` nodes.
|
||||
*/
|
||||
class ChainContext {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {ChainContext} upperContext The previous `ChainContext`.
|
||||
*/
|
||||
constructor(upperContext) {
|
||||
|
||||
/**
|
||||
* The previous `ChainContext`
|
||||
* @type {ChainContext}
|
||||
*/
|
||||
this.upper = upperContext;
|
||||
|
||||
/**
|
||||
* The number of choice contexts inside of the `ChainContext`.
|
||||
* @type {number}
|
||||
*/
|
||||
this.choiceContextCount = 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a choice in the code path.
|
||||
*
|
||||
* Choices are created by logical operators such as `&&`, loops, conditionals,
|
||||
* and `if` statements. This is the point at which the code path has a choice of
|
||||
* which direction to go.
|
||||
*
|
||||
* The result of a choice might be in the left (test) expression of another choice,
|
||||
* and in that case, may create a new fork. For example, `a || b` is a choice
|
||||
* but does not create a new fork because the result of the expression is
|
||||
* not used as the test expression in another expression. In this case,
|
||||
* `isForkingAsResult` is false. In the expression `a || b || c`, the `a || b`
|
||||
* expression appears as the test expression for `|| c`, so the
|
||||
* result of `a || b` creates a fork because execution may or may not
|
||||
* continue to `|| c`. `isForkingAsResult` for `a || b` in this case is true
|
||||
* while `isForkingAsResult` for `|| c` is false. (`isForkingAsResult` is always
|
||||
* false for `if` statements, conditional expressions, and loops.)
|
||||
*
|
||||
* All of the choices except one (`??`) operate on a true/false fork, meaning if
|
||||
* true go one way and if false go the other (tracked by `trueForkContext` and
|
||||
* `falseForkContext`). The `??` operator doesn't operate on true/false because
|
||||
* the left expression is evaluated to be nullish or not, so only if nullish do
|
||||
* we fork to the right expression (tracked by `nullishForkContext`).
|
||||
*/
|
||||
class ChoiceContext {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {ChoiceContext} upperContext The previous `ChoiceContext`.
|
||||
* @param {string} kind The kind of choice. If it's a logical or assignment expression, this
|
||||
* is `"&&"` or `"||"` or `"??"`; if it's an `if` statement or
|
||||
* conditional expression, this is `"test"`; otherwise, this is `"loop"`.
|
||||
* @param {boolean} isForkingAsResult Indicates if the result of the choice
|
||||
* creates a fork.
|
||||
* @param {ForkContext} forkContext The containing `ForkContext`.
|
||||
*/
|
||||
constructor(upperContext, kind, isForkingAsResult, forkContext) {
|
||||
|
||||
/**
|
||||
* The previous `ChoiceContext`
|
||||
* @type {ChoiceContext}
|
||||
*/
|
||||
this.upper = upperContext;
|
||||
|
||||
/**
|
||||
* The kind of choice. If it's a logical or assignment expression, this
|
||||
* is `"&&"` or `"||"` or `"??"`; if it's an `if` statement or
|
||||
* conditional expression, this is `"test"`; otherwise, this is `"loop"`.
|
||||
* @type {string}
|
||||
*/
|
||||
this.kind = kind;
|
||||
|
||||
/**
|
||||
* Indicates if the result of the choice forks the code path.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.isForkingAsResult = isForkingAsResult;
|
||||
|
||||
/**
|
||||
* The fork context for the `true` path of the choice.
|
||||
* @type {ForkContext}
|
||||
*/
|
||||
this.trueForkContext = ForkContext.newEmpty(forkContext);
|
||||
|
||||
/**
|
||||
* The fork context for the `false` path of the choice.
|
||||
* @type {ForkContext}
|
||||
*/
|
||||
this.falseForkContext = ForkContext.newEmpty(forkContext);
|
||||
|
||||
/**
|
||||
* The fork context for when the choice result is `null` or `undefined`.
|
||||
* @type {ForkContext}
|
||||
*/
|
||||
this.nullishForkContext = ForkContext.newEmpty(forkContext);
|
||||
|
||||
/**
|
||||
* Indicates if any of `trueForkContext`, `falseForkContext`, or
|
||||
* `nullishForkContext` have been updated with segments from a child context.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.processed = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for all loop contexts.
|
||||
*/
|
||||
class LoopContextBase {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
||||
* @param {string} type The AST node's `type` for the loop.
|
||||
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
||||
* @param {BreakContext} breakContext The context for breaking the loop.
|
||||
*/
|
||||
constructor(upperContext, type, label, breakContext) {
|
||||
|
||||
/**
|
||||
* The previous `LoopContext`.
|
||||
* @type {LoopContext}
|
||||
*/
|
||||
this.upper = upperContext;
|
||||
|
||||
/**
|
||||
* The AST node's `type` for the loop.
|
||||
* @type {string}
|
||||
*/
|
||||
this.type = type;
|
||||
|
||||
/**
|
||||
* The label for the loop from an enclosing `LabeledStatement`.
|
||||
* @type {string|null}
|
||||
*/
|
||||
this.label = label;
|
||||
|
||||
/**
|
||||
* The fork context for when `break` is encountered.
|
||||
* @type {ForkContext}
|
||||
*/
|
||||
this.brokenForkContext = breakContext.brokenForkContext;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the context for a `while` loop.
|
||||
*/
|
||||
class WhileLoopContext extends LoopContextBase {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
||||
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
||||
* @param {BreakContext} breakContext The context for breaking the loop.
|
||||
*/
|
||||
constructor(upperContext, label, breakContext) {
|
||||
super(upperContext, "WhileStatement", label, breakContext);
|
||||
|
||||
/**
|
||||
* The hardcoded literal boolean test condition for
|
||||
* the loop. Used to catch infinite or skipped loops.
|
||||
* @type {boolean|undefined}
|
||||
*/
|
||||
this.test = void 0;
|
||||
|
||||
/**
|
||||
* The segments representing the test condition where `continue` will
|
||||
* jump to. The test condition will typically have just one segment but
|
||||
* it's possible for there to be more than one.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.continueDestSegments = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the context for a `do-while` loop.
|
||||
*/
|
||||
class DoWhileLoopContext extends LoopContextBase {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
||||
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
||||
* @param {BreakContext} breakContext The context for breaking the loop.
|
||||
* @param {ForkContext} forkContext The enclosing fork context.
|
||||
*/
|
||||
constructor(upperContext, label, breakContext, forkContext) {
|
||||
super(upperContext, "DoWhileStatement", label, breakContext);
|
||||
|
||||
/**
|
||||
* The hardcoded literal boolean test condition for
|
||||
* the loop. Used to catch infinite or skipped loops.
|
||||
* @type {boolean|undefined}
|
||||
*/
|
||||
this.test = void 0;
|
||||
|
||||
/**
|
||||
* The segments at the start of the loop body. This is the only loop
|
||||
* where the test comes at the end, so the first iteration always
|
||||
* happens and we need a reference to the first statements.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.entrySegments = null;
|
||||
|
||||
/**
|
||||
* The fork context to follow when a `continue` is found.
|
||||
* @type {ForkContext}
|
||||
*/
|
||||
this.continueForkContext = ForkContext.newEmpty(forkContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the context for a `for` loop.
|
||||
*/
|
||||
class ForLoopContext extends LoopContextBase {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
||||
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
||||
* @param {BreakContext} breakContext The context for breaking the loop.
|
||||
*/
|
||||
constructor(upperContext, label, breakContext) {
|
||||
super(upperContext, "ForStatement", label, breakContext);
|
||||
|
||||
/**
|
||||
* The hardcoded literal boolean test condition for
|
||||
* the loop. Used to catch infinite or skipped loops.
|
||||
* @type {boolean|undefined}
|
||||
*/
|
||||
this.test = void 0;
|
||||
|
||||
/**
|
||||
* The end of the init expression. This may change during the lifetime
|
||||
* of the instance as we traverse the loop because some loops don't have
|
||||
* an init expression.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.endOfInitSegments = null;
|
||||
|
||||
/**
|
||||
* The start of the test expression. This may change during the lifetime
|
||||
* of the instance as we traverse the loop because some loops don't have
|
||||
* a test expression.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.testSegments = null;
|
||||
|
||||
/**
|
||||
* The end of the test expression. This may change during the lifetime
|
||||
* of the instance as we traverse the loop because some loops don't have
|
||||
* a test expression.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.endOfTestSegments = null;
|
||||
|
||||
/**
|
||||
* The start of the update expression. This may change during the lifetime
|
||||
* of the instance as we traverse the loop because some loops don't have
|
||||
* an update expression.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.updateSegments = null;
|
||||
|
||||
/**
|
||||
* The end of the update expresion. This may change during the lifetime
|
||||
* of the instance as we traverse the loop because some loops don't have
|
||||
* an update expression.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.endOfUpdateSegments = null;
|
||||
|
||||
/**
|
||||
* The segments representing the test condition where `continue` will
|
||||
* jump to. The test condition will typically have just one segment but
|
||||
* it's possible for there to be more than one. This may change during the
|
||||
* lifetime of the instance as we traverse the loop because some loops
|
||||
* don't have an update expression. When there is an update expression, this
|
||||
* will end up pointing to that expression; otherwise it will end up pointing
|
||||
* to the test expression.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.continueDestSegments = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the context for a `for-in` loop.
|
||||
*
|
||||
* Terminology:
|
||||
* - "left" means the part of the loop to the left of the `in` keyword. For
|
||||
* example, in `for (var x in y)`, the left is `var x`.
|
||||
* - "right" means the part of the loop to the right of the `in` keyword. For
|
||||
* example, in `for (var x in y)`, the right is `y`.
|
||||
*/
|
||||
class ForInLoopContext extends LoopContextBase {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
||||
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
||||
* @param {BreakContext} breakContext The context for breaking the loop.
|
||||
*/
|
||||
constructor(upperContext, label, breakContext) {
|
||||
super(upperContext, "ForInStatement", label, breakContext);
|
||||
|
||||
/**
|
||||
* The segments that came immediately before the start of the loop.
|
||||
* This allows you to traverse backwards out of the loop into the
|
||||
* surrounding code. This is necessary to evaluate the right expression
|
||||
* correctly, as it must be evaluated in the same way as the left
|
||||
* expression, but the pointer to these segments would otherwise be
|
||||
* lost if not stored on the instance. Once the right expression has
|
||||
* been evaluated, this property is no longer used.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.prevSegments = null;
|
||||
|
||||
/**
|
||||
* Segments representing the start of everything to the left of the
|
||||
* `in` keyword. This can be used to move forward towards
|
||||
* `endOfLeftSegments`. `leftSegments` and `endOfLeftSegments` are
|
||||
* effectively the head and tail of a doubly-linked list.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.leftSegments = null;
|
||||
|
||||
/**
|
||||
* Segments representing the end of everything to the left of the
|
||||
* `in` keyword. This can be used to move backward towards `leftSegments`.
|
||||
* `leftSegments` and `endOfLeftSegments` are effectively the head
|
||||
* and tail of a doubly-linked list.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.endOfLeftSegments = null;
|
||||
|
||||
/**
|
||||
* The segments representing the left expression where `continue` will
|
||||
* jump to. In `for-in` loops, `continue` must always re-execute the
|
||||
* left expression each time through the loop. This contains the same
|
||||
* segments as `leftSegments`, but is duplicated here so each loop
|
||||
* context has the same property pointing to where `continue` should
|
||||
* end up.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.continueDestSegments = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the context for a `for-of` loop.
|
||||
*/
|
||||
class ForOfLoopContext extends LoopContextBase {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {LoopContext|null} upperContext The previous `LoopContext`.
|
||||
* @param {string|null} label The label for the loop from an enclosing `LabeledStatement`.
|
||||
* @param {BreakContext} breakContext The context for breaking the loop.
|
||||
*/
|
||||
constructor(upperContext, label, breakContext) {
|
||||
super(upperContext, "ForOfStatement", label, breakContext);
|
||||
|
||||
/**
|
||||
* The segments that came immediately before the start of the loop.
|
||||
* This allows you to traverse backwards out of the loop into the
|
||||
* surrounding code. This is necessary to evaluate the right expression
|
||||
* correctly, as it must be evaluated in the same way as the left
|
||||
* expression, but the pointer to these segments would otherwise be
|
||||
* lost if not stored on the instance. Once the right expression has
|
||||
* been evaluated, this property is no longer used.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.prevSegments = null;
|
||||
|
||||
/**
|
||||
* Segments representing the start of everything to the left of the
|
||||
* `of` keyword. This can be used to move forward towards
|
||||
* `endOfLeftSegments`. `leftSegments` and `endOfLeftSegments` are
|
||||
* effectively the head and tail of a doubly-linked list.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.leftSegments = null;
|
||||
|
||||
/**
|
||||
* Segments representing the end of everything to the left of the
|
||||
* `of` keyword. This can be used to move backward towards `leftSegments`.
|
||||
* `leftSegments` and `endOfLeftSegments` are effectively the head
|
||||
* and tail of a doubly-linked list.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.endOfLeftSegments = null;
|
||||
|
||||
/**
|
||||
* The segments representing the left expression where `continue` will
|
||||
* jump to. In `for-in` loops, `continue` must always re-execute the
|
||||
* left expression each time through the loop. This contains the same
|
||||
* segments as `leftSegments`, but is duplicated here so each loop
|
||||
* context has the same property pointing to where `continue` should
|
||||
* end up.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.continueDestSegments = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the context for any loop.
|
||||
* @typedef {WhileLoopContext|DoWhileLoopContext|ForLoopContext|ForInLoopContext|ForOfLoopContext} LoopContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the context for a `switch` statement.
|
||||
*/
|
||||
class SwitchContext {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {SwitchContext} upperContext The previous context.
|
||||
* @param {boolean} hasCase Indicates if there is at least one `case` statement.
|
||||
* `default` doesn't count.
|
||||
*/
|
||||
constructor(upperContext, hasCase) {
|
||||
|
||||
/**
|
||||
* The previous context.
|
||||
* @type {SwitchContext}
|
||||
*/
|
||||
this.upper = upperContext;
|
||||
|
||||
/**
|
||||
* Indicates if there is at least one `case` statement. `default` doesn't count.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.hasCase = hasCase;
|
||||
|
||||
/**
|
||||
* The `default` keyword.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.defaultSegments = null;
|
||||
|
||||
/**
|
||||
* The default case body starting segments.
|
||||
* @type {Array<CodePathSegment>|null}
|
||||
*/
|
||||
this.defaultBodySegments = null;
|
||||
|
||||
/**
|
||||
* Indicates if a `default` case and is empty exists.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.foundEmptyDefault = false;
|
||||
|
||||
/**
|
||||
* Indicates that a `default` exists and is the last case.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.lastIsDefault = false;
|
||||
|
||||
/**
|
||||
* The number of fork contexts created. This is equivalent to the
|
||||
* number of `case` statements plus a `default` statement (if present).
|
||||
* @type {number}
|
||||
*/
|
||||
this.forkCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the context for a `try` statement.
|
||||
*/
|
||||
class TryContext {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {TryContext} upperContext The previous context.
|
||||
* @param {boolean} hasFinalizer Indicates if the `try` statement has a
|
||||
* `finally` block.
|
||||
* @param {ForkContext} forkContext The enclosing fork context.
|
||||
*/
|
||||
constructor(upperContext, hasFinalizer, forkContext) {
|
||||
|
||||
/**
|
||||
* The previous context.
|
||||
* @type {TryContext}
|
||||
*/
|
||||
this.upper = upperContext;
|
||||
|
||||
/**
|
||||
* Indicates if the `try` statement has a `finally` block.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.hasFinalizer = hasFinalizer;
|
||||
|
||||
/**
|
||||
* Tracks the traversal position inside of the `try` statement. This is
|
||||
* used to help determine the context necessary to create paths because
|
||||
* a `try` statement may or may not have `catch` or `finally` blocks,
|
||||
* and code paths behave differently in those blocks.
|
||||
* @type {"try"|"catch"|"finally"}
|
||||
*/
|
||||
this.position = "try";
|
||||
|
||||
/**
|
||||
* If the `try` statement has a `finally` block, this affects how a
|
||||
* `return` statement behaves in the `try` block. Without `finally`,
|
||||
* `return` behaves as usual and doesn't require a fork; with `finally`,
|
||||
* `return` forks into the `finally` block, so we need a fork context
|
||||
* to track it.
|
||||
* @type {ForkContext|null}
|
||||
*/
|
||||
this.returnedForkContext = hasFinalizer
|
||||
? ForkContext.newEmpty(forkContext)
|
||||
: null;
|
||||
|
||||
/**
|
||||
* When a `throw` occurs inside of a `try` block, the code path forks
|
||||
* into the `catch` or `finally` blocks, and this fork context tracks
|
||||
* that path.
|
||||
* @type {ForkContext}
|
||||
*/
|
||||
this.thrownForkContext = ForkContext.newEmpty(forkContext);
|
||||
|
||||
/**
|
||||
* Indicates if the last segment in the `try` block is reachable.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.lastOfTryIsReachable = false;
|
||||
|
||||
/**
|
||||
* Indicates if the last segment in the `catch` block is reachable.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.lastOfCatchIsReachable = false;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Adds given segments into the `dest` array.
|
||||
* If the `others` array does not include the given segments, adds to the `all`
|
||||
* array as well.
|
||||
*
|
||||
* This adds only reachable and used segments.
|
||||
* @param {CodePathSegment[]} dest A destination array (`returnedSegments` or `thrownSegments`).
|
||||
* @param {CodePathSegment[]} others Another destination array (`returnedSegments` or `thrownSegments`).
|
||||
* @param {CodePathSegment[]} all The unified destination array (`finalSegments`).
|
||||
* @param {CodePathSegment[]} segments Segments to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
function addToReturnedOrThrown(dest, others, all, segments) {
|
||||
for (let i = 0; i < segments.length; ++i) {
|
||||
const segment = segments[i];
|
||||
|
||||
dest.push(segment);
|
||||
if (!others.includes(segment)) {
|
||||
all.push(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a loop context for a `continue` statement based on a given label.
|
||||
* @param {CodePathState} state The state to search within.
|
||||
* @param {string|null} label The label of a `continue` statement.
|
||||
* @returns {LoopContext} A loop-context for a `continue` statement.
|
||||
*/
|
||||
function getContinueContext(state, label) {
|
||||
if (!label) {
|
||||
return state.loopContext;
|
||||
}
|
||||
|
||||
let context = state.loopContext;
|
||||
|
||||
while (context) {
|
||||
if (context.label === label) {
|
||||
return context;
|
||||
}
|
||||
context = context.upper;
|
||||
}
|
||||
|
||||
/* c8 ignore next */
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a context for a `break` statement.
|
||||
* @param {CodePathState} state The state to search within.
|
||||
* @param {string|null} label The label of a `break` statement.
|
||||
* @returns {BreakContext} A context for a `break` statement.
|
||||
*/
|
||||
function getBreakContext(state, label) {
|
||||
let context = state.breakContext;
|
||||
|
||||
while (context) {
|
||||
if (label ? context.label === label : context.breakable) {
|
||||
return context;
|
||||
}
|
||||
context = context.upper;
|
||||
}
|
||||
|
||||
/* c8 ignore next */
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a context for a `return` statement. There is just one special case:
|
||||
* if there is a `try` statement with a `finally` block, because that alters
|
||||
* how `return` behaves; otherwise, this just passes through the given state.
|
||||
* @param {CodePathState} state The state to search within
|
||||
* @returns {TryContext|CodePathState} A context for a `return` statement.
|
||||
*/
|
||||
function getReturnContext(state) {
|
||||
let context = state.tryContext;
|
||||
|
||||
while (context) {
|
||||
if (context.hasFinalizer && context.position !== "finally") {
|
||||
return context;
|
||||
}
|
||||
context = context.upper;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a context for a `throw` statement. There is just one special case:
|
||||
* if there is a `try` statement with a `finally` block and we are inside of
|
||||
* a `catch` because that changes how `throw` behaves; otherwise, this just
|
||||
* passes through the given state.
|
||||
* @param {CodePathState} state The state to search within.
|
||||
* @returns {TryContext|CodePathState} A context for a `throw` statement.
|
||||
*/
|
||||
function getThrowContext(state) {
|
||||
let context = state.tryContext;
|
||||
|
||||
while (context) {
|
||||
if (context.position === "try" ||
|
||||
(context.hasFinalizer && context.position === "catch")
|
||||
) {
|
||||
return context;
|
||||
}
|
||||
context = context.upper;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given value from a given array.
|
||||
* @param {any[]} elements An array to remove the specific element.
|
||||
* @param {any} value The value to be removed.
|
||||
* @returns {void}
|
||||
*/
|
||||
function removeFromArray(elements, value) {
|
||||
elements.splice(elements.indexOf(value), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect given segments.
|
||||
*
|
||||
* This is used in a process for switch statements.
|
||||
* If there is the "default" chunk before other cases, the order is different
|
||||
* between node's and running's.
|
||||
* @param {CodePathSegment[]} prevSegments Forward segments to disconnect.
|
||||
* @param {CodePathSegment[]} nextSegments Backward segments to disconnect.
|
||||
* @returns {void}
|
||||
*/
|
||||
function disconnectSegments(prevSegments, nextSegments) {
|
||||
for (let i = 0; i < prevSegments.length; ++i) {
|
||||
const prevSegment = prevSegments[i];
|
||||
const nextSegment = nextSegments[i];
|
||||
|
||||
removeFromArray(prevSegment.nextSegments, nextSegment);
|
||||
removeFromArray(prevSegment.allNextSegments, nextSegment);
|
||||
removeFromArray(nextSegment.prevSegments, prevSegment);
|
||||
removeFromArray(nextSegment.allPrevSegments, prevSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates looping path between two arrays of segments, ensuring that there are
|
||||
* paths going between matching segments in the arrays.
|
||||
* @param {CodePathState} state The state to operate on.
|
||||
* @param {CodePathSegment[]} unflattenedFromSegments Segments which are source.
|
||||
* @param {CodePathSegment[]} unflattenedToSegments Segments which are destination.
|
||||
* @returns {void}
|
||||
*/
|
||||
function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) {
|
||||
|
||||
const fromSegments = CodePathSegment.flattenUnusedSegments(unflattenedFromSegments);
|
||||
const toSegments = CodePathSegment.flattenUnusedSegments(unflattenedToSegments);
|
||||
const end = Math.min(fromSegments.length, toSegments.length);
|
||||
|
||||
/*
|
||||
* This loop effectively updates a doubly-linked list between two collections
|
||||
* of segments making sure that segments in the same array indices are
|
||||
* combined to create a path.
|
||||
*/
|
||||
for (let i = 0; i < end; ++i) {
|
||||
|
||||
// get the segments in matching array indices
|
||||
const fromSegment = fromSegments[i];
|
||||
const toSegment = toSegments[i];
|
||||
|
||||
/*
|
||||
* If the destination segment is reachable, then create a path from the
|
||||
* source segment to the destination segment.
|
||||
*/
|
||||
if (toSegment.reachable) {
|
||||
fromSegment.nextSegments.push(toSegment);
|
||||
}
|
||||
|
||||
/*
|
||||
* If the source segment is reachable, then create a path from the
|
||||
* destination segment back to the source segment.
|
||||
*/
|
||||
if (fromSegment.reachable) {
|
||||
toSegment.prevSegments.push(fromSegment);
|
||||
}
|
||||
|
||||
/*
|
||||
* Also update the arrays that don't care if the segments are reachable
|
||||
* or not. This should always happen regardless of anything else.
|
||||
*/
|
||||
fromSegment.allNextSegments.push(toSegment);
|
||||
toSegment.allPrevSegments.push(fromSegment);
|
||||
|
||||
/*
|
||||
* If the destination segment has at least two previous segments in its
|
||||
* path then that means there was one previous segment before this iteration
|
||||
* of the loop was executed. So, we need to mark the source segment as
|
||||
* looped.
|
||||
*/
|
||||
if (toSegment.allPrevSegments.length >= 2) {
|
||||
CodePathSegment.markPrevSegmentAsLooped(toSegment, fromSegment);
|
||||
}
|
||||
|
||||
// let the code path analyzer know that there's been a loop created
|
||||
state.notifyLooped(fromSegment, toSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes segments of `test` chunk of a ForStatement.
|
||||
*
|
||||
* - Adds `false` paths to paths which are leaving from the loop.
|
||||
* - Sets `true` paths to paths which go to the body.
|
||||
* @param {LoopContext} context A loop context to modify.
|
||||
* @param {ChoiceContext} choiceContext A choice context of this loop.
|
||||
* @param {CodePathSegment[]} head The current head paths.
|
||||
* @returns {void}
|
||||
*/
|
||||
function finalizeTestSegmentsOfFor(context, choiceContext, head) {
|
||||
|
||||
/*
|
||||
* If this choice context doesn't already contain paths from a
|
||||
* child context, then add the current head to each potential path.
|
||||
*/
|
||||
if (!choiceContext.processed) {
|
||||
choiceContext.trueForkContext.add(head);
|
||||
choiceContext.falseForkContext.add(head);
|
||||
choiceContext.nullishForkContext.add(head);
|
||||
}
|
||||
|
||||
/*
|
||||
* If the test condition isn't a hardcoded truthy value, then `break`
|
||||
* must follow the same path as if the test condition is false. To represent
|
||||
* that, we append the path for when the loop test is false (represented by
|
||||
* `falseForkContext`) to the `brokenForkContext`.
|
||||
*/
|
||||
if (context.test !== true) {
|
||||
context.brokenForkContext.addAll(choiceContext.falseForkContext);
|
||||
}
|
||||
|
||||
context.endOfTestSegments = choiceContext.trueForkContext.makeNext(0, -1);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A class which manages state to analyze code paths.
|
||||
*/
|
||||
class CodePathState {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {IdGenerator} idGenerator An id generator to generate id for code
|
||||
* path segments.
|
||||
* @param {Function} onLooped A callback function to notify looping.
|
||||
*/
|
||||
constructor(idGenerator, onLooped) {
|
||||
|
||||
/**
|
||||
* The ID generator to use when creating new segments.
|
||||
* @type {IdGenerator}
|
||||
*/
|
||||
this.idGenerator = idGenerator;
|
||||
|
||||
/**
|
||||
* A callback function to call when there is a loop.
|
||||
* @type {Function}
|
||||
*/
|
||||
this.notifyLooped = onLooped;
|
||||
|
||||
/**
|
||||
* The root fork context for this state.
|
||||
* @type {ForkContext}
|
||||
*/
|
||||
this.forkContext = ForkContext.newRoot(idGenerator);
|
||||
|
||||
/**
|
||||
* Context for logical expressions, conditional expressions, `if` statements,
|
||||
* and loops.
|
||||
* @type {ChoiceContext}
|
||||
*/
|
||||
this.choiceContext = null;
|
||||
|
||||
/**
|
||||
* Context for `switch` statements.
|
||||
* @type {SwitchContext}
|
||||
*/
|
||||
this.switchContext = null;
|
||||
|
||||
/**
|
||||
* Context for `try` statements.
|
||||
* @type {TryContext}
|
||||
*/
|
||||
this.tryContext = null;
|
||||
|
||||
/**
|
||||
* Context for loop statements.
|
||||
* @type {LoopContext}
|
||||
*/
|
||||
this.loopContext = null;
|
||||
|
||||
/**
|
||||
* Context for `break` statements.
|
||||
* @type {BreakContext}
|
||||
*/
|
||||
this.breakContext = null;
|
||||
|
||||
/**
|
||||
* Context for `ChainExpression` nodes.
|
||||
* @type {ChainContext}
|
||||
*/
|
||||
this.chainContext = null;
|
||||
|
||||
/**
|
||||
* An array that tracks the current segments in the state. The array
|
||||
* starts empty and segments are added with each `onCodePathSegmentStart`
|
||||
* event and removed with each `onCodePathSegmentEnd` event. Effectively,
|
||||
* this is tracking the code path segment traversal as the state is
|
||||
* modified.
|
||||
* @type {Array<CodePathSegment>}
|
||||
*/
|
||||
this.currentSegments = [];
|
||||
|
||||
/**
|
||||
* Tracks the starting segment for this path. This value never changes.
|
||||
* @type {CodePathSegment}
|
||||
*/
|
||||
this.initialSegment = this.forkContext.head[0];
|
||||
|
||||
/**
|
||||
* The final segments of the code path which are either `return` or `throw`.
|
||||
* This is a union of the segments in `returnedForkContext` and `thrownForkContext`.
|
||||
* @type {Array<CodePathSegment>}
|
||||
*/
|
||||
this.finalSegments = [];
|
||||
|
||||
/**
|
||||
* The final segments of the code path which are `return`. These
|
||||
* segments are also contained in `finalSegments`.
|
||||
* @type {Array<CodePathSegment>}
|
||||
*/
|
||||
this.returnedForkContext = [];
|
||||
|
||||
/**
|
||||
* The final segments of the code path which are `throw`. These
|
||||
* segments are also contained in `finalSegments`.
|
||||
* @type {Array<CodePathSegment>}
|
||||
*/
|
||||
this.thrownForkContext = [];
|
||||
|
||||
/*
|
||||
* We add an `add` method so that these look more like fork contexts and
|
||||
* can be used interchangeably when a fork context is needed to add more
|
||||
* segments to a path.
|
||||
*
|
||||
* Ultimately, we want anything added to `returned` or `thrown` to also
|
||||
* be added to `final`. We only add reachable and used segments to these
|
||||
* arrays.
|
||||
*/
|
||||
const final = this.finalSegments;
|
||||
const returned = this.returnedForkContext;
|
||||
const thrown = this.thrownForkContext;
|
||||
|
||||
returned.add = addToReturnedOrThrown.bind(null, returned, thrown, final);
|
||||
thrown.add = addToReturnedOrThrown.bind(null, thrown, returned, final);
|
||||
}
|
||||
|
||||
/**
|
||||
* A passthrough property exposing the current pointer as part of the API.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get headSegments() {
|
||||
return this.forkContext.head;
|
||||
}
|
||||
|
||||
/**
|
||||
* The parent forking context.
|
||||
* This is used for the root of new forks.
|
||||
* @type {ForkContext}
|
||||
*/
|
||||
get parentForkContext() {
|
||||
const current = this.forkContext;
|
||||
|
||||
return current && current.upper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and stacks new forking context.
|
||||
* @param {boolean} forkLeavingPath A flag which shows being in a
|
||||
* "finally" block.
|
||||
* @returns {ForkContext} The created context.
|
||||
*/
|
||||
pushForkContext(forkLeavingPath) {
|
||||
this.forkContext = ForkContext.newEmpty(
|
||||
this.forkContext,
|
||||
forkLeavingPath
|
||||
);
|
||||
|
||||
return this.forkContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops and merges the last forking context.
|
||||
* @returns {ForkContext} The last context.
|
||||
*/
|
||||
popForkContext() {
|
||||
const lastContext = this.forkContext;
|
||||
|
||||
this.forkContext = lastContext.upper;
|
||||
this.forkContext.replaceHead(lastContext.makeNext(0, -1));
|
||||
|
||||
return lastContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new path.
|
||||
* @returns {void}
|
||||
*/
|
||||
forkPath() {
|
||||
this.forkContext.add(this.parentForkContext.makeNext(-1, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bypass path.
|
||||
* This is used for such as IfStatement which does not have "else" chunk.
|
||||
* @returns {void}
|
||||
*/
|
||||
forkBypassPath() {
|
||||
this.forkContext.add(this.parentForkContext.head);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// ConditionalExpression, LogicalExpression, IfStatement
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a context for ConditionalExpression, LogicalExpression, AssignmentExpression (logical assignments only),
|
||||
* IfStatement, WhileStatement, DoWhileStatement, or ForStatement.
|
||||
*
|
||||
* LogicalExpressions have cases that it goes different paths between the
|
||||
* `true` case and the `false` case.
|
||||
*
|
||||
* For Example:
|
||||
*
|
||||
* if (a || b) {
|
||||
* foo();
|
||||
* } else {
|
||||
* bar();
|
||||
* }
|
||||
*
|
||||
* In this case, `b` is evaluated always in the code path of the `else`
|
||||
* block, but it's not so in the code path of the `if` block.
|
||||
* So there are 3 paths.
|
||||
*
|
||||
* a -> foo();
|
||||
* a -> b -> foo();
|
||||
* a -> b -> bar();
|
||||
* @param {string} kind A kind string.
|
||||
* If the new context is LogicalExpression's or AssignmentExpression's, this is `"&&"` or `"||"` or `"??"`.
|
||||
* If it's IfStatement's or ConditionalExpression's, this is `"test"`.
|
||||
* Otherwise, this is `"loop"`.
|
||||
* @param {boolean} isForkingAsResult Indicates if the result of the choice
|
||||
* creates a fork.
|
||||
* @returns {void}
|
||||
*/
|
||||
pushChoiceContext(kind, isForkingAsResult) {
|
||||
this.choiceContext = new ChoiceContext(this.choiceContext, kind, isForkingAsResult, this.forkContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the last choice context and finalizes it.
|
||||
* This is called upon leaving a node that represents a choice.
|
||||
* @throws {Error} (Unreachable.)
|
||||
* @returns {ChoiceContext} The popped context.
|
||||
*/
|
||||
popChoiceContext() {
|
||||
const poppedChoiceContext = this.choiceContext;
|
||||
const forkContext = this.forkContext;
|
||||
const head = forkContext.head;
|
||||
|
||||
this.choiceContext = poppedChoiceContext.upper;
|
||||
|
||||
switch (poppedChoiceContext.kind) {
|
||||
case "&&":
|
||||
case "||":
|
||||
case "??":
|
||||
|
||||
/*
|
||||
* The `head` are the path of the right-hand operand.
|
||||
* If we haven't previously added segments from child contexts,
|
||||
* then we add these segments to all possible forks.
|
||||
*/
|
||||
if (!poppedChoiceContext.processed) {
|
||||
poppedChoiceContext.trueForkContext.add(head);
|
||||
poppedChoiceContext.falseForkContext.add(head);
|
||||
poppedChoiceContext.nullishForkContext.add(head);
|
||||
}
|
||||
|
||||
/*
|
||||
* If this context is the left (test) expression for another choice
|
||||
* context, such as `a || b` in the expression `a || b || c`,
|
||||
* then we take the segments for this context and move them up
|
||||
* to the parent context.
|
||||
*/
|
||||
if (poppedChoiceContext.isForkingAsResult) {
|
||||
const parentContext = this.choiceContext;
|
||||
|
||||
parentContext.trueForkContext.addAll(poppedChoiceContext.trueForkContext);
|
||||
parentContext.falseForkContext.addAll(poppedChoiceContext.falseForkContext);
|
||||
parentContext.nullishForkContext.addAll(poppedChoiceContext.nullishForkContext);
|
||||
parentContext.processed = true;
|
||||
|
||||
// Exit early so we don't collapse all paths into one.
|
||||
return poppedChoiceContext;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "test":
|
||||
if (!poppedChoiceContext.processed) {
|
||||
|
||||
/*
|
||||
* The head segments are the path of the `if` block here.
|
||||
* Updates the `true` path with the end of the `if` block.
|
||||
*/
|
||||
poppedChoiceContext.trueForkContext.clear();
|
||||
poppedChoiceContext.trueForkContext.add(head);
|
||||
} else {
|
||||
|
||||
/*
|
||||
* The head segments are the path of the `else` block here.
|
||||
* Updates the `false` path with the end of the `else`
|
||||
* block.
|
||||
*/
|
||||
poppedChoiceContext.falseForkContext.clear();
|
||||
poppedChoiceContext.falseForkContext.add(head);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "loop":
|
||||
|
||||
/*
|
||||
* Loops are addressed in `popLoopContext()` so just return
|
||||
* the context without modification.
|
||||
*/
|
||||
return poppedChoiceContext;
|
||||
|
||||
/* c8 ignore next */
|
||||
default:
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
/*
|
||||
* Merge the true path with the false path to create a single path.
|
||||
*/
|
||||
const combinedForkContext = poppedChoiceContext.trueForkContext;
|
||||
|
||||
combinedForkContext.addAll(poppedChoiceContext.falseForkContext);
|
||||
forkContext.replaceHead(combinedForkContext.makeNext(0, -1));
|
||||
|
||||
return poppedChoiceContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a code path segment to represent right-hand operand of a logical
|
||||
* expression.
|
||||
* This is called in the preprocessing phase when entering a node.
|
||||
* @throws {Error} (Unreachable.)
|
||||
* @returns {void}
|
||||
*/
|
||||
makeLogicalRight() {
|
||||
const currentChoiceContext = this.choiceContext;
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
if (currentChoiceContext.processed) {
|
||||
|
||||
/*
|
||||
* This context was already assigned segments from a child
|
||||
* choice context. In this case, we are concerned only about
|
||||
* the path that does not short-circuit and so ends up on the
|
||||
* right-hand operand of the logical expression.
|
||||
*/
|
||||
let prevForkContext;
|
||||
|
||||
switch (currentChoiceContext.kind) {
|
||||
case "&&": // if true then go to the right-hand side.
|
||||
prevForkContext = currentChoiceContext.trueForkContext;
|
||||
break;
|
||||
case "||": // if false then go to the right-hand side.
|
||||
prevForkContext = currentChoiceContext.falseForkContext;
|
||||
break;
|
||||
case "??": // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's nullishForkContext.
|
||||
prevForkContext = currentChoiceContext.nullishForkContext;
|
||||
break;
|
||||
default:
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
/*
|
||||
* Create the segment for the right-hand operand of the logical expression
|
||||
* and adjust the fork context pointer to point there. The right-hand segment
|
||||
* is added at the end of all segments in `prevForkContext`.
|
||||
*/
|
||||
forkContext.replaceHead(prevForkContext.makeNext(0, -1));
|
||||
|
||||
/*
|
||||
* We no longer need this list of segments.
|
||||
*
|
||||
* Reset `processed` because we've removed the segments from the child
|
||||
* choice context. This allows `popChoiceContext()` to continue adding
|
||||
* segments later.
|
||||
*/
|
||||
prevForkContext.clear();
|
||||
currentChoiceContext.processed = false;
|
||||
|
||||
} else {
|
||||
|
||||
/*
|
||||
* This choice context was not assigned segments from a child
|
||||
* choice context, which means that it's a terminal logical
|
||||
* expression.
|
||||
*
|
||||
* `head` is the segments for the left-hand operand of the
|
||||
* logical expression.
|
||||
*
|
||||
* Each of the fork contexts below are empty at this point. We choose
|
||||
* the path(s) that will short-circuit and add the segment for the
|
||||
* left-hand operand to it. Ultimately, this will be the only segment
|
||||
* in that path due to the short-circuting, so we are just seeding
|
||||
* these paths to start.
|
||||
*/
|
||||
switch (currentChoiceContext.kind) {
|
||||
case "&&":
|
||||
|
||||
/*
|
||||
* In most contexts, when a && expression evaluates to false,
|
||||
* it short circuits, so we need to account for that by setting
|
||||
* the `falseForkContext` to the left operand.
|
||||
*
|
||||
* When a && expression is the left-hand operand for a ??
|
||||
* expression, such as `(a && b) ?? c`, a nullish value will
|
||||
* also short-circuit in a different way than a false value,
|
||||
* so we also set the `nullishForkContext` to the left operand.
|
||||
* This path is only used with a ?? expression and is thrown
|
||||
* away for any other type of logical expression, so it's safe
|
||||
* to always add.
|
||||
*/
|
||||
currentChoiceContext.falseForkContext.add(forkContext.head);
|
||||
currentChoiceContext.nullishForkContext.add(forkContext.head);
|
||||
break;
|
||||
case "||": // the true path can short-circuit.
|
||||
currentChoiceContext.trueForkContext.add(forkContext.head);
|
||||
break;
|
||||
case "??": // both can short-circuit.
|
||||
currentChoiceContext.trueForkContext.add(forkContext.head);
|
||||
currentChoiceContext.falseForkContext.add(forkContext.head);
|
||||
break;
|
||||
default:
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
/*
|
||||
* Create the segment for the right-hand operand of the logical expression
|
||||
* and adjust the fork context pointer to point there.
|
||||
*/
|
||||
forkContext.replaceHead(forkContext.makeNext(-1, -1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment of the `if` block.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeIfConsequent() {
|
||||
const context = this.choiceContext;
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
/*
|
||||
* If any result were not transferred from child contexts,
|
||||
* this sets the head segments to both cases.
|
||||
* The head segments are the path of the test expression.
|
||||
*/
|
||||
if (!context.processed) {
|
||||
context.trueForkContext.add(forkContext.head);
|
||||
context.falseForkContext.add(forkContext.head);
|
||||
context.nullishForkContext.add(forkContext.head);
|
||||
}
|
||||
|
||||
context.processed = false;
|
||||
|
||||
// Creates new path from the `true` case.
|
||||
forkContext.replaceHead(
|
||||
context.trueForkContext.makeNext(0, -1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment of the `else` block.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeIfAlternate() {
|
||||
const context = this.choiceContext;
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
/*
|
||||
* The head segments are the path of the `if` block.
|
||||
* Updates the `true` path with the end of the `if` block.
|
||||
*/
|
||||
context.trueForkContext.clear();
|
||||
context.trueForkContext.add(forkContext.head);
|
||||
context.processed = true;
|
||||
|
||||
// Creates new path from the `false` case.
|
||||
forkContext.replaceHead(
|
||||
context.falseForkContext.makeNext(0, -1)
|
||||
);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// ChainExpression
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Pushes a new `ChainExpression` context to the stack. This method is
|
||||
* called when entering a `ChainExpression` node. A chain context is used to
|
||||
* count forking in the optional chain then merge them on the exiting from the
|
||||
* `ChainExpression` node.
|
||||
* @returns {void}
|
||||
*/
|
||||
pushChainContext() {
|
||||
this.chainContext = new ChainContext(this.chainContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop a `ChainExpression` context from the stack. This method is called on
|
||||
* exiting from each `ChainExpression` node. This merges all forks of the
|
||||
* last optional chaining.
|
||||
* @returns {void}
|
||||
*/
|
||||
popChainContext() {
|
||||
const context = this.chainContext;
|
||||
|
||||
this.chainContext = context.upper;
|
||||
|
||||
// pop all choice contexts of this.
|
||||
for (let i = context.choiceContextCount; i > 0; --i) {
|
||||
this.popChoiceContext();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a choice context for optional access.
|
||||
* This method is called on entering to each `(Call|Member)Expression[optional=true]` node.
|
||||
* This creates a choice context as similar to `LogicalExpression[operator="??"]` node.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeOptionalNode() {
|
||||
if (this.chainContext) {
|
||||
this.chainContext.choiceContextCount += 1;
|
||||
this.pushChoiceContext("??", false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fork.
|
||||
* This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeOptionalRight() {
|
||||
if (this.chainContext) {
|
||||
this.makeLogicalRight();
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// SwitchStatement
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a context object of SwitchStatement and stacks it.
|
||||
* @param {boolean} hasCase `true` if the switch statement has one or more
|
||||
* case parts.
|
||||
* @param {string|null} label The label text.
|
||||
* @returns {void}
|
||||
*/
|
||||
pushSwitchContext(hasCase, label) {
|
||||
this.switchContext = new SwitchContext(this.switchContext, hasCase);
|
||||
this.pushBreakContext(true, label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the last context of SwitchStatement and finalizes it.
|
||||
*
|
||||
* - Disposes all forking stack for `case` and `default`.
|
||||
* - Creates the next code path segment from `context.brokenForkContext`.
|
||||
* - If the last `SwitchCase` node is not a `default` part, creates a path
|
||||
* to the `default` body.
|
||||
* @returns {void}
|
||||
*/
|
||||
popSwitchContext() {
|
||||
const context = this.switchContext;
|
||||
|
||||
this.switchContext = context.upper;
|
||||
|
||||
const forkContext = this.forkContext;
|
||||
const brokenForkContext = this.popBreakContext().brokenForkContext;
|
||||
|
||||
if (context.forkCount === 0) {
|
||||
|
||||
/*
|
||||
* When there is only one `default` chunk and there is one or more
|
||||
* `break` statements, even if forks are nothing, it needs to merge
|
||||
* those.
|
||||
*/
|
||||
if (!brokenForkContext.empty) {
|
||||
brokenForkContext.add(forkContext.makeNext(-1, -1));
|
||||
forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const lastSegments = forkContext.head;
|
||||
|
||||
this.forkBypassPath();
|
||||
const lastCaseSegments = forkContext.head;
|
||||
|
||||
/*
|
||||
* `brokenForkContext` is used to make the next segment.
|
||||
* It must add the last segment into `brokenForkContext`.
|
||||
*/
|
||||
brokenForkContext.add(lastSegments);
|
||||
|
||||
/*
|
||||
* Any value that doesn't match a `case` test should flow to the default
|
||||
* case. That happens normally when the default case is last in the `switch`,
|
||||
* but if it's not, we need to rewire some of the paths to be correct.
|
||||
*/
|
||||
if (!context.lastIsDefault) {
|
||||
if (context.defaultBodySegments) {
|
||||
|
||||
/*
|
||||
* There is a non-empty default case, so remove the path from the `default`
|
||||
* label to its body for an accurate representation.
|
||||
*/
|
||||
disconnectSegments(context.defaultSegments, context.defaultBodySegments);
|
||||
|
||||
/*
|
||||
* Connect the path from the last non-default case to the body of the
|
||||
* default case.
|
||||
*/
|
||||
makeLooped(this, lastCaseSegments, context.defaultBodySegments);
|
||||
|
||||
} else {
|
||||
|
||||
/*
|
||||
* There is no default case, so we treat this as if the last case
|
||||
* had a `break` in it.
|
||||
*/
|
||||
brokenForkContext.add(lastCaseSegments);
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse up to the original fork context for the `switch` statement
|
||||
for (let i = 0; i < context.forkCount; ++i) {
|
||||
this.forkContext = this.forkContext.upper;
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a path from all `brokenForkContext` paths.
|
||||
* This is a path after `switch` statement.
|
||||
*/
|
||||
this.forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for a `SwitchCase` node.
|
||||
* @param {boolean} isCaseBodyEmpty `true` if the body is empty.
|
||||
* @param {boolean} isDefaultCase `true` if the body is the default case.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeSwitchCaseBody(isCaseBodyEmpty, isDefaultCase) {
|
||||
const context = this.switchContext;
|
||||
|
||||
if (!context.hasCase) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Merge forks.
|
||||
* The parent fork context has two segments.
|
||||
* Those are from the current `case` and the body of the previous case.
|
||||
*/
|
||||
const parentForkContext = this.forkContext;
|
||||
const forkContext = this.pushForkContext();
|
||||
|
||||
forkContext.add(parentForkContext.makeNext(0, -1));
|
||||
|
||||
/*
|
||||
* Add information about the default case.
|
||||
*
|
||||
* The purpose of this is to identify the starting segments for the
|
||||
* default case to make sure there is a path there.
|
||||
*/
|
||||
if (isDefaultCase) {
|
||||
|
||||
/*
|
||||
* This is the default case in the `switch`.
|
||||
*
|
||||
* We first save the current pointer as `defaultSegments` to point
|
||||
* to the `default` keyword.
|
||||
*/
|
||||
context.defaultSegments = parentForkContext.head;
|
||||
|
||||
/*
|
||||
* If the body of the case is empty then we just set
|
||||
* `foundEmptyDefault` to true; otherwise, we save a reference
|
||||
* to the current pointer as `defaultBodySegments`.
|
||||
*/
|
||||
if (isCaseBodyEmpty) {
|
||||
context.foundEmptyDefault = true;
|
||||
} else {
|
||||
context.defaultBodySegments = forkContext.head;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
/*
|
||||
* This is not the default case in the `switch`.
|
||||
*
|
||||
* If it's not empty and there is already an empty default case found,
|
||||
* that means the default case actually comes before this case,
|
||||
* and that it will fall through to this case. So, we can now
|
||||
* ignore the previous default case (reset `foundEmptyDefault` to false)
|
||||
* and set `defaultBodySegments` to the current segments because this is
|
||||
* effectively the new default case.
|
||||
*/
|
||||
if (!isCaseBodyEmpty && context.foundEmptyDefault) {
|
||||
context.foundEmptyDefault = false;
|
||||
context.defaultBodySegments = forkContext.head;
|
||||
}
|
||||
}
|
||||
|
||||
// keep track if the default case ends up last
|
||||
context.lastIsDefault = isDefaultCase;
|
||||
context.forkCount += 1;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// TryStatement
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a context object of TryStatement and stacks it.
|
||||
* @param {boolean} hasFinalizer `true` if the try statement has a
|
||||
* `finally` block.
|
||||
* @returns {void}
|
||||
*/
|
||||
pushTryContext(hasFinalizer) {
|
||||
this.tryContext = new TryContext(this.tryContext, hasFinalizer, this.forkContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the last context of TryStatement and finalizes it.
|
||||
* @returns {void}
|
||||
*/
|
||||
popTryContext() {
|
||||
const context = this.tryContext;
|
||||
|
||||
this.tryContext = context.upper;
|
||||
|
||||
/*
|
||||
* If we're inside the `catch` block, that means there is no `finally`,
|
||||
* so we can process the `try` and `catch` blocks the simple way and
|
||||
* merge their two paths.
|
||||
*/
|
||||
if (context.position === "catch") {
|
||||
this.popForkContext();
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* The following process is executed only when there is a `finally`
|
||||
* block.
|
||||
*/
|
||||
|
||||
const originalReturnedForkContext = context.returnedForkContext;
|
||||
const originalThrownForkContext = context.thrownForkContext;
|
||||
|
||||
// no `return` or `throw` in `try` or `catch` so there's nothing left to do
|
||||
if (originalReturnedForkContext.empty && originalThrownForkContext.empty) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* The following process is executed only when there is a `finally`
|
||||
* block and there was a `return` or `throw` in the `try` or `catch`
|
||||
* blocks.
|
||||
*/
|
||||
|
||||
// Separate head to normal paths and leaving paths.
|
||||
const headSegments = this.forkContext.head;
|
||||
|
||||
this.forkContext = this.forkContext.upper;
|
||||
const normalSegments = headSegments.slice(0, headSegments.length / 2 | 0);
|
||||
const leavingSegments = headSegments.slice(headSegments.length / 2 | 0);
|
||||
|
||||
// Forwards the leaving path to upper contexts.
|
||||
if (!originalReturnedForkContext.empty) {
|
||||
getReturnContext(this).returnedForkContext.add(leavingSegments);
|
||||
}
|
||||
if (!originalThrownForkContext.empty) {
|
||||
getThrowContext(this).thrownForkContext.add(leavingSegments);
|
||||
}
|
||||
|
||||
// Sets the normal path as the next.
|
||||
this.forkContext.replaceHead(normalSegments);
|
||||
|
||||
/*
|
||||
* If both paths of the `try` block and the `catch` block are
|
||||
* unreachable, the next path becomes unreachable as well.
|
||||
*/
|
||||
if (!context.lastOfTryIsReachable && !context.lastOfCatchIsReachable) {
|
||||
this.forkContext.makeUnreachable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for a `catch` block.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeCatchBlock() {
|
||||
const context = this.tryContext;
|
||||
const forkContext = this.forkContext;
|
||||
const originalThrownForkContext = context.thrownForkContext;
|
||||
|
||||
/*
|
||||
* We are now in a catch block so we need to update the context
|
||||
* with that information. This includes creating a new fork
|
||||
* context in case we encounter any `throw` statements here.
|
||||
*/
|
||||
context.position = "catch";
|
||||
context.thrownForkContext = ForkContext.newEmpty(forkContext);
|
||||
context.lastOfTryIsReachable = forkContext.reachable;
|
||||
|
||||
// Merge the thrown paths from the `try` and `catch` blocks
|
||||
originalThrownForkContext.add(forkContext.head);
|
||||
const thrownSegments = originalThrownForkContext.makeNext(0, -1);
|
||||
|
||||
// Fork to a bypass and the merged thrown path.
|
||||
this.pushForkContext();
|
||||
this.forkBypassPath();
|
||||
this.forkContext.add(thrownSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for a `finally` block.
|
||||
*
|
||||
* In the `finally` block, parallel paths are created. The parallel paths
|
||||
* are used as leaving-paths. The leaving-paths are paths from `return`
|
||||
* statements and `throw` statements in a `try` block or a `catch` block.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeFinallyBlock() {
|
||||
const context = this.tryContext;
|
||||
let forkContext = this.forkContext;
|
||||
const originalReturnedForkContext = context.returnedForkContext;
|
||||
const originalThrownForContext = context.thrownForkContext;
|
||||
const headOfLeavingSegments = forkContext.head;
|
||||
|
||||
// Update state.
|
||||
if (context.position === "catch") {
|
||||
|
||||
// Merges two paths from the `try` block and `catch` block.
|
||||
this.popForkContext();
|
||||
forkContext = this.forkContext;
|
||||
|
||||
context.lastOfCatchIsReachable = forkContext.reachable;
|
||||
} else {
|
||||
context.lastOfTryIsReachable = forkContext.reachable;
|
||||
}
|
||||
|
||||
|
||||
context.position = "finally";
|
||||
|
||||
/*
|
||||
* If there was no `return` or `throw` in either the `try` or `catch`
|
||||
* blocks, then there's no further code paths to create for `finally`.
|
||||
*/
|
||||
if (originalReturnedForkContext.empty && originalThrownForContext.empty) {
|
||||
|
||||
// This path does not leave.
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a parallel segment from merging returned and thrown.
|
||||
* This segment will leave at the end of this `finally` block.
|
||||
*/
|
||||
const segments = forkContext.makeNext(-1, -1);
|
||||
|
||||
for (let i = 0; i < forkContext.count; ++i) {
|
||||
const prevSegsOfLeavingSegment = [headOfLeavingSegments[i]];
|
||||
|
||||
for (let j = 0; j < originalReturnedForkContext.segmentsList.length; ++j) {
|
||||
prevSegsOfLeavingSegment.push(originalReturnedForkContext.segmentsList[j][i]);
|
||||
}
|
||||
for (let j = 0; j < originalThrownForContext.segmentsList.length; ++j) {
|
||||
prevSegsOfLeavingSegment.push(originalThrownForContext.segmentsList[j][i]);
|
||||
}
|
||||
|
||||
segments.push(
|
||||
CodePathSegment.newNext(
|
||||
this.idGenerator.next(),
|
||||
prevSegsOfLeavingSegment
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.pushForkContext(true);
|
||||
this.forkContext.add(segments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment from the first throwable node to the `catch`
|
||||
* block or the `finally` block.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeFirstThrowablePathInTryBlock() {
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
if (!forkContext.reachable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getThrowContext(this);
|
||||
|
||||
if (context === this ||
|
||||
context.position !== "try" ||
|
||||
!context.thrownForkContext.empty
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.thrownForkContext.add(forkContext.head);
|
||||
forkContext.replaceHead(forkContext.makeNext(-1, -1));
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Loop Statements
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a context object of a loop statement and stacks it.
|
||||
* @param {string} type The type of the node which was triggered. One of
|
||||
* `WhileStatement`, `DoWhileStatement`, `ForStatement`, `ForInStatement`,
|
||||
* and `ForStatement`.
|
||||
* @param {string|null} label A label of the node which was triggered.
|
||||
* @throws {Error} (Unreachable - unknown type.)
|
||||
* @returns {void}
|
||||
*/
|
||||
pushLoopContext(type, label) {
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
// All loops need a path to account for `break` statements
|
||||
const breakContext = this.pushBreakContext(true, label);
|
||||
|
||||
switch (type) {
|
||||
case "WhileStatement":
|
||||
this.pushChoiceContext("loop", false);
|
||||
this.loopContext = new WhileLoopContext(this.loopContext, label, breakContext);
|
||||
break;
|
||||
|
||||
case "DoWhileStatement":
|
||||
this.pushChoiceContext("loop", false);
|
||||
this.loopContext = new DoWhileLoopContext(this.loopContext, label, breakContext, forkContext);
|
||||
break;
|
||||
|
||||
case "ForStatement":
|
||||
this.pushChoiceContext("loop", false);
|
||||
this.loopContext = new ForLoopContext(this.loopContext, label, breakContext);
|
||||
break;
|
||||
|
||||
case "ForInStatement":
|
||||
this.loopContext = new ForInLoopContext(this.loopContext, label, breakContext);
|
||||
break;
|
||||
|
||||
case "ForOfStatement":
|
||||
this.loopContext = new ForOfLoopContext(this.loopContext, label, breakContext);
|
||||
break;
|
||||
|
||||
/* c8 ignore next */
|
||||
default:
|
||||
throw new Error(`unknown type: "${type}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the last context of a loop statement and finalizes it.
|
||||
* @throws {Error} (Unreachable - unknown type.)
|
||||
* @returns {void}
|
||||
*/
|
||||
popLoopContext() {
|
||||
const context = this.loopContext;
|
||||
|
||||
this.loopContext = context.upper;
|
||||
|
||||
const forkContext = this.forkContext;
|
||||
const brokenForkContext = this.popBreakContext().brokenForkContext;
|
||||
|
||||
// Creates a looped path.
|
||||
switch (context.type) {
|
||||
case "WhileStatement":
|
||||
case "ForStatement":
|
||||
this.popChoiceContext();
|
||||
|
||||
/*
|
||||
* Creates the path from the end of the loop body up to the
|
||||
* location where `continue` would jump to.
|
||||
*/
|
||||
makeLooped(
|
||||
this,
|
||||
forkContext.head,
|
||||
context.continueDestSegments
|
||||
);
|
||||
break;
|
||||
|
||||
case "DoWhileStatement": {
|
||||
const choiceContext = this.popChoiceContext();
|
||||
|
||||
if (!choiceContext.processed) {
|
||||
choiceContext.trueForkContext.add(forkContext.head);
|
||||
choiceContext.falseForkContext.add(forkContext.head);
|
||||
}
|
||||
|
||||
/*
|
||||
* If this isn't a hardcoded `true` condition, then `break`
|
||||
* should continue down the path as if the condition evaluated
|
||||
* to false.
|
||||
*/
|
||||
if (context.test !== true) {
|
||||
brokenForkContext.addAll(choiceContext.falseForkContext);
|
||||
}
|
||||
|
||||
/*
|
||||
* When the condition is true, the loop continues back to the top,
|
||||
* so create a path from each possible true condition back to the
|
||||
* top of the loop.
|
||||
*/
|
||||
const segmentsList = choiceContext.trueForkContext.segmentsList;
|
||||
|
||||
for (let i = 0; i < segmentsList.length; ++i) {
|
||||
makeLooped(
|
||||
this,
|
||||
segmentsList[i],
|
||||
context.entrySegments
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
brokenForkContext.add(forkContext.head);
|
||||
|
||||
/*
|
||||
* Creates the path from the end of the loop body up to the
|
||||
* left expression (left of `in` or `of`) of the loop.
|
||||
*/
|
||||
makeLooped(
|
||||
this,
|
||||
forkContext.head,
|
||||
context.leftSegments
|
||||
);
|
||||
break;
|
||||
|
||||
/* c8 ignore next */
|
||||
default:
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
/*
|
||||
* If there wasn't a `break` statement in the loop, then we're at
|
||||
* the end of the loop's path, so we make an unreachable segment
|
||||
* to mark that.
|
||||
*
|
||||
* If there was a `break` statement, then we continue on into the
|
||||
* `brokenForkContext`.
|
||||
*/
|
||||
if (brokenForkContext.empty) {
|
||||
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
|
||||
} else {
|
||||
forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for the test part of a WhileStatement.
|
||||
* @param {boolean|undefined} test The test value (only when constant).
|
||||
* @returns {void}
|
||||
*/
|
||||
makeWhileTest(test) {
|
||||
const context = this.loopContext;
|
||||
const forkContext = this.forkContext;
|
||||
const testSegments = forkContext.makeNext(0, -1);
|
||||
|
||||
// Update state.
|
||||
context.test = test;
|
||||
context.continueDestSegments = testSegments;
|
||||
forkContext.replaceHead(testSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for the body part of a WhileStatement.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeWhileBody() {
|
||||
const context = this.loopContext;
|
||||
const choiceContext = this.choiceContext;
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
if (!choiceContext.processed) {
|
||||
choiceContext.trueForkContext.add(forkContext.head);
|
||||
choiceContext.falseForkContext.add(forkContext.head);
|
||||
}
|
||||
|
||||
/*
|
||||
* If this isn't a hardcoded `true` condition, then `break`
|
||||
* should continue down the path as if the condition evaluated
|
||||
* to false.
|
||||
*/
|
||||
if (context.test !== true) {
|
||||
context.brokenForkContext.addAll(choiceContext.falseForkContext);
|
||||
}
|
||||
forkContext.replaceHead(choiceContext.trueForkContext.makeNext(0, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for the body part of a DoWhileStatement.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeDoWhileBody() {
|
||||
const context = this.loopContext;
|
||||
const forkContext = this.forkContext;
|
||||
const bodySegments = forkContext.makeNext(-1, -1);
|
||||
|
||||
// Update state.
|
||||
context.entrySegments = bodySegments;
|
||||
forkContext.replaceHead(bodySegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for the test part of a DoWhileStatement.
|
||||
* @param {boolean|undefined} test The test value (only when constant).
|
||||
* @returns {void}
|
||||
*/
|
||||
makeDoWhileTest(test) {
|
||||
const context = this.loopContext;
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
context.test = test;
|
||||
|
||||
/*
|
||||
* If there is a `continue` statement in the loop then `continueForkContext`
|
||||
* won't be empty. We wire up the path from `continue` to the loop
|
||||
* test condition and then continue the traversal in the root fork context.
|
||||
*/
|
||||
if (!context.continueForkContext.empty) {
|
||||
context.continueForkContext.add(forkContext.head);
|
||||
const testSegments = context.continueForkContext.makeNext(0, -1);
|
||||
|
||||
forkContext.replaceHead(testSegments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for the test part of a ForStatement.
|
||||
* @param {boolean|undefined} test The test value (only when constant).
|
||||
* @returns {void}
|
||||
*/
|
||||
makeForTest(test) {
|
||||
const context = this.loopContext;
|
||||
const forkContext = this.forkContext;
|
||||
const endOfInitSegments = forkContext.head;
|
||||
const testSegments = forkContext.makeNext(-1, -1);
|
||||
|
||||
/*
|
||||
* Update the state.
|
||||
*
|
||||
* The `continueDestSegments` are set to `testSegments` because we
|
||||
* don't yet know if there is an update expression in this loop. So,
|
||||
* from what we already know at this point, a `continue` statement
|
||||
* will jump back to the test expression.
|
||||
*/
|
||||
context.test = test;
|
||||
context.endOfInitSegments = endOfInitSegments;
|
||||
context.continueDestSegments = context.testSegments = testSegments;
|
||||
forkContext.replaceHead(testSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for the update part of a ForStatement.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeForUpdate() {
|
||||
const context = this.loopContext;
|
||||
const choiceContext = this.choiceContext;
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
// Make the next paths of the test.
|
||||
if (context.testSegments) {
|
||||
finalizeTestSegmentsOfFor(
|
||||
context,
|
||||
choiceContext,
|
||||
forkContext.head
|
||||
);
|
||||
} else {
|
||||
context.endOfInitSegments = forkContext.head;
|
||||
}
|
||||
|
||||
/*
|
||||
* Update the state.
|
||||
*
|
||||
* The `continueDestSegments` are now set to `updateSegments` because we
|
||||
* know there is an update expression in this loop. So, a `continue` statement
|
||||
* in the loop will jump to the update expression first, and then to any
|
||||
* test expression the loop might have.
|
||||
*/
|
||||
const updateSegments = forkContext.makeDisconnected(-1, -1);
|
||||
|
||||
context.continueDestSegments = context.updateSegments = updateSegments;
|
||||
forkContext.replaceHead(updateSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for the body part of a ForStatement.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeForBody() {
|
||||
const context = this.loopContext;
|
||||
const choiceContext = this.choiceContext;
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
/*
|
||||
* Determine what to do based on which part of the `for` loop are present.
|
||||
* 1. If there is an update expression, then `updateSegments` is not null and
|
||||
* we need to assign `endOfUpdateSegments`, and if there is a test
|
||||
* expression, we then need to create the looped path to get back to
|
||||
* the test condition.
|
||||
* 2. If there is no update expression but there is a test expression,
|
||||
* then we only need to update the test segment information.
|
||||
* 3. If there is no update expression and no test expression, then we
|
||||
* just save `endOfInitSegments`.
|
||||
*/
|
||||
if (context.updateSegments) {
|
||||
context.endOfUpdateSegments = forkContext.head;
|
||||
|
||||
/*
|
||||
* In a `for` loop that has both an update expression and a test
|
||||
* condition, execution flows from the test expression into the
|
||||
* loop body, to the update expression, and then back to the test
|
||||
* expression to determine if the loop should continue.
|
||||
*
|
||||
* To account for that, we need to make a path from the end of the
|
||||
* update expression to the start of the test expression. This is
|
||||
* effectively what creates the loop in the code path.
|
||||
*/
|
||||
if (context.testSegments) {
|
||||
makeLooped(
|
||||
this,
|
||||
context.endOfUpdateSegments,
|
||||
context.testSegments
|
||||
);
|
||||
}
|
||||
} else if (context.testSegments) {
|
||||
finalizeTestSegmentsOfFor(
|
||||
context,
|
||||
choiceContext,
|
||||
forkContext.head
|
||||
);
|
||||
} else {
|
||||
context.endOfInitSegments = forkContext.head;
|
||||
}
|
||||
|
||||
let bodySegments = context.endOfTestSegments;
|
||||
|
||||
/*
|
||||
* If there is a test condition, then there `endOfTestSegments` is also
|
||||
* the start of the loop body. If there isn't a test condition then
|
||||
* `bodySegments` will be null and we need to look elsewhere to find
|
||||
* the start of the body.
|
||||
*
|
||||
* The body starts at the end of the init expression and ends at the end
|
||||
* of the update expression, so we use those locations to determine the
|
||||
* body segments.
|
||||
*/
|
||||
if (!bodySegments) {
|
||||
|
||||
const prevForkContext = ForkContext.newEmpty(forkContext);
|
||||
|
||||
prevForkContext.add(context.endOfInitSegments);
|
||||
if (context.endOfUpdateSegments) {
|
||||
prevForkContext.add(context.endOfUpdateSegments);
|
||||
}
|
||||
|
||||
bodySegments = prevForkContext.makeNext(0, -1);
|
||||
}
|
||||
|
||||
/*
|
||||
* If there was no test condition and no update expression, then
|
||||
* `continueDestSegments` will be null. In that case, a
|
||||
* `continue` should skip directly to the body of the loop.
|
||||
* Otherwise, we want to keep the current `continueDestSegments`.
|
||||
*/
|
||||
context.continueDestSegments = context.continueDestSegments || bodySegments;
|
||||
|
||||
// move pointer to the body
|
||||
forkContext.replaceHead(bodySegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for the left part of a ForInStatement and a
|
||||
* ForOfStatement.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeForInOfLeft() {
|
||||
const context = this.loopContext;
|
||||
const forkContext = this.forkContext;
|
||||
const leftSegments = forkContext.makeDisconnected(-1, -1);
|
||||
|
||||
// Update state.
|
||||
context.prevSegments = forkContext.head;
|
||||
context.leftSegments = context.continueDestSegments = leftSegments;
|
||||
forkContext.replaceHead(leftSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for the right part of a ForInStatement and a
|
||||
* ForOfStatement.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeForInOfRight() {
|
||||
const context = this.loopContext;
|
||||
const forkContext = this.forkContext;
|
||||
const temp = ForkContext.newEmpty(forkContext);
|
||||
|
||||
temp.add(context.prevSegments);
|
||||
const rightSegments = temp.makeNext(-1, -1);
|
||||
|
||||
// Update state.
|
||||
context.endOfLeftSegments = forkContext.head;
|
||||
forkContext.replaceHead(rightSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a code path segment for the body part of a ForInStatement and a
|
||||
* ForOfStatement.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeForInOfBody() {
|
||||
const context = this.loopContext;
|
||||
const forkContext = this.forkContext;
|
||||
const temp = ForkContext.newEmpty(forkContext);
|
||||
|
||||
temp.add(context.endOfLeftSegments);
|
||||
const bodySegments = temp.makeNext(-1, -1);
|
||||
|
||||
// Make a path: `right` -> `left`.
|
||||
makeLooped(this, forkContext.head, context.leftSegments);
|
||||
|
||||
// Update state.
|
||||
context.brokenForkContext.add(forkContext.head);
|
||||
forkContext.replaceHead(bodySegments);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Control Statements
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates new context in which a `break` statement can be used. This occurs inside of a loop,
|
||||
* labeled statement, or switch statement.
|
||||
* @param {boolean} breakable Indicates if we are inside a statement where
|
||||
* `break` without a label will exit the statement.
|
||||
* @param {string|null} label The label associated with the statement.
|
||||
* @returns {BreakContext} The new context.
|
||||
*/
|
||||
pushBreakContext(breakable, label) {
|
||||
this.breakContext = new BreakContext(this.breakContext, breakable, label, this.forkContext);
|
||||
return this.breakContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the top item of the break context stack.
|
||||
* @returns {Object} The removed context.
|
||||
*/
|
||||
popBreakContext() {
|
||||
const context = this.breakContext;
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
this.breakContext = context.upper;
|
||||
|
||||
// Process this context here for other than switches and loops.
|
||||
if (!context.breakable) {
|
||||
const brokenForkContext = context.brokenForkContext;
|
||||
|
||||
if (!brokenForkContext.empty) {
|
||||
brokenForkContext.add(forkContext.head);
|
||||
forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
|
||||
}
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a path for a `break` statement.
|
||||
*
|
||||
* It registers the head segment to a context of `break`.
|
||||
* It makes new unreachable segment, then it set the head with the segment.
|
||||
* @param {string|null} label A label of the break statement.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeBreak(label) {
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
if (!forkContext.reachable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getBreakContext(this, label);
|
||||
|
||||
|
||||
if (context) {
|
||||
context.brokenForkContext.add(forkContext.head);
|
||||
}
|
||||
|
||||
/* c8 ignore next */
|
||||
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a path for a `continue` statement.
|
||||
*
|
||||
* It makes a looping path.
|
||||
* It makes new unreachable segment, then it set the head with the segment.
|
||||
* @param {string|null} label A label of the continue statement.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeContinue(label) {
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
if (!forkContext.reachable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContinueContext(this, label);
|
||||
|
||||
if (context) {
|
||||
if (context.continueDestSegments) {
|
||||
makeLooped(this, forkContext.head, context.continueDestSegments);
|
||||
|
||||
// If the context is a for-in/of loop, this affects a break also.
|
||||
if (context.type === "ForInStatement" ||
|
||||
context.type === "ForOfStatement"
|
||||
) {
|
||||
context.brokenForkContext.add(forkContext.head);
|
||||
}
|
||||
} else {
|
||||
context.continueForkContext.add(forkContext.head);
|
||||
}
|
||||
}
|
||||
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a path for a `return` statement.
|
||||
*
|
||||
* It registers the head segment to a context of `return`.
|
||||
* It makes new unreachable segment, then it set the head with the segment.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeReturn() {
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
if (forkContext.reachable) {
|
||||
getReturnContext(this).returnedForkContext.add(forkContext.head);
|
||||
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a path for a `throw` statement.
|
||||
*
|
||||
* It registers the head segment to a context of `throw`.
|
||||
* It makes new unreachable segment, then it set the head with the segment.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeThrow() {
|
||||
const forkContext = this.forkContext;
|
||||
|
||||
if (forkContext.reachable) {
|
||||
getThrowContext(this).thrownForkContext.add(forkContext.head);
|
||||
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the final path.
|
||||
* @returns {void}
|
||||
*/
|
||||
makeFinal() {
|
||||
const segments = this.currentSegments;
|
||||
|
||||
if (segments.length > 0 && segments[0].reachable) {
|
||||
this.returnedForkContext.add(segments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodePathState;
|
||||
342
node_modules/eslint/lib/linter/code-path-analysis/code-path.js
generated
vendored
Normal file
342
node_modules/eslint/lib/linter/code-path-analysis/code-path.js
generated
vendored
Normal file
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the traversal of the code path through each segment. This array
|
||||
* starts empty and segments are added or removed as the code path is
|
||||
* traversed. This array always ends up empty at the end of a code path
|
||||
* traversal. The `CodePathState` uses this to track its progress through
|
||||
* the code path.
|
||||
* This is a passthrough to the underlying `CodePathState`.
|
||||
* @type {CodePathSegment[]}
|
||||
* @deprecated
|
||||
*/
|
||||
get currentSegments() {
|
||||
return this.internal.currentSegments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = null;
|
||||
let index = 0;
|
||||
let end = 0;
|
||||
let segment = null;
|
||||
|
||||
// segments that have already been visited during traversal
|
||||
const visited = new Set();
|
||||
|
||||
// tracks the traversal steps
|
||||
const stack = [[startSegment, 0]];
|
||||
|
||||
// tracks the last skipped segment during traversal
|
||||
let skippedSegment = null;
|
||||
|
||||
// indicates if we exited early from the traversal
|
||||
let broken = false;
|
||||
|
||||
/**
|
||||
* Maintains traversal state.
|
||||
*/
|
||||
const controller = {
|
||||
|
||||
/**
|
||||
* Skip the following segments in this branch.
|
||||
* @returns {void}
|
||||
*/
|
||||
skip() {
|
||||
if (stack.length <= 1) {
|
||||
broken = true;
|
||||
} else {
|
||||
skippedSegment = stack[stack.length - 2][0];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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)
|
||||
);
|
||||
}
|
||||
|
||||
// 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[stack.length - 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;
|
||||
}
|
||||
|
||||
// Reset the skipping flag if all branches have been skipped.
|
||||
if (skippedSegment && segment.prevSegments.includes(skippedSegment)) {
|
||||
skippedSegment = null;
|
||||
}
|
||||
visited.add(segment);
|
||||
|
||||
/*
|
||||
* If the most recent segment hasn't been skipped, then we call
|
||||
* the callback, passing in the segment and the controller.
|
||||
*/
|
||||
if (!skippedSegment) {
|
||||
resolvedCallback.call(this, segment, controller);
|
||||
|
||||
// exit if we're at the last segment
|
||||
if (segment === lastSegment) {
|
||||
controller.skip();
|
||||
}
|
||||
|
||||
/*
|
||||
* If the previous statement was executed, or if the callback
|
||||
* called a method on the controller, we might need to exit the
|
||||
* loop, so check for that and break accordingly.
|
||||
*/
|
||||
if (broken) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
203
node_modules/eslint/lib/linter/code-path-analysis/debug-helpers.js
generated
vendored
Normal file
203
node_modules/eslint/lib/linter/code-path-analysis/debug-helpers.js
generated
vendored
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* @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 */
|
||||
function getId(segment) { // eslint-disable-line jsdoc/require-jsdoc -- Ignoring
|
||||
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);
|
||||
|
||||
for (const id in traceMap) { // eslint-disable-line guard-for-in -- Want ability to traverse prototype
|
||||
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};`;
|
||||
}
|
||||
};
|
||||
349
node_modules/eslint/lib/linter/code-path-analysis/fork-context.js
generated
vendored
Normal file
349
node_modules/eslint/lib/linter/code-path-analysis/fork-context.js
generated
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* @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("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[list.length - 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;
|
||||
45
node_modules/eslint/lib/linter/code-path-analysis/id-generator.js
generated
vendored
Normal file
45
node_modules/eslint/lib/linter/code-path-analysis/id-generator.js
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @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;
|
||||
185
node_modules/eslint/lib/linter/config-comment-parser.js
generated
vendored
Normal file
185
node_modules/eslint/lib/linter/config-comment-parser.js
generated
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* @fileoverview Config Comment Parser
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
/* eslint class-methods-use-this: off -- Methods desired on instance */
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const levn = require("levn"),
|
||||
{
|
||||
Legacy: {
|
||||
ConfigOps
|
||||
}
|
||||
} = require("@eslint/eslintrc/universal"),
|
||||
{
|
||||
directivesPattern
|
||||
} = require("../shared/directives");
|
||||
|
||||
const debug = require("debug")("eslint:config-comment-parser");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @typedef {import("../shared/types").LintMessage} LintMessage */
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Object to parse ESLint configuration comments inside JavaScript files.
|
||||
* @name ConfigCommentParser
|
||||
*/
|
||||
module.exports = class ConfigCommentParser {
|
||||
|
||||
/**
|
||||
* Parses a list of "name:string_value" or/and "name" options divided by comma or
|
||||
* whitespace. Used for "global" and "exported" comments.
|
||||
* @param {string} string The string to parse.
|
||||
* @param {Comment} comment The comment node which has the string.
|
||||
* @returns {Object} Result map object of names and string values, or null values if no value was provided
|
||||
*/
|
||||
parseStringConfig(string, comment) {
|
||||
debug("Parsing String config");
|
||||
|
||||
const items = {};
|
||||
|
||||
// Collapse whitespace around `:` and `,` to make parsing easier
|
||||
const trimmedString = string.replace(/\s*([:,])\s*/gu, "$1");
|
||||
|
||||
trimmedString.split(/\s|,+/u).forEach(name => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// value defaults to null (if not provided), e.g: "foo" => ["foo", null]
|
||||
const [key, value = null] = name.split(":");
|
||||
|
||||
items[key] = { value, comment };
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON-like config.
|
||||
* @param {string} string The string to parse.
|
||||
* @param {Object} location Start line and column of comments for potential error message.
|
||||
* @returns {({success: true, config: Object}|{success: false, error: LintMessage})} Result map object
|
||||
*/
|
||||
parseJsonConfig(string, location) {
|
||||
debug("Parsing JSON config");
|
||||
|
||||
let items = {};
|
||||
|
||||
// Parses a JSON-like comment by the same way as parsing CLI option.
|
||||
try {
|
||||
items = levn.parse("Object", string) || {};
|
||||
|
||||
// Some tests say that it should ignore invalid comments such as `/*eslint no-alert:abc*/`.
|
||||
// Also, commaless notations have invalid severity:
|
||||
// "no-alert: 2 no-console: 2" --> {"no-alert": "2 no-console: 2"}
|
||||
// Should ignore that case as well.
|
||||
if (ConfigOps.isEverySeverityValid(items)) {
|
||||
return {
|
||||
success: true,
|
||||
config: items
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
|
||||
debug("Levn parsing failed; falling back to manual parsing.");
|
||||
|
||||
// ignore to parse the string by a fallback.
|
||||
}
|
||||
|
||||
/*
|
||||
* Optionator cannot parse commaless notations.
|
||||
* But we are supporting that. So this is a fallback for that.
|
||||
*/
|
||||
items = {};
|
||||
const normalizedString = string.replace(/([-a-zA-Z0-9/]+):/gu, "\"$1\":").replace(/(\]|[0-9])\s+(?=")/u, "$1,");
|
||||
|
||||
try {
|
||||
items = JSON.parse(`{${normalizedString}}`);
|
||||
} catch (ex) {
|
||||
debug("Manual parsing failed.");
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
ruleId: null,
|
||||
fatal: true,
|
||||
severity: 2,
|
||||
message: `Failed to parse JSON from '${normalizedString}': ${ex.message}`,
|
||||
line: location.start.line,
|
||||
column: location.start.column + 1,
|
||||
nodeType: null
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
config: items
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a config of values separated by comma.
|
||||
* @param {string} string The string to parse.
|
||||
* @returns {Object} Result map of values and true values
|
||||
*/
|
||||
parseListConfig(string) {
|
||||
debug("Parsing list config");
|
||||
|
||||
const items = {};
|
||||
|
||||
string.split(",").forEach(name => {
|
||||
const trimmedName = name.trim().replace(/^(?<quote>['"]?)(?<ruleId>.*)\k<quote>$/us, "$<ruleId>");
|
||||
|
||||
if (trimmedName) {
|
||||
items[trimmedName] = true;
|
||||
}
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the directive and the justification from a given directive comment and trim them.
|
||||
* @param {string} value The comment text to extract.
|
||||
* @returns {{directivePart: string, justificationPart: string}} The extracted directive and justification.
|
||||
*/
|
||||
extractDirectiveComment(value) {
|
||||
const match = /\s-{2,}\s/u.exec(value);
|
||||
|
||||
if (!match) {
|
||||
return { directivePart: value.trim(), justificationPart: "" };
|
||||
}
|
||||
|
||||
const directive = value.slice(0, match.index).trim();
|
||||
const justification = value.slice(match.index + match[0].length).trim();
|
||||
|
||||
return { directivePart: directive, justificationPart: justification };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a directive comment into directive text and value.
|
||||
* @param {Comment} comment The comment node with the directive to be parsed.
|
||||
* @returns {{directiveText: string, directiveValue: string}} The directive text and value.
|
||||
*/
|
||||
parseDirective(comment) {
|
||||
const { directivePart } = this.extractDirectiveComment(comment.value);
|
||||
const match = directivesPattern.exec(directivePart);
|
||||
const directiveText = match[1];
|
||||
const directiveValue = directivePart.slice(match.index + directiveText.length);
|
||||
|
||||
return { directiveText, directiveValue };
|
||||
}
|
||||
};
|
||||
13
node_modules/eslint/lib/linter/index.js
generated
vendored
Normal file
13
node_modules/eslint/lib/linter/index.js
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
|
||||
const { Linter } = require("./linter");
|
||||
const interpolate = require("./interpolate");
|
||||
const SourceCodeFixer = require("./source-code-fixer");
|
||||
|
||||
module.exports = {
|
||||
Linter,
|
||||
|
||||
// For testers.
|
||||
SourceCodeFixer,
|
||||
interpolate
|
||||
};
|
||||
28
node_modules/eslint/lib/linter/interpolate.js
generated
vendored
Normal file
28
node_modules/eslint/lib/linter/interpolate.js
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @fileoverview Interpolate keys from an object into a string with {{ }} markers.
|
||||
* @author Jed Fox
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = (text, data) => {
|
||||
if (!data) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// Substitution content for any {{ }} markers.
|
||||
return text.replace(/\{\{([^{}]+?)\}\}/gu, (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;
|
||||
});
|
||||
};
|
||||
2119
node_modules/eslint/lib/linter/linter.js
generated
vendored
Normal file
2119
node_modules/eslint/lib/linter/linter.js
generated
vendored
Normal file
@@ -0,0 +1,2119 @@
|
||||
/**
|
||||
* @fileoverview Main Linter Class
|
||||
* @author Gyandeep Singh
|
||||
* @author aladdin-add
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const
|
||||
path = require("path"),
|
||||
eslintScope = require("eslint-scope"),
|
||||
evk = require("eslint-visitor-keys"),
|
||||
espree = require("espree"),
|
||||
merge = require("lodash.merge"),
|
||||
pkg = require("../../package.json"),
|
||||
astUtils = require("../shared/ast-utils"),
|
||||
{
|
||||
directivesPattern
|
||||
} = require("../shared/directives"),
|
||||
{
|
||||
Legacy: {
|
||||
ConfigOps,
|
||||
ConfigValidator,
|
||||
environments: BuiltInEnvironments
|
||||
}
|
||||
} = require("@eslint/eslintrc/universal"),
|
||||
Traverser = require("../shared/traverser"),
|
||||
{ SourceCode } = require("../source-code"),
|
||||
CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
|
||||
applyDisableDirectives = require("./apply-disable-directives"),
|
||||
ConfigCommentParser = require("./config-comment-parser"),
|
||||
NodeEventGenerator = require("./node-event-generator"),
|
||||
createReportTranslator = require("./report-translator"),
|
||||
Rules = require("./rules"),
|
||||
createEmitter = require("./safe-emitter"),
|
||||
SourceCodeFixer = require("./source-code-fixer"),
|
||||
timing = require("./timing"),
|
||||
ruleReplacements = require("../../conf/replacements.json");
|
||||
const { getRuleFromConfig } = require("../config/flat-config-helpers");
|
||||
const { FlatConfigArray } = require("../config/flat-config-array");
|
||||
const { RuleValidator } = require("../config/rule-validator");
|
||||
const { assertIsRuleOptions, assertIsRuleSeverity } = require("../config/flat-config-schema");
|
||||
const { normalizeSeverityToString } = require("../shared/severity");
|
||||
const debug = require("debug")("eslint:linter");
|
||||
const MAX_AUTOFIX_PASSES = 10;
|
||||
const DEFAULT_PARSER_NAME = "espree";
|
||||
const DEFAULT_ECMA_VERSION = 5;
|
||||
const commentParser = new ConfigCommentParser();
|
||||
const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } };
|
||||
const parserSymbol = Symbol.for("eslint.RuleTester.parser");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @typedef {InstanceType<import("../cli-engine/config-array").ConfigArray>} ConfigArray */
|
||||
/** @typedef {InstanceType<import("../cli-engine/config-array").ExtractedConfig>} ExtractedConfig */
|
||||
/** @typedef {import("../shared/types").ConfigData} ConfigData */
|
||||
/** @typedef {import("../shared/types").Environment} Environment */
|
||||
/** @typedef {import("../shared/types").GlobalConf} GlobalConf */
|
||||
/** @typedef {import("../shared/types").LintMessage} LintMessage */
|
||||
/** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */
|
||||
/** @typedef {import("../shared/types").ParserOptions} ParserOptions */
|
||||
/** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */
|
||||
/** @typedef {import("../shared/types").Processor} Processor */
|
||||
/** @typedef {import("../shared/types").Rule} Rule */
|
||||
|
||||
/* eslint-disable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{ [P in keyof T]-?: T[P] }} Required
|
||||
*/
|
||||
/* eslint-enable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */
|
||||
|
||||
/**
|
||||
* @typedef {Object} DisableDirective
|
||||
* @property {("disable"|"enable"|"disable-line"|"disable-next-line")} type Type of directive
|
||||
* @property {number} line The line number
|
||||
* @property {number} column The column number
|
||||
* @property {(string|null)} ruleId The rule ID
|
||||
* @property {string} justification The justification of directive
|
||||
*/
|
||||
|
||||
/**
|
||||
* The private data for `Linter` instance.
|
||||
* @typedef {Object} LinterInternalSlots
|
||||
* @property {ConfigArray|null} lastConfigArray The `ConfigArray` instance that the last `verify()` call used.
|
||||
* @property {SourceCode|null} lastSourceCode The `SourceCode` instance that the last `verify()` call used.
|
||||
* @property {SuppressedLintMessage[]} lastSuppressedMessages The `SuppressedLintMessage[]` instance that the last `verify()` call produced.
|
||||
* @property {Map<string, Parser>} parserMap The loaded parsers.
|
||||
* @property {Rules} ruleMap The loaded rules.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} VerifyOptions
|
||||
* @property {boolean} [allowInlineConfig] Allow/disallow inline comments' ability
|
||||
* to change config once it is set. Defaults to true if not supplied.
|
||||
* Useful if you want to validate JS without comments overriding rules.
|
||||
* @property {boolean} [disableFixes] if `true` then the linter doesn't make `fix`
|
||||
* properties into the lint result.
|
||||
* @property {string} [filename] the filename of the source code.
|
||||
* @property {boolean | "off" | "warn" | "error"} [reportUnusedDisableDirectives] Adds reported errors for
|
||||
* unused `eslint-disable` directives.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ProcessorOptions
|
||||
* @property {(filename:string, text:string) => boolean} [filterCodeBlock] the
|
||||
* predicate function that selects adopt code blocks.
|
||||
* @property {Processor.postprocess} [postprocess] postprocessor for report
|
||||
* messages. If provided, this should accept an array of the message lists
|
||||
* for each code block returned from the preprocessor, apply a mapping to
|
||||
* the messages as appropriate, and return a one-dimensional array of
|
||||
* messages.
|
||||
* @property {Processor.preprocess} [preprocess] preprocessor for source text.
|
||||
* If provided, this should accept a string of source text, and return an
|
||||
* array of code blocks to lint.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} FixOptions
|
||||
* @property {boolean | ((message: LintMessage) => boolean)} [fix] Determines
|
||||
* whether fixes should be applied.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} InternalOptions
|
||||
* @property {string | null} warnInlineConfig The config name what `noInlineConfig` setting came from. If `noInlineConfig` setting didn't exist, this is null. If this is a config name, then the linter warns directive comments.
|
||||
* @property {"off" | "warn" | "error"} reportUnusedDisableDirectives (boolean values were normalized)
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines if a given object is Espree.
|
||||
* @param {Object} parser The parser to check.
|
||||
* @returns {boolean} True if the parser is Espree or false if not.
|
||||
*/
|
||||
function isEspree(parser) {
|
||||
return !!(parser === espree || parser[parserSymbol] === espree);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that variables representing built-in properties of the Global Object,
|
||||
* and any globals declared by special block comments, are present in the global
|
||||
* scope.
|
||||
* @param {Scope} globalScope The global scope.
|
||||
* @param {Object} configGlobals The globals declared in configuration
|
||||
* @param {{exportedVariables: Object, enabledGlobals: Object}} commentDirectives Directives from comment configuration
|
||||
* @returns {void}
|
||||
*/
|
||||
function addDeclaredGlobals(globalScope, configGlobals, { exportedVariables, enabledGlobals }) {
|
||||
|
||||
// Define configured global variables.
|
||||
for (const id of new Set([...Object.keys(configGlobals), ...Object.keys(enabledGlobals)])) {
|
||||
|
||||
/*
|
||||
* `ConfigOps.normalizeConfigGlobal` will throw an error if a configured global value is invalid. However, these errors would
|
||||
* typically be caught when validating a config anyway (validity for inline global comments is checked separately).
|
||||
*/
|
||||
const configValue = configGlobals[id] === void 0 ? void 0 : ConfigOps.normalizeConfigGlobal(configGlobals[id]);
|
||||
const commentValue = enabledGlobals[id] && enabledGlobals[id].value;
|
||||
const value = commentValue || configValue;
|
||||
const sourceComments = enabledGlobals[id] && enabledGlobals[id].comments;
|
||||
|
||||
if (value === "off") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let variable = globalScope.set.get(id);
|
||||
|
||||
if (!variable) {
|
||||
variable = new eslintScope.Variable(id, globalScope);
|
||||
|
||||
globalScope.variables.push(variable);
|
||||
globalScope.set.set(id, variable);
|
||||
}
|
||||
|
||||
variable.eslintImplicitGlobalSetting = configValue;
|
||||
variable.eslintExplicitGlobal = sourceComments !== void 0;
|
||||
variable.eslintExplicitGlobalComments = sourceComments;
|
||||
variable.writeable = (value === "writable");
|
||||
}
|
||||
|
||||
// mark all exported variables as such
|
||||
Object.keys(exportedVariables).forEach(name => {
|
||||
const variable = globalScope.set.get(name);
|
||||
|
||||
if (variable) {
|
||||
variable.eslintUsed = true;
|
||||
variable.eslintExported = true;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* "through" contains all references which definitions cannot be found.
|
||||
* Since we augment the global scope using configuration, we need to update
|
||||
* references and remove the ones that were added by configuration.
|
||||
*/
|
||||
globalScope.through = globalScope.through.filter(reference => {
|
||||
const name = reference.identifier.name;
|
||||
const variable = globalScope.set.get(name);
|
||||
|
||||
if (variable) {
|
||||
|
||||
/*
|
||||
* Links the variable and the reference.
|
||||
* And this reference is removed from `Scope#through`.
|
||||
*/
|
||||
reference.resolved = variable;
|
||||
variable.references.push(reference);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a missing-rule message.
|
||||
* @param {string} ruleId the ruleId to create
|
||||
* @returns {string} created error message
|
||||
* @private
|
||||
*/
|
||||
function createMissingRuleMessage(ruleId) {
|
||||
return Object.prototype.hasOwnProperty.call(ruleReplacements.rules, ruleId)
|
||||
? `Rule '${ruleId}' was removed and replaced by: ${ruleReplacements.rules[ruleId].join(", ")}`
|
||||
: `Definition for rule '${ruleId}' was not found.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a linting problem
|
||||
* @param {Object} options to create linting error
|
||||
* @param {string} [options.ruleId] the ruleId to report
|
||||
* @param {Object} [options.loc] the loc to report
|
||||
* @param {string} [options.message] the error message to report
|
||||
* @param {string} [options.severity] the error message to report
|
||||
* @returns {LintMessage} created problem, returns a missing-rule problem if only provided ruleId.
|
||||
* @private
|
||||
*/
|
||||
function createLintingProblem(options) {
|
||||
const {
|
||||
ruleId = null,
|
||||
loc = DEFAULT_ERROR_LOC,
|
||||
message = createMissingRuleMessage(options.ruleId),
|
||||
severity = 2
|
||||
} = options;
|
||||
|
||||
return {
|
||||
ruleId,
|
||||
message,
|
||||
line: loc.start.line,
|
||||
column: loc.start.column + 1,
|
||||
endLine: loc.end.line,
|
||||
endColumn: loc.end.column + 1,
|
||||
severity,
|
||||
nodeType: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a collection of disable directives from a comment
|
||||
* @param {Object} options to create disable directives
|
||||
* @param {("disable"|"enable"|"disable-line"|"disable-next-line")} options.type The type of directive comment
|
||||
* @param {token} options.commentToken The Comment token
|
||||
* @param {string} options.value The value after the directive in the comment
|
||||
* comment specified no specific rules, so it applies to all rules (e.g. `eslint-disable`)
|
||||
* @param {string} options.justification The justification of the directive
|
||||
* @param {function(string): {create: Function}} options.ruleMapper A map from rule IDs to defined rules
|
||||
* @returns {Object} Directives and problems from the comment
|
||||
*/
|
||||
function createDisableDirectives(options) {
|
||||
const { commentToken, type, value, justification, ruleMapper } = options;
|
||||
const ruleIds = Object.keys(commentParser.parseListConfig(value));
|
||||
const directiveRules = ruleIds.length ? ruleIds : [null];
|
||||
const result = {
|
||||
directives: [], // valid disable directives
|
||||
directiveProblems: [] // problems in directives
|
||||
};
|
||||
|
||||
const parentComment = { commentToken, ruleIds };
|
||||
|
||||
for (const ruleId of directiveRules) {
|
||||
|
||||
// push to directives, if the rule is defined(including null, e.g. /*eslint enable*/)
|
||||
if (ruleId === null || !!ruleMapper(ruleId)) {
|
||||
if (type === "disable-next-line") {
|
||||
result.directives.push({
|
||||
parentComment,
|
||||
type,
|
||||
line: commentToken.loc.end.line,
|
||||
column: commentToken.loc.end.column + 1,
|
||||
ruleId,
|
||||
justification
|
||||
});
|
||||
} else {
|
||||
result.directives.push({
|
||||
parentComment,
|
||||
type,
|
||||
line: commentToken.loc.start.line,
|
||||
column: commentToken.loc.start.column + 1,
|
||||
ruleId,
|
||||
justification
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result.directiveProblems.push(createLintingProblem({ ruleId, loc: commentToken.loc }));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses comments in file to extract file-specific config of rules, globals
|
||||
* and environments and merges them with global config; also code blocks
|
||||
* where reporting is disabled or enabled and merges them with reporting config.
|
||||
* @param {SourceCode} sourceCode The SourceCode object to get comments from.
|
||||
* @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
|
||||
* @param {string|null} warnInlineConfig If a string then it should warn directive comments as disabled. The string value is the config name what the setting came from.
|
||||
* @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: LintMessage[], disableDirectives: DisableDirective[]}}
|
||||
* A collection of the directive comments that were found, along with any problems that occurred when parsing
|
||||
*/
|
||||
function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) {
|
||||
const configuredRules = {};
|
||||
const enabledGlobals = Object.create(null);
|
||||
const exportedVariables = {};
|
||||
const problems = [];
|
||||
const disableDirectives = [];
|
||||
const validator = new ConfigValidator({
|
||||
builtInRules: Rules
|
||||
});
|
||||
|
||||
sourceCode.getInlineConfigNodes().filter(token => token.type !== "Shebang").forEach(comment => {
|
||||
const { directivePart, justificationPart } = commentParser.extractDirectiveComment(comment.value);
|
||||
|
||||
const match = directivesPattern.exec(directivePart);
|
||||
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const directiveText = match[1];
|
||||
const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(directiveText);
|
||||
|
||||
if (comment.type === "Line" && !lineCommentSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (warnInlineConfig) {
|
||||
const kind = comment.type === "Block" ? `/*${directiveText}*/` : `//${directiveText}`;
|
||||
|
||||
problems.push(createLintingProblem({
|
||||
ruleId: null,
|
||||
message: `'${kind}' has no effect because you have 'noInlineConfig' setting in ${warnInlineConfig}.`,
|
||||
loc: comment.loc,
|
||||
severity: 1
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (directiveText === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) {
|
||||
const message = `${directiveText} comment should not span multiple lines.`;
|
||||
|
||||
problems.push(createLintingProblem({
|
||||
ruleId: null,
|
||||
message,
|
||||
loc: comment.loc
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const directiveValue = directivePart.slice(match.index + directiveText.length);
|
||||
|
||||
switch (directiveText) {
|
||||
case "eslint-disable":
|
||||
case "eslint-enable":
|
||||
case "eslint-disable-next-line":
|
||||
case "eslint-disable-line": {
|
||||
const directiveType = directiveText.slice("eslint-".length);
|
||||
const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper };
|
||||
const { directives, directiveProblems } = createDisableDirectives(options);
|
||||
|
||||
disableDirectives.push(...directives);
|
||||
problems.push(...directiveProblems);
|
||||
break;
|
||||
}
|
||||
|
||||
case "exported":
|
||||
Object.assign(exportedVariables, commentParser.parseStringConfig(directiveValue, comment));
|
||||
break;
|
||||
|
||||
case "globals":
|
||||
case "global":
|
||||
for (const [id, { value }] of Object.entries(commentParser.parseStringConfig(directiveValue, comment))) {
|
||||
let normalizedValue;
|
||||
|
||||
try {
|
||||
normalizedValue = ConfigOps.normalizeConfigGlobal(value);
|
||||
} catch (err) {
|
||||
problems.push(createLintingProblem({
|
||||
ruleId: null,
|
||||
loc: comment.loc,
|
||||
message: err.message
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enabledGlobals[id]) {
|
||||
enabledGlobals[id].comments.push(comment);
|
||||
enabledGlobals[id].value = normalizedValue;
|
||||
} else {
|
||||
enabledGlobals[id] = {
|
||||
comments: [comment],
|
||||
value: normalizedValue
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "eslint": {
|
||||
const parseResult = commentParser.parseJsonConfig(directiveValue, comment.loc);
|
||||
|
||||
if (parseResult.success) {
|
||||
Object.keys(parseResult.config).forEach(name => {
|
||||
const rule = ruleMapper(name);
|
||||
const ruleValue = parseResult.config[name];
|
||||
|
||||
if (!rule) {
|
||||
problems.push(createLintingProblem({ ruleId: name, loc: comment.loc }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
validator.validateRuleOptions(rule, name, ruleValue);
|
||||
} catch (err) {
|
||||
problems.push(createLintingProblem({
|
||||
ruleId: name,
|
||||
message: err.message,
|
||||
loc: comment.loc
|
||||
}));
|
||||
|
||||
// do not apply the config, if found invalid options.
|
||||
return;
|
||||
}
|
||||
|
||||
configuredRules[name] = ruleValue;
|
||||
});
|
||||
} else {
|
||||
problems.push(parseResult.error);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// no default
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
configuredRules,
|
||||
enabledGlobals,
|
||||
exportedVariables,
|
||||
problems,
|
||||
disableDirectives
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses comments in file to extract disable directives.
|
||||
* @param {SourceCode} sourceCode The SourceCode object to get comments from.
|
||||
* @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
|
||||
* @returns {{problems: LintMessage[], disableDirectives: DisableDirective[]}}
|
||||
* A collection of the directive comments that were found, along with any problems that occurred when parsing
|
||||
*/
|
||||
function getDirectiveCommentsForFlatConfig(sourceCode, ruleMapper) {
|
||||
const problems = [];
|
||||
const disableDirectives = [];
|
||||
|
||||
sourceCode.getInlineConfigNodes().filter(token => token.type !== "Shebang").forEach(comment => {
|
||||
const { directivePart, justificationPart } = commentParser.extractDirectiveComment(comment.value);
|
||||
|
||||
const match = directivesPattern.exec(directivePart);
|
||||
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const directiveText = match[1];
|
||||
const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(directiveText);
|
||||
|
||||
if (comment.type === "Line" && !lineCommentSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (directiveText === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) {
|
||||
const message = `${directiveText} comment should not span multiple lines.`;
|
||||
|
||||
problems.push(createLintingProblem({
|
||||
ruleId: null,
|
||||
message,
|
||||
loc: comment.loc
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const directiveValue = directivePart.slice(match.index + directiveText.length);
|
||||
|
||||
switch (directiveText) {
|
||||
case "eslint-disable":
|
||||
case "eslint-enable":
|
||||
case "eslint-disable-next-line":
|
||||
case "eslint-disable-line": {
|
||||
const directiveType = directiveText.slice("eslint-".length);
|
||||
const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper };
|
||||
const { directives, directiveProblems } = createDisableDirectives(options);
|
||||
|
||||
disableDirectives.push(...directives);
|
||||
problems.push(...directiveProblems);
|
||||
break;
|
||||
}
|
||||
|
||||
// no default
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
problems,
|
||||
disableDirectives
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize ECMAScript version from the initial config
|
||||
* @param {Parser} parser The parser which uses this options.
|
||||
* @param {number} ecmaVersion ECMAScript version from the initial config
|
||||
* @returns {number} normalized ECMAScript version
|
||||
*/
|
||||
function normalizeEcmaVersion(parser, ecmaVersion) {
|
||||
|
||||
if (isEspree(parser)) {
|
||||
if (ecmaVersion === "latest") {
|
||||
return espree.latestEcmaVersion;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate ECMAScript edition number from official year version starting with
|
||||
* ES2015, which corresponds with ES6 (or a difference of 2009).
|
||||
*/
|
||||
return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize ECMAScript version from the initial config into languageOptions (year)
|
||||
* format.
|
||||
* @param {any} [ecmaVersion] ECMAScript version from the initial config
|
||||
* @returns {number} normalized ECMAScript version
|
||||
*/
|
||||
function normalizeEcmaVersionForLanguageOptions(ecmaVersion) {
|
||||
|
||||
switch (ecmaVersion) {
|
||||
case 3:
|
||||
return 3;
|
||||
|
||||
// void 0 = no ecmaVersion specified so use the default
|
||||
case 5:
|
||||
case void 0:
|
||||
return 5;
|
||||
|
||||
default:
|
||||
if (typeof ecmaVersion === "number") {
|
||||
return ecmaVersion >= 2015 ? ecmaVersion : ecmaVersion + 2009;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* We default to the latest supported ecmaVersion for everything else.
|
||||
* Remember, this is for languageOptions.ecmaVersion, which sets the version
|
||||
* that is used for a number of processes inside of ESLint. It's normally
|
||||
* safe to assume people want the latest unless otherwise specified.
|
||||
*/
|
||||
return espree.latestEcmaVersion + 2009;
|
||||
}
|
||||
|
||||
const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)(?:\*\/|$)/gsu;
|
||||
|
||||
/**
|
||||
* Checks whether or not there is a comment which has "eslint-env *" in a given text.
|
||||
* @param {string} text A source code text to check.
|
||||
* @returns {Object|null} A result of parseListConfig() with "eslint-env *" comment.
|
||||
*/
|
||||
function findEslintEnv(text) {
|
||||
let match, retv;
|
||||
|
||||
eslintEnvPattern.lastIndex = 0;
|
||||
|
||||
while ((match = eslintEnvPattern.exec(text)) !== null) {
|
||||
if (match[0].endsWith("*/")) {
|
||||
retv = Object.assign(
|
||||
retv || {},
|
||||
commentParser.parseListConfig(commentParser.extractDirectiveComment(match[1]).directivePart)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return retv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert "/path/to/<text>" to "<text>".
|
||||
* `CLIEngine#executeOnText()` method gives "/path/to/<text>" if the filename
|
||||
* was omitted because `configArray.extractConfig()` requires an absolute path.
|
||||
* But the linter should pass `<text>` to `RuleContext#filename` in that
|
||||
* case.
|
||||
* Also, code blocks can have their virtual filename. If the parent filename was
|
||||
* `<text>`, the virtual filename is `<text>/0_foo.js` or something like (i.e.,
|
||||
* it's not an absolute path).
|
||||
* @param {string} filename The filename to normalize.
|
||||
* @returns {string} The normalized filename.
|
||||
*/
|
||||
function normalizeFilename(filename) {
|
||||
const parts = filename.split(path.sep);
|
||||
const index = parts.lastIndexOf("<text>");
|
||||
|
||||
return index === -1 ? filename : parts.slice(index).join(path.sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the possible options for `linter.verify` and `linter.verifyAndFix` to a
|
||||
* consistent shape.
|
||||
* @param {VerifyOptions} providedOptions Options
|
||||
* @param {ConfigData} config Config.
|
||||
* @returns {Required<VerifyOptions> & InternalOptions} Normalized options
|
||||
*/
|
||||
function normalizeVerifyOptions(providedOptions, config) {
|
||||
|
||||
const linterOptions = config.linterOptions || config;
|
||||
|
||||
// .noInlineConfig for eslintrc, .linterOptions.noInlineConfig for flat
|
||||
const disableInlineConfig = linterOptions.noInlineConfig === true;
|
||||
const ignoreInlineConfig = providedOptions.allowInlineConfig === false;
|
||||
const configNameOfNoInlineConfig = config.configNameOfNoInlineConfig
|
||||
? ` (${config.configNameOfNoInlineConfig})`
|
||||
: "";
|
||||
|
||||
let reportUnusedDisableDirectives = providedOptions.reportUnusedDisableDirectives;
|
||||
|
||||
if (typeof reportUnusedDisableDirectives === "boolean") {
|
||||
reportUnusedDisableDirectives = reportUnusedDisableDirectives ? "error" : "off";
|
||||
}
|
||||
if (typeof reportUnusedDisableDirectives !== "string") {
|
||||
if (typeof linterOptions.reportUnusedDisableDirectives === "boolean") {
|
||||
reportUnusedDisableDirectives = linterOptions.reportUnusedDisableDirectives ? "warn" : "off";
|
||||
} else {
|
||||
reportUnusedDisableDirectives = linterOptions.reportUnusedDisableDirectives === void 0 ? "off" : normalizeSeverityToString(linterOptions.reportUnusedDisableDirectives);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filename: normalizeFilename(providedOptions.filename || "<input>"),
|
||||
allowInlineConfig: !ignoreInlineConfig,
|
||||
warnInlineConfig: disableInlineConfig && !ignoreInlineConfig
|
||||
? `your config${configNameOfNoInlineConfig}`
|
||||
: null,
|
||||
reportUnusedDisableDirectives,
|
||||
disableFixes: Boolean(providedOptions.disableFixes)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the provided parserOptions with the options from environments
|
||||
* @param {Parser} parser The parser which uses this options.
|
||||
* @param {ParserOptions} providedOptions The provided 'parserOptions' key in a config
|
||||
* @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments
|
||||
* @returns {ParserOptions} Resulting parser options after merge
|
||||
*/
|
||||
function resolveParserOptions(parser, providedOptions, enabledEnvironments) {
|
||||
|
||||
const parserOptionsFromEnv = enabledEnvironments
|
||||
.filter(env => env.parserOptions)
|
||||
.reduce((parserOptions, env) => merge(parserOptions, env.parserOptions), {});
|
||||
const mergedParserOptions = merge(parserOptionsFromEnv, providedOptions || {});
|
||||
const isModule = mergedParserOptions.sourceType === "module";
|
||||
|
||||
if (isModule) {
|
||||
|
||||
/*
|
||||
* can't have global return inside of modules
|
||||
* TODO: espree validate parserOptions.globalReturn when sourceType is setting to module.(@aladdin-add)
|
||||
*/
|
||||
mergedParserOptions.ecmaFeatures = Object.assign({}, mergedParserOptions.ecmaFeatures, { globalReturn: false });
|
||||
}
|
||||
|
||||
mergedParserOptions.ecmaVersion = normalizeEcmaVersion(parser, mergedParserOptions.ecmaVersion);
|
||||
|
||||
return mergedParserOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts parserOptions to languageOptions for backwards compatibility with eslintrc.
|
||||
* @param {ConfigData} config Config object.
|
||||
* @param {Object} config.globals Global variable definitions.
|
||||
* @param {Parser} config.parser The parser to use.
|
||||
* @param {ParserOptions} config.parserOptions The parserOptions to use.
|
||||
* @returns {LanguageOptions} The languageOptions equivalent.
|
||||
*/
|
||||
function createLanguageOptions({ globals: configuredGlobals, parser, parserOptions }) {
|
||||
|
||||
const {
|
||||
ecmaVersion,
|
||||
sourceType
|
||||
} = parserOptions;
|
||||
|
||||
return {
|
||||
globals: configuredGlobals,
|
||||
ecmaVersion: normalizeEcmaVersionForLanguageOptions(ecmaVersion),
|
||||
sourceType,
|
||||
parser,
|
||||
parserOptions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the provided globals object with the globals from environments
|
||||
* @param {Record<string, GlobalConf>} providedGlobals The 'globals' key in a config
|
||||
* @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments
|
||||
* @returns {Record<string, GlobalConf>} The resolved globals object
|
||||
*/
|
||||
function resolveGlobals(providedGlobals, enabledEnvironments) {
|
||||
return Object.assign(
|
||||
{},
|
||||
...enabledEnvironments.filter(env => env.globals).map(env => env.globals),
|
||||
providedGlobals
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips Unicode BOM from a given text.
|
||||
* @param {string} text A text to strip.
|
||||
* @returns {string} The stripped text.
|
||||
*/
|
||||
function stripUnicodeBOM(text) {
|
||||
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
if (text.charCodeAt(0) === 0xFEFF) {
|
||||
return text.slice(1);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the options for a rule (not including severity), if any
|
||||
* @param {Array|number} ruleConfig rule configuration
|
||||
* @returns {Array} of rule options, empty Array if none
|
||||
*/
|
||||
function getRuleOptions(ruleConfig) {
|
||||
if (Array.isArray(ruleConfig)) {
|
||||
return ruleConfig.slice(1);
|
||||
}
|
||||
return [];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze scope of the given AST.
|
||||
* @param {ASTNode} ast The `Program` node to analyze.
|
||||
* @param {LanguageOptions} languageOptions The parser options.
|
||||
* @param {Record<string, string[]>} visitorKeys The visitor keys.
|
||||
* @returns {ScopeManager} The analysis result.
|
||||
*/
|
||||
function analyzeScope(ast, languageOptions, visitorKeys) {
|
||||
const parserOptions = languageOptions.parserOptions;
|
||||
const ecmaFeatures = parserOptions.ecmaFeatures || {};
|
||||
const ecmaVersion = languageOptions.ecmaVersion || DEFAULT_ECMA_VERSION;
|
||||
|
||||
return eslintScope.analyze(ast, {
|
||||
ignoreEval: true,
|
||||
nodejsScope: ecmaFeatures.globalReturn,
|
||||
impliedStrict: ecmaFeatures.impliedStrict,
|
||||
ecmaVersion: typeof ecmaVersion === "number" ? ecmaVersion : 6,
|
||||
sourceType: languageOptions.sourceType || "script",
|
||||
childVisitorKeys: visitorKeys || evk.KEYS,
|
||||
fallback: Traverser.getKeys
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses text into an AST. Moved out here because the try-catch prevents
|
||||
* optimization of functions, so it's best to keep the try-catch as isolated
|
||||
* as possible
|
||||
* @param {string} text The text to parse.
|
||||
* @param {LanguageOptions} languageOptions Options to pass to the parser
|
||||
* @param {string} filePath The path to the file being parsed.
|
||||
* @returns {{success: false, error: LintMessage}|{success: true, sourceCode: SourceCode}}
|
||||
* An object containing the AST and parser services if parsing was successful, or the error if parsing failed
|
||||
* @private
|
||||
*/
|
||||
function parse(text, languageOptions, filePath) {
|
||||
const textToParse = stripUnicodeBOM(text).replace(astUtils.shebangPattern, (match, captured) => `//${captured}`);
|
||||
const { ecmaVersion, sourceType, parser } = languageOptions;
|
||||
const parserOptions = Object.assign(
|
||||
{ ecmaVersion, sourceType },
|
||||
languageOptions.parserOptions,
|
||||
{
|
||||
loc: true,
|
||||
range: true,
|
||||
raw: true,
|
||||
tokens: true,
|
||||
comment: true,
|
||||
eslintVisitorKeys: true,
|
||||
eslintScopeManager: true,
|
||||
filePath
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* Check for parsing errors first. If there's a parsing error, nothing
|
||||
* else can happen. However, a parsing error does not throw an error
|
||||
* from this method - it's just considered a fatal error message, a
|
||||
* problem that ESLint identified just like any other.
|
||||
*/
|
||||
try {
|
||||
debug("Parsing:", filePath);
|
||||
const parseResult = (typeof parser.parseForESLint === "function")
|
||||
? parser.parseForESLint(textToParse, parserOptions)
|
||||
: { ast: parser.parse(textToParse, parserOptions) };
|
||||
|
||||
debug("Parsing successful:", filePath);
|
||||
const ast = parseResult.ast;
|
||||
const parserServices = parseResult.services || {};
|
||||
const visitorKeys = parseResult.visitorKeys || evk.KEYS;
|
||||
|
||||
debug("Scope analysis:", filePath);
|
||||
const scopeManager = parseResult.scopeManager || analyzeScope(ast, languageOptions, visitorKeys);
|
||||
|
||||
debug("Scope analysis successful:", filePath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
/*
|
||||
* Save all values that `parseForESLint()` returned.
|
||||
* If a `SourceCode` object is given as the first parameter instead of source code text,
|
||||
* linter skips the parsing process and reuses the source code object.
|
||||
* In that case, linter needs all the values that `parseForESLint()` returned.
|
||||
*/
|
||||
sourceCode: new SourceCode({
|
||||
text,
|
||||
ast,
|
||||
parserServices,
|
||||
scopeManager,
|
||||
visitorKeys
|
||||
})
|
||||
};
|
||||
} catch (ex) {
|
||||
|
||||
// If the message includes a leading line number, strip it:
|
||||
const message = `Parsing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`;
|
||||
|
||||
debug("%s\n%s", message, ex.stack);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
ruleId: null,
|
||||
fatal: true,
|
||||
severity: 2,
|
||||
message,
|
||||
line: ex.lineNumber,
|
||||
column: ex.column,
|
||||
nodeType: null
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a rule, and gets its listeners
|
||||
* @param {Rule} rule A normalized rule with a `create` method
|
||||
* @param {Context} ruleContext The context that should be passed to the rule
|
||||
* @throws {any} Any error during the rule's `create`
|
||||
* @returns {Object} A map of selector listeners provided by the rule
|
||||
*/
|
||||
function createRuleListeners(rule, ruleContext) {
|
||||
try {
|
||||
return rule.create(ruleContext);
|
||||
} catch (ex) {
|
||||
ex.message = `Error while loading rule '${ruleContext.id}': ${ex.message}`;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
// methods that exist on SourceCode object
|
||||
const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
|
||||
getSource: "getText",
|
||||
getSourceLines: "getLines",
|
||||
getAllComments: "getAllComments",
|
||||
getNodeByRangeIndex: "getNodeByRangeIndex",
|
||||
getComments: "getComments",
|
||||
getCommentsBefore: "getCommentsBefore",
|
||||
getCommentsAfter: "getCommentsAfter",
|
||||
getCommentsInside: "getCommentsInside",
|
||||
getJSDocComment: "getJSDocComment",
|
||||
getFirstToken: "getFirstToken",
|
||||
getFirstTokens: "getFirstTokens",
|
||||
getLastToken: "getLastToken",
|
||||
getLastTokens: "getLastTokens",
|
||||
getTokenAfter: "getTokenAfter",
|
||||
getTokenBefore: "getTokenBefore",
|
||||
getTokenByRangeStart: "getTokenByRangeStart",
|
||||
getTokens: "getTokens",
|
||||
getTokensAfter: "getTokensAfter",
|
||||
getTokensBefore: "getTokensBefore",
|
||||
getTokensBetween: "getTokensBetween"
|
||||
};
|
||||
|
||||
|
||||
const BASE_TRAVERSAL_CONTEXT = Object.freeze(
|
||||
Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce(
|
||||
(contextInfo, methodName) =>
|
||||
Object.assign(contextInfo, {
|
||||
[methodName](...args) {
|
||||
return this.sourceCode[DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]](...args);
|
||||
}
|
||||
}),
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Runs the given rules on the given SourceCode object
|
||||
* @param {SourceCode} sourceCode A SourceCode object for the given text
|
||||
* @param {Object} configuredRules The rules configuration
|
||||
* @param {function(string): Rule} ruleMapper A mapper function from rule names to rules
|
||||
* @param {string | undefined} parserName The name of the parser in the config
|
||||
* @param {LanguageOptions} languageOptions The options for parsing the code.
|
||||
* @param {Object} settings The settings that were enabled in the config
|
||||
* @param {string} filename The reported filename of the code
|
||||
* @param {boolean} disableFixes If true, it doesn't make `fix` properties.
|
||||
* @param {string | undefined} cwd cwd of the cli
|
||||
* @param {string} physicalFilename The full path of the file on disk without any code block information
|
||||
* @returns {LintMessage[]} An array of reported problems
|
||||
*/
|
||||
function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename) {
|
||||
const emitter = createEmitter();
|
||||
const nodeQueue = [];
|
||||
let currentNode = sourceCode.ast;
|
||||
|
||||
Traverser.traverse(sourceCode.ast, {
|
||||
enter(node, parent) {
|
||||
node.parent = parent;
|
||||
nodeQueue.push({ isEntering: true, node });
|
||||
},
|
||||
leave(node) {
|
||||
nodeQueue.push({ isEntering: false, node });
|
||||
},
|
||||
visitorKeys: sourceCode.visitorKeys
|
||||
});
|
||||
|
||||
/*
|
||||
* Create a frozen object with the ruleContext properties and methods that are shared by all rules.
|
||||
* All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
|
||||
* properties once for each rule.
|
||||
*/
|
||||
const sharedTraversalContext = Object.freeze(
|
||||
Object.assign(
|
||||
Object.create(BASE_TRAVERSAL_CONTEXT),
|
||||
{
|
||||
getAncestors: () => sourceCode.getAncestors(currentNode),
|
||||
getDeclaredVariables: node => sourceCode.getDeclaredVariables(node),
|
||||
getCwd: () => cwd,
|
||||
cwd,
|
||||
getFilename: () => filename,
|
||||
filename,
|
||||
getPhysicalFilename: () => physicalFilename || filename,
|
||||
physicalFilename: physicalFilename || filename,
|
||||
getScope: () => sourceCode.getScope(currentNode),
|
||||
getSourceCode: () => sourceCode,
|
||||
sourceCode,
|
||||
markVariableAsUsed: name => sourceCode.markVariableAsUsed(name, currentNode),
|
||||
parserOptions: {
|
||||
...languageOptions.parserOptions
|
||||
},
|
||||
parserPath: parserName,
|
||||
languageOptions,
|
||||
parserServices: sourceCode.parserServices,
|
||||
settings
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const lintingProblems = [];
|
||||
|
||||
Object.keys(configuredRules).forEach(ruleId => {
|
||||
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
|
||||
|
||||
// not load disabled rules
|
||||
if (severity === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rule = ruleMapper(ruleId);
|
||||
|
||||
if (!rule) {
|
||||
lintingProblems.push(createLintingProblem({ ruleId }));
|
||||
return;
|
||||
}
|
||||
|
||||
const messageIds = rule.meta && rule.meta.messages;
|
||||
let reportTranslator = null;
|
||||
const ruleContext = Object.freeze(
|
||||
Object.assign(
|
||||
Object.create(sharedTraversalContext),
|
||||
{
|
||||
id: ruleId,
|
||||
options: getRuleOptions(configuredRules[ruleId]),
|
||||
report(...args) {
|
||||
|
||||
/*
|
||||
* Create a report translator lazily.
|
||||
* In a vast majority of cases, any given rule reports zero errors on a given
|
||||
* piece of code. Creating a translator lazily avoids the performance cost of
|
||||
* creating a new translator function for each rule that usually doesn't get
|
||||
* called.
|
||||
*
|
||||
* Using lazy report translators improves end-to-end performance by about 3%
|
||||
* with Node 8.4.0.
|
||||
*/
|
||||
if (reportTranslator === null) {
|
||||
reportTranslator = createReportTranslator({
|
||||
ruleId,
|
||||
severity,
|
||||
sourceCode,
|
||||
messageIds,
|
||||
disableFixes
|
||||
});
|
||||
}
|
||||
const problem = reportTranslator(...args);
|
||||
|
||||
if (problem.fix && !(rule.meta && rule.meta.fixable)) {
|
||||
throw new Error("Fixable rules must set the `meta.fixable` property to \"code\" or \"whitespace\".");
|
||||
}
|
||||
if (problem.suggestions && !(rule.meta && rule.meta.hasSuggestions === true)) {
|
||||
if (rule.meta && rule.meta.docs && typeof rule.meta.docs.suggestion !== "undefined") {
|
||||
|
||||
// Encourage migration from the former property name.
|
||||
throw new Error("Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint.");
|
||||
}
|
||||
throw new Error("Rules with suggestions must set the `meta.hasSuggestions` property to `true`.");
|
||||
}
|
||||
lintingProblems.push(problem);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ruleListeners = timing.enabled ? timing.time(ruleId, createRuleListeners)(rule, ruleContext) : createRuleListeners(rule, ruleContext);
|
||||
|
||||
/**
|
||||
* Include `ruleId` in error logs
|
||||
* @param {Function} ruleListener A rule method that listens for a node.
|
||||
* @returns {Function} ruleListener wrapped in error handler
|
||||
*/
|
||||
function addRuleErrorHandler(ruleListener) {
|
||||
return function ruleErrorHandler(...listenerArgs) {
|
||||
try {
|
||||
return ruleListener(...listenerArgs);
|
||||
} catch (e) {
|
||||
e.ruleId = ruleId;
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof ruleListeners === "undefined" || ruleListeners === null) {
|
||||
throw new Error(`The create() function for rule '${ruleId}' did not return an object.`);
|
||||
}
|
||||
|
||||
// add all the selectors from the rule as listeners
|
||||
Object.keys(ruleListeners).forEach(selector => {
|
||||
const ruleListener = timing.enabled
|
||||
? timing.time(ruleId, ruleListeners[selector])
|
||||
: ruleListeners[selector];
|
||||
|
||||
emitter.on(
|
||||
selector,
|
||||
addRuleErrorHandler(ruleListener)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// only run code path analyzer if the top level node is "Program", skip otherwise
|
||||
const eventGenerator = nodeQueue[0].node.type === "Program"
|
||||
? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
|
||||
: new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
|
||||
|
||||
nodeQueue.forEach(traversalInfo => {
|
||||
currentNode = traversalInfo.node;
|
||||
|
||||
try {
|
||||
if (traversalInfo.isEntering) {
|
||||
eventGenerator.enterNode(currentNode);
|
||||
} else {
|
||||
eventGenerator.leaveNode(currentNode);
|
||||
}
|
||||
} catch (err) {
|
||||
err.currentNode = currentNode;
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
return lintingProblems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the source code to be a string.
|
||||
* @param {string|SourceCode} textOrSourceCode The text or source code object.
|
||||
* @returns {string} The source code text.
|
||||
*/
|
||||
function ensureText(textOrSourceCode) {
|
||||
if (typeof textOrSourceCode === "object") {
|
||||
const { hasBOM, text } = textOrSourceCode;
|
||||
const bom = hasBOM ? "\uFEFF" : "";
|
||||
|
||||
return bom + text;
|
||||
}
|
||||
|
||||
return String(textOrSourceCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an environment.
|
||||
* @param {LinterInternalSlots} slots The internal slots of Linter.
|
||||
* @param {string} envId The environment ID to get.
|
||||
* @returns {Environment|null} The environment.
|
||||
*/
|
||||
function getEnv(slots, envId) {
|
||||
return (
|
||||
(slots.lastConfigArray && slots.lastConfigArray.pluginEnvironments.get(envId)) ||
|
||||
BuiltInEnvironments.get(envId) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a rule.
|
||||
* @param {LinterInternalSlots} slots The internal slots of Linter.
|
||||
* @param {string} ruleId The rule ID to get.
|
||||
* @returns {Rule|null} The rule.
|
||||
*/
|
||||
function getRule(slots, ruleId) {
|
||||
return (
|
||||
(slots.lastConfigArray && slots.lastConfigArray.pluginRules.get(ruleId)) ||
|
||||
slots.ruleMap.get(ruleId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the value of the cwd
|
||||
* @param {string | undefined} cwd raw value of the cwd, path to a directory that should be considered as the current working directory, can be undefined.
|
||||
* @returns {string | undefined} normalized cwd
|
||||
*/
|
||||
function normalizeCwd(cwd) {
|
||||
if (cwd) {
|
||||
return cwd;
|
||||
}
|
||||
if (typeof process === "object") {
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
// It's more explicit to assign the undefined
|
||||
// eslint-disable-next-line no-undefined -- Consistently returning a value
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The map to store private data.
|
||||
* @type {WeakMap<Linter, LinterInternalSlots>}
|
||||
*/
|
||||
const internalSlotsMap = new WeakMap();
|
||||
|
||||
/**
|
||||
* Throws an error when the given linter is in flat config mode.
|
||||
* @param {Linter} linter The linter to check.
|
||||
* @returns {void}
|
||||
* @throws {Error} If the linter is in flat config mode.
|
||||
*/
|
||||
function assertEslintrcConfig(linter) {
|
||||
const { configType } = internalSlotsMap.get(linter);
|
||||
|
||||
if (configType === "flat") {
|
||||
throw new Error("This method cannot be used with flat config. Add your entries directly into the config array.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Object that is responsible for verifying JavaScript text
|
||||
* @name Linter
|
||||
*/
|
||||
class Linter {
|
||||
|
||||
/**
|
||||
* Initialize the Linter.
|
||||
* @param {Object} [config] the config object
|
||||
* @param {string} [config.cwd] path to a directory that should be considered as the current working directory, can be undefined.
|
||||
* @param {"flat"|"eslintrc"} [config.configType="eslintrc"] the type of config used.
|
||||
*/
|
||||
constructor({ cwd, configType } = {}) {
|
||||
internalSlotsMap.set(this, {
|
||||
cwd: normalizeCwd(cwd),
|
||||
lastConfigArray: null,
|
||||
lastSourceCode: null,
|
||||
lastSuppressedMessages: [],
|
||||
configType, // TODO: Remove after flat config conversion
|
||||
parserMap: new Map([["espree", espree]]),
|
||||
ruleMap: new Rules()
|
||||
});
|
||||
|
||||
this.version = pkg.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for package version.
|
||||
* @static
|
||||
* @returns {string} The version from package.json.
|
||||
*/
|
||||
static get version() {
|
||||
return pkg.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as linter.verify, except without support for processors.
|
||||
* @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
|
||||
* @param {ConfigData} providedConfig An ESLintConfig instance to configure everything.
|
||||
* @param {VerifyOptions} [providedOptions] The optional filename of the file being checked.
|
||||
* @throws {Error} If during rule execution.
|
||||
* @returns {(LintMessage|SuppressedLintMessage)[]} The results as an array of messages or an empty array if no messages.
|
||||
*/
|
||||
_verifyWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) {
|
||||
const slots = internalSlotsMap.get(this);
|
||||
const config = providedConfig || {};
|
||||
const options = normalizeVerifyOptions(providedOptions, config);
|
||||
let text;
|
||||
|
||||
// evaluate arguments
|
||||
if (typeof textOrSourceCode === "string") {
|
||||
slots.lastSourceCode = null;
|
||||
text = textOrSourceCode;
|
||||
} else {
|
||||
slots.lastSourceCode = textOrSourceCode;
|
||||
text = textOrSourceCode.text;
|
||||
}
|
||||
|
||||
// Resolve parser.
|
||||
let parserName = DEFAULT_PARSER_NAME;
|
||||
let parser = espree;
|
||||
|
||||
if (typeof config.parser === "object" && config.parser !== null) {
|
||||
parserName = config.parser.filePath;
|
||||
parser = config.parser.definition;
|
||||
} else if (typeof config.parser === "string") {
|
||||
if (!slots.parserMap.has(config.parser)) {
|
||||
return [{
|
||||
ruleId: null,
|
||||
fatal: true,
|
||||
severity: 2,
|
||||
message: `Configured parser '${config.parser}' was not found.`,
|
||||
line: 0,
|
||||
column: 0,
|
||||
nodeType: null
|
||||
}];
|
||||
}
|
||||
parserName = config.parser;
|
||||
parser = slots.parserMap.get(config.parser);
|
||||
}
|
||||
|
||||
// search and apply "eslint-env *".
|
||||
const envInFile = options.allowInlineConfig && !options.warnInlineConfig
|
||||
? findEslintEnv(text)
|
||||
: {};
|
||||
const resolvedEnvConfig = Object.assign({ builtin: true }, config.env, envInFile);
|
||||
const enabledEnvs = Object.keys(resolvedEnvConfig)
|
||||
.filter(envName => resolvedEnvConfig[envName])
|
||||
.map(envName => getEnv(slots, envName))
|
||||
.filter(env => env);
|
||||
|
||||
const parserOptions = resolveParserOptions(parser, config.parserOptions || {}, enabledEnvs);
|
||||
const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs);
|
||||
const settings = config.settings || {};
|
||||
const languageOptions = createLanguageOptions({
|
||||
globals: config.globals,
|
||||
parser,
|
||||
parserOptions
|
||||
});
|
||||
|
||||
if (!slots.lastSourceCode) {
|
||||
const parseResult = parse(
|
||||
text,
|
||||
languageOptions,
|
||||
options.filename
|
||||
);
|
||||
|
||||
if (!parseResult.success) {
|
||||
return [parseResult.error];
|
||||
}
|
||||
|
||||
slots.lastSourceCode = parseResult.sourceCode;
|
||||
} else {
|
||||
|
||||
/*
|
||||
* If the given source code object as the first argument does not have scopeManager, analyze the scope.
|
||||
* This is for backward compatibility (SourceCode is frozen so it cannot rebind).
|
||||
*/
|
||||
if (!slots.lastSourceCode.scopeManager) {
|
||||
slots.lastSourceCode = new SourceCode({
|
||||
text: slots.lastSourceCode.text,
|
||||
ast: slots.lastSourceCode.ast,
|
||||
parserServices: slots.lastSourceCode.parserServices,
|
||||
visitorKeys: slots.lastSourceCode.visitorKeys,
|
||||
scopeManager: analyzeScope(slots.lastSourceCode.ast, languageOptions)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sourceCode = slots.lastSourceCode;
|
||||
const commentDirectives = options.allowInlineConfig
|
||||
? getDirectiveComments(sourceCode, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
|
||||
: { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };
|
||||
|
||||
// augment global scope with declared global variables
|
||||
addDeclaredGlobals(
|
||||
sourceCode.scopeManager.scopes[0],
|
||||
configuredGlobals,
|
||||
{ exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals }
|
||||
);
|
||||
|
||||
const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);
|
||||
let lintingProblems;
|
||||
|
||||
try {
|
||||
lintingProblems = runRules(
|
||||
sourceCode,
|
||||
configuredRules,
|
||||
ruleId => getRule(slots, ruleId),
|
||||
parserName,
|
||||
languageOptions,
|
||||
settings,
|
||||
options.filename,
|
||||
options.disableFixes,
|
||||
slots.cwd,
|
||||
providedOptions.physicalFilename
|
||||
);
|
||||
} catch (err) {
|
||||
err.message += `\nOccurred while linting ${options.filename}`;
|
||||
debug("An error occurred while traversing");
|
||||
debug("Filename:", options.filename);
|
||||
if (err.currentNode) {
|
||||
const { line } = err.currentNode.loc.start;
|
||||
|
||||
debug("Line:", line);
|
||||
err.message += `:${line}`;
|
||||
}
|
||||
debug("Parser Options:", parserOptions);
|
||||
debug("Parser Path:", parserName);
|
||||
debug("Settings:", settings);
|
||||
|
||||
if (err.ruleId) {
|
||||
err.message += `\nRule: "${err.ruleId}"`;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
return applyDisableDirectives({
|
||||
directives: commentDirectives.disableDirectives,
|
||||
disableFixes: options.disableFixes,
|
||||
problems: lintingProblems
|
||||
.concat(commentDirectives.problems)
|
||||
.sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
|
||||
reportUnusedDisableDirectives: options.reportUnusedDisableDirectives
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the text against the rules specified by the second argument.
|
||||
* @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
|
||||
* @param {ConfigData|ConfigArray} config An ESLintConfig instance to configure everything.
|
||||
* @param {(string|(VerifyOptions&ProcessorOptions))} [filenameOrOptions] The optional filename of the file being checked.
|
||||
* If this is not set, the filename will default to '<input>' in the rule context. If
|
||||
* an object, then it has "filename", "allowInlineConfig", and some properties.
|
||||
* @returns {LintMessage[]} The results as an array of messages or an empty array if no messages.
|
||||
*/
|
||||
verify(textOrSourceCode, config, filenameOrOptions) {
|
||||
debug("Verify");
|
||||
|
||||
const { configType, cwd } = internalSlotsMap.get(this);
|
||||
|
||||
const options = typeof filenameOrOptions === "string"
|
||||
? { filename: filenameOrOptions }
|
||||
: filenameOrOptions || {};
|
||||
|
||||
if (config) {
|
||||
if (configType === "flat") {
|
||||
|
||||
/*
|
||||
* Because of how Webpack packages up the files, we can't
|
||||
* compare directly to `FlatConfigArray` using `instanceof`
|
||||
* because it's not the same `FlatConfigArray` as in the tests.
|
||||
* So, we work around it by assuming an array is, in fact, a
|
||||
* `FlatConfigArray` if it has a `getConfig()` method.
|
||||
*/
|
||||
let configArray = config;
|
||||
|
||||
if (!Array.isArray(config) || typeof config.getConfig !== "function") {
|
||||
configArray = new FlatConfigArray(config, { basePath: cwd });
|
||||
configArray.normalizeSync();
|
||||
}
|
||||
|
||||
return this._distinguishSuppressedMessages(this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options, true));
|
||||
}
|
||||
|
||||
if (typeof config.extractConfig === "function") {
|
||||
return this._distinguishSuppressedMessages(this._verifyWithConfigArray(textOrSourceCode, config, options));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If we get to here, it means `config` is just an object rather
|
||||
* than a config array so we can go right into linting.
|
||||
*/
|
||||
|
||||
/*
|
||||
* `Linter` doesn't support `overrides` property in configuration.
|
||||
* So we cannot apply multiple processors.
|
||||
*/
|
||||
if (options.preprocess || options.postprocess) {
|
||||
return this._distinguishSuppressedMessages(this._verifyWithProcessor(textOrSourceCode, config, options));
|
||||
}
|
||||
return this._distinguishSuppressedMessages(this._verifyWithoutProcessors(textOrSourceCode, config, options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify with a processor.
|
||||
* @param {string|SourceCode} textOrSourceCode The source code.
|
||||
* @param {FlatConfig} config The config array.
|
||||
* @param {VerifyOptions&ProcessorOptions} options The options.
|
||||
* @param {FlatConfigArray} [configForRecursive] The `ConfigArray` object to apply multiple processors recursively.
|
||||
* @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
|
||||
*/
|
||||
_verifyWithFlatConfigArrayAndProcessor(textOrSourceCode, config, options, configForRecursive) {
|
||||
const filename = options.filename || "<input>";
|
||||
const filenameToExpose = normalizeFilename(filename);
|
||||
const physicalFilename = options.physicalFilename || filenameToExpose;
|
||||
const text = ensureText(textOrSourceCode);
|
||||
const preprocess = options.preprocess || (rawText => [rawText]);
|
||||
const postprocess = options.postprocess || (messagesList => messagesList.flat());
|
||||
const filterCodeBlock =
|
||||
options.filterCodeBlock ||
|
||||
(blockFilename => blockFilename.endsWith(".js"));
|
||||
const originalExtname = path.extname(filename);
|
||||
|
||||
let blocks;
|
||||
|
||||
try {
|
||||
blocks = preprocess(text, filenameToExpose);
|
||||
} catch (ex) {
|
||||
|
||||
// If the message includes a leading line number, strip it:
|
||||
const message = `Preprocessing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`;
|
||||
|
||||
debug("%s\n%s", message, ex.stack);
|
||||
|
||||
return [
|
||||
{
|
||||
ruleId: null,
|
||||
fatal: true,
|
||||
severity: 2,
|
||||
message,
|
||||
line: ex.lineNumber,
|
||||
column: ex.column,
|
||||
nodeType: null
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const messageLists = blocks.map((block, i) => {
|
||||
debug("A code block was found: %o", block.filename || "(unnamed)");
|
||||
|
||||
// Keep the legacy behavior.
|
||||
if (typeof block === "string") {
|
||||
return this._verifyWithFlatConfigArrayAndWithoutProcessors(block, config, options);
|
||||
}
|
||||
|
||||
const blockText = block.text;
|
||||
const blockName = path.join(filename, `${i}_${block.filename}`);
|
||||
|
||||
// Skip this block if filtered.
|
||||
if (!filterCodeBlock(blockName, blockText)) {
|
||||
debug("This code block was skipped.");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Resolve configuration again if the file content or extension was changed.
|
||||
if (configForRecursive && (text !== blockText || path.extname(blockName) !== originalExtname)) {
|
||||
debug("Resolving configuration again because the file content or extension was changed.");
|
||||
return this._verifyWithFlatConfigArray(
|
||||
blockText,
|
||||
configForRecursive,
|
||||
{ ...options, filename: blockName, physicalFilename }
|
||||
);
|
||||
}
|
||||
|
||||
// Does lint.
|
||||
return this._verifyWithFlatConfigArrayAndWithoutProcessors(
|
||||
blockText,
|
||||
config,
|
||||
{ ...options, filename: blockName, physicalFilename }
|
||||
);
|
||||
});
|
||||
|
||||
return postprocess(messageLists, filenameToExpose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as linter.verify, except without support for processors.
|
||||
* @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
|
||||
* @param {FlatConfig} providedConfig An ESLintConfig instance to configure everything.
|
||||
* @param {VerifyOptions} [providedOptions] The optional filename of the file being checked.
|
||||
* @throws {Error} If during rule execution.
|
||||
* @returns {(LintMessage|SuppressedLintMessage)[]} The results as an array of messages or an empty array if no messages.
|
||||
*/
|
||||
_verifyWithFlatConfigArrayAndWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) {
|
||||
const slots = internalSlotsMap.get(this);
|
||||
const config = providedConfig || {};
|
||||
const options = normalizeVerifyOptions(providedOptions, config);
|
||||
let text;
|
||||
|
||||
// evaluate arguments
|
||||
if (typeof textOrSourceCode === "string") {
|
||||
slots.lastSourceCode = null;
|
||||
text = textOrSourceCode;
|
||||
} else {
|
||||
slots.lastSourceCode = textOrSourceCode;
|
||||
text = textOrSourceCode.text;
|
||||
}
|
||||
|
||||
const languageOptions = config.languageOptions;
|
||||
|
||||
languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions(
|
||||
languageOptions.ecmaVersion
|
||||
);
|
||||
|
||||
// double check that there is a parser to avoid mysterious error messages
|
||||
if (!languageOptions.parser) {
|
||||
throw new TypeError(`No parser specified for ${options.filename}`);
|
||||
}
|
||||
|
||||
// Espree expects this information to be passed in
|
||||
if (isEspree(languageOptions.parser)) {
|
||||
const parserOptions = languageOptions.parserOptions;
|
||||
|
||||
if (languageOptions.sourceType) {
|
||||
|
||||
parserOptions.sourceType = languageOptions.sourceType;
|
||||
|
||||
if (
|
||||
parserOptions.sourceType === "module" &&
|
||||
parserOptions.ecmaFeatures &&
|
||||
parserOptions.ecmaFeatures.globalReturn
|
||||
) {
|
||||
parserOptions.ecmaFeatures.globalReturn = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const settings = config.settings || {};
|
||||
|
||||
if (!slots.lastSourceCode) {
|
||||
const parseResult = parse(
|
||||
text,
|
||||
languageOptions,
|
||||
options.filename
|
||||
);
|
||||
|
||||
if (!parseResult.success) {
|
||||
return [parseResult.error];
|
||||
}
|
||||
|
||||
slots.lastSourceCode = parseResult.sourceCode;
|
||||
} else {
|
||||
|
||||
/*
|
||||
* If the given source code object as the first argument does not have scopeManager, analyze the scope.
|
||||
* This is for backward compatibility (SourceCode is frozen so it cannot rebind).
|
||||
*/
|
||||
if (!slots.lastSourceCode.scopeManager) {
|
||||
slots.lastSourceCode = new SourceCode({
|
||||
text: slots.lastSourceCode.text,
|
||||
ast: slots.lastSourceCode.ast,
|
||||
parserServices: slots.lastSourceCode.parserServices,
|
||||
visitorKeys: slots.lastSourceCode.visitorKeys,
|
||||
scopeManager: analyzeScope(slots.lastSourceCode.ast, languageOptions)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sourceCode = slots.lastSourceCode;
|
||||
|
||||
/*
|
||||
* Make adjustments based on the language options. For JavaScript,
|
||||
* this is primarily about adding variables into the global scope
|
||||
* to account for ecmaVersion and configured globals.
|
||||
*/
|
||||
sourceCode.applyLanguageOptions(languageOptions);
|
||||
|
||||
const mergedInlineConfig = {
|
||||
rules: {}
|
||||
};
|
||||
const inlineConfigProblems = [];
|
||||
|
||||
/*
|
||||
* Inline config can be either enabled or disabled. If disabled, it's possible
|
||||
* to detect the inline config and emit a warning (though this is not required).
|
||||
* So we first check to see if inline config is allowed at all, and if so, we
|
||||
* need to check if it's a warning or not.
|
||||
*/
|
||||
if (options.allowInlineConfig) {
|
||||
|
||||
// if inline config should warn then add the warnings
|
||||
if (options.warnInlineConfig) {
|
||||
sourceCode.getInlineConfigNodes().forEach(node => {
|
||||
inlineConfigProblems.push(createLintingProblem({
|
||||
ruleId: null,
|
||||
message: `'${sourceCode.text.slice(node.range[0], node.range[1])}' has no effect because you have 'noInlineConfig' setting in ${options.warnInlineConfig}.`,
|
||||
loc: node.loc,
|
||||
severity: 1
|
||||
}));
|
||||
|
||||
});
|
||||
} else {
|
||||
const inlineConfigResult = sourceCode.applyInlineConfig();
|
||||
|
||||
inlineConfigProblems.push(
|
||||
...inlineConfigResult.problems
|
||||
.map(createLintingProblem)
|
||||
.map(problem => {
|
||||
problem.fatal = true;
|
||||
return problem;
|
||||
})
|
||||
);
|
||||
|
||||
// next we need to verify information about the specified rules
|
||||
const ruleValidator = new RuleValidator();
|
||||
|
||||
for (const { config: inlineConfig, node } of inlineConfigResult.configs) {
|
||||
|
||||
Object.keys(inlineConfig.rules).forEach(ruleId => {
|
||||
const rule = getRuleFromConfig(ruleId, config);
|
||||
const ruleValue = inlineConfig.rules[ruleId];
|
||||
|
||||
if (!rule) {
|
||||
inlineConfigProblems.push(createLintingProblem({ ruleId, loc: node.loc }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
||||
|
||||
assertIsRuleOptions(ruleId, ruleValue);
|
||||
assertIsRuleSeverity(ruleId, ruleOptions[0]);
|
||||
|
||||
ruleValidator.validate({
|
||||
plugins: config.plugins,
|
||||
rules: {
|
||||
[ruleId]: ruleOptions
|
||||
}
|
||||
});
|
||||
mergedInlineConfig.rules[ruleId] = ruleValue;
|
||||
} catch (err) {
|
||||
|
||||
let baseMessage = err.message.slice(
|
||||
err.message.startsWith("Key \"rules\":")
|
||||
? err.message.indexOf(":", 12) + 1
|
||||
: err.message.indexOf(":") + 1
|
||||
).trim();
|
||||
|
||||
if (err.messageTemplate) {
|
||||
baseMessage += ` You passed "${ruleValue}".`;
|
||||
}
|
||||
|
||||
inlineConfigProblems.push(createLintingProblem({
|
||||
ruleId,
|
||||
message: `Inline configuration for rule "${ruleId}" is invalid:\n\t${baseMessage}\n`,
|
||||
loc: node.loc
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const commentDirectives = options.allowInlineConfig && !options.warnInlineConfig
|
||||
? getDirectiveCommentsForFlatConfig(
|
||||
sourceCode,
|
||||
ruleId => getRuleFromConfig(ruleId, config)
|
||||
)
|
||||
: { problems: [], disableDirectives: [] };
|
||||
|
||||
const configuredRules = Object.assign({}, config.rules, mergedInlineConfig.rules);
|
||||
let lintingProblems;
|
||||
|
||||
sourceCode.finalize();
|
||||
|
||||
try {
|
||||
lintingProblems = runRules(
|
||||
sourceCode,
|
||||
configuredRules,
|
||||
ruleId => getRuleFromConfig(ruleId, config),
|
||||
void 0,
|
||||
languageOptions,
|
||||
settings,
|
||||
options.filename,
|
||||
options.disableFixes,
|
||||
slots.cwd,
|
||||
providedOptions.physicalFilename
|
||||
);
|
||||
} catch (err) {
|
||||
err.message += `\nOccurred while linting ${options.filename}`;
|
||||
debug("An error occurred while traversing");
|
||||
debug("Filename:", options.filename);
|
||||
if (err.currentNode) {
|
||||
const { line } = err.currentNode.loc.start;
|
||||
|
||||
debug("Line:", line);
|
||||
err.message += `:${line}`;
|
||||
}
|
||||
debug("Parser Options:", languageOptions.parserOptions);
|
||||
|
||||
// debug("Parser Path:", parserName);
|
||||
debug("Settings:", settings);
|
||||
|
||||
if (err.ruleId) {
|
||||
err.message += `\nRule: "${err.ruleId}"`;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
return applyDisableDirectives({
|
||||
directives: commentDirectives.disableDirectives,
|
||||
disableFixes: options.disableFixes,
|
||||
problems: lintingProblems
|
||||
.concat(commentDirectives.problems)
|
||||
.concat(inlineConfigProblems)
|
||||
.sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
|
||||
reportUnusedDisableDirectives: options.reportUnusedDisableDirectives
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a given code with `ConfigArray`.
|
||||
* @param {string|SourceCode} textOrSourceCode The source code.
|
||||
* @param {ConfigArray} configArray The config array.
|
||||
* @param {VerifyOptions&ProcessorOptions} options The options.
|
||||
* @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
|
||||
*/
|
||||
_verifyWithConfigArray(textOrSourceCode, configArray, options) {
|
||||
debug("With ConfigArray: %s", options.filename);
|
||||
|
||||
// Store the config array in order to get plugin envs and rules later.
|
||||
internalSlotsMap.get(this).lastConfigArray = configArray;
|
||||
|
||||
// Extract the final config for this file.
|
||||
const config = configArray.extractConfig(options.filename);
|
||||
const processor =
|
||||
config.processor &&
|
||||
configArray.pluginProcessors.get(config.processor);
|
||||
|
||||
// Verify.
|
||||
if (processor) {
|
||||
debug("Apply the processor: %o", config.processor);
|
||||
const { preprocess, postprocess, supportsAutofix } = processor;
|
||||
const disableFixes = options.disableFixes || !supportsAutofix;
|
||||
|
||||
return this._verifyWithProcessor(
|
||||
textOrSourceCode,
|
||||
config,
|
||||
{ ...options, disableFixes, postprocess, preprocess },
|
||||
configArray
|
||||
);
|
||||
}
|
||||
return this._verifyWithoutProcessors(textOrSourceCode, config, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a given code with a flat config.
|
||||
* @param {string|SourceCode} textOrSourceCode The source code.
|
||||
* @param {FlatConfigArray} configArray The config array.
|
||||
* @param {VerifyOptions&ProcessorOptions} options The options.
|
||||
* @param {boolean} [firstCall=false] Indicates if this is being called directly
|
||||
* from verify(). (TODO: Remove once eslintrc is removed.)
|
||||
* @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
|
||||
*/
|
||||
_verifyWithFlatConfigArray(textOrSourceCode, configArray, options, firstCall = false) {
|
||||
debug("With flat config: %s", options.filename);
|
||||
|
||||
// we need a filename to match configs against
|
||||
const filename = options.filename || "__placeholder__.js";
|
||||
|
||||
// Store the config array in order to get plugin envs and rules later.
|
||||
internalSlotsMap.get(this).lastConfigArray = configArray;
|
||||
const config = configArray.getConfig(filename);
|
||||
|
||||
if (!config) {
|
||||
return [
|
||||
{
|
||||
ruleId: null,
|
||||
severity: 1,
|
||||
message: `No matching configuration found for ${filename}.`,
|
||||
line: 0,
|
||||
column: 0,
|
||||
nodeType: null
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Verify.
|
||||
if (config.processor) {
|
||||
debug("Apply the processor: %o", config.processor);
|
||||
const { preprocess, postprocess, supportsAutofix } = config.processor;
|
||||
const disableFixes = options.disableFixes || !supportsAutofix;
|
||||
|
||||
return this._verifyWithFlatConfigArrayAndProcessor(
|
||||
textOrSourceCode,
|
||||
config,
|
||||
{ ...options, filename, disableFixes, postprocess, preprocess },
|
||||
configArray
|
||||
);
|
||||
}
|
||||
|
||||
// check for options-based processing
|
||||
if (firstCall && (options.preprocess || options.postprocess)) {
|
||||
return this._verifyWithFlatConfigArrayAndProcessor(textOrSourceCode, config, options);
|
||||
}
|
||||
|
||||
return this._verifyWithFlatConfigArrayAndWithoutProcessors(textOrSourceCode, config, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify with a processor.
|
||||
* @param {string|SourceCode} textOrSourceCode The source code.
|
||||
* @param {ConfigData|ExtractedConfig} config The config array.
|
||||
* @param {VerifyOptions&ProcessorOptions} options The options.
|
||||
* @param {ConfigArray} [configForRecursive] The `ConfigArray` object to apply multiple processors recursively.
|
||||
* @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
|
||||
*/
|
||||
_verifyWithProcessor(textOrSourceCode, config, options, configForRecursive) {
|
||||
const filename = options.filename || "<input>";
|
||||
const filenameToExpose = normalizeFilename(filename);
|
||||
const physicalFilename = options.physicalFilename || filenameToExpose;
|
||||
const text = ensureText(textOrSourceCode);
|
||||
const preprocess = options.preprocess || (rawText => [rawText]);
|
||||
const postprocess = options.postprocess || (messagesList => messagesList.flat());
|
||||
const filterCodeBlock =
|
||||
options.filterCodeBlock ||
|
||||
(blockFilename => blockFilename.endsWith(".js"));
|
||||
const originalExtname = path.extname(filename);
|
||||
|
||||
let blocks;
|
||||
|
||||
try {
|
||||
blocks = preprocess(text, filenameToExpose);
|
||||
} catch (ex) {
|
||||
|
||||
// If the message includes a leading line number, strip it:
|
||||
const message = `Preprocessing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`;
|
||||
|
||||
debug("%s\n%s", message, ex.stack);
|
||||
|
||||
return [
|
||||
{
|
||||
ruleId: null,
|
||||
fatal: true,
|
||||
severity: 2,
|
||||
message,
|
||||
line: ex.lineNumber,
|
||||
column: ex.column,
|
||||
nodeType: null
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const messageLists = blocks.map((block, i) => {
|
||||
debug("A code block was found: %o", block.filename || "(unnamed)");
|
||||
|
||||
// Keep the legacy behavior.
|
||||
if (typeof block === "string") {
|
||||
return this._verifyWithoutProcessors(block, config, options);
|
||||
}
|
||||
|
||||
const blockText = block.text;
|
||||
const blockName = path.join(filename, `${i}_${block.filename}`);
|
||||
|
||||
// Skip this block if filtered.
|
||||
if (!filterCodeBlock(blockName, blockText)) {
|
||||
debug("This code block was skipped.");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Resolve configuration again if the file content or extension was changed.
|
||||
if (configForRecursive && (text !== blockText || path.extname(blockName) !== originalExtname)) {
|
||||
debug("Resolving configuration again because the file content or extension was changed.");
|
||||
return this._verifyWithConfigArray(
|
||||
blockText,
|
||||
configForRecursive,
|
||||
{ ...options, filename: blockName, physicalFilename }
|
||||
);
|
||||
}
|
||||
|
||||
// Does lint.
|
||||
return this._verifyWithoutProcessors(
|
||||
blockText,
|
||||
config,
|
||||
{ ...options, filename: blockName, physicalFilename }
|
||||
);
|
||||
});
|
||||
|
||||
return postprocess(messageLists, filenameToExpose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of reported problems, distinguish problems between normal messages and suppressed messages.
|
||||
* The normal messages will be returned and the suppressed messages will be stored as lastSuppressedMessages.
|
||||
* @param {Array<LintMessage|SuppressedLintMessage>} problems A list of reported problems.
|
||||
* @returns {LintMessage[]} A list of LintMessage.
|
||||
*/
|
||||
_distinguishSuppressedMessages(problems) {
|
||||
const messages = [];
|
||||
const suppressedMessages = [];
|
||||
const slots = internalSlotsMap.get(this);
|
||||
|
||||
for (const problem of problems) {
|
||||
if (problem.suppressions) {
|
||||
suppressedMessages.push(problem);
|
||||
} else {
|
||||
messages.push(problem);
|
||||
}
|
||||
}
|
||||
|
||||
slots.lastSuppressedMessages = suppressedMessages;
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SourceCode object representing the parsed source.
|
||||
* @returns {SourceCode} The SourceCode object.
|
||||
*/
|
||||
getSourceCode() {
|
||||
return internalSlotsMap.get(this).lastSourceCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of SuppressedLintMessage produced in the last running.
|
||||
* @returns {SuppressedLintMessage[]} The list of SuppressedLintMessage
|
||||
*/
|
||||
getSuppressedMessages() {
|
||||
return internalSlotsMap.get(this).lastSuppressedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a new linting rule.
|
||||
* @param {string} ruleId A unique rule identifier
|
||||
* @param {Function | Rule} ruleModule Function from context to object mapping AST node types to event handlers
|
||||
* @returns {void}
|
||||
*/
|
||||
defineRule(ruleId, ruleModule) {
|
||||
assertEslintrcConfig(this);
|
||||
internalSlotsMap.get(this).ruleMap.define(ruleId, ruleModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines many new linting rules.
|
||||
* @param {Record<string, Function | Rule>} rulesToDefine map from unique rule identifier to rule
|
||||
* @returns {void}
|
||||
*/
|
||||
defineRules(rulesToDefine) {
|
||||
assertEslintrcConfig(this);
|
||||
Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => {
|
||||
this.defineRule(ruleId, rulesToDefine[ruleId]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an object with all loaded rules.
|
||||
* @returns {Map<string, Rule>} All loaded rules
|
||||
*/
|
||||
getRules() {
|
||||
assertEslintrcConfig(this);
|
||||
const { lastConfigArray, ruleMap } = internalSlotsMap.get(this);
|
||||
|
||||
return new Map(function *() {
|
||||
yield* ruleMap;
|
||||
|
||||
if (lastConfigArray) {
|
||||
yield* lastConfigArray.pluginRules;
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a new parser module
|
||||
* @param {string} parserId Name of the parser
|
||||
* @param {Parser} parserModule The parser object
|
||||
* @returns {void}
|
||||
*/
|
||||
defineParser(parserId, parserModule) {
|
||||
assertEslintrcConfig(this);
|
||||
internalSlotsMap.get(this).parserMap.set(parserId, parserModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs multiple autofix passes over the text until as many fixes as possible
|
||||
* have been applied.
|
||||
* @param {string} text The source text to apply fixes to.
|
||||
* @param {ConfigData|ConfigArray|FlatConfigArray} config The ESLint config object to use.
|
||||
* @param {VerifyOptions&ProcessorOptions&FixOptions} options The ESLint options object to use.
|
||||
* @returns {{fixed:boolean,messages:LintMessage[],output:string}} The result of the fix operation as returned from the
|
||||
* SourceCodeFixer.
|
||||
*/
|
||||
verifyAndFix(text, config, options) {
|
||||
let messages = [],
|
||||
fixedResult,
|
||||
fixed = false,
|
||||
passNumber = 0,
|
||||
currentText = text;
|
||||
const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`;
|
||||
const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true;
|
||||
|
||||
/**
|
||||
* This loop continues until one of the following is true:
|
||||
*
|
||||
* 1. No more fixes have been applied.
|
||||
* 2. Ten passes have been made.
|
||||
*
|
||||
* That means anytime a fix is successfully applied, there will be another pass.
|
||||
* Essentially, guaranteeing a minimum of two passes.
|
||||
*/
|
||||
do {
|
||||
passNumber++;
|
||||
|
||||
debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
|
||||
messages = this.verify(currentText, config, options);
|
||||
|
||||
debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
|
||||
fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);
|
||||
|
||||
/*
|
||||
* stop if there are any syntax errors.
|
||||
* 'fixedResult.output' is a empty string.
|
||||
*/
|
||||
if (messages.length === 1 && messages[0].fatal) {
|
||||
break;
|
||||
}
|
||||
|
||||
// keep track if any fixes were ever applied - important for return value
|
||||
fixed = fixed || fixedResult.fixed;
|
||||
|
||||
// update to use the fixed output instead of the original text
|
||||
currentText = fixedResult.output;
|
||||
|
||||
} while (
|
||||
fixedResult.fixed &&
|
||||
passNumber < MAX_AUTOFIX_PASSES
|
||||
);
|
||||
|
||||
/*
|
||||
* If the last result had fixes, we need to lint again to be sure we have
|
||||
* the most up-to-date information.
|
||||
*/
|
||||
if (fixedResult.fixed) {
|
||||
fixedResult.messages = this.verify(currentText, config, options);
|
||||
}
|
||||
|
||||
// ensure the last result properly reflects if fixes were done
|
||||
fixedResult.fixed = fixed;
|
||||
fixedResult.output = currentText;
|
||||
|
||||
return fixedResult;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Linter,
|
||||
|
||||
/**
|
||||
* Get the internal slots of a given Linter instance for tests.
|
||||
* @param {Linter} instance The Linter instance to get.
|
||||
* @returns {LinterInternalSlots} The internal slots.
|
||||
*/
|
||||
getLinterInternalSlots(instance) {
|
||||
return internalSlotsMap.get(instance);
|
||||
}
|
||||
};
|
||||
354
node_modules/eslint/lib/linter/node-event-generator.js
generated
vendored
Normal file
354
node_modules/eslint/lib/linter/node-event-generator.js
generated
vendored
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* @fileoverview The event generator for AST nodes.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const esquery = require("esquery");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* An object describing an AST selector
|
||||
* @typedef {Object} ASTSelector
|
||||
* @property {string} rawSelector The string that was parsed into this selector
|
||||
* @property {boolean} isExit `true` if this should be emitted when exiting the node rather than when entering
|
||||
* @property {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
|
||||
* @property {string[]|null} listenerTypes A list of node types that could possibly cause the selector to match,
|
||||
* or `null` if all node types could cause a match
|
||||
* @property {number} attributeCount The total number of classes, pseudo-classes, and attribute queries in this selector
|
||||
* @property {number} identifierCount The total number of identifier queries in this selector
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the possible types of a selector
|
||||
* @param {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
|
||||
* @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
|
||||
*/
|
||||
function getPossibleTypes(parsedSelector) {
|
||||
switch (parsedSelector.type) {
|
||||
case "identifier":
|
||||
return [parsedSelector.value];
|
||||
|
||||
case "matches": {
|
||||
const typesForComponents = parsedSelector.selectors.map(getPossibleTypes);
|
||||
|
||||
if (typesForComponents.every(Boolean)) {
|
||||
return union(...typesForComponents);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
case "compound": {
|
||||
const typesForComponents = parsedSelector.selectors.map(getPossibleTypes).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 "child":
|
||||
case "descendant":
|
||||
case "sibling":
|
||||
case "adjacent":
|
||||
return getPossibleTypes(parsedSelector.right);
|
||||
|
||||
case "class":
|
||||
if (parsedSelector.name === "function") {
|
||||
return ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"];
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of class, pseudo-class, and attribute queries in this selector
|
||||
* @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
|
||||
* @returns {number} The number of class, pseudo-class, and attribute queries in this selector
|
||||
*/
|
||||
function countClassAttributes(parsedSelector) {
|
||||
switch (parsedSelector.type) {
|
||||
case "child":
|
||||
case "descendant":
|
||||
case "sibling":
|
||||
case "adjacent":
|
||||
return countClassAttributes(parsedSelector.left) + countClassAttributes(parsedSelector.right);
|
||||
|
||||
case "compound":
|
||||
case "not":
|
||||
case "matches":
|
||||
return parsedSelector.selectors.reduce((sum, childSelector) => sum + countClassAttributes(childSelector), 0);
|
||||
|
||||
case "attribute":
|
||||
case "field":
|
||||
case "nth-child":
|
||||
case "nth-last-child":
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of identifier queries in this selector
|
||||
* @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
|
||||
* @returns {number} The number of identifier queries
|
||||
*/
|
||||
function countIdentifiers(parsedSelector) {
|
||||
switch (parsedSelector.type) {
|
||||
case "child":
|
||||
case "descendant":
|
||||
case "sibling":
|
||||
case "adjacent":
|
||||
return countIdentifiers(parsedSelector.left) + countIdentifiers(parsedSelector.right);
|
||||
|
||||
case "compound":
|
||||
case "not":
|
||||
case "matches":
|
||||
return parsedSelector.selectors.reduce((sum, childSelector) => sum + countIdentifiers(childSelector), 0);
|
||||
|
||||
case "identifier":
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the specificity of two selector objects, with CSS-like rules.
|
||||
* @param {ASTSelector} selectorA An AST selector descriptor
|
||||
* @param {ASTSelector} selectorB Another AST selector descriptor
|
||||
* @returns {number}
|
||||
* a value less than 0 if selectorA is less specific than selectorB
|
||||
* a value greater than 0 if selectorA is more specific than selectorB
|
||||
* a value less than 0 if selectorA and selectorB have the same specificity, and selectorA <= selectorB alphabetically
|
||||
* a value greater than 0 if selectorA and selectorB have the same specificity, and selectorA > selectorB alphabetically
|
||||
*/
|
||||
function compareSpecificity(selectorA, selectorB) {
|
||||
return selectorA.attributeCount - selectorB.attributeCount ||
|
||||
selectorA.identifierCount - selectorB.identifierCount ||
|
||||
(selectorA.rawSelector <= selectorB.rawSelector ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a raw selector string, and throws a useful error if parsing fails.
|
||||
* @param {string} rawSelector A raw AST selector
|
||||
* @returns {Object} An object (from esquery) describing the matching behavior of this selector
|
||||
* @throws {Error} An error if the selector is invalid
|
||||
*/
|
||||
function tryParseSelector(rawSelector) {
|
||||
try {
|
||||
return esquery.parse(rawSelector.replace(/:exit$/u, ""));
|
||||
} catch (err) {
|
||||
if (err.location && err.location.start && typeof err.location.start.offset === "number") {
|
||||
throw new SyntaxError(`Syntax error in selector "${rawSelector}" at position ${err.location.start.offset}: ${err.message}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const selectorCache = new Map();
|
||||
|
||||
/**
|
||||
* Parses a raw selector string, and returns the parsed selector along with specificity and type information.
|
||||
* @param {string} rawSelector A raw AST selector
|
||||
* @returns {ASTSelector} A selector descriptor
|
||||
*/
|
||||
function parseSelector(rawSelector) {
|
||||
if (selectorCache.has(rawSelector)) {
|
||||
return selectorCache.get(rawSelector);
|
||||
}
|
||||
|
||||
const parsedSelector = tryParseSelector(rawSelector);
|
||||
|
||||
const result = {
|
||||
rawSelector,
|
||||
isExit: rawSelector.endsWith(":exit"),
|
||||
parsedSelector,
|
||||
listenerTypes: getPossibleTypes(parsedSelector),
|
||||
attributeCount: countClassAttributes(parsedSelector),
|
||||
identifierCount: countIdentifiers(parsedSelector)
|
||||
};
|
||||
|
||||
selectorCache.set(rawSelector, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The event generator for AST nodes.
|
||||
* This implements below interface.
|
||||
*
|
||||
* ```ts
|
||||
* interface EventGenerator {
|
||||
* emitter: SafeEmitter;
|
||||
* enterNode(node: ASTNode): void;
|
||||
* leaveNode(node: ASTNode): void;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class NodeEventGenerator {
|
||||
|
||||
/**
|
||||
* @param {SafeEmitter} emitter
|
||||
* An SafeEmitter which is the destination of events. This emitter must already
|
||||
* have registered listeners for all of the events that it needs to listen for.
|
||||
* (See lib/linter/safe-emitter.js for more details on `SafeEmitter`.)
|
||||
* @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
|
||||
* @returns {NodeEventGenerator} new instance
|
||||
*/
|
||||
constructor(emitter, esqueryOptions) {
|
||||
this.emitter = emitter;
|
||||
this.esqueryOptions = esqueryOptions;
|
||||
this.currentAncestry = [];
|
||||
this.enterSelectorsByNodeType = new Map();
|
||||
this.exitSelectorsByNodeType = new Map();
|
||||
this.anyTypeEnterSelectors = [];
|
||||
this.anyTypeExitSelectors = [];
|
||||
|
||||
emitter.eventNames().forEach(rawSelector => {
|
||||
const selector = parseSelector(rawSelector);
|
||||
|
||||
if (selector.listenerTypes) {
|
||||
const typeMap = selector.isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType;
|
||||
|
||||
selector.listenerTypes.forEach(nodeType => {
|
||||
if (!typeMap.has(nodeType)) {
|
||||
typeMap.set(nodeType, []);
|
||||
}
|
||||
typeMap.get(nodeType).push(selector);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const selectors = selector.isExit ? this.anyTypeExitSelectors : this.anyTypeEnterSelectors;
|
||||
|
||||
selectors.push(selector);
|
||||
});
|
||||
|
||||
this.anyTypeEnterSelectors.sort(compareSpecificity);
|
||||
this.anyTypeExitSelectors.sort(compareSpecificity);
|
||||
this.enterSelectorsByNodeType.forEach(selectorList => selectorList.sort(compareSpecificity));
|
||||
this.exitSelectorsByNodeType.forEach(selectorList => selectorList.sort(compareSpecificity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a selector against a node, and emits it if it matches
|
||||
* @param {ASTNode} node The node to check
|
||||
* @param {ASTSelector} selector An AST selector descriptor
|
||||
* @returns {void}
|
||||
*/
|
||||
applySelector(node, selector) {
|
||||
if (esquery.matches(node, selector.parsedSelector, this.currentAncestry, this.esqueryOptions)) {
|
||||
this.emitter.emit(selector.rawSelector, node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all appropriate selectors to a node, in specificity order
|
||||
* @param {ASTNode} node The node to check
|
||||
* @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
|
||||
* @returns {void}
|
||||
*/
|
||||
applySelectors(node, isExit) {
|
||||
const selectorsByNodeType = (isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType).get(node.type) || [];
|
||||
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 selectorsByTypeIndex = 0;
|
||||
let anyTypeSelectorsIndex = 0;
|
||||
|
||||
while (selectorsByTypeIndex < selectorsByNodeType.length || anyTypeSelectorsIndex < anyTypeSelectors.length) {
|
||||
if (
|
||||
selectorsByTypeIndex >= selectorsByNodeType.length ||
|
||||
anyTypeSelectorsIndex < anyTypeSelectors.length &&
|
||||
compareSpecificity(anyTypeSelectors[anyTypeSelectorsIndex], selectorsByNodeType[selectorsByTypeIndex]) < 0
|
||||
) {
|
||||
this.applySelector(node, anyTypeSelectors[anyTypeSelectorsIndex++]);
|
||||
} else {
|
||||
this.applySelector(node, selectorsByNodeType[selectorsByTypeIndex++]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event of entering AST node.
|
||||
* @param {ASTNode} node A node which was entered.
|
||||
* @returns {void}
|
||||
*/
|
||||
enterNode(node) {
|
||||
if (node.parent) {
|
||||
this.currentAncestry.unshift(node.parent);
|
||||
}
|
||||
this.applySelectors(node, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event of leaving AST node.
|
||||
* @param {ASTNode} node A node which was left.
|
||||
* @returns {void}
|
||||
*/
|
||||
leaveNode(node) {
|
||||
this.applySelectors(node, true);
|
||||
this.currentAncestry.shift();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NodeEventGenerator;
|
||||
369
node_modules/eslint/lib/linter/report-translator.js
generated
vendored
Normal file
369
node_modules/eslint/lib/linter/report-translator.js
generated
vendored
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* @fileoverview A helper that translates context.report() calls from the rule API into generic problem objects
|
||||
* @author Teddy Katz
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const assert = require("assert");
|
||||
const ruleFixer = require("./rule-fixer");
|
||||
const interpolate = require("./interpolate");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @typedef {import("../shared/types").LintMessage} LintMessage */
|
||||
|
||||
/**
|
||||
* An error message description
|
||||
* @typedef {Object} MessageDescriptor
|
||||
* @property {ASTNode} [node] The reported node
|
||||
* @property {Location} loc The location of the problem.
|
||||
* @property {string} message The problem message.
|
||||
* @property {Object} [data] Optional data to use to fill in placeholders in the
|
||||
* message.
|
||||
* @property {Function} [fix] The function to call that creates a fix command.
|
||||
* @property {Array<{desc?: string, messageId?: string, fix: Function}>} suggest Suggestion descriptions and functions to create a the associated fixes.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Module Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* Translates a multi-argument context.report() call into a single object argument call
|
||||
* @param {...*} args A list of arguments passed to `context.report`
|
||||
* @returns {MessageDescriptor} A normalized object containing report information
|
||||
*/
|
||||
function normalizeMultiArgReportCall(...args) {
|
||||
|
||||
// If there is one argument, it is considered to be a new-style call already.
|
||||
if (args.length === 1) {
|
||||
|
||||
// Shallow clone the object to avoid surprises if reusing the descriptor
|
||||
return Object.assign({}, args[0]);
|
||||
}
|
||||
|
||||
// If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
|
||||
if (typeof args[1] === "string") {
|
||||
return {
|
||||
node: args[0],
|
||||
message: args[1],
|
||||
data: args[2],
|
||||
fix: args[3]
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
|
||||
return {
|
||||
node: args[0],
|
||||
loc: args[1],
|
||||
message: args[2],
|
||||
data: args[3],
|
||||
fix: args[4]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that either a loc or a node was provided, and the node is valid if it was provided.
|
||||
* @param {MessageDescriptor} descriptor A descriptor to validate
|
||||
* @returns {void}
|
||||
* @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object
|
||||
*/
|
||||
function assertValidNodeInfo(descriptor) {
|
||||
if (descriptor.node) {
|
||||
assert(typeof descriptor.node === "object", "Node must be an object");
|
||||
} else {
|
||||
assert(descriptor.loc, "Node must be provided when reporting error if location is not provided");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
|
||||
* @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
|
||||
* @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
|
||||
* from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
|
||||
*/
|
||||
function normalizeReportLoc(descriptor) {
|
||||
if (descriptor.loc) {
|
||||
if (descriptor.loc.start) {
|
||||
return descriptor.loc;
|
||||
}
|
||||
return { start: descriptor.loc, end: null };
|
||||
}
|
||||
return descriptor.node.loc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the given fix object.
|
||||
* @param {Fix|null} fix The fix to clone.
|
||||
* @returns {Fix|null} Deep cloned fix object or `null` if `null` or `undefined` was passed in.
|
||||
*/
|
||||
function cloneFix(fix) {
|
||||
if (!fix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
range: [fix.range[0], fix.range[1]],
|
||||
text: fix.text
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a fix has a valid range.
|
||||
* @param {Fix|null} fix The fix to validate.
|
||||
* @returns {void}
|
||||
*/
|
||||
function assertValidFix(fix) {
|
||||
if (fix) {
|
||||
assert(fix.range && typeof fix.range[0] === "number" && typeof fix.range[1] === "number", `Fix has invalid range: ${JSON.stringify(fix, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares items in a fixes array by range.
|
||||
* @param {Fix} a The first message.
|
||||
* @param {Fix} b The second message.
|
||||
* @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
|
||||
* @private
|
||||
*/
|
||||
function compareFixesByRange(a, b) {
|
||||
return a.range[0] - b.range[0] || a.range[1] - b.range[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the given fixes array into one.
|
||||
* @param {Fix[]} fixes The fixes to merge.
|
||||
* @param {SourceCode} sourceCode The source code object to get the text between fixes.
|
||||
* @returns {{text: string, range: number[]}} The merged fixes
|
||||
*/
|
||||
function mergeFixes(fixes, sourceCode) {
|
||||
for (const fix of fixes) {
|
||||
assertValidFix(fix);
|
||||
}
|
||||
|
||||
if (fixes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (fixes.length === 1) {
|
||||
return cloneFix(fixes[0]);
|
||||
}
|
||||
|
||||
fixes.sort(compareFixesByRange);
|
||||
|
||||
const originalText = sourceCode.text;
|
||||
const start = fixes[0].range[0];
|
||||
const end = fixes[fixes.length - 1].range[1];
|
||||
let text = "";
|
||||
let lastPos = Number.MIN_SAFE_INTEGER;
|
||||
|
||||
for (const fix of fixes) {
|
||||
assert(fix.range[0] >= lastPos, "Fix objects must not be overlapped in a report.");
|
||||
|
||||
if (fix.range[0] >= 0) {
|
||||
text += originalText.slice(Math.max(0, start, lastPos), fix.range[0]);
|
||||
}
|
||||
text += fix.text;
|
||||
lastPos = fix.range[1];
|
||||
}
|
||||
text += originalText.slice(Math.max(0, start, lastPos), end);
|
||||
|
||||
return { range: [start, end], text };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets one fix object from the given descriptor.
|
||||
* If the descriptor retrieves multiple fixes, this merges those to one.
|
||||
* @param {MessageDescriptor} descriptor The report descriptor.
|
||||
* @param {SourceCode} sourceCode The source code object to get text between fixes.
|
||||
* @returns {({text: string, range: number[]}|null)} The fix for the descriptor
|
||||
*/
|
||||
function normalizeFixes(descriptor, sourceCode) {
|
||||
if (typeof descriptor.fix !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// @type {null | Fix | Fix[] | IterableIterator<Fix>}
|
||||
const fix = descriptor.fix(ruleFixer);
|
||||
|
||||
// Merge to one.
|
||||
if (fix && Symbol.iterator in fix) {
|
||||
return mergeFixes(Array.from(fix), sourceCode);
|
||||
}
|
||||
|
||||
assertValidFix(fix);
|
||||
return cloneFix(fix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of suggestion objects from the given descriptor.
|
||||
* @param {MessageDescriptor} descriptor The report descriptor.
|
||||
* @param {SourceCode} sourceCode The source code object to get text between fixes.
|
||||
* @param {Object} messages Object of meta messages for the rule.
|
||||
* @returns {Array<SuggestionResult>} The suggestions for the descriptor
|
||||
*/
|
||||
function mapSuggestions(descriptor, sourceCode, messages) {
|
||||
if (!descriptor.suggest || !Array.isArray(descriptor.suggest)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return descriptor.suggest
|
||||
.map(suggestInfo => {
|
||||
const computedDesc = suggestInfo.desc || messages[suggestInfo.messageId];
|
||||
|
||||
return {
|
||||
...suggestInfo,
|
||||
desc: interpolate(computedDesc, suggestInfo.data),
|
||||
fix: normalizeFixes(suggestInfo, sourceCode)
|
||||
};
|
||||
})
|
||||
|
||||
// Remove suggestions that didn't provide a fix
|
||||
.filter(({ fix }) => fix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates information about the report from a descriptor
|
||||
* @param {Object} options Information about the problem
|
||||
* @param {string} options.ruleId Rule ID
|
||||
* @param {(0|1|2)} options.severity Rule severity
|
||||
* @param {(ASTNode|null)} options.node Node
|
||||
* @param {string} options.message Error message
|
||||
* @param {string} [options.messageId] The error message ID.
|
||||
* @param {{start: SourceLocation, end: (SourceLocation|null)}} options.loc Start and end location
|
||||
* @param {{text: string, range: (number[]|null)}} options.fix The fix object
|
||||
* @param {Array<{text: string, range: (number[]|null)}>} options.suggestions The array of suggestions objects
|
||||
* @returns {LintMessage} Information about the report
|
||||
*/
|
||||
function createProblem(options) {
|
||||
const problem = {
|
||||
ruleId: options.ruleId,
|
||||
severity: options.severity,
|
||||
message: options.message,
|
||||
line: options.loc.start.line,
|
||||
column: options.loc.start.column + 1,
|
||||
nodeType: options.node && options.node.type || null
|
||||
};
|
||||
|
||||
/*
|
||||
* If this isn’t in the conditional, some of the tests fail
|
||||
* because `messageId` is present in the problem object
|
||||
*/
|
||||
if (options.messageId) {
|
||||
problem.messageId = options.messageId;
|
||||
}
|
||||
|
||||
if (options.loc.end) {
|
||||
problem.endLine = options.loc.end.line;
|
||||
problem.endColumn = options.loc.end.column + 1;
|
||||
}
|
||||
|
||||
if (options.fix) {
|
||||
problem.fix = options.fix;
|
||||
}
|
||||
|
||||
if (options.suggestions && options.suggestions.length > 0) {
|
||||
problem.suggestions = options.suggestions;
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that suggestions are properly defined. Throws if an error is detected.
|
||||
* @param {Array<{ desc?: string, messageId?: string }>} suggest The incoming suggest data.
|
||||
* @param {Object} messages Object of meta messages for the rule.
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateSuggestions(suggest, messages) {
|
||||
if (suggest && Array.isArray(suggest)) {
|
||||
suggest.forEach(suggestion => {
|
||||
if (suggestion.messageId) {
|
||||
const { messageId } = suggestion;
|
||||
|
||||
if (!messages) {
|
||||
throw new TypeError(`context.report() called with a suggest option with a messageId '${messageId}', but no messages were present in the rule metadata.`);
|
||||
}
|
||||
|
||||
if (!messages[messageId]) {
|
||||
throw new TypeError(`context.report() called with a suggest option with a messageId '${messageId}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
|
||||
}
|
||||
|
||||
if (suggestion.desc) {
|
||||
throw new TypeError("context.report() called with a suggest option that defines both a 'messageId' and an 'desc'. Please only pass one.");
|
||||
}
|
||||
} else if (!suggestion.desc) {
|
||||
throw new TypeError("context.report() called with a suggest option that doesn't have either a `desc` or `messageId`");
|
||||
}
|
||||
|
||||
if (typeof suggestion.fix !== "function") {
|
||||
throw new TypeError(`context.report() called with a suggest option without a fix function. See: ${suggestion}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that converts the arguments of a `context.report` call from a rule into a reported
|
||||
* problem for the Node.js API.
|
||||
* @param {{ruleId: string, severity: number, sourceCode: SourceCode, messageIds: Object, disableFixes: boolean}} metadata Metadata for the reported problem
|
||||
* @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted
|
||||
* @returns {function(...args): LintMessage} Function that returns information about the report
|
||||
*/
|
||||
|
||||
module.exports = function createReportTranslator(metadata) {
|
||||
|
||||
/*
|
||||
* `createReportTranslator` gets called once per enabled rule per file. It needs to be very performant.
|
||||
* The report translator itself (i.e. the function that `createReportTranslator` returns) gets
|
||||
* called every time a rule reports a problem, which happens much less frequently (usually, the vast
|
||||
* majority of rules don't report any problems for a given file).
|
||||
*/
|
||||
return (...args) => {
|
||||
const descriptor = normalizeMultiArgReportCall(...args);
|
||||
const messages = metadata.messageIds;
|
||||
|
||||
assertValidNodeInfo(descriptor);
|
||||
|
||||
let computedMessage;
|
||||
|
||||
if (descriptor.messageId) {
|
||||
if (!messages) {
|
||||
throw new TypeError("context.report() called with a messageId, but no messages were present in the rule metadata.");
|
||||
}
|
||||
const id = descriptor.messageId;
|
||||
|
||||
if (descriptor.message) {
|
||||
throw new TypeError("context.report() called with a message and a messageId. Please only pass one.");
|
||||
}
|
||||
if (!messages || !Object.prototype.hasOwnProperty.call(messages, id)) {
|
||||
throw new TypeError(`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
|
||||
}
|
||||
computedMessage = messages[id];
|
||||
} else if (descriptor.message) {
|
||||
computedMessage = descriptor.message;
|
||||
} else {
|
||||
throw new TypeError("Missing `message` property in report() call; add a message that describes the linting problem.");
|
||||
}
|
||||
|
||||
validateSuggestions(descriptor.suggest, messages);
|
||||
|
||||
return createProblem({
|
||||
ruleId: metadata.ruleId,
|
||||
severity: metadata.severity,
|
||||
node: descriptor.node,
|
||||
message: interpolate(computedMessage, descriptor.data),
|
||||
messageId: descriptor.messageId,
|
||||
loc: normalizeReportLoc(descriptor),
|
||||
fix: metadata.disableFixes ? null : normalizeFixes(descriptor, metadata.sourceCode),
|
||||
suggestions: metadata.disableFixes ? [] : mapSuggestions(descriptor, metadata.sourceCode, messages)
|
||||
});
|
||||
};
|
||||
};
|
||||
140
node_modules/eslint/lib/linter/rule-fixer.js
generated
vendored
Normal file
140
node_modules/eslint/lib/linter/rule-fixer.js
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @fileoverview An object that creates fix commands for rules.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// none!
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a fix command that inserts text at the specified index in the source text.
|
||||
* @param {int} 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.
|
||||
*/
|
||||
|
||||
const ruleFixer = Object.freeze({
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return this.insertTextAfterRange(nodeOrToken.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 {int[]} 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) {
|
||||
return this.insertTextBeforeRange(nodeOrToken.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 {int[]} 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) {
|
||||
return this.replaceTextRange(nodeOrToken.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 {int[]} 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) {
|
||||
return this.removeRange(nodeOrToken.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 {int[]} 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;
|
||||
80
node_modules/eslint/lib/linter/rules.js
generated
vendored
Normal file
80
node_modules/eslint/lib/linter/rules.js
generated
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @fileoverview Defines a storage for rules.
|
||||
* @author Nicholas C. Zakas
|
||||
* @author aladdin-add
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const builtInRules = require("../rules");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Normalizes a rule module to the new-style API
|
||||
* @param {(Function|{create: Function})} rule A rule object, which can either be a function
|
||||
* ("old-style") or an object with a `create` method ("new-style")
|
||||
* @returns {{create: Function}} A new-style rule.
|
||||
*/
|
||||
function normalizeRule(rule) {
|
||||
return typeof rule === "function" ? Object.assign({ create: rule }, rule) : 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 {Function} ruleModule Rule handler.
|
||||
* @returns {void}
|
||||
*/
|
||||
define(ruleId, ruleModule) {
|
||||
this._rules[ruleId] = normalizeRule(ruleModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access rule handler by id (file name).
|
||||
* @param {string} ruleId Rule id (file name).
|
||||
* @returns {{create: Function, schema: JsonSchema[]}}
|
||||
* A rule. This is normalized to always have the new-style shape with a `create` method.
|
||||
*/
|
||||
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;
|
||||
52
node_modules/eslint/lib/linter/safe-emitter.js
generated
vendored
Normal file
52
node_modules/eslint/lib/linter/safe-emitter.js
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @fileoverview A variant of EventEmitter which does not give listeners information about each other
|
||||
* @author Teddy Katz
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* An event emitter
|
||||
* @typedef {Object} SafeEmitter
|
||||
* @property {(eventName: string, listenerFunc: Function) => void} on Adds a listener for a given event name
|
||||
* @property {(eventName: string, arg1?: any, arg2?: any, arg3?: any) => void} emit Emits an event with a given name.
|
||||
* This calls all the listeners that were listening for that name, with `arg1`, `arg2`, and `arg3` as arguments.
|
||||
* @property {function(): string[]} eventNames Gets the list of event names that have registered listeners.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an object which can listen for and emit events.
|
||||
* This is similar to the EventEmitter API in Node's standard library, but it has a few differences.
|
||||
* The goal is to allow multiple modules to attach arbitrary listeners to the same emitter, without
|
||||
* letting the modules know about each other at all.
|
||||
* 1. It has no special keys like `error` and `newListener`, which would allow modules to detect when
|
||||
* another module throws an error or registers a listener.
|
||||
* 2. It calls listener functions without any `this` value. (`EventEmitter` calls listeners with a
|
||||
* `this` value of the emitter instance, which would give listeners access to other listeners.)
|
||||
* @returns {SafeEmitter} An emitter
|
||||
*/
|
||||
module.exports = () => {
|
||||
const listeners = Object.create(null);
|
||||
|
||||
return Object.freeze({
|
||||
on(eventName, listener) {
|
||||
if (eventName in listeners) {
|
||||
listeners[eventName].push(listener);
|
||||
} else {
|
||||
listeners[eventName] = [listener];
|
||||
}
|
||||
},
|
||||
emit(eventName, ...args) {
|
||||
if (eventName in listeners) {
|
||||
listeners[eventName].forEach(listener => listener(...args));
|
||||
}
|
||||
},
|
||||
eventNames() {
|
||||
return Object.keys(listeners);
|
||||
}
|
||||
});
|
||||
};
|
||||
152
node_modules/eslint/lib/linter/source-code-fixer.js
generated
vendored
Normal file
152
node_modules/eslint/lib/linter/source-code-fixer.js
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @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 {int} -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 {int} -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.prototype.hasOwnProperty.call(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;
|
||||
161
node_modules/eslint/lib/linter/timing.js
generated
vendored
Normal file
161
node_modules/eslint/lib/linter/timing.js
generated
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @fileoverview Tracks performance of individual rules.
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/* c8 ignore next */
|
||||
/**
|
||||
* Align the string to left
|
||||
* @param {string} str string to evaluate
|
||||
* @param {int} 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 {int} 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);
|
||||
|
||||
/**
|
||||
* Time the run
|
||||
* @param {any} key key from the data object
|
||||
* @param {Function} fn function to be called
|
||||
* @returns {Function} function to be executed
|
||||
* @private
|
||||
*/
|
||||
function time(key, fn) {
|
||||
if (typeof data[key] === "undefined") {
|
||||
data[key] = 0;
|
||||
}
|
||||
|
||||
return function(...args) {
|
||||
let t = process.hrtime();
|
||||
const result = fn(...args);
|
||||
|
||||
t = process.hrtime(t);
|
||||
data[key] += t[0] * 1e3 + t[1] / 1e6;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
process.on("exit", () => {
|
||||
display(data);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
time,
|
||||
enabled,
|
||||
getListSize
|
||||
};
|
||||
|
||||
}());
|
||||
Reference in New Issue
Block a user