Current Dev State

This commit is contained in:
Tim Lorsbach
2025-06-23 20:13:54 +02:00
parent b4f9bb277d
commit ded50edaa2
22617 changed files with 4345095 additions and 174 deletions

View File

@ -0,0 +1,116 @@
/**
* @fileoverview The event generator for comments.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Check collection of comments to prevent double event for comment as
* leading and trailing, then emit event if passing
* @param {ASTNode[]} comments - Collection of comment nodes
* @param {EventEmitter} emitter - The event emitter which is the destination of events.
* @param {Object[]} locs - List of locations of previous comment nodes
* @param {string} eventName - Event name postfix
* @returns {void}
*/
function emitComments(comments, emitter, locs, eventName) {
if (comments.length > 0) {
comments.forEach(node => {
const index = locs.indexOf(node.loc);
if (index >= 0) {
locs.splice(index, 1);
} else {
locs.push(node.loc);
emitter.emit(node.type + eventName, node);
}
});
}
}
/**
* Shortcut to check and emit enter of comment nodes
* @param {CommentEventGenerator} generator - A generator to emit.
* @param {ASTNode[]} comments - Collection of comment nodes
* @returns {void}
*/
function emitCommentsEnter(generator, comments) {
emitComments(
comments,
generator.emitter,
generator.commentLocsEnter,
"Comment");
}
/**
* Shortcut to check and emit exit of comment nodes
* @param {CommentEventGenerator} generator - A generator to emit.
* @param {ASTNode[]} comments Collection of comment nodes
* @returns {void}
*/
function emitCommentsExit(generator, comments) {
emitComments(
comments,
generator.emitter,
generator.commentLocsExit,
"Comment:exit");
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* The event generator for comments.
* This is the decorator pattern.
* This generates events of comments before/after events which are generated the original generator.
*
* Comment event generator class
*/
class CommentEventGenerator {
/**
* @param {EventGenerator} originalEventGenerator - An event generator which is the decoration target.
* @param {SourceCode} sourceCode - A source code which has comments.
*/
constructor(originalEventGenerator, sourceCode) {
this.original = originalEventGenerator;
this.emitter = originalEventGenerator.emitter;
this.sourceCode = sourceCode;
this.commentLocsEnter = [];
this.commentLocsExit = [];
}
/**
* Emits an event of entering comments.
* @param {ASTNode} node - A node which was entered.
* @returns {void}
*/
enterNode(node) {
const comments = this.sourceCode.getComments(node);
emitCommentsEnter(this, comments.leading);
this.original.enterNode(node);
emitCommentsEnter(this, comments.trailing);
}
/**
* Emits an event of leaving comments.
* @param {ASTNode} node - A node which was left.
* @returns {void}
*/
leaveNode(node) {
const comments = this.sourceCode.getComments(node);
emitCommentsExit(this, comments.trailing);
this.original.leaveNode(node);
emitCommentsExit(this, comments.leading);
}
}
module.exports = CommentEventGenerator;

View File

@ -0,0 +1,121 @@
/**
* @fileoverview Helper class to aid in constructing fix commands.
* @author Alan Pierce
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("../ast-utils");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* A helper class to combine fix options into a fix command. Currently, it
* exposes some "retain" methods that extend the range of the text being
* replaced so that other fixes won't touch that region in the same pass.
*/
class FixTracker {
/**
* Create a new FixTracker.
*
* @param {ruleFixer} fixer A ruleFixer instance.
* @param {SourceCode} sourceCode A SourceCode object for the current code.
*/
constructor(fixer, sourceCode) {
this.fixer = fixer;
this.sourceCode = sourceCode;
this.retainedRange = null;
}
/**
* Mark the given range as "retained", meaning that other fixes may not
* may not modify this region in the same pass.
*
* @param {int[]} range The range to retain.
* @returns {FixTracker} The same RuleFixer, for chained calls.
*/
retainRange(range) {
this.retainedRange = range;
return this;
}
/**
* Given a node, find the function containing it (or the entire program) and
* mark it as retained, meaning that other fixes may not modify it in this
* pass. This is useful for avoiding conflicts in fixes that modify control
* flow.
*
* @param {ASTNode} node The node to use as a starting point.
* @returns {FixTracker} The same RuleFixer, for chained calls.
*/
retainEnclosingFunction(node) {
const functionNode = astUtils.getUpperFunction(node);
return this.retainRange(
functionNode ? functionNode.range : this.sourceCode.ast.range);
}
/**
* Given a node or token, find the token before and afterward, and mark that
* range as retained, meaning that other fixes may not modify it in this
* pass. This is useful for avoiding conflicts in fixes that make a small
* change to the code where the AST should not be changed.
*
* @param {ASTNode|Token} nodeOrToken The node or token to use as a starting
* point. The token to the left and right are use in the range.
* @returns {FixTracker} The same RuleFixer, for chained calls.
*/
retainSurroundingTokens(nodeOrToken) {
const tokenBefore = this.sourceCode.getTokenBefore(nodeOrToken) || nodeOrToken;
const tokenAfter = this.sourceCode.getTokenAfter(nodeOrToken) || nodeOrToken;
return this.retainRange([tokenBefore.range[0], tokenAfter.range[1]]);
}
/**
* Create a fix command that replaces the given range with the given text,
* accounting for any retained ranges.
*
* @param {int[]} range The range to remove in the fix.
* @param {string} text The text to insert in place of the range.
* @returns {Object} The fix command.
*/
replaceTextRange(range, text) {
let actualRange;
if (this.retainedRange) {
actualRange = [
Math.min(this.retainedRange[0], range[0]),
Math.max(this.retainedRange[1], range[1])
];
} else {
actualRange = range;
}
return this.fixer.replaceTextRange(
actualRange,
this.sourceCode.text.slice(actualRange[0], range[0]) +
text +
this.sourceCode.text.slice(range[1], actualRange[1])
);
}
/**
* Create a fix command that removes the given node or token, accounting for
* any retained ranges.
*
* @param {ASTNode|Token} nodeOrToken The node or token to remove.
* @returns {Object} The fix command.
*/
remove(nodeOrToken) {
return this.replaceTextRange(nodeOrToken.range, "");
}
}
module.exports = FixTracker;

View File

@ -0,0 +1,183 @@
/**
* @fileoverview Utilities for working with globs and the filesystem.
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path"),
GlobSync = require("./glob"),
shell = require("shelljs"),
pathUtil = require("./path-util"),
IgnoredPaths = require("../ignored-paths");
const debug = require("debug")("eslint:glob-util");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks if a provided path is a directory and returns a glob string matching
* all files under that directory if so, the path itself otherwise.
*
* Reason for this is that `glob` needs `/**` to collect all the files under a
* directory where as our previous implementation without `glob` simply walked
* a directory that is passed. So this is to maintain backwards compatibility.
*
* Also makes sure all path separators are POSIX style for `glob` compatibility.
*
* @param {Object} [options] An options object
* @param {string[]} [options.extensions=[".js"]] An array of accepted extensions
* @param {string} [options.cwd=process.cwd()] The cwd to use to resolve relative pathnames
* @returns {Function} A function that takes a pathname and returns a glob that
* matches all files with the provided extensions if
* pathname is a directory.
*/
function processPath(options) {
const cwd = (options && options.cwd) || process.cwd();
let extensions = (options && options.extensions) || [".js"];
extensions = extensions.map(ext => ext.replace(/^\./, ""));
let suffix = "/**";
if (extensions.length === 1) {
suffix += `/*.${extensions[0]}`;
} else {
suffix += `/*.{${extensions.join(",")}}`;
}
/**
* A function that converts a directory name to a glob pattern
*
* @param {string} pathname The directory path to be modified
* @returns {string} The glob path or the file path itself
* @private
*/
return function(pathname) {
let newPath = pathname;
const resolvedPath = path.resolve(cwd, pathname);
if (shell.test("-d", resolvedPath)) {
newPath = pathname.replace(/[/\\]$/, "") + suffix;
}
return pathUtil.convertPathToPosix(newPath);
};
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Resolves any directory patterns into glob-based patterns for easier handling.
* @param {string[]} patterns File patterns (such as passed on the command line).
* @param {Object} options An options object.
* @returns {string[]} The equivalent glob patterns and filepath strings.
*/
function resolveFileGlobPatterns(patterns, options) {
const processPathExtensions = processPath(options);
return patterns.filter(p => p.length).map(processPathExtensions);
}
/**
* Build a list of absolute filesnames on which ESLint will act.
* Ignored files are excluded from the results, as are duplicates.
*
* @param {string[]} globPatterns Glob patterns.
* @param {Object} [options] An options object.
* @param {string} [options.cwd] CWD (considered for relative filenames)
* @param {boolean} [options.ignore] False disables use of .eslintignore.
* @param {string} [options.ignorePath] The ignore file to use instead of .eslintignore.
* @param {string} [options.ignorePattern] A pattern of files to ignore.
* @returns {string[]} Resolved absolute filenames.
*/
function listFilesToProcess(globPatterns, options) {
options = options || { ignore: true };
const files = [],
added = {};
const cwd = (options && options.cwd) || process.cwd();
/**
* Executes the linter on a file defined by the `filename`. Skips
* unsupported file extensions and any files that are already linted.
* @param {string} filename The file to be processed
* @param {boolean} shouldWarnIgnored Whether or not a report should be made if
* the file is ignored
* @param {IgnoredPaths} ignoredPaths An instance of IgnoredPaths
* @returns {void}
*/
function addFile(filename, shouldWarnIgnored, ignoredPaths) {
let ignored = false;
let isSilentlyIgnored;
if (ignoredPaths.contains(filename, "default")) {
ignored = (options.ignore !== false) && shouldWarnIgnored;
isSilentlyIgnored = !shouldWarnIgnored;
}
if (options.ignore !== false) {
if (ignoredPaths.contains(filename, "custom")) {
if (shouldWarnIgnored) {
ignored = true;
} else {
isSilentlyIgnored = true;
}
}
}
if (isSilentlyIgnored && !ignored) {
return;
}
if (added[filename]) {
return;
}
files.push({ filename, ignored });
added[filename] = true;
}
debug("Creating list of files to process.");
globPatterns.forEach(pattern => {
const file = path.resolve(cwd, pattern);
if (shell.test("-f", file)) {
const ignoredPaths = new IgnoredPaths(options);
addFile(fs.realpathSync(file), !shell.test("-d", file), ignoredPaths);
} else {
// regex to find .hidden or /.hidden patterns, but not ./relative or ../relative
const globIncludesDotfiles = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/.test(pattern);
const ignoredPaths = new IgnoredPaths(Object.assign({}, options, { dotfiles: options.dotfiles || globIncludesDotfiles }));
const shouldIgnore = ignoredPaths.getIgnoredFoldersGlobChecker();
const globOptions = {
nodir: true,
dot: true,
cwd
};
new GlobSync(pattern, globOptions, shouldIgnore).found.forEach(globMatch => {
addFile(path.resolve(cwd, globMatch), false, ignoredPaths);
});
}
});
return files;
}
module.exports = {
resolveFileGlobPatterns,
listFilesToProcess
};

View File

@ -0,0 +1,63 @@
/**
* @fileoverview An inherited `glob.GlobSync` to support .gitignore patterns.
* @author Kael Zhang
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const Sync = require("glob").GlobSync,
util = require("util");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
const IGNORE = Symbol("ignore");
/**
* Subclass of `glob.GlobSync`
* @param {string} pattern Pattern to be matched.
* @param {Object} options `options` for `glob`
* @param {function()} shouldIgnore Method to check whether a directory should be ignored.
* @constructor
*/
function GlobSync(pattern, options, shouldIgnore) {
/**
* We don't put this thing to argument `options` to avoid
* further problems, such as `options` validation.
*
* Use `Symbol` as much as possible to avoid confliction.
*/
this[IGNORE] = shouldIgnore;
Sync.call(this, pattern, options);
}
util.inherits(GlobSync, Sync);
/* eslint no-underscore-dangle: ["error", { "allow": ["_readdir", "_mark"] }] */
GlobSync.prototype._readdir = function(abs, inGlobStar) {
/**
* `options.nodir` makes `options.mark` as `true`.
* Mark `abs` first
* to make sure `"node_modules"` will be ignored immediately with ignore pattern `"node_modules/"`.
* There is a built-in cache about marked `File.Stat` in `glob`, so that we could not worry about the extra invocation of `this._mark()`
*/
const marked = this._mark(abs);
if (this[IGNORE](marked)) {
return null;
}
return Sync.prototype._readdir.call(this, abs, inGlobStar);
};
module.exports = GlobSync;

View File

@ -0,0 +1,35 @@
/**
* @fileoverview Defining the hashing function in one place.
* @author Michael Ficarra
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const murmur = require("imurmurhash");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* hash the given string
* @param {string} str the string to hash
* @returns {string} the hash
*/
function hash(str) {
return murmur(str).result().toString(36);
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = hash;

View File

@ -0,0 +1,67 @@
/**
* @fileoverview A shared list of ES3 keywords.
* @author Josh Perez
*/
"use strict";
module.exports = [
"abstract",
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"double",
"else",
"enum",
"export",
"extends",
"false",
"final",
"finally",
"float",
"for",
"function",
"goto",
"if",
"implements",
"import",
"in",
"instanceof",
"int",
"interface",
"long",
"native",
"new",
"null",
"package",
"private",
"protected",
"public",
"return",
"short",
"static",
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"true",
"try",
"typeof",
"var",
"void",
"volatile",
"while",
"with"
];

View File

@ -0,0 +1,85 @@
/**
* @fileoverview Implements the Node.js require.resolve algorithm
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const Module = require("module");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
const DEFAULT_OPTIONS = {
/*
* module.paths is an array of paths to search for resolving things relative
* to this file. Module.globalPaths contains all of the special Node.js
* directories that can also be searched for modules.
*
* Need to check for existence of module.paths because Jest seems not to
* include it. See https://github.com/eslint/eslint/issues/5791.
*/
lookupPaths: module.paths ? module.paths.concat(Module.globalPaths) : Module.globalPaths.concat()
};
/**
* Resolves modules based on a set of options.
*/
class ModuleResolver {
/**
* Resolves modules based on a set of options.
* @param {Object} options The options for resolving modules.
* @param {string[]} options.lookupPaths An array of paths to include in the
* lookup with the highest priority paths coming first.
*/
constructor(options) {
this.options = Object.assign({}, DEFAULT_OPTIONS, options || {});
}
/**
* Resolves the file location of a given module relative to the configured
* lookup paths.
* @param {string} name The module name to resolve.
* @param {string} extraLookupPath An extra path to look into for the module.
* This path is used with the highest priority.
* @returns {string} The resolved file path for the module.
* @throws {Error} If the module cannot be resolved.
*/
resolve(name, extraLookupPath) {
/*
* First, clone the lookup paths so we're not messing things up for
* subsequent calls to this function. Then, move the extraLookupPath to the
* top of the lookup paths list so it will be searched first.
*/
const lookupPaths = this.options.lookupPaths.concat();
lookupPaths.unshift(extraLookupPath);
/**
* Module._findPath is an internal method to Node.js, then one they use to
* lookup file paths when require() is called. So, we are hooking into the
* exact same logic that Node.js uses.
*/
const result = Module._findPath(name, lookupPaths); // eslint-disable-line no-underscore-dangle
if (!result) {
throw new Error(`Cannot find module '${name}'`);
}
return result;
}
}
//------------------------------------------------------------------------------
// Public API
//------------------------------------------------------------------------------
module.exports = ModuleResolver;

View File

@ -0,0 +1,322 @@
/**
* @fileoverview The event generator for AST nodes.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const esquery = require("esquery");
const lodash = require("lodash");
//------------------------------------------------------------------------------
// 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
//------------------------------------------------------------------------------
/**
* 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(typesForComponent => typesForComponent)) {
return lodash.union.apply(null, 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 lodash.intersection.apply(null, typesForComponents);
}
case "child":
case "descendant":
case "sibling":
case "adjacent":
return getPossibleTypes(parsedSelector.right);
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$/, ""));
} catch (err) {
if (typeof err.offset === "number") {
throw new Error(`Syntax error in selector "${rawSelector}" at position ${err.offset}: ${err.message}`);
}
throw err;
}
}
/**
* 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
*/
const parseSelector = lodash.memoize(rawSelector => {
const parsedSelector = tryParseSelector(rawSelector);
return {
rawSelector,
isExit: rawSelector.endsWith(":exit"),
parsedSelector,
listenerTypes: getPossibleTypes(parsedSelector),
attributeCount: countClassAttributes(parsedSelector),
identifierCount: countIdentifiers(parsedSelector)
};
});
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* The event generator for AST nodes.
* This implements below interface.
*
* ```ts
* interface EventGenerator {
* emitter: EventEmitter;
* enterNode(node: ASTNode): void;
* leaveNode(node: ASTNode): void;
* }
* ```
*/
class NodeEventGenerator {
/**
* @param {EventEmitter} emitter - An event emitter which is the destination of events. This emitter must already
* have registered listeners for all of the events that it needs to listen for.
* @returns {NodeEventGenerator} new instance
*/
constructor(emitter) {
this.emitter = emitter;
this.currentAncestry = [];
this.enterSelectorsByNodeType = new Map();
this.exitSelectorsByNodeType = new Map();
this.anyTypeEnterSelectors = [];
this.anyTypeExitSelectors = [];
const eventNames = typeof emitter.eventNames === "function"
// Use the built-in eventNames() function if available (Node 6+)
? emitter.eventNames()
/*
* Otherwise, use the private _events property.
* Using a private property isn't ideal here, but this seems to
* be the best way to get a list of event names without overriding
* addEventListener, which would hurt performance. This property
* is widely used and unlikely to be removed in a future version
* (see https://github.com/nodejs/node/issues/1817). Also, future
* node versions will have eventNames() anyway.
*/
: Object.keys(emitter._events); // eslint-disable-line no-underscore-dangle
eventNames.forEach(rawSelector => {
const selector = parseSelector(rawSelector);
if (selector.listenerTypes) {
selector.listenerTypes.forEach(nodeType => {
const typeMap = selector.isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType;
if (!typeMap.has(nodeType)) {
typeMap.set(nodeType, []);
}
typeMap.get(nodeType).push(selector);
});
} else {
(selector.isExit ? this.anyTypeExitSelectors : this.anyTypeEnterSelectors).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.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;

View File

@ -0,0 +1,146 @@
/**
* @fileoverview Utility for executing npm commands.
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path"),
shell = require("shelljs"),
log = require("../logging");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Find the closest package.json file, starting at process.cwd (by default),
* and working up to root.
*
* @param {string} [startDir=process.cwd()] Starting directory
* @returns {string} Absolute path to closest package.json file
*/
function findPackageJson(startDir) {
let dir = path.resolve(startDir || process.cwd());
do {
const pkgfile = path.join(dir, "package.json");
if (!shell.test("-f", pkgfile)) {
dir = path.join(dir, "..");
continue;
}
return pkgfile;
} while (dir !== path.resolve(dir, ".."));
return null;
}
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* Install node modules synchronously and save to devDependencies in package.json
* @param {string|string[]} packages Node module or modules to install
* @returns {void}
*/
function installSyncSaveDev(packages) {
if (Array.isArray(packages)) {
packages = packages.join(" ");
}
shell.exec(`npm i --save-dev ${packages}`, { stdio: "inherit" });
}
/**
* Check whether node modules are include in a project's package.json.
*
* @param {string[]} packages Array of node module names
* @param {Object} opt Options Object
* @param {boolean} opt.dependencies Set to true to check for direct dependencies
* @param {boolean} opt.devDependencies Set to true to check for development dependencies
* @param {boolean} opt.startdir Directory to begin searching from
* @returns {Object} An object whose keys are the module names
* and values are booleans indicating installation.
*/
function check(packages, opt) {
let deps = [];
const pkgJson = (opt) ? findPackageJson(opt.startDir) : findPackageJson();
let fileJson;
if (!pkgJson) {
throw new Error("Could not find a package.json file. Run 'npm init' to create one.");
}
try {
fileJson = JSON.parse(fs.readFileSync(pkgJson, "utf8"));
} catch (e) {
log.info("Could not read package.json file. Please check that the file contains valid JSON.");
throw new Error(e);
}
if (opt.devDependencies && typeof fileJson.devDependencies === "object") {
deps = deps.concat(Object.keys(fileJson.devDependencies));
}
if (opt.dependencies && typeof fileJson.dependencies === "object") {
deps = deps.concat(Object.keys(fileJson.dependencies));
}
return packages.reduce((status, pkg) => {
status[pkg] = deps.indexOf(pkg) !== -1;
return status;
}, {});
}
/**
* Check whether node modules are included in the dependencies of a project's
* package.json.
*
* Convienience wrapper around check().
*
* @param {string[]} packages Array of node modules to check.
* @param {string} rootDir The directory contianing a package.json
* @returns {Object} An object whose keys are the module names
* and values are booleans indicating installation.
*/
function checkDeps(packages, rootDir) {
return check(packages, { dependencies: true, startDir: rootDir });
}
/**
* Check whether node modules are included in the devDependencies of a project's
* package.json.
*
* Convienience wrapper around check().
*
* @param {string[]} packages Array of node modules to check.
* @returns {Object} An object whose keys are the module names
* and values are booleans indicating installation.
*/
function checkDevDeps(packages) {
return check(packages, { devDependencies: true });
}
/**
* Check whether package.json is found in current path.
*
* @param {string=} startDir Starting directory
* @returns {boolean} Whether a package.json is found in current path.
*/
function checkPackageJson(startDir) {
return !!findPackageJson(startDir);
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
installSyncSaveDev,
checkDeps,
checkDevDeps,
checkPackageJson
};

View File

@ -0,0 +1,74 @@
/**
* @fileoverview Common helpers for operations on filenames and paths
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const path = require("path");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* Replace Windows with posix style paths
*
* @param {string} filepath Path to convert
* @returns {string} Converted filepath
*/
function convertPathToPosix(filepath) {
const normalizedFilepath = path.normalize(filepath);
const posixFilepath = normalizedFilepath.replace(/\\/g, "/");
return posixFilepath;
}
/**
* Converts an absolute filepath to a relative path from a given base path
*
* For example, if the filepath is `/my/awesome/project/foo.bar`,
* and the base directory is `/my/awesome/project/`,
* then this function should return `foo.bar`.
*
* path.relative() does something similar, but it requires a baseDir (`from` argument).
* This function makes it optional and just removes a leading slash if the baseDir is not given.
*
* It does not take into account symlinks (for now).
*
* @param {string} filepath Path to convert to relative path. If already relative,
* it will be assumed to be relative to process.cwd(),
* converted to absolute, and then processed.
* @param {string} [baseDir] Absolute base directory to resolve the filepath from.
* If not provided, all this function will do is remove
* a leading slash.
* @returns {string} Relative filepath
*/
function getRelativePath(filepath, baseDir) {
let relativePath;
if (!path.isAbsolute(filepath)) {
filepath = path.resolve(filepath);
}
if (baseDir) {
if (!path.isAbsolute(baseDir)) {
throw new Error("baseDir should be an absolute path");
}
relativePath = path.relative(baseDir, filepath);
} else {
relativePath = filepath.replace(/^\//, "");
}
return relativePath;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
convertPathToPosix,
getRelativePath
};

File diff suppressed because one or more lines are too long

View 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;

View File

@ -0,0 +1,131 @@
/**
* @fileoverview An object that caches and applies source code fixes.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const debug = require("debug")("eslint:text-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 {SourceCode} sourceCode The source code to apply the changes to.
* @param {Message[]} messages The array of messages reported by ESLint.
* @returns {Object} An object containing the fixed text and any unfixed messages.
*/
SourceCodeFixer.applyFixes = function(sourceCode, messages) {
debug("Applying fixes");
if (!sourceCode) {
debug("No source code to fix");
return {
fixed: false,
messages,
output: ""
};
}
// clone the array
const remainingMessages = [],
fixes = [],
bom = (sourceCode.hasBOM ? BOM : ""),
text = sourceCode.text;
let lastPos = Number.NEGATIVE_INFINITY,
output = bom;
messages.forEach(problem => {
if (problem.hasOwnProperty("fix")) {
fixes.push(problem);
} else {
remainingMessages.push(problem);
}
});
if (fixes.length) {
debug("Found fixes to apply");
for (const problem of fixes.sort(compareMessagesByFixRange)) {
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);
continue;
}
// 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;
}
output += text.slice(Math.max(0, lastPos));
return {
fixed: true,
messages: remainingMessages.sort(compareMessagesByLocation),
output
};
}
debug("No fixes to apply");
return {
fixed: false,
messages,
output: bom + text
};
};
module.exports = SourceCodeFixer;

View File

@ -0,0 +1,110 @@
/**
* @fileoverview Tools for obtaining SourceCode objects.
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const CLIEngine = require("../cli-engine"),
eslint = require("../eslint"),
globUtil = require("./glob-util"),
baseDefaultOptions = require("../../conf/cli-options");
const debug = require("debug")("eslint:source-code-util");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Get the SourceCode object for a single file
* @param {string} filename The fully resolved filename to get SourceCode from.
* @param {Object} options A CLIEngine options object.
* @returns {Array} Array of the SourceCode object representing the file
* and fatal error message.
*/
function getSourceCodeOfFile(filename, options) {
debug("getting sourceCode of", filename);
const opts = Object.assign({}, options, { rules: {} });
const cli = new CLIEngine(opts);
const results = cli.executeOnFiles([filename]);
if (results && results.results[0] && results.results[0].messages[0] && results.results[0].messages[0].fatal) {
const msg = results.results[0].messages[0];
throw new Error(`(${filename}:${msg.line}:${msg.column}) ${msg.message}`);
}
const sourceCode = eslint.getSourceCode();
return sourceCode;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* This callback is used to measure execution status in a progress bar
* @callback progressCallback
* @param {number} The total number of times the callback will be called.
*/
/**
* Gets the SourceCode of a single file, or set of files.
* @param {string[]|string} patterns A filename, directory name, or glob,
* or an array of them
* @param {Object} [options] A CLIEngine options object. If not provided,
* the default cli options will be used.
* @param {progressCallback} [cb] Callback for reporting execution status
* @returns {Object} The SourceCode of all processed files.
*/
function getSourceCodeOfFiles(patterns, options, cb) {
const sourceCodes = {};
let opts;
if (typeof patterns === "string") {
patterns = [patterns];
}
const defaultOptions = Object.assign({}, baseDefaultOptions, { cwd: process.cwd() });
if (typeof options === "undefined") {
opts = defaultOptions;
} else if (typeof options === "function") {
cb = options;
opts = defaultOptions;
} else if (typeof options === "object") {
opts = Object.assign({}, defaultOptions, options);
}
debug("constructed options:", opts);
patterns = globUtil.resolveFileGlobPatterns(patterns, opts);
const filenames = globUtil.listFilesToProcess(patterns, opts)
.filter(fileInfo => !fileInfo.ignored)
.reduce((files, fileInfo) => files.concat(fileInfo.filename), []);
if (filenames.length === 0) {
debug(`Did not find any files matching pattern(s): ${patterns}`);
}
filenames.forEach(filename => {
const sourceCode = getSourceCodeOfFile(filename, opts);
if (sourceCode) {
debug("got sourceCode of", filename);
sourceCodes[filename] = sourceCode;
}
if (cb) {
cb(filenames.length); // eslint-disable-line callback-return
}
});
return sourceCodes;
}
module.exports = {
getSourceCodeOfFiles
};

View File

@ -0,0 +1,416 @@
/**
* @fileoverview Abstraction of JavaScript source code.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const TokenStore = require("../token-store"),
Traverser = require("./traverser"),
astUtils = require("../ast-utils"),
lodash = require("lodash");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* Validates that the given AST has the required information.
* @param {ASTNode} ast The Program node of the AST to check.
* @throws {Error} If the AST doesn't contain the correct information.
* @returns {void}
* @private
*/
function validate(ast) {
if (!ast.tokens) {
throw new Error("AST is missing the tokens array.");
}
if (!ast.comments) {
throw new Error("AST is missing the comments array.");
}
if (!ast.loc) {
throw new Error("AST is missing location information.");
}
if (!ast.range) {
throw new Error("AST is missing range information");
}
}
/**
* Finds a JSDoc comment node in an array of comment nodes.
* @param {ASTNode[]} comments The array of comment nodes to search.
* @param {int} line Line number to look around
* @returns {ASTNode} The node if found, null if not.
* @private
*/
function findJSDocComment(comments, line) {
if (comments) {
for (let i = comments.length - 1; i >= 0; i--) {
if (comments[i].type === "Block" && comments[i].value.charAt(0) === "*") {
if (line - comments[i].loc.end.line <= 1) {
return comments[i];
}
break;
}
}
}
return null;
}
/**
* Check to see if its a ES6 export declaration
* @param {ASTNode} astNode - any node
* @returns {boolean} whether the given node represents a export declaration
* @private
*/
function looksLikeExport(astNode) {
return astNode.type === "ExportDefaultDeclaration" || astNode.type === "ExportNamedDeclaration" ||
astNode.type === "ExportAllDeclaration" || astNode.type === "ExportSpecifier";
}
/**
* Merges two sorted lists into a larger sorted list in O(n) time
* @param {Token[]} tokens The list of tokens
* @param {Token[]} comments The list of comments
* @returns {Token[]} A sorted list of tokens and comments
*/
function sortedMerge(tokens, comments) {
const result = [];
let tokenIndex = 0;
let commentIndex = 0;
while (tokenIndex < tokens.length || commentIndex < comments.length) {
if (commentIndex >= comments.length || tokenIndex < tokens.length && tokens[tokenIndex].range[0] < comments[commentIndex].range[0]) {
result.push(tokens[tokenIndex++]);
} else {
result.push(comments[commentIndex++]);
}
}
return result;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Represents parsed source code.
* @param {string} text - The source code text.
* @param {ASTNode} ast - The Program node of the AST representing the code. This AST should be created from the text that BOM was stripped.
* @constructor
*/
function SourceCode(text, ast) {
validate(ast);
/**
* The flag to indicate that the source code has Unicode BOM.
* @type boolean
*/
this.hasBOM = (text.charCodeAt(0) === 0xFEFF);
/**
* The original text source code.
* BOM was stripped from this text.
* @type string
*/
this.text = (this.hasBOM ? text.slice(1) : text);
/**
* The parsed AST for the source code.
* @type ASTNode
*/
this.ast = ast;
/**
* The source code split into lines according to ECMA-262 specification.
* This is done to avoid each rule needing to do so separately.
* @type string[]
*/
this.lines = [];
this.lineStartIndices = [0];
const lineEndingPattern = astUtils.createGlobalLinebreakMatcher();
let match;
/*
* Previously, this was implemented using a regex that
* matched a sequence of non-linebreak characters followed by a
* linebreak, then adding the lengths of the matches. However,
* this caused a catastrophic backtracking issue when the end
* of a file contained a large number of non-newline characters.
* To avoid this, the current implementation just matches newlines
* and uses match.index to get the correct line start indices.
*/
while ((match = lineEndingPattern.exec(this.text))) {
this.lines.push(this.text.slice(this.lineStartIndices[this.lineStartIndices.length - 1], match.index));
this.lineStartIndices.push(match.index + match[0].length);
}
this.lines.push(this.text.slice(this.lineStartIndices[this.lineStartIndices.length - 1]));
this.tokensAndComments = sortedMerge(ast.tokens, ast.comments);
// create token store methods
const tokenStore = new TokenStore(ast.tokens, ast.comments);
for (const methodName of TokenStore.PUBLIC_METHODS) {
this[methodName] = tokenStore[methodName].bind(tokenStore);
}
// don't allow modification of this object
Object.freeze(this);
Object.freeze(this.lines);
}
/**
* Split the source code into multiple lines based on the line delimiters
* @param {string} text Source code as a string
* @returns {string[]} Array of source code lines
* @public
*/
SourceCode.splitLines = function(text) {
return text.split(astUtils.createGlobalLinebreakMatcher());
};
SourceCode.prototype = {
constructor: SourceCode,
/**
* Gets the source code for the given node.
* @param {ASTNode=} node The AST node to get the text for.
* @param {int=} beforeCount The number of characters before the node to retrieve.
* @param {int=} afterCount The number of characters after the node to retrieve.
* @returns {string} The text representing the AST node.
*/
getText(node, beforeCount, afterCount) {
if (node) {
return this.text.slice(Math.max(node.range[0] - (beforeCount || 0), 0),
node.range[1] + (afterCount || 0));
}
return this.text;
},
/**
* Gets the entire source text split into an array of lines.
* @returns {Array} The source text as an array of lines.
*/
getLines() {
return this.lines;
},
/**
* Retrieves an array containing all comments in the source code.
* @returns {ASTNode[]} An array of comment nodes.
*/
getAllComments() {
return this.ast.comments;
},
/**
* Gets all comments for the given node.
* @param {ASTNode} node The AST node to get the comments for.
* @returns {Object} The list of comments indexed by their position.
* @public
*/
getComments(node) {
let leadingComments = node.leadingComments || [];
const trailingComments = node.trailingComments || [];
/*
* espree adds a "comments" array on Program nodes rather than
* leadingComments/trailingComments. Comments are only left in the
* Program node comments array if there is no executable code.
*/
if (node.type === "Program") {
if (node.body.length === 0) {
leadingComments = node.comments;
}
}
return {
leading: leadingComments,
trailing: trailingComments
};
},
/**
* Retrieves the JSDoc comment for a given node.
* @param {ASTNode} node The AST node to get the comment for.
* @returns {ASTNode} The BlockComment node containing the JSDoc for the
* given node or null if not found.
* @public
*/
getJSDocComment(node) {
let parent = node.parent;
switch (node.type) {
case "ClassDeclaration":
case "FunctionDeclaration":
if (looksLikeExport(parent)) {
return findJSDocComment(parent.leadingComments, parent.loc.start.line);
}
return findJSDocComment(node.leadingComments, node.loc.start.line);
case "ClassExpression":
return findJSDocComment(parent.parent.leadingComments, parent.parent.loc.start.line);
case "ArrowFunctionExpression":
case "FunctionExpression":
if (parent.type !== "CallExpression" && parent.type !== "NewExpression") {
while (parent && !parent.leadingComments && !/Function/.test(parent.type) && parent.type !== "MethodDefinition" && parent.type !== "Property") {
parent = parent.parent;
}
return parent && (parent.type !== "FunctionDeclaration") ? findJSDocComment(parent.leadingComments, parent.loc.start.line) : null;
} else if (node.leadingComments) {
return findJSDocComment(node.leadingComments, node.loc.start.line);
}
// falls through
default:
return null;
}
},
/**
* Gets the deepest node containing a range index.
* @param {int} index Range index of the desired node.
* @returns {ASTNode} The node if found or null if not found.
*/
getNodeByRangeIndex(index) {
let result = null,
resultParent = null;
const traverser = new Traverser();
traverser.traverse(this.ast, {
enter(node, parent) {
if (node.range[0] <= index && index < node.range[1]) {
result = node;
resultParent = parent;
} else {
this.skip();
}
},
leave(node) {
if (node === result) {
this.break();
}
}
});
return result ? Object.assign({ parent: resultParent }, result) : null;
},
/**
* Determines if two tokens have at least one whitespace character
* between them. This completely disregards comments in making the
* determination, so comments count as zero-length substrings.
* @param {Token} first The token to check after.
* @param {Token} second The token to check before.
* @returns {boolean} True if there is only space between tokens, false
* if there is anything other than whitespace between tokens.
*/
isSpaceBetweenTokens(first, second) {
const text = this.text.slice(first.range[1], second.range[0]);
return /\s/.test(text.replace(/\/\*.*?\*\//g, ""));
},
/**
* Converts a source text index into a (line, column) pair.
* @param {number} index The index of a character in a file
* @returns {Object} A {line, column} location object with a 0-indexed column
*/
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}).`);
}
/*
* For an argument of this.text.length, return the location one "spot" past the last character
* of the file. If the last character is a linebreak, the location will be column 0 of the next
* line; otherwise, the location will be in the next column on the same line.
*
* See getIndexFromLoc for the motivation for this special case.
*/
if (index === this.text.length) {
return { line: this.lines.length, column: this.lines[this.lines.length - 1].length };
}
/*
* To figure out which line rangeIndex is on, determine the last index at which rangeIndex could
* be inserted into lineIndices to keep the list sorted.
*/
const lineNumber = lodash.sortedLastIndex(this.lineStartIndices, index);
return { line: lineNumber, column: index - this.lineStartIndices[lineNumber - 1] };
},
/**
* Converts a (line, column) pair into a range index.
* @param {Object} loc A line/column location
* @param {number} loc.line The line number of the location (1-indexed)
* @param {number} loc.column The column number of the location (0-indexed)
* @returns {number} The range index of the location in the file.
*/
getIndexFromLoc(loc) {
if (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.");
}
if (loc.line <= 0) {
throw new RangeError(`Line number out of range (line ${loc.line} requested). Line numbers should be 1-based.`);
}
if (loc.line > this.lineStartIndices.length) {
throw new RangeError(`Line number out of range (line ${loc.line} requested, but only ${this.lineStartIndices.length} lines present).`);
}
const lineStartIndex = this.lineStartIndices[loc.line - 1];
const lineEndIndex = loc.line === this.lineStartIndices.length ? this.text.length : this.lineStartIndices[loc.line];
const positionIndex = lineStartIndex + loc.column;
/*
* By design, getIndexFromLoc({ line: lineNum, column: 0 }) should return the start index of
* the given line, provided that the line number is valid element of this.lines. Since the
* last element of this.lines is an empty string for files with trailing newlines, add a
* special case where getting the index for the first location after the end of the file
* will return the length of the file, rather than throwing an error. This allows rules to
* use getIndexFromLoc consistently without worrying about edge cases at the end of a file.
*/
if (
loc.line === this.lineStartIndices.length && positionIndex > lineEndIndex ||
loc.line < this.lineStartIndices.length && positionIndex >= lineEndIndex
) {
throw new RangeError(`Column number out of range (column ${loc.column} requested, but the length of line ${loc.line} is ${lineEndIndex - lineStartIndex}).`);
}
return positionIndex;
}
};
module.exports = SourceCode;

View File

@ -0,0 +1,45 @@
/**
* @fileoverview Wrapper around estraverse
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const estraverse = require("estraverse");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const KEY_BLACKLIST = new Set([
"parent",
"leadingComments",
"trailingComments"
]);
/**
* Wrapper around an estraverse controller that ensures the correct keys
* are visited.
* @constructor
*/
class Traverser extends estraverse.Controller {
traverse(node, visitor) {
visitor.fallback = Traverser.getKeys;
return super.traverse(node, visitor);
}
/**
* Calculates the keys to use for traversal.
* @param {ASTNode} node The node to read keys from.
* @returns {string[]} An array of keys to visit on the node.
* @private
*/
static getKeys(node) {
return Object.keys(node).filter(key => !KEY_BLACKLIST.has(key));
}
}
module.exports = Traverser;

View File

@ -0,0 +1,34 @@
/**
* @fileoverview XML character escaper
* @author George Chung
*/
"use strict";
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Returns the escaped value for a character
* @param {string} s string to examine
* @returns {string} severity level
* @private
*/
module.exports = function(s) {
return (`${s}`).replace(/[<>&"'\x00-\x1F\x7F\u0080-\uFFFF]/g, c => { // eslint-disable-line no-control-regex
switch (c) {
case "<":
return "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
case "\"":
return "&quot;";
case "'":
return "&apos;";
default:
return `&#${c.charCodeAt(0)};`;
}
});
};