878 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			878 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
// @ts-self-types="./index.d.ts"
 | 
						||
import levn from '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.");
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
export { CallMethodStep, ConfigCommentParser, Directive, TextSourceCodeBase, VisitNodeStep };
 |