2025-04-19 23:12:19 -04:00

335 lines
11 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,
});
},
};