883 lines
25 KiB
JavaScript
883 lines
25 KiB
JavaScript
'use strict';
|
||
|
||
var levn = require('levn');
|
||
|
||
/**
|
||
* @fileoverview Config Comment Parser
|
||
* @author Nicholas C. Zakas
|
||
*/
|
||
|
||
|
||
//-----------------------------------------------------------------------------
|
||
// Type Definitions
|
||
//-----------------------------------------------------------------------------
|
||
|
||
/** @typedef {import("@eslint/core").RuleConfig} RuleConfig */
|
||
/** @typedef {import("@eslint/core").RulesConfig} RulesConfig */
|
||
/** @typedef {import("./types.ts").StringConfig} StringConfig */
|
||
/** @typedef {import("./types.ts").BooleanConfig} BooleanConfig */
|
||
|
||
//-----------------------------------------------------------------------------
|
||
// Helpers
|
||
//-----------------------------------------------------------------------------
|
||
|
||
const directivesPattern = /^([a-z]+(?:-[a-z]+)*)(?:\s|$)/u;
|
||
const validSeverities = new Set([0, 1, 2, "off", "warn", "error"]);
|
||
|
||
/**
|
||
* Determines if the severity in the rule configuration is valid.
|
||
* @param {RuleConfig} ruleConfig A rule's configuration.
|
||
* @returns {boolean} `true` if the severity is valid, otherwise `false`.
|
||
*/
|
||
function isSeverityValid(ruleConfig) {
|
||
const severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
|
||
return validSeverities.has(severity);
|
||
}
|
||
|
||
/**
|
||
* Determines if all severities in the rules configuration are valid.
|
||
* @param {RulesConfig} rulesConfig The rules configuration to check.
|
||
* @returns {boolean} `true` if all severities are valid, otherwise `false`.
|
||
*/
|
||
function isEverySeverityValid(rulesConfig) {
|
||
return Object.values(rulesConfig).every(isSeverityValid);
|
||
}
|
||
|
||
/**
|
||
* Represents a directive comment.
|
||
*/
|
||
class DirectiveComment {
|
||
/**
|
||
* The label of the directive, such as "eslint", "eslint-disable", etc.
|
||
* @type {string}
|
||
*/
|
||
label = "";
|
||
|
||
/**
|
||
* The value of the directive (the string after the label).
|
||
* @type {string}
|
||
*/
|
||
value = "";
|
||
|
||
/**
|
||
* The justification of the directive (the string after the --).
|
||
* @type {string}
|
||
*/
|
||
justification = "";
|
||
|
||
/**
|
||
* Creates a new directive comment.
|
||
* @param {string} label The label of the directive.
|
||
* @param {string} value The value of the directive.
|
||
* @param {string} justification The justification of the directive.
|
||
*/
|
||
constructor(label, value, justification) {
|
||
this.label = label;
|
||
this.value = value;
|
||
this.justification = justification;
|
||
}
|
||
}
|
||
|
||
//------------------------------------------------------------------------------
|
||
// Public Interface
|
||
//------------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Object to parse ESLint configuration comments.
|
||
*/
|
||
class ConfigCommentParser {
|
||
/**
|
||
* Parses a list of "name:string_value" or/and "name" options divided by comma or
|
||
* whitespace. Used for "global" comments.
|
||
* @param {string} string The string to parse.
|
||
* @returns {StringConfig} Result map object of names and string values, or null values if no value was provided.
|
||
*/
|
||
parseStringConfig(string) {
|
||
const items = /** @type {StringConfig} */ ({});
|
||
|
||
// Collapse whitespace around `:` and `,` to make parsing easier
|
||
const trimmedString = string
|
||
.trim()
|
||
.replace(/(?<!\s)\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;
|
||
});
|
||
|
||
return items;
|
||
}
|
||
|
||
/**
|
||
* Parses a JSON-like config.
|
||
* @param {string} string The string to parse.
|
||
* @returns {({ok: true, config: RulesConfig}|{ok: false, error: {message: string}})} Result map object
|
||
*/
|
||
parseJSONLikeConfig(string) {
|
||
// Parses a JSON-like comment by the same way as parsing CLI option.
|
||
try {
|
||
const items =
|
||
/** @type {RulesConfig} */ (levn.parse("Object", string)) || {};
|
||
|
||
/*
|
||
* When the configuration has any invalid severities, it should be completely
|
||
* ignored. This is because the configuration is not valid and should not be
|
||
* applied.
|
||
*
|
||
* For example, the following configuration is invalid:
|
||
*
|
||
* "no-alert: 2 no-console: 2"
|
||
*
|
||
* This results in a configuration of { "no-alert": "2 no-console: 2" }, which is
|
||
* not valid. In this case, the configuration should be ignored.
|
||
*/
|
||
if (isEverySeverityValid(items)) {
|
||
return {
|
||
ok: true,
|
||
config: items,
|
||
};
|
||
}
|
||
} catch {
|
||
// levn parsing error: 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.
|
||
*/
|
||
const normalizedString = string
|
||
.replace(/(?<![-a-zA-Z0-9/])([-a-zA-Z0-9/]+):/gu, '"$1":')
|
||
.replace(/([\]0-9])\s+(?=")/u, "$1,");
|
||
|
||
try {
|
||
const items = JSON.parse(`{${normalizedString}}`);
|
||
|
||
return {
|
||
ok: true,
|
||
config: items,
|
||
};
|
||
} catch (ex) {
|
||
const errorMessage = ex instanceof Error ? ex.message : String(ex);
|
||
|
||
return {
|
||
ok: false,
|
||
error: {
|
||
message: `Failed to parse JSON from '${normalizedString}': ${errorMessage}`,
|
||
},
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Parses a config of values separated by comma.
|
||
* @param {string} string The string to parse.
|
||
* @returns {BooleanConfig} Result map of values and true values
|
||
*/
|
||
parseListConfig(string) {
|
||
const items = /** @type {BooleanConfig} */ ({});
|
||
|
||
string.split(",").forEach(name => {
|
||
const trimmedName = name
|
||
.trim()
|
||
.replace(
|
||
/^(?<quote>['"]?)(?<ruleId>.*)\k<quote>$/su,
|
||
"$<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 {string} string The string with the directive to be parsed.
|
||
* @returns {DirectiveComment|undefined} The parsed directive or `undefined` if the directive is invalid.
|
||
*/
|
||
parseDirective(string) {
|
||
const { directivePart, justificationPart } =
|
||
this.#extractDirectiveComment(string);
|
||
const match = directivesPattern.exec(directivePart);
|
||
|
||
if (!match) {
|
||
return undefined;
|
||
}
|
||
|
||
const directiveText = match[1];
|
||
const directiveValue = directivePart.slice(
|
||
match.index + directiveText.length,
|
||
);
|
||
|
||
return new DirectiveComment(
|
||
directiveText,
|
||
directiveValue.trim(),
|
||
justificationPart,
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @fileoverview A collection of helper classes for implementing `SourceCode`.
|
||
* @author Nicholas C. Zakas
|
||
*/
|
||
|
||
/* eslint class-methods-use-this: off -- Required to complete interface. */
|
||
|
||
//-----------------------------------------------------------------------------
|
||
// Type Definitions
|
||
//-----------------------------------------------------------------------------
|
||
|
||
/** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */
|
||
/** @typedef {import("@eslint/core").CallTraversalStep} CallTraversalStep */
|
||
/** @typedef {import("@eslint/core").TraversalStep} TraversalStep */
|
||
/** @typedef {import("@eslint/core").SourceLocation} SourceLocation */
|
||
/** @typedef {import("@eslint/core").SourceLocationWithOffset} SourceLocationWithOffset */
|
||
/** @typedef {import("@eslint/core").SourceRange} SourceRange */
|
||
/** @typedef {import("@eslint/core").Directive} IDirective */
|
||
/** @typedef {import("@eslint/core").DirectiveType} DirectiveType */
|
||
/** @typedef {import("@eslint/core").SourceCodeBaseTypeOptions} SourceCodeBaseTypeOptions */
|
||
/**
|
||
* @typedef {import("@eslint/core").TextSourceCode<Options>} TextSourceCode<Options>
|
||
* @template {SourceCodeBaseTypeOptions} [Options=SourceCodeBaseTypeOptions]
|
||
*/
|
||
|
||
//-----------------------------------------------------------------------------
|
||
// Helpers
|
||
//-----------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Determines if a node has ESTree-style loc information.
|
||
* @param {object} node The node to check.
|
||
* @returns {node is {loc:SourceLocation}} `true` if the node has ESTree-style loc information, `false` if not.
|
||
*/
|
||
function hasESTreeStyleLoc(node) {
|
||
return "loc" in node;
|
||
}
|
||
|
||
/**
|
||
* Determines if a node has position-style loc information.
|
||
* @param {object} node The node to check.
|
||
* @returns {node is {position:SourceLocation}} `true` if the node has position-style range information, `false` if not.
|
||
*/
|
||
function hasPosStyleLoc(node) {
|
||
return "position" in node;
|
||
}
|
||
|
||
/**
|
||
* Determines if a node has ESTree-style range information.
|
||
* @param {object} node The node to check.
|
||
* @returns {node is {range:SourceRange}} `true` if the node has ESTree-style range information, `false` if not.
|
||
*/
|
||
function hasESTreeStyleRange(node) {
|
||
return "range" in node;
|
||
}
|
||
|
||
/**
|
||
* Determines if a node has position-style range information.
|
||
* @param {object} node The node to check.
|
||
* @returns {node is {position:SourceLocationWithOffset}} `true` if the node has position-style range information, `false` if not.
|
||
*/
|
||
function hasPosStyleRange(node) {
|
||
return "position" in node;
|
||
}
|
||
|
||
/**
|
||
* Performs binary search to find the line number containing a given target index.
|
||
* Returns the lower bound - the index of the first element greater than the target.
|
||
* **Please note that the `lineStartIndices` should be sorted in ascending order**.
|
||
* - Time Complexity: O(log n) - Significantly faster than linear search for large files.
|
||
* @param {number[]} lineStartIndices Sorted array of line start indices.
|
||
* @param {number} targetIndex The target index to find the line number for.
|
||
* @returns {number} The line number for the target index.
|
||
*/
|
||
function findLineNumberBinarySearch(lineStartIndices, targetIndex) {
|
||
let low = 0;
|
||
let high = lineStartIndices.length - 1;
|
||
|
||
while (low < high) {
|
||
const mid = ((low + high) / 2) | 0; // Use bitwise OR to floor the division.
|
||
|
||
if (targetIndex < lineStartIndices[mid]) {
|
||
high = mid;
|
||
} else {
|
||
low = mid + 1;
|
||
}
|
||
}
|
||
|
||
return low;
|
||
}
|
||
|
||
//-----------------------------------------------------------------------------
|
||
// Exports
|
||
//-----------------------------------------------------------------------------
|
||
|
||
/**
|
||
* A class to represent a step in the traversal process where a node is visited.
|
||
* @implements {VisitTraversalStep}
|
||
*/
|
||
class VisitNodeStep {
|
||
/**
|
||
* The type of the step.
|
||
* @type {"visit"}
|
||
* @readonly
|
||
*/
|
||
type = "visit";
|
||
|
||
/**
|
||
* The kind of the step. Represents the same data as the `type` property
|
||
* but it's a number for performance.
|
||
* @type {1}
|
||
* @readonly
|
||
*/
|
||
kind = 1;
|
||
|
||
/**
|
||
* The target of the step.
|
||
* @type {object}
|
||
*/
|
||
target;
|
||
|
||
/**
|
||
* The phase of the step.
|
||
* @type {1|2}
|
||
*/
|
||
phase;
|
||
|
||
/**
|
||
* The arguments of the step.
|
||
* @type {Array<any>}
|
||
*/
|
||
args;
|
||
|
||
/**
|
||
* Creates a new instance.
|
||
* @param {Object} options The options for the step.
|
||
* @param {object} options.target The target of the step.
|
||
* @param {1|2} options.phase The phase of the step.
|
||
* @param {Array<any>} options.args The arguments of the step.
|
||
*/
|
||
constructor({ target, phase, args }) {
|
||
this.target = target;
|
||
this.phase = phase;
|
||
this.args = args;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A class to represent a step in the traversal process where a
|
||
* method is called.
|
||
* @implements {CallTraversalStep}
|
||
*/
|
||
class CallMethodStep {
|
||
/**
|
||
* The type of the step.
|
||
* @type {"call"}
|
||
* @readonly
|
||
*/
|
||
type = "call";
|
||
|
||
/**
|
||
* The kind of the step. Represents the same data as the `type` property
|
||
* but it's a number for performance.
|
||
* @type {2}
|
||
* @readonly
|
||
*/
|
||
kind = 2;
|
||
|
||
/**
|
||
* The name of the method to call.
|
||
* @type {string}
|
||
*/
|
||
target;
|
||
|
||
/**
|
||
* The arguments to pass to the method.
|
||
* @type {Array<any>}
|
||
*/
|
||
args;
|
||
|
||
/**
|
||
* Creates a new instance.
|
||
* @param {Object} options The options for the step.
|
||
* @param {string} options.target The target of the step.
|
||
* @param {Array<any>} options.args The arguments of the step.
|
||
*/
|
||
constructor({ target, args }) {
|
||
this.target = target;
|
||
this.args = args;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A class to represent a directive comment.
|
||
* @implements {IDirective}
|
||
*/
|
||
class Directive {
|
||
/**
|
||
* The type of directive.
|
||
* @type {DirectiveType}
|
||
* @readonly
|
||
*/
|
||
type;
|
||
|
||
/**
|
||
* The node representing the directive.
|
||
* @type {unknown}
|
||
* @readonly
|
||
*/
|
||
node;
|
||
|
||
/**
|
||
* Everything after the "eslint-disable" portion of the directive,
|
||
* but before the "--" that indicates the justification.
|
||
* @type {string}
|
||
* @readonly
|
||
*/
|
||
value;
|
||
|
||
/**
|
||
* The justification for the directive.
|
||
* @type {string}
|
||
* @readonly
|
||
*/
|
||
justification;
|
||
|
||
/**
|
||
* Creates a new instance.
|
||
* @param {Object} options The options for the directive.
|
||
* @param {"disable"|"enable"|"disable-next-line"|"disable-line"} options.type The type of directive.
|
||
* @param {unknown} options.node The node representing the directive.
|
||
* @param {string} options.value The value of the directive.
|
||
* @param {string} options.justification The justification for the directive.
|
||
*/
|
||
constructor({ type, node, value, justification }) {
|
||
this.type = type;
|
||
this.node = node;
|
||
this.value = value;
|
||
this.justification = justification;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Source Code Base Object
|
||
* @template {SourceCodeBaseTypeOptions & {RootNode: object, SyntaxElementWithLoc: object}} [Options=SourceCodeBaseTypeOptions & {RootNode: object, SyntaxElementWithLoc: object}]
|
||
* @implements {TextSourceCode<Options>}
|
||
*/
|
||
class TextSourceCodeBase {
|
||
/**
|
||
* The lines of text in the source code.
|
||
* @type {Array<string>}
|
||
*/
|
||
#lines = [];
|
||
|
||
/**
|
||
* The indices of the start of each line in the source code.
|
||
* @type {Array<number>}
|
||
*/
|
||
#lineStartIndices = [0];
|
||
|
||
/**
|
||
* The pattern to match lineEndings in the source code.
|
||
* @type {RegExp}
|
||
*/
|
||
#lineEndingPattern;
|
||
|
||
/**
|
||
* The AST of the source code.
|
||
* @type {Options['RootNode']}
|
||
*/
|
||
ast;
|
||
|
||
/**
|
||
* The text of the source code.
|
||
* @type {string}
|
||
*/
|
||
text;
|
||
|
||
/**
|
||
* Creates a new instance.
|
||
* @param {Object} options The options for the instance.
|
||
* @param {string} options.text The source code text.
|
||
* @param {Options['RootNode']} options.ast The root AST node.
|
||
* @param {RegExp} [options.lineEndingPattern] The pattern to match lineEndings in the source code. Defaults to `/\r?\n/u`.
|
||
*/
|
||
constructor({ text, ast, lineEndingPattern = /\r?\n/u }) {
|
||
this.ast = ast;
|
||
this.text = text;
|
||
// Remove the global(`g`) and sticky(`y`) flags from the `lineEndingPattern` to avoid issues with lastIndex.
|
||
this.#lineEndingPattern = new RegExp(
|
||
lineEndingPattern.source,
|
||
lineEndingPattern.flags.replace(/[gy]/gu, ""),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Finds the next line in the source text and updates `#lines` and `#lineStartIndices`.
|
||
* @param {string} text The text to search for the next line.
|
||
* @returns {boolean} `true` if a next line was found, `false` otherwise.
|
||
*/
|
||
#findNextLine(text) {
|
||
const match = this.#lineEndingPattern.exec(text);
|
||
|
||
if (!match) {
|
||
return false;
|
||
}
|
||
|
||
this.#lines.push(text.slice(0, match.index));
|
||
this.#lineStartIndices.push(
|
||
(this.#lineStartIndices.at(-1) ?? 0) +
|
||
match.index +
|
||
match[0].length,
|
||
);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Ensures `#lines` is lazily calculated from the source text.
|
||
* @returns {void}
|
||
*/
|
||
#ensureLines() {
|
||
// If `#lines` has already been calculated, do nothing.
|
||
if (this.#lines.length === this.#lineStartIndices.length) {
|
||
return;
|
||
}
|
||
|
||
while (
|
||
this.#findNextLine(this.text.slice(this.#lineStartIndices.at(-1)))
|
||
) {
|
||
// Continue parsing until no more matches are found.
|
||
}
|
||
|
||
this.#lines.push(this.text.slice(this.#lineStartIndices.at(-1)));
|
||
|
||
Object.freeze(this.#lines);
|
||
}
|
||
|
||
/**
|
||
* Ensures `#lineStartIndices` is lazily calculated up to the specified index.
|
||
* @param {number} index The index of a character in a file.
|
||
* @returns {void}
|
||
*/
|
||
#ensureLineStartIndicesFromIndex(index) {
|
||
// If we've already parsed up to or beyond this index, do nothing.
|
||
if (index <= (this.#lineStartIndices.at(-1) ?? 0)) {
|
||
return;
|
||
}
|
||
|
||
while (
|
||
index > (this.#lineStartIndices.at(-1) ?? 0) &&
|
||
this.#findNextLine(this.text.slice(this.#lineStartIndices.at(-1)))
|
||
) {
|
||
// Continue parsing until no more matches are found.
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ensures `#lineStartIndices` is lazily calculated up to the specified loc.
|
||
* @param {Object} loc A line/column location.
|
||
* @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.)
|
||
* @param {number} lineStart The line number at which the parser starts counting.
|
||
* @returns {void}
|
||
*/
|
||
#ensureLineStartIndicesFromLoc(loc, lineStart) {
|
||
// Calculate line indices up to the potentially next line, as it is needed for the follow‑up calculation.
|
||
const nextLocLineIndex = loc.line - lineStart + 1;
|
||
const lastCalculatedLineIndex = this.#lineStartIndices.length - 1;
|
||
let additionalLinesNeeded = nextLocLineIndex - lastCalculatedLineIndex;
|
||
|
||
// If we've already parsed up to or beyond this line, do nothing.
|
||
if (additionalLinesNeeded <= 0) {
|
||
return;
|
||
}
|
||
|
||
while (
|
||
additionalLinesNeeded > 0 &&
|
||
this.#findNextLine(this.text.slice(this.#lineStartIndices.at(-1)))
|
||
) {
|
||
// Continue parsing until no more matches are found or we have enough lines.
|
||
additionalLinesNeeded -= 1;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns the loc information for the given node or token.
|
||
* @param {Options['SyntaxElementWithLoc']} nodeOrToken The node or token to get the loc information for.
|
||
* @returns {SourceLocation} The loc information for the node or token.
|
||
* @throws {Error} If the node or token does not have loc information.
|
||
*/
|
||
getLoc(nodeOrToken) {
|
||
if (hasESTreeStyleLoc(nodeOrToken)) {
|
||
return nodeOrToken.loc;
|
||
}
|
||
|
||
if (hasPosStyleLoc(nodeOrToken)) {
|
||
return nodeOrToken.position;
|
||
}
|
||
|
||
throw new Error(
|
||
"Custom getLoc() method must be implemented in the subclass.",
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Converts a source text index into a `{ line: number, column: number }` pair.
|
||
* @param {number} index The index of a character in a file.
|
||
* @throws {TypeError|RangeError} If non-numeric index or index out of range.
|
||
* @returns {{line: number, column: number}} A `{ line: number, column: number }` location object with 0 or 1-indexed line and 0 or 1-indexed column based on language.
|
||
* @public
|
||
*/
|
||
getLocFromIndex(index) {
|
||
if (typeof index !== "number") {
|
||
throw new TypeError("Expected `index` to be a number.");
|
||
}
|
||
|
||
if (index < 0 || index > this.text.length) {
|
||
throw new RangeError(
|
||
`Index out of range (requested index ${index}, but source text has length ${this.text.length}).`,
|
||
);
|
||
}
|
||
|
||
const {
|
||
start: { line: lineStart, column: columnStart },
|
||
end: { line: lineEnd, column: columnEnd },
|
||
} = this.getLoc(this.ast);
|
||
|
||
// If the index is at the start, return the start location of the root node.
|
||
if (index === 0) {
|
||
return {
|
||
line: lineStart,
|
||
column: columnStart,
|
||
};
|
||
}
|
||
|
||
// If the index is `this.text.length`, return the location one "spot" past the last character of the file.
|
||
if (index === this.text.length) {
|
||
return {
|
||
line: lineEnd,
|
||
column: columnEnd,
|
||
};
|
||
}
|
||
|
||
// Ensure `#lineStartIndices` are lazily calculated.
|
||
this.#ensureLineStartIndicesFromIndex(index);
|
||
|
||
/*
|
||
* To figure out which line `index` is on, determine the last place at which index could
|
||
* be inserted into `#lineStartIndices` to keep the list sorted.
|
||
*/
|
||
const lineNumber =
|
||
(index >= (this.#lineStartIndices.at(-1) ?? 0)
|
||
? this.#lineStartIndices.length
|
||
: findLineNumberBinarySearch(this.#lineStartIndices, index)) -
|
||
1 +
|
||
lineStart;
|
||
|
||
return {
|
||
line: lineNumber,
|
||
column:
|
||
index -
|
||
this.#lineStartIndices[lineNumber - lineStart] +
|
||
columnStart,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Converts a `{ line: number, column: number }` pair into a source text index.
|
||
* @param {Object} loc A line/column location.
|
||
* @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.)
|
||
* @param {number} loc.column The column number of the location. (0 or 1-indexed based on language.)
|
||
* @throws {TypeError|RangeError} If `loc` is not an object with a numeric
|
||
* `line` and `column`, if the `line` is less than or equal to zero or
|
||
* the `line` or `column` is out of the expected range.
|
||
* @returns {number} The index of the line/column location in a file.
|
||
* @public
|
||
*/
|
||
getIndexFromLoc(loc) {
|
||
if (
|
||
loc === null ||
|
||
typeof loc !== "object" ||
|
||
typeof loc.line !== "number" ||
|
||
typeof loc.column !== "number"
|
||
) {
|
||
throw new TypeError(
|
||
"Expected `loc` to be an object with numeric `line` and `column` properties.",
|
||
);
|
||
}
|
||
|
||
const {
|
||
start: { line: lineStart, column: columnStart },
|
||
end: { line: lineEnd, column: columnEnd },
|
||
} = this.getLoc(this.ast);
|
||
|
||
if (loc.line < lineStart || lineEnd < loc.line) {
|
||
throw new RangeError(
|
||
`Line number out of range (line ${loc.line} requested). Valid range: ${lineStart}-${lineEnd}`,
|
||
);
|
||
}
|
||
|
||
// If the loc is at the start, return the start index of the root node.
|
||
if (loc.line === lineStart && loc.column === columnStart) {
|
||
return 0;
|
||
}
|
||
|
||
// If the loc is at the end, return the index one "spot" past the last character of the file.
|
||
if (loc.line === lineEnd && loc.column === columnEnd) {
|
||
return this.text.length;
|
||
}
|
||
|
||
// Ensure `#lineStartIndices` are lazily calculated.
|
||
this.#ensureLineStartIndicesFromLoc(loc, lineStart);
|
||
|
||
const isLastLine = loc.line === lineEnd;
|
||
const lineStartIndex = this.#lineStartIndices[loc.line - lineStart];
|
||
const lineEndIndex = isLastLine
|
||
? this.text.length
|
||
: this.#lineStartIndices[loc.line - lineStart + 1];
|
||
const positionIndex = lineStartIndex + loc.column - columnStart;
|
||
|
||
if (
|
||
loc.column < columnStart ||
|
||
(isLastLine && positionIndex > lineEndIndex) ||
|
||
(!isLastLine && positionIndex >= lineEndIndex)
|
||
) {
|
||
throw new RangeError(
|
||
`Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${columnStart}-${lineEndIndex - lineStartIndex + columnStart + (isLastLine ? 0 : -1)}`,
|
||
);
|
||
}
|
||
|
||
return positionIndex;
|
||
}
|
||
|
||
/**
|
||
* Returns the range information for the given node or token.
|
||
* @param {Options['SyntaxElementWithLoc']} nodeOrToken The node or token to get the range information for.
|
||
* @returns {SourceRange} The range information for the node or token.
|
||
* @throws {Error} If the node or token does not have range information.
|
||
*/
|
||
getRange(nodeOrToken) {
|
||
if (hasESTreeStyleRange(nodeOrToken)) {
|
||
return nodeOrToken.range;
|
||
}
|
||
|
||
if (hasPosStyleRange(nodeOrToken)) {
|
||
return [
|
||
nodeOrToken.position.start.offset,
|
||
nodeOrToken.position.end.offset,
|
||
];
|
||
}
|
||
|
||
throw new Error(
|
||
"Custom getRange() method must be implemented in the subclass.",
|
||
);
|
||
}
|
||
|
||
/* eslint-disable no-unused-vars -- Required to complete interface. */
|
||
/**
|
||
* Returns the parent of the given node.
|
||
* @param {Options['SyntaxElementWithLoc']} node The node to get the parent of.
|
||
* @returns {Options['SyntaxElementWithLoc']|undefined} The parent of the node.
|
||
* @throws {Error} If the method is not implemented in the subclass.
|
||
*/
|
||
getParent(node) {
|
||
throw new Error("Not implemented.");
|
||
}
|
||
/* eslint-enable no-unused-vars -- Required to complete interface. */
|
||
|
||
/**
|
||
* Gets all the ancestors of a given node
|
||
* @param {Options['SyntaxElementWithLoc']} node The node
|
||
* @returns {Array<Options['SyntaxElementWithLoc']>} All the ancestor nodes in the AST, not including the provided node, starting
|
||
* from the root node at index 0 and going inwards to the parent node.
|
||
* @throws {TypeError} When `node` is missing.
|
||
*/
|
||
getAncestors(node) {
|
||
if (!node) {
|
||
throw new TypeError("Missing required argument: node.");
|
||
}
|
||
|
||
const ancestorsStartingAtParent = [];
|
||
|
||
for (
|
||
let ancestor = this.getParent(node);
|
||
ancestor;
|
||
ancestor = this.getParent(ancestor)
|
||
) {
|
||
ancestorsStartingAtParent.push(ancestor);
|
||
}
|
||
|
||
return ancestorsStartingAtParent.reverse();
|
||
}
|
||
|
||
/**
|
||
* Gets the source code for the given node.
|
||
* @param {Options['SyntaxElementWithLoc']} [node] The AST node to get the text for.
|
||
* @param {number} [beforeCount] The number of characters before the node to retrieve.
|
||
* @param {number} [afterCount] The number of characters after the node to retrieve.
|
||
* @returns {string} The text representing the AST node.
|
||
* @public
|
||
*/
|
||
getText(node, beforeCount, afterCount) {
|
||
if (node) {
|
||
const range = this.getRange(node);
|
||
return this.text.slice(
|
||
Math.max(range[0] - (beforeCount || 0), 0),
|
||
range[1] + (afterCount || 0),
|
||
);
|
||
}
|
||
return this.text;
|
||
}
|
||
|
||
/**
|
||
* Gets the entire source text split into an array of lines.
|
||
* @returns {Array<string>} The source text as an array of lines.
|
||
* @public
|
||
*/
|
||
get lines() {
|
||
this.#ensureLines(); // Ensure `#lines` is lazily calculated.
|
||
|
||
return this.#lines;
|
||
}
|
||
|
||
/**
|
||
* Traverse the source code and return the steps that were taken.
|
||
* @returns {Iterable<TraversalStep>} The steps that were taken while traversing the source code.
|
||
*/
|
||
traverse() {
|
||
throw new Error("Not implemented.");
|
||
}
|
||
}
|
||
|
||
exports.CallMethodStep = CallMethodStep;
|
||
exports.ConfigCommentParser = ConfigCommentParser;
|
||
exports.Directive = Directive;
|
||
exports.TextSourceCodeBase = TextSourceCodeBase;
|
||
exports.VisitNodeStep = VisitNodeStep;
|