mirror of
https://github.com/10h30/odin-javascript-exercises.git
synced 2026-07-14 20:26:14 +09:00
remove timer and simon
This commit is contained in:
+29
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
};
|
||||
+36
File diff suppressed because one or more lines are too long
+140
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case "&":
|
||||
return "&";
|
||||
case "\"":
|
||||
return """;
|
||||
case "'":
|
||||
return "'";
|
||||
default:
|
||||
return `&#${c.charCodeAt(0)};`;
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user