337 lines
12 KiB
JavaScript
337 lines
12 KiB
JavaScript
/**
|
|
* @fileoverview JavaScript Language Object
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Requirements
|
|
//-----------------------------------------------------------------------------
|
|
|
|
const { SourceCode } = require("./source-code");
|
|
const createDebug = require("debug");
|
|
const astUtils = require("../../shared/ast-utils");
|
|
const espree = require("espree");
|
|
const eslintScope = require("eslint-scope");
|
|
const evk = require("eslint-visitor-keys");
|
|
const { validateLanguageOptions } = require("./validate-language-options");
|
|
const { LATEST_ECMA_VERSION } = require("../../../conf/ecma-version");
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Type Definitions
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/** @typedef {import("@eslint/core").File} File */
|
|
/** @typedef {import("@eslint/core").Language} Language */
|
|
/** @typedef {import("@eslint/core").OkParseResult} OkParseResult */
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
const debug = createDebug("eslint:languages:js");
|
|
const DEFAULT_ECMA_VERSION = 5;
|
|
const parserSymbol = Symbol.for("eslint.RuleTester.parser");
|
|
|
|
/**
|
|
* Analyze scope of the given AST.
|
|
* @param {ASTNode} ast The `Program` node to analyze.
|
|
* @param {LanguageOptions} languageOptions The parser options.
|
|
* @param {Record<string, string[]>} visitorKeys The visitor keys.
|
|
* @returns {ScopeManager} The analysis result.
|
|
*/
|
|
function analyzeScope(ast, languageOptions, visitorKeys) {
|
|
const parserOptions = languageOptions.parserOptions;
|
|
const ecmaFeatures = parserOptions.ecmaFeatures || {};
|
|
const ecmaVersion = languageOptions.ecmaVersion || DEFAULT_ECMA_VERSION;
|
|
|
|
return eslintScope.analyze(ast, {
|
|
ignoreEval: true,
|
|
nodejsScope: ecmaFeatures.globalReturn,
|
|
impliedStrict: ecmaFeatures.impliedStrict,
|
|
ecmaVersion: typeof ecmaVersion === "number" ? ecmaVersion : 6,
|
|
sourceType: languageOptions.sourceType || "script",
|
|
childVisitorKeys: visitorKeys || evk.KEYS,
|
|
fallback: evk.getKeys
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Determines if a given object is Espree.
|
|
* @param {Object} parser The parser to check.
|
|
* @returns {boolean} True if the parser is Espree or false if not.
|
|
*/
|
|
function isEspree(parser) {
|
|
return !!(parser === espree || parser[parserSymbol] === espree);
|
|
}
|
|
|
|
/**
|
|
* Normalize ECMAScript version from the initial config into languageOptions (year)
|
|
* format.
|
|
* @param {any} [ecmaVersion] ECMAScript version from the initial config
|
|
* @returns {number} normalized ECMAScript version
|
|
*/
|
|
function normalizeEcmaVersionForLanguageOptions(ecmaVersion) {
|
|
|
|
switch (ecmaVersion) {
|
|
case 3:
|
|
return 3;
|
|
|
|
// void 0 = no ecmaVersion specified so use the default
|
|
case 5:
|
|
case void 0:
|
|
return 5;
|
|
|
|
default:
|
|
if (typeof ecmaVersion === "number") {
|
|
return ecmaVersion >= 2015 ? ecmaVersion : ecmaVersion + 2009;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We default to the latest supported ecmaVersion for everything else.
|
|
* Remember, this is for languageOptions.ecmaVersion, which sets the version
|
|
* that is used for a number of processes inside of ESLint. It's normally
|
|
* safe to assume people want the latest unless otherwise specified.
|
|
*/
|
|
return LATEST_ECMA_VERSION;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @type {Language}
|
|
*/
|
|
module.exports = {
|
|
|
|
fileType: "text",
|
|
lineStart: 1,
|
|
columnStart: 0,
|
|
nodeTypeKey: "type",
|
|
visitorKeys: evk.KEYS,
|
|
|
|
defaultLanguageOptions: {
|
|
sourceType: "module",
|
|
ecmaVersion: "latest",
|
|
parser: espree,
|
|
parserOptions: {}
|
|
},
|
|
|
|
validateLanguageOptions,
|
|
|
|
/**
|
|
* Normalizes the language options.
|
|
* @param {Object} languageOptions The language options to normalize.
|
|
* @returns {Object} The normalized language options.
|
|
*/
|
|
normalizeLanguageOptions(languageOptions) {
|
|
|
|
languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions(
|
|
languageOptions.ecmaVersion
|
|
);
|
|
|
|
// Espree expects this information to be passed in
|
|
if (isEspree(languageOptions.parser)) {
|
|
const parserOptions = languageOptions.parserOptions;
|
|
|
|
if (languageOptions.sourceType) {
|
|
|
|
parserOptions.sourceType = languageOptions.sourceType;
|
|
|
|
if (
|
|
parserOptions.sourceType === "module" &&
|
|
parserOptions.ecmaFeatures &&
|
|
parserOptions.ecmaFeatures.globalReturn
|
|
) {
|
|
parserOptions.ecmaFeatures.globalReturn = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return languageOptions;
|
|
|
|
},
|
|
|
|
/**
|
|
* Determines if a given node matches a given selector class.
|
|
* @param {string} className The class name to check.
|
|
* @param {ASTNode} node The node to check.
|
|
* @param {Array<ASTNode>} ancestry The ancestry of the node.
|
|
* @returns {boolean} True if there's a match, false if not.
|
|
* @throws {Error} When an unknown class name is passed.
|
|
*/
|
|
matchesSelectorClass(className, node, ancestry) {
|
|
|
|
/*
|
|
* Copyright (c) 2013, Joel Feenstra
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * Neither the name of the ESQuery nor the names of its contributors may
|
|
* be used to endorse or promote products derived from this software without
|
|
* specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL JOEL FEENSTRA BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
switch (className.toLowerCase()) {
|
|
|
|
case "statement":
|
|
if (node.type.slice(-9) === "Statement") {
|
|
return true;
|
|
}
|
|
|
|
// fallthrough: interface Declaration <: Statement { }
|
|
|
|
case "declaration":
|
|
return node.type.slice(-11) === "Declaration";
|
|
|
|
case "pattern":
|
|
if (node.type.slice(-7) === "Pattern") {
|
|
return true;
|
|
}
|
|
|
|
// fallthrough: interface Expression <: Node, Pattern { }
|
|
|
|
case "expression":
|
|
return node.type.slice(-10) === "Expression" ||
|
|
node.type.slice(-7) === "Literal" ||
|
|
(
|
|
node.type === "Identifier" &&
|
|
(ancestry.length === 0 || ancestry[0].type !== "MetaProperty")
|
|
) ||
|
|
node.type === "MetaProperty";
|
|
|
|
case "function":
|
|
return node.type === "FunctionDeclaration" ||
|
|
node.type === "FunctionExpression" ||
|
|
node.type === "ArrowFunctionExpression";
|
|
|
|
default:
|
|
throw new Error(`Unknown class name: ${className}`);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Parses the given file into an AST.
|
|
* @param {File} file The virtual file to parse.
|
|
* @param {Object} options Additional options passed from ESLint.
|
|
* @param {LanguageOptions} options.languageOptions The language options.
|
|
* @returns {Object} The result of parsing.
|
|
*/
|
|
parse(file, { languageOptions }) {
|
|
|
|
// Note: BOM already removed
|
|
const { body: text, path: filePath } = file;
|
|
const textToParse = text.replace(astUtils.shebangPattern, (match, captured) => `//${captured}`);
|
|
const { ecmaVersion, sourceType, parser } = languageOptions;
|
|
const parserOptions = Object.assign(
|
|
{ ecmaVersion, sourceType },
|
|
languageOptions.parserOptions,
|
|
{
|
|
loc: true,
|
|
range: true,
|
|
raw: true,
|
|
tokens: true,
|
|
comment: true,
|
|
eslintVisitorKeys: true,
|
|
eslintScopeManager: true,
|
|
filePath
|
|
}
|
|
);
|
|
|
|
/*
|
|
* Check for parsing errors first. If there's a parsing error, nothing
|
|
* else can happen. However, a parsing error does not throw an error
|
|
* from this method - it's just considered a fatal error message, a
|
|
* problem that ESLint identified just like any other.
|
|
*/
|
|
try {
|
|
debug("Parsing:", filePath);
|
|
const parseResult = (typeof parser.parseForESLint === "function")
|
|
? parser.parseForESLint(textToParse, parserOptions)
|
|
: { ast: parser.parse(textToParse, parserOptions) };
|
|
|
|
debug("Parsing successful:", filePath);
|
|
|
|
const {
|
|
ast,
|
|
services: parserServices = {},
|
|
visitorKeys = evk.KEYS,
|
|
scopeManager
|
|
} = parseResult;
|
|
|
|
return {
|
|
ok: true,
|
|
ast,
|
|
parserServices,
|
|
visitorKeys,
|
|
scopeManager
|
|
};
|
|
} catch (ex) {
|
|
|
|
// If the message includes a leading line number, strip it:
|
|
const message = ex.message.replace(/^line \d+:/iu, "").trim();
|
|
|
|
debug("%s\n%s", message, ex.stack);
|
|
|
|
return {
|
|
ok: false,
|
|
errors: [{
|
|
message,
|
|
line: ex.lineNumber,
|
|
column: ex.column
|
|
}]
|
|
};
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
* Creates a new `SourceCode` object from the given information.
|
|
* @param {File} file The virtual file to create a `SourceCode` object from.
|
|
* @param {OkParseResult} parseResult The result returned from `parse()`.
|
|
* @param {Object} options Additional options passed from ESLint.
|
|
* @param {LanguageOptions} options.languageOptions The language options.
|
|
* @returns {SourceCode} The new `SourceCode` object.
|
|
*/
|
|
createSourceCode(file, parseResult, { languageOptions }) {
|
|
|
|
const { body: text, path: filePath, bom: hasBOM } = file;
|
|
const { ast, parserServices, visitorKeys } = parseResult;
|
|
|
|
debug("Scope analysis:", filePath);
|
|
const scopeManager = parseResult.scopeManager || analyzeScope(ast, languageOptions, visitorKeys);
|
|
|
|
debug("Scope analysis successful:", filePath);
|
|
|
|
return new SourceCode({
|
|
text,
|
|
ast,
|
|
hasBOM,
|
|
parserServices,
|
|
scopeManager,
|
|
visitorKeys
|
|
});
|
|
}
|
|
|
|
};
|