remove timer and simon

This commit is contained in:
Cody Loyd
2017-12-15 12:56:14 -06:00
parent 14cc7d40bd
commit 834d78b8b2
2928 changed files with 303453 additions and 313 deletions
+29
View File
@@ -0,0 +1,29 @@
/**
* @fileoverview The instance of Ajv validator.
* @author Evgeny Poberezkin
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const Ajv = require("ajv"),
metaSchema = require("ajv/lib/refs/json-schema-draft-04.json");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
const ajv = new Ajv({
meta: false,
validateSchema: false,
missingRefs: "ignore",
verbose: true
});
ajv.addMetaSchema(metaSchema);
// eslint-disable-next-line no-underscore-dangle
ajv._opts.defaultMeta = metaSchema.id;
module.exports = ajv;
+160
View File
@@ -0,0 +1,160 @@
/**
* @fileoverview A module that filters reported problems based on `eslint-disable` and `eslint-enable` comments
* @author Teddy Katz
*/
"use strict";
const lodash = require("lodash");
/**
* Compares the locations of two objects in a source file
* @param {{line: number, column: number}} itemA The first object
* @param {{line: number, column: number}} itemB The second object
* @returns {number} A value less than 1 if itemA appears before itemB in the source file, greater than 1 if
* itemA appears after itemB in the source file, or 0 if itemA and itemB have the same location.
*/
function compareLocations(itemA, itemB) {
return itemA.line - itemB.line || itemA.column - itemB.column;
}
/**
* This is the same as the exported function, except that it
* doesn't handle disable-line and disable-next-line directives, and it always reports unused
* disable directives.
* @param {Object} options options for applying directives. This is the same as the options
* for the exported function, except that `reportUnusedDisableDirectives` is not supported
* (this function always reports unused disable directives).
* @returns {{problems: Problem[], unusedDisableDirectives: Problem[]}} An object with a list
* of filtered problems and unused eslint-disable directives
*/
function applyDirectives(options) {
const problems = [];
let nextDirectiveIndex = 0;
let currentGlobalDisableDirective = null;
const disabledRuleMap = new Map();
// enabledRules is only used when there is a current global disable directive.
const enabledRules = new Set();
const usedDisableDirectives = new Set();
for (const problem of options.problems) {
while (
nextDirectiveIndex < options.directives.length &&
compareLocations(options.directives[nextDirectiveIndex], problem) <= 0
) {
const directive = options.directives[nextDirectiveIndex++];
switch (directive.type) {
case "disable":
if (directive.ruleId === null) {
currentGlobalDisableDirective = directive;
disabledRuleMap.clear();
enabledRules.clear();
} else if (currentGlobalDisableDirective) {
enabledRules.delete(directive.ruleId);
disabledRuleMap.set(directive.ruleId, directive);
} else {
disabledRuleMap.set(directive.ruleId, directive);
}
break;
case "enable":
if (directive.ruleId === null) {
currentGlobalDisableDirective = null;
disabledRuleMap.clear();
} else if (currentGlobalDisableDirective) {
enabledRules.add(directive.ruleId);
disabledRuleMap.delete(directive.ruleId);
} else {
disabledRuleMap.delete(directive.ruleId);
}
break;
// no default
}
}
if (disabledRuleMap.has(problem.ruleId)) {
usedDisableDirectives.add(disabledRuleMap.get(problem.ruleId));
} else if (currentGlobalDisableDirective && !enabledRules.has(problem.ruleId)) {
usedDisableDirectives.add(currentGlobalDisableDirective);
} else {
problems.push(problem);
}
}
const unusedDisableDirectives = options.directives
.filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive))
.map(directive => ({
ruleId: null,
message: directive.ruleId
? `Unused eslint-disable directive (no problems were reported from '${directive.ruleId}').`
: "Unused eslint-disable directive (no problems were reported).",
line: directive.unprocessedDirective.line,
column: directive.unprocessedDirective.column,
severity: 2,
source: null,
nodeType: null
}));
return { problems, unusedDisableDirectives };
}
/**
* Given a list of directive comments (i.e. metadata about eslint-disable and eslint-enable comments) and a list
* of reported problems, determines which problems should be reported.
* @param {Object} options Information about directives and problems
* @param {{
* type: ("disable"|"enable"|"disable-line"|"disable-next-line"),
* ruleId: (string|null),
* line: number,
* column: number
* }} options.directives Directive comments found in the file, with one-based columns.
* Two directive comments can only have the same location if they also have the same type (e.g. a single eslint-disable
* comment for two different rules is represented as two directives).
* @param {{ruleId: (string|null), line: number, column: number}[]} options.problems
* A list of problems reported by rules, sorted by increasing location in the file, with one-based columns.
* @param {boolean} options.reportUnusedDisableDirectives If `true`, adds additional problems for unused directives
* @returns {{ruleId: (string|null), line: number, column: number}[]}
* A list of reported problems that were not disabled by the directive comments.
*/
module.exports = options => {
const blockDirectives = options.directives
.filter(directive => directive.type === "disable" || directive.type === "enable")
.map(directive => Object.assign({}, directive, { unprocessedDirective: directive }))
.sort(compareLocations);
const lineDirectives = lodash.flatMap(options.directives, directive => {
switch (directive.type) {
case "disable":
case "enable":
return [];
case "disable-line":
return [
{ type: "disable", line: directive.line, column: 1, ruleId: directive.ruleId, unprocessedDirective: directive },
{ type: "enable", line: directive.line + 1, column: 0, ruleId: directive.ruleId, unprocessedDirective: directive }
];
case "disable-next-line":
return [
{ type: "disable", line: directive.line + 1, column: 1, ruleId: directive.ruleId, unprocessedDirective: directive },
{ type: "enable", line: directive.line + 2, column: 0, ruleId: directive.ruleId, unprocessedDirective: directive }
];
default:
throw new TypeError(`Unrecognized directive type '${directive.type}'`);
}
}).sort(compareLocations);
const blockDirectivesResult = applyDirectives({ problems: options.problems, directives: blockDirectives });
const lineDirectivesResult = applyDirectives({ problems: blockDirectivesResult.problems, directives: lineDirectives });
return options.reportUnusedDisableDirectives
? lineDirectivesResult.problems
.concat(blockDirectivesResult.unusedDisableDirectives)
.concat(lineDirectivesResult.unusedDisableDirectives)
.sort(compareLocations)
: lineDirectivesResult.problems;
};
+120
View File
@@ -0,0 +1,120 @@
/**
* @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;
+182
View File
@@ -0,0 +1,182 @@
/**
* @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"),
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 (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
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 (fs.existsSync(file) && fs.statSync(file).isFile()) {
const ignoredPaths = new IgnoredPaths(options);
addFile(fs.realpathSync(file), true, 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
};
+63
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;
+35
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;
+67
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"
];
+85
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;
+112
View File
@@ -0,0 +1,112 @@
/**
* @fileoverview Common helpers for naming of plugins, formatters and configs
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const pathUtil = require("../util/path-util");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
const NAMESPACE_REGEX = /^@.*\//i;
/**
* Brings package name to correct format based on prefix
* @param {string} name The name of the package.
* @param {string} prefix Can be either "eslint-plugin", "eslint-config" or "eslint-formatter"
* @returns {string} Normalized name of the package
* @private
*/
function normalizePackageName(name, prefix) {
/**
* On Windows, name can come in with Windows slashes instead of Unix slashes.
* Normalize to Unix first to avoid errors later on.
* https://github.com/eslint/eslint/issues/5644
*/
if (name.indexOf("\\") > -1) {
name = pathUtil.convertPathToPosix(name);
}
if (name.charAt(0) === "@") {
/**
* it's a scoped package
* package name is the prefix, or just a username
*/
const scopedPackageShortcutRegex = new RegExp(`^(@[^/]+)(?:/(?:${prefix})?)?$`),
scopedPackageNameRegex = new RegExp(`^${prefix}(-|$)`);
if (scopedPackageShortcutRegex.test(name)) {
name = name.replace(scopedPackageShortcutRegex, `$1/${prefix}`);
} else if (!scopedPackageNameRegex.test(name.split("/")[1])) {
/**
* for scoped packages, insert the prefix after the first / unless
* the path is already @scope/eslint or @scope/eslint-xxx-yyy
*/
name = name.replace(/^@([^/]+)\/(.*)$/, `@$1/${prefix}-$2`);
}
} else if (name.indexOf(`${prefix}-`) !== 0) {
name = `${prefix}-${name}`;
}
return name;
}
/**
* Removes the prefix from a term.
* @param {string} prefix The prefix to remove.
* @param {string} term The term which may have the prefix.
* @returns {string} The term without prefix.
*/
function removePrefixFromTerm(prefix, term) {
return term.startsWith(prefix) ? term.slice(prefix.length) : term;
}
/**
* Adds a prefix to a term.
* @param {string} prefix The prefix to add.
* @param {string} term The term which may not have the prefix.
* @returns {string} The term with prefix.
*/
function addPrefixToTerm(prefix, term) {
return term.startsWith(prefix) ? term : `${prefix}${term}`;
}
/**
* Gets the scope (namespace) of a term.
* @param {string} term The term which may have the namespace.
* @returns {string} The namepace of the term if it has one.
*/
function getNamespaceFromTerm(term) {
const match = term.match(NAMESPACE_REGEX);
return match ? match[0] : "";
}
/**
* Removes the namespace from a term.
* @param {string} term The term which may have the namespace.
* @returns {string} The name of the plugin without the namespace.
*/
function removeNamespaceFromTerm(term) {
return term.replace(NAMESPACE_REGEX, "");
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
normalizePackageName,
removePrefixFromTerm,
addPrefixToTerm,
getNamespaceFromTerm,
removeNamespaceFromTerm
};
+308
View File
@@ -0,0 +1,308 @@
/**
* @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 SyntaxError(`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: SafeEmitter;
* enterNode(node: ASTNode): void;
* leaveNode(node: ASTNode): void;
* }
* ```
*/
class NodeEventGenerator {
/**
* @param {SafeEmitter} emitter
* An SafeEmitter which is the destination of events. This emitter must already
* have registered listeners for all of the events that it needs to listen for.
* (See lib/util/safe-emitter.js for more details on `SafeEmitter`.)
* @returns {NodeEventGenerator} new instance
*/
constructor(emitter) {
this.emitter = emitter;
this.currentAncestry = [];
this.enterSelectorsByNodeType = new Map();
this.exitSelectorsByNodeType = new Map();
this.anyTypeEnterSelectors = [];
this.anyTypeExitSelectors = [];
emitter.eventNames().forEach(rawSelector => {
const selector = parseSelector(rawSelector);
if (selector.listenerTypes) {
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;
+179
View File
@@ -0,0 +1,179 @@
/**
* @fileoverview Utility for executing npm commands.
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
spawn = require("cross-spawn"),
path = require("path"),
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 (!fs.existsSync(pkgFile) || !fs.statSync(pkgFile).isFile()) {
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];
}
const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packages),
{ stdio: "inherit" });
const error = npmProcess.error;
if (error && error.code === "ENOENT") {
const pluralS = packages.length > 1 ? "s" : "";
log.error(`Could not execute npm. Please install the following package${pluralS} with your package manager of choice: ${packages.join(", ")}`);
}
}
/**
* Fetch `peerDependencies` of the given package by `npm show` command.
* @param {string} packageName The package name to fetch peerDependencies.
* @returns {Object} Gotten peerDependencies. Returns null if npm was not found.
*/
function fetchPeerDependencies(packageName) {
const npmProcess = spawn.sync(
"npm",
["show", "--json", packageName, "peerDependencies"],
{ encoding: "utf8" }
);
const error = npmProcess.error;
if (error && error.code === "ENOENT") {
return null;
}
const fetchedText = npmProcess.stdout.trim();
return JSON.parse(fetchedText || "{}");
}
/**
* 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,
fetchPeerDependencies,
checkDeps,
checkDevDeps,
checkPackageJson
};
+74
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
+140
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;
+54
View File
@@ -0,0 +1,54 @@
/**
* @fileoverview A variant of EventEmitter which does not give listeners information about each other
* @author Teddy Katz
*/
"use strict";
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/**
* An event emitter
* @typedef {Object} SafeEmitter
* @property {function(eventName: string, listenerFunc: Function): void} on Adds a listener for a given event name
* @property {function(eventName: string, arg1?: any, arg2?: any, arg3?: any)} emit Emits an event with a given name.
* This calls all the listeners that were listening for that name, with `arg1`, `arg2`, and `arg3` as arguments.
* @property {function(): string[]} eventNames Gets the list of event names that have registered listeners.
*/
/**
* Creates an object which can listen for and emit events.
* This is similar to the EventEmitter API in Node's standard library, but it has a few differences.
* The goal is to allow multiple modules to attach arbitrary listeners to the same emitter, without
* letting the modules know about each other at all.
* 1. It has no special keys like `error` and `newListener`, which would allow modules to detect when
* another module throws an error or registers a listener.
* 2. It calls listener functions without any `this` value. (`EventEmitter` calls listeners with a
* `this` value of the emitter instance, which would give listeners access to other listeners.)
* 3. Events can be emitted with at most 3 arguments. (For example: when using `emitter.emit('foo', a, b, c)`,
* the arguments `a`, `b`, and `c` will be passed to the listener functions.)
* @returns {SafeEmitter} An emitter
*/
module.exports = () => {
const listeners = Object.create(null);
return Object.freeze({
on(eventName, listener) {
if (eventName in listeners) {
listeners[eventName].push(listener);
} else {
listeners[eventName] = [listener];
}
},
emit(eventName, a, b, c) {
if (eventName in listeners) {
listeners[eventName].forEach(listener => listener(a, b, c));
}
},
eventNames() {
return Object.keys(listeners);
}
});
};
+152
View File
@@ -0,0 +1,152 @@
/**
* @fileoverview An object that caches and applies source code fixes.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const debug = require("debug")("eslint: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 {string} sourceText The text to apply the changes to.
* @param {Message[]} messages The array of messages reported by ESLint.
* @param {boolean|Function} [shouldFix=true] Determines whether each message should be fixed
* @returns {Object} An object containing the fixed text and any unfixed messages.
*/
SourceCodeFixer.applyFixes = function(sourceText, messages, shouldFix) {
debug("Applying fixes");
if (shouldFix === false) {
debug("shouldFix parameter was false, not attempting fixes");
return {
fixed: false,
messages,
output: sourceText
};
}
// clone the array
const remainingMessages = [],
fixes = [],
bom = sourceText.startsWith(BOM) ? BOM : "",
text = bom ? sourceText.slice(1) : sourceText;
let lastPos = Number.NEGATIVE_INFINITY,
output = bom;
/**
* Try to use the 'fix' from a problem.
* @param {Message} problem The message object to apply fixes from
* @returns {boolean} Whether fix was successfully applied
*/
function attemptFix(problem) {
const fix = problem.fix;
const start = fix.range[0];
const end = fix.range[1];
// Remain it as a problem if it's overlapped or it's a negative range
if (lastPos >= start || start > end) {
remainingMessages.push(problem);
return false;
}
// Remove BOM.
if ((start < 0 && end >= 0) || (start === 0 && fix.text.startsWith(BOM))) {
output = "";
}
// Make output to this fix.
output += text.slice(Math.max(0, lastPos), Math.max(0, start));
output += fix.text;
lastPos = end;
return true;
}
messages.forEach(problem => {
if (problem.hasOwnProperty("fix")) {
fixes.push(problem);
} else {
remainingMessages.push(problem);
}
});
if (fixes.length) {
debug("Found fixes to apply");
let fixesWereApplied = false;
for (const problem of fixes.sort(compareMessagesByFixRange)) {
if (typeof shouldFix !== "function" || shouldFix(problem)) {
attemptFix(problem);
/*
* The only time attemptFix will fail is if a previous fix was
* applied which conflicts with it. So we can mark this as true.
*/
fixesWereApplied = true;
} else {
remainingMessages.push(problem);
}
}
output += text.slice(Math.max(0, lastPos));
return {
fixed: fixesWereApplied,
messages: remainingMessages.sort(compareMessagesByLocation),
output
};
}
debug("No fixes to apply");
return {
fixed: false,
messages,
output: bom + text
};
};
module.exports = SourceCodeFixer;
+109
View File
@@ -0,0 +1,109 @@
/**
* @fileoverview Tools for obtaining SourceCode objects.
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const CLIEngine = require("../cli-engine"),
globUtil = require("./glob-util"),
baseDefaultOptions = require("../../conf/default-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 = cli.linter.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
};
+472
View File
@@ -0,0 +1,472 @@
/**
* @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");
}
}
/**
* Check to see if its a ES6 export declaration.
* @param {ASTNode} astNode An AST node.
* @returns {boolean} whether the given node represents an 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.
* @private
*/
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
//------------------------------------------------------------------------------
class SourceCode extends TokenStore {
/**
* 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
*/
constructor(text, ast) {
validate(ast);
super(ast.tokens, ast.comments);
/**
* 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;
// Check the source text for the presence of a shebang since it is parsed as a standard line comment.
const shebangMatched = this.text.match(astUtils.SHEBANG_MATCHER);
const hasShebang = shebangMatched && ast.comments.length && ast.comments[0].value === shebangMatched[1];
if (hasShebang) {
ast.comments[0].type = "Shebang";
}
this.tokensAndComments = sortedMerge(ast.tokens, ast.comments);
/**
* 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]));
// Cache for comments found using getComments().
this._commentCache = new WeakMap();
// 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
*/
static splitLines(text) {
return text.split(astUtils.createGlobalLinebreakMatcher());
}
/**
* 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.
* @public
*/
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.
* @public
*/
getLines() {
return this.lines;
}
/**
* Retrieves an array containing all comments in the source code.
* @returns {ASTNode[]} An array of comment nodes.
* @public
*/
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} An object containing a leading and trailing array
* of comments indexed by their position.
* @public
*/
getComments(node) {
if (this._commentCache.has(node)) {
return this._commentCache.get(node);
}
const comments = {
leading: [],
trailing: []
};
/*
* Return all comments as leading comments of the Program node when
* there is no executable code.
*/
if (node.type === "Program") {
if (node.body.length === 0) {
comments.leading = node.comments;
}
} else {
/*
* Return comments as trailing comments of nodes that only contain
* comments (to mimic the comment attachment behavior present in Espree).
*/
if ((node.type === "BlockStatement" || node.type === "ClassBody") && node.body.length === 0 ||
node.type === "ObjectExpression" && node.properties.length === 0 ||
node.type === "ArrayExpression" && node.elements.length === 0 ||
node.type === "SwitchStatement" && node.cases.length === 0
) {
comments.trailing = this.getTokens(node, {
includeComments: true,
filter: astUtils.isCommentToken
});
}
/*
* Iterate over tokens before and after node and collect comment tokens.
* Do not include comments that exist outside of the parent node
* to avoid duplication.
*/
let currentToken = this.getTokenBefore(node, { includeComments: true });
while (currentToken && astUtils.isCommentToken(currentToken)) {
if (node.parent && (currentToken.start < node.parent.start)) {
break;
}
comments.leading.push(currentToken);
currentToken = this.getTokenBefore(currentToken, { includeComments: true });
}
comments.leading.reverse();
currentToken = this.getTokenAfter(node, { includeComments: true });
while (currentToken && astUtils.isCommentToken(currentToken)) {
if (node.parent && (currentToken.end > node.parent.end)) {
break;
}
comments.trailing.push(currentToken);
currentToken = this.getTokenAfter(currentToken, { includeComments: true });
}
}
this._commentCache.set(node, comments);
return comments;
}
/**
* Retrieves the JSDoc comment for a given node.
* @param {ASTNode} node The AST node to get the comment for.
* @returns {Token|null} The Block comment token containing the JSDoc comment
* for the given node or null if not found.
* @public
*/
getJSDocComment(node) {
/**
* Checks for the presence of a JSDoc comment for the given node and returns it.
* @param {ASTNode} astNode The AST node to get the comment for.
* @returns {Token|null} The Block comment token containing the JSDoc comment
* for the given node or null if not found.
* @private
*/
const findJSDocComment = astNode => {
const tokenBefore = this.getTokenBefore(astNode, { includeComments: true });
if (
tokenBefore &&
astUtils.isCommentToken(tokenBefore) &&
tokenBefore.type === "Block" &&
tokenBefore.value.charAt(0) === "*" &&
astNode.loc.start.line - tokenBefore.loc.end.line <= 1
) {
return tokenBefore;
}
return null;
};
let parent = node.parent;
switch (node.type) {
case "ClassDeclaration":
case "FunctionDeclaration":
return findJSDocComment(looksLikeExport(parent) ? parent : node);
case "ClassExpression":
return findJSDocComment(parent.parent);
case "ArrowFunctionExpression":
case "FunctionExpression":
if (parent.type !== "CallExpression" && parent.type !== "NewExpression") {
while (
!this.getCommentsBefore(parent).length &&
!/Function/.test(parent.type) &&
parent.type !== "MethodDefinition" &&
parent.type !== "Property"
) {
parent = parent.parent;
if (!parent) {
break;
}
}
if (parent && parent.type !== "FunctionDeclaration" && parent.type !== "Program") {
return findJSDocComment(parent);
}
}
return findJSDocComment(node);
// 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.
* @public
*/
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.
* @public
*/
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
* @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}).`);
}
/*
* 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.
* @public
*/
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;
+45
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;
+34
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)};`;
}
});
};