From a9da57beaf45941f8284ffe1b8949cff3a18b4e1 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 18 Oct 2021 14:33:54 -0700 Subject: [PATCH 01/37] Update: Flat config support in Linter (refs #13481) --- karma.conf.js | 9 + lib/config/default-config.js | 5 +- lib/config/flat-config-array.js | 4 +- lib/config/flat-config-helpers.js | 67 +++++ lib/config/flat-config-schema.js | 21 +- lib/config/rule-validator.js | 37 ++- lib/linter/linter.js | 293 ++++++++++++++++++++- package.json | 2 +- tests/lib/config/flat-config-array.js | 4 +- tests/lib/linter/linter.js | 349 ++++++++++++++++++++++++++ 10 files changed, 745 insertions(+), 46 deletions(-) create mode 100644 lib/config/flat-config-helpers.js diff --git a/karma.conf.js b/karma.conf.js index 05f1a1ceb6e..d52da0a3c28 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -16,6 +16,15 @@ module.exports = function(config) { // base path that will be used to resolve all patterns (eg. files, exclude) basePath: "", + // next three sections allow console.log to work + client: { + captureConsole: true + }, + + browserConsoleLogOptions: { + terminal: true, + level: "log" + }, /* * frameworks to use diff --git a/lib/config/default-config.js b/lib/config/default-config.js index cb6f403380d..51853ce4280 100644 --- a/lib/config/default-config.js +++ b/lib/config/default-config.js @@ -26,7 +26,7 @@ exports.defaultConfig = [ /* * Because we try to delay loading rules until absolutely - * necessary, a proxy allows us to hook into the lazy-loading + * necessary, a proxy allows us to hook into the lazy-loading * aspect of the rules map while still keeping all of the * relevant configuration inside of the config array. */ @@ -46,7 +46,8 @@ exports.defaultConfig = [ ".git/**" ], languageOptions: { - parser: "@/espree" + parser: "@/espree", + parserOptions: {} } } ]; diff --git a/lib/config/flat-config-array.js b/lib/config/flat-config-array.js index ef9cb33ca28..c06fd92a383 100644 --- a/lib/config/flat-config-array.js +++ b/lib/config/flat-config-array.js @@ -52,13 +52,13 @@ class FlatConfigArray extends ConfigArray { * @param {{basePath: string, baseConfig: FlatConfig}} options The options * to use for the config array instance. */ - constructor(configs, { basePath, baseConfig = defaultConfig }) { + constructor(configs, { basePath, baseConfig = defaultConfig } = {}) { super(configs, { basePath, schema: flatConfigSchema }); - this.unshift(baseConfig); + this.unshift(...baseConfig); } /* eslint-disable class-methods-use-this -- Desired as instance method */ diff --git a/lib/config/flat-config-helpers.js b/lib/config/flat-config-helpers.js new file mode 100644 index 00000000000..778f12925e1 --- /dev/null +++ b/lib/config/flat-config-helpers.js @@ -0,0 +1,67 @@ +/** + * @fileoverview Shared functions to work with configs. + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Functions +//----------------------------------------------------------------------------- + +/** + * Parses a ruleId into its plugin and rule parts. + * @param {string} ruleId The rule ID to parse. + * @returns {{pluginName:string,ruleName:string}} The plugin and rule + * parts of the ruleId; + */ +function parseRuleId(ruleId) { + let pluginName, ruleName; + + // distinguish between core rules and plugin rules + if (ruleId.includes("/")) { + pluginName = ruleId.slice(0, ruleId.lastIndexOf("/")); + ruleName = ruleId.slice(pluginName.length + 1); + } else { + pluginName = "@"; + ruleName = ruleId; + } + + return { + pluginName, + ruleName + }; +} + +/** + * Retrieves a rule instance from a given config based on the ruleId. + * @param {string} ruleId The rule ID to look for. + * @param {FlatConfig} config The config to search. + * @returns {import("../shared/types").Rule|undefined} The rule if found + * or undefined if not. + */ +function getRuleFromConfig(ruleId, config) { + + const { pluginName, ruleName } = parseRuleId(ruleId); + + const plugin = config.plugins && config.plugins[pluginName]; + let rule = plugin && plugin.rules && plugin.rules[ruleName]; + + // normalize function rules into objects + if (rule && typeof rule === "function") { + rule = { + create: rule + }; + } + + return rule; +} + +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- + +module.exports = { + parseRuleId, + getRuleFromConfig +}; diff --git a/lib/config/flat-config-schema.js b/lib/config/flat-config-schema.js index c8cc7114940..cb8e7961add 100644 --- a/lib/config/flat-config-schema.js +++ b/lib/config/flat-config-schema.js @@ -195,13 +195,6 @@ function assertIsObjectOrString(value) { // Low-Level Schemas //----------------------------------------------------------------------------- - -/** @type {ObjectPropertySchema} */ -const numberSchema = { - merge: "replace", - validate: "number" -}; - /** @type {ObjectPropertySchema} */ const booleanSchema = { merge: "replace", @@ -415,6 +408,18 @@ const rulesSchema = { } }; +/** @type {ObjectPropertySchema} */ +const ecmaVersionSchema = { + merge: "replace", + validate(value) { + if (typeof value === "number" || value === "latest") { + return; + } + + throw new TypeError("Expected a number or \"latest\"."); + } +}; + /** @type {ObjectPropertySchema} */ const sourceTypeSchema = { merge: "replace", @@ -439,7 +444,7 @@ exports.flatConfigSchema = { }, languageOptions: { schema: { - ecmaVersion: numberSchema, + ecmaVersion: ecmaVersionSchema, sourceType: sourceTypeSchema, globals: globalsSchema, parser: parserSchema, diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js index 527a56e1799..706b4fb03cc 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -10,30 +10,24 @@ //----------------------------------------------------------------------------- const ajv = require("../shared/ajv")(); +const { parseRuleId, getRuleFromConfig } = require("./flat-config-helpers"); //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- /** - * Finds a rule with the given ID in the given config. - * @param {string} ruleId The ID of the rule to find. + * Throws a helpful error when a rule cannot be found. + * @param {Object} ruleId The rule identifier. + * @param {string} ruleId.pluginName The ID of the rule to find. + * @param {string} ruleId.ruleName The ID of the rule to find. * @param {Object} config The config to search in. * @throws {TypeError} For missing plugin or rule. - * @returns {{create: Function, schema: (Array|null)}} THe rule object. + * @returns {void} */ -function findRuleDefinition(ruleId, config) { - const ruleIdParts = ruleId.split("/"); - let pluginName, ruleName; - - // built-in rule - if (ruleIdParts.length === 1) { - pluginName = "@"; - ruleName = ruleIdParts[0]; - } else { - ruleName = ruleIdParts.pop(); - pluginName = ruleIdParts.join("/"); - } +function throwRuleNotFoundError({ pluginName, ruleName }, config) { + + const ruleId = pluginName === "@" ? ruleName : `${pluginName}/${ruleName}`; const errorMessageHeader = `Key "rules": Key "${ruleId}"`; let errorMessage = `${errorMessageHeader}: Could not find plugin "${pluginName}".`; @@ -41,13 +35,6 @@ function findRuleDefinition(ruleId, config) { // if the plugin exists then we need to check if the rule exists if (config.plugins && config.plugins[pluginName]) { - const plugin = config.plugins[pluginName]; - - // first check for exact rule match - if (plugin.rules && plugin.rules[ruleName]) { - return config.plugins[pluginName].rules[ruleName]; - } - errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`; // otherwise, let's see if we can find the rule name elsewhere @@ -154,7 +141,11 @@ class RuleValidator { continue; } - const rule = findRuleDefinition(ruleId, config); + const rule = getRuleFromConfig(ruleId, config); + + if (!rule) { + throwRuleNotFoundError(parseRuleId(ruleId), config); + } // Precompile and cache validator the first time if (!this.validators.has(rule)) { diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 4e07a25751e..9dcc8d0f6f8 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -37,6 +37,8 @@ const SourceCodeFixer = require("./source-code-fixer"), timing = require("./timing"), ruleReplacements = require("../../conf/replacements.json"); +const { FlatConfigArray } = require("../config/flat-config-array"); +const { getRuleFromConfig } = require("../config/flat-config-helpers"); const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; @@ -511,7 +513,11 @@ function normalizeFilename(filename) { * @returns {Required & InternalOptions} Normalized options */ function normalizeVerifyOptions(providedOptions, config) { - const disableInlineConfig = config.noInlineConfig === true; + + const linterOptions = config.linterOptions || config; + + // .noInlineConfig for eslintrc, .linterOptions.noInlineConfig for flat + const disableInlineConfig = linterOptions.noInlineConfig === true; const ignoreInlineConfig = providedOptions.allowInlineConfig === false; const configNameOfNoInlineConfig = config.configNameOfNoInlineConfig ? ` (${config.configNameOfNoInlineConfig})` @@ -523,7 +529,9 @@ function normalizeVerifyOptions(providedOptions, config) { reportUnusedDisableDirectives = reportUnusedDisableDirectives ? "error" : "off"; } if (typeof reportUnusedDisableDirectives !== "string") { - reportUnusedDisableDirectives = config.reportUnusedDisableDirectives ? "warn" : "off"; + reportUnusedDisableDirectives = + linterOptions.reportUnusedDisableDirectives + ? "warn" : "off"; } return { @@ -1074,13 +1082,28 @@ function normalizeCwd(cwd) { */ const internalSlotsMap = new WeakMap(); +/** + * Throws an error when the given linter is in flat config mode. + * @param {Linter} linter The linter to check. + * @returns {void} + * @throws {Error} If the linter is in flat config mode. + */ +function assertEslintrcConfig(linter) { + const { configType } = internalSlotsMap.get(linter); + + if (configType === "flat") { + throw new Error("This method cannot be used with flat config. Add your entries directly into the config array."); + } +} + + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ /** * Object that is responsible for verifying JavaScript text - * @name eslint + * @name Linter */ class Linter { @@ -1088,12 +1111,14 @@ class Linter { * Initialize the Linter. * @param {Object} [config] the config object * @param {string} [config.cwd] path to a directory that should be considered as the current working directory, can be undefined. + * @param {"flat"|"eslintrc"} [config.configType="eslintrc"] the type of config used. */ - constructor({ cwd } = {}) { + constructor({ cwd, configType } = {}) { internalSlotsMap.set(this, { cwd: normalizeCwd(cwd), lastConfigArray: null, lastSourceCode: null, + configType, // TODO: Remove after flat config conversion parserMap: new Map([["espree", espree]]), ruleMap: new Rules() }); @@ -1270,15 +1295,43 @@ class Linter { */ verify(textOrSourceCode, config, filenameOrOptions) { debug("Verify"); + + const { configType } = internalSlotsMap.get(this); + const options = typeof filenameOrOptions === "string" ? { filename: filenameOrOptions } : filenameOrOptions || {}; - // CLIEngine passes a `ConfigArray` object. - if (config && typeof config.extractConfig === "function") { - return this._verifyWithConfigArray(textOrSourceCode, config, options); + if (config) { + if (configType === "flat") { + + /* + * Because of how Webpack packages up the files, we can't + * compare directly to `FlatConfigArray` using `instanceof` + * because it's not the same `FlatConfigArray` as in the tests. + * So, we work around it by assuming an array is, in fact, a + * `FlatConfigArray` and otherwise we have to create one. + */ + let configArray = config; + + if (!Array.isArray(config)) { + configArray = new FlatConfigArray(config); + configArray.normalizeSync(); + } + + return this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options); + } + + if (typeof config.extractConfig === "function") { + return this._verifyWithConfigArray(textOrSourceCode, config, options); + } } + /* + * If we get to here, it means `config` is just an object rather + * than a config array so we can go right into linting. + */ + /* * `Linter` doesn't support `overrides` property in configuration. * So we cannot apply multiple processors. @@ -1289,6 +1342,186 @@ class Linter { return this._verifyWithoutProcessors(textOrSourceCode, config, options); } + /** + * Verify with a processor. + * @param {string|SourceCode} textOrSourceCode The source code. + * @param {FlatConfig} config The config array. + * @param {VerifyOptions&ProcessorOptions} options The options. + * @param {FlatConfigArray} [configForRecursive] The `ConfigArray` object to apply multiple processors recursively. + * @returns {LintMessage[]} The found problems. + */ + _verifyWithFlatConfigArrayAndProcessor(textOrSourceCode, config, options, configForRecursive) { + const filename = options.filename || ""; + const filenameToExpose = normalizeFilename(filename); + const physicalFilename = options.physicalFilename || filenameToExpose; + const text = ensureText(textOrSourceCode); + const preprocess = options.preprocess || (rawText => [rawText]); + const postprocess = options.postprocess || (messagesList => messagesList.flat()); + const filterCodeBlock = + options.filterCodeBlock || + (blockFilename => blockFilename.endsWith(".js")); + const originalExtname = path.extname(filename); + const messageLists = preprocess(text, filenameToExpose).map((block, i) => { + debug("A code block was found: %o", block.filename || "(unnamed)"); + + // Keep the legacy behavior. + if (typeof block === "string") { + return this._verifyWithFlatConfigArrayAndWithoutProcessors(block, config, options); + } + + const blockText = block.text; + const blockName = path.join(filename, `${i}_${block.filename}`); + + // Skip this block if filtered. + if (!filterCodeBlock(blockName, blockText)) { + debug("This code block was skipped."); + return []; + } + + // Resolve configuration again if the file content or extension was changed. + if (configForRecursive && (text !== blockText || path.extname(blockName) !== originalExtname)) { + debug("Resolving configuration again because the file content or extension was changed."); + return this._verifyWithFlatConfigArray( + blockText, + configForRecursive, + { ...options, filename: blockName, physicalFilename } + ); + } + + // Does lint. + return this._verifyWithFlatConfigArrayAndWithoutProcessors( + blockText, + config, + { ...options, filename: blockName, physicalFilename } + ); + }); + + return postprocess(messageLists, filenameToExpose); + } + + /** + * Same as linter.verify, except without support for processors. + * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object. + * @param {FlatConfig} providedConfig An ESLintConfig instance to configure everything. + * @param {VerifyOptions} [providedOptions] The optional filename of the file being checked. + * @throws {Error} If during rule execution. + * @returns {LintMessage[]} The results as an array of messages or an empty array if no messages. + */ + _verifyWithFlatConfigArrayAndWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) { + const slots = internalSlotsMap.get(this); + const config = providedConfig || {}; + const options = normalizeVerifyOptions(providedOptions, config); + let text; + + // evaluate arguments + if (typeof textOrSourceCode === "string") { + slots.lastSourceCode = null; + text = textOrSourceCode; + } else { + slots.lastSourceCode = textOrSourceCode; + text = textOrSourceCode.text; + } + + const languageOptions = config.languageOptions; + const configuredGlobals = languageOptions.globals || {}; + const settings = config.settings || {}; + + if (!slots.lastSourceCode) { + const parseResult = parse( + text, + languageOptions.parser, + languageOptions.parserOptions, + options.filename + ); + + if (!parseResult.success) { + return [parseResult.error]; + } + + slots.lastSourceCode = parseResult.sourceCode; + } else { + + /* + * If the given source code object as the first argument does not have scopeManager, analyze the scope. + * This is for backward compatibility (SourceCode is frozen so it cannot rebind). + */ + if (!slots.lastSourceCode.scopeManager) { + slots.lastSourceCode = new SourceCode({ + text: slots.lastSourceCode.text, + ast: slots.lastSourceCode.ast, + parserServices: slots.lastSourceCode.parserServices, + visitorKeys: slots.lastSourceCode.visitorKeys, + scopeManager: analyzeScope(slots.lastSourceCode.ast, languageOptions.parserOptions) + }); + } + } + + const sourceCode = slots.lastSourceCode; + const commentDirectives = options.allowInlineConfig + ? getDirectiveComments( + options.filename, + sourceCode.ast, + ruleId => FlatConfigArray.getRuleFromConfig(ruleId, config), + options.warnInlineConfig + ) + : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] }; + + // augment global scope with declared global variables + addDeclaredGlobals( + sourceCode.scopeManager.scopes[0], + configuredGlobals, + { exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals } + ); + + const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules); + + let lintingProblems; + + try { + lintingProblems = runRules( + sourceCode, + configuredRules, + ruleId => getRuleFromConfig(ruleId, config), + languageOptions.parserOptions, + String(languageOptions.parser), + settings, + options.filename, + options.disableFixes, + slots.cwd, + providedOptions.physicalFilename + ); + } catch (err) { + err.message += `\nOccurred while linting ${options.filename}`; + debug("An error occurred while traversing"); + debug("Filename:", options.filename); + if (err.currentNode) { + const { line } = err.currentNode.loc.start; + + debug("Line:", line); + err.message += `:${line}`; + } + debug("Parser Options:", languageOptions.parserOptions); + + // debug("Parser Path:", parserName); + debug("Settings:", settings); + + if (err.ruleId) { + err.message += `\nRule: "${err.ruleId}"`; + } + + throw err; + } + + return applyDisableDirectives({ + directives: commentDirectives.disableDirectives, + disableFixes: options.disableFixes, + problems: lintingProblems + .concat(commentDirectives.problems) + .sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column), + reportUnusedDisableDirectives: options.reportUnusedDisableDirectives + }); + } + /** * Verify a given code with `ConfigArray`. * @param {string|SourceCode} textOrSourceCode The source code. @@ -1324,6 +1557,46 @@ class Linter { return this._verifyWithoutProcessors(textOrSourceCode, config, options); } + /** + * Verify a given code with a flat config. + * @param {string|SourceCode} textOrSourceCode The source code. + * @param {FlatConfigArray} configArray The config array. + * @param {VerifyOptions&ProcessorOptions} options The options. + * @returns {LintMessage[]} The found problems. + */ + _verifyWithFlatConfigArray(textOrSourceCode, configArray, options) { + debug("With flat config: %s", options.filename); + + // we need a filename to match configs against + const filename = options.filename || "code.js"; + + // Store the config array in order to get plugin envs and rules later. + internalSlotsMap.get(this).lastConfigArray = configArray; + const config = configArray.getConfig(filename); + + // Verify. + if (config.processor) { + debug("Apply the processor: %o", config.processor); + const { preprocess, postprocess, supportsAutofix } = config.processor; + const disableFixes = options.disableFixes || !supportsAutofix; + + return this._verifyWithFlatConfigArrayAndProcessor( + textOrSourceCode, + config, + { ...options, filename, disableFixes, postprocess, preprocess }, + configArray + ); + } + + + // check for options-based processing + if (options.preprocess || options.postprocess) { + return this._verifyWithFlatConfigArrayAndProcessor(textOrSourceCode, config, options); + } + + return this._verifyWithFlatConfigArrayAndWithoutProcessors(textOrSourceCode, config, options); + } + /** * Verify with a processor. * @param {string|SourceCode} textOrSourceCode The source code. @@ -1397,6 +1670,7 @@ class Linter { * @returns {void} */ defineRule(ruleId, ruleModule) { + assertEslintrcConfig(this); internalSlotsMap.get(this).ruleMap.define(ruleId, ruleModule); } @@ -1406,6 +1680,7 @@ class Linter { * @returns {void} */ defineRules(rulesToDefine) { + assertEslintrcConfig(this); Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => { this.defineRule(ruleId, rulesToDefine[ruleId]); }); @@ -1416,6 +1691,7 @@ class Linter { * @returns {Map} All loaded rules */ getRules() { + assertEslintrcConfig(this); const { lastConfigArray, ruleMap } = internalSlotsMap.get(this); return new Map(function *() { @@ -1434,6 +1710,7 @@ class Linter { * @returns {void} */ defineParser(parserId, parserModule) { + assertEslintrcConfig(this); internalSlotsMap.get(this).parserMap.set(parserId, parserModule); } @@ -1441,7 +1718,7 @@ class Linter { * Performs multiple autofix passes over the text until as many fixes as possible * have been applied. * @param {string} text The source text to apply fixes to. - * @param {ConfigData|ConfigArray} config The ESLint config object to use. + * @param {ConfigData|ConfigArray|FlatConfigArray} config The ESLint config object to use. * @param {VerifyOptions&ProcessorOptions&FixOptions} options The ESLint options object to use. * @returns {{fixed:boolean,messages:LintMessage[],output:string}} The result of the fix operation as returned from the * SourceCodeFixer. diff --git a/package.json b/package.json index 1b555897697..d1515bd9c7e 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "bugs": "https://github.com/eslint/eslint/issues/", "dependencies": { "@eslint/eslintrc": "^1.0.4", - "@humanwhocodes/config-array": "^0.6.0", + "@humanwhocodes/config-array": "^0.9.1", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index f6b099096e1..64ed2253476 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -78,7 +78,7 @@ const baseConfig = { function createFlatConfigArray(configs) { return new FlatConfigArray(configs, { basePath: __dirname, - baseConfig + baseConfig: [baseConfig] }); } @@ -638,7 +638,7 @@ describe("FlatConfigArray", () => { ecmaVersion: "true" } } - ], "Expected a number."); + ], /Key "languageOptions": Key "ecmaVersion": Expected a number or "latest"\./u); }); it("should merge two objects when second object has overrides", () => assertMergedResult([ diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 24dc4de43ce..cb93031ca3c 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -16,6 +16,7 @@ const assert = require("chai").assert, testParsers = require("../../fixtures/parsers/linter-test-parsers"); const { Linter } = require("../../../lib/linter"); +const { FlatConfigArray } = require("../../../lib/config/flat-config-array"); //------------------------------------------------------------------------------ // Constants @@ -6242,3 +6243,351 @@ var a = "test2"; }); }); }); + +describe("Linter with FlatConfigArray", () => { + + let linter; + const filename = "filename.js"; + + /** + * Creates a config array with some default properties. + * @param {FlatConfig|FlatConfig[]} value The value to base the + * config array on. + * @returns {FlatConfigArray} The created config array. + */ + function createFlatConfigArray(value) { + return new FlatConfigArray(value, { basePath: "" }); + } + + beforeEach(() => { + linter = new Linter({ configType: "flat" }); + }); + + describe("verify()", () => { + + it("rule should run as warning when set to 1", () => { + const ruleId = "semi", + configs = createFlatConfigArray({ + files: ["**/*.js"], + rules: { + [ruleId]: 1 + } + }); + + configs.normalizeSync(); + const messages = linter.verify("foo", configs, filename, true); + + assert.strictEqual(messages.length, 1, "Message length is wrong"); + assert.strictEqual(messages[0].ruleId, ruleId); + }); + + }); + + describe("defineRule()", () => { + it("should throw an error when called in flat config mode", () => { + assert.throws(() => { + linter.defineRule("foo", () => {}); + }, /This method cannot be used with flat config/u); + }); + }); + + describe("defineRules()", () => { + it("should throw an error when called in flat config mode", () => { + assert.throws(() => { + linter.defineRules({}); + }, /This method cannot be used with flat config/u); + }); + }); + + describe("defineParser()", () => { + it("should throw an error when called in flat config mode", () => { + assert.throws(() => { + linter.defineParser("foo", {}); + }, /This method cannot be used with flat config/u); + }); + }); + + describe("processors", () => { + let receivedFilenames = []; + let receivedPhysicalFilenames = []; + const extraConfig = { + plugins: { + test: { + rules: { + "report-original-text": { + meta: { + + }, + create(context) { + return { + Program(ast) { + receivedFilenames.push(context.getFilename()); + receivedPhysicalFilenames.push(context.getPhysicalFilename()); + context.report({ node: ast, message: context.getSourceCode().text }); + } + }; + } + } + } + } + } + }; + + beforeEach(() => { + receivedFilenames = []; + receivedPhysicalFilenames = []; + }); + + describe("preprocessors", () => { + it("should receive text and filename.", () => { + const code = "foo bar baz"; + const preprocess = sinon.spy(text => text.split(" ")); + const configs = createFlatConfigArray({}); + + configs.normalizeSync(); + + linter.verify(code, configs, { filename, preprocess }); + + assert.strictEqual(preprocess.calledOnce, true, "preprocess wasn't called"); + assert.deepStrictEqual(preprocess.args[0], [code, filename], "preprocess was called with the wrong arguments"); + }); + + it("should apply a preprocessor to the code, and lint each code sample separately", () => { + const code = "foo bar baz"; + const configs = createFlatConfigArray([ + extraConfig, + { rules: { "test/report-original-text": "error" } } + ]); + + configs.normalizeSync(); + + const problems = linter.verify( + code, + configs, + { + filename, + + // Apply a preprocessor that splits the source text into spaces and lints each word individually + preprocess(input) { + return input.split(" "); + } + } + ); + + assert.strictEqual(problems.length, 3); + assert.deepStrictEqual(problems.map(problem => problem.message), ["foo", "bar", "baz"]); + }); + + it("should apply a preprocessor to the code even if the preprocessor returned code block objects.", () => { + const code = "foo bar baz"; + const configs = createFlatConfigArray([ + extraConfig, + { rules: { "test/report-original-text": "error" } } + ]); + + configs.normalizeSync(); + + const problems = linter.verify( + code, + configs, + { + filename, + + // Apply a preprocessor that splits the source text into spaces and lints each word individually + preprocess(input) { + return input.split(" ").map(text => ({ + filename: "block.js", + text + })); + } + } + ); + + assert.strictEqual(problems.length, 3); + assert.deepStrictEqual(problems.map(problem => problem.message), ["foo", "bar", "baz"]); + + // filename + assert.strictEqual(receivedFilenames.length, 3); + assert(/^filename\.js[/\\]0_block\.js/u.test(receivedFilenames[0])); + assert(/^filename\.js[/\\]1_block\.js/u.test(receivedFilenames[1])); + assert(/^filename\.js[/\\]2_block\.js/u.test(receivedFilenames[2])); + + // physical filename + assert.strictEqual(receivedPhysicalFilenames.length, 3); + assert.strictEqual(receivedPhysicalFilenames.every(name => name === filename), true); + }); + + it("should receive text even if a SourceCode object was given.", () => { + const code = "foo"; + const preprocess = sinon.spy(text => text.split(" ")); + const configs = createFlatConfigArray([ + extraConfig + ]); + + configs.normalizeSync(); + + linter.verify(code, configs); + const sourceCode = linter.getSourceCode(); + + linter.verify(sourceCode, configs, { filename, preprocess }); + + assert.strictEqual(preprocess.calledOnce, true); + assert.deepStrictEqual(preprocess.args[0], [code, filename]); + }); + + it("should receive text even if a SourceCode object was given (with BOM).", () => { + const code = "\uFEFFfoo"; + const preprocess = sinon.spy(text => text.split(" ")); + const configs = createFlatConfigArray([ + extraConfig + ]); + + configs.normalizeSync(); + + linter.verify(code, configs); + const sourceCode = linter.getSourceCode(); + + linter.verify(sourceCode, configs, { filename, preprocess }); + + assert.strictEqual(preprocess.calledOnce, true); + assert.deepStrictEqual(preprocess.args[0], [code, filename]); + }); + }); + + describe("postprocessors", () => { + it("should receive result and filename.", () => { + const code = "foo bar baz"; + const preprocess = sinon.spy(text => text.split(" ")); + const postprocess = sinon.spy(text => [text]); + const configs = createFlatConfigArray([ + extraConfig + ]); + + configs.normalizeSync(); + + linter.verify(code, configs, { filename, postprocess, preprocess }); + + assert.strictEqual(postprocess.calledOnce, true); + assert.deepStrictEqual(postprocess.args[0], [[[], [], []], filename]); + }); + + it("should apply a postprocessor to the reported messages", () => { + const code = "foo bar baz"; + const configs = createFlatConfigArray([ + extraConfig, + { rules: { "test/report-original-text": "error" } } + ]); + + configs.normalizeSync(); + + const problems = linter.verify( + code, + configs, + { + preprocess: input => input.split(" "), + + /* + * Apply a postprocessor that updates the locations of the reported problems + * to make sure they correspond to the locations in the original text. + */ + postprocess(problemLists) { + problemLists.forEach(problemList => assert.strictEqual(problemList.length, 1)); + return problemLists.reduce( + (combinedList, problemList, index) => + combinedList.concat( + problemList.map( + problem => + Object.assign( + {}, + problem, + { + message: problem.message.toUpperCase(), + column: problem.column + index * 4 + } + ) + ) + ), + [] + ); + } + } + ); + + assert.strictEqual(problems.length, 3); + assert.deepStrictEqual(problems.map(problem => problem.message), ["FOO", "BAR", "BAZ"]); + assert.deepStrictEqual(problems.map(problem => problem.column), [1, 5, 9]); + }); + + it("should use postprocessed problem ranges when applying autofixes", () => { + const code = "foo bar baz"; + const configs = createFlatConfigArray([ + extraConfig, + { + plugins: { + test2: { + rules: { + "capitalize-identifiers": { + meta: { + fixable: "code" + }, + create(context) { + return { + Identifier(node) { + if (node.name !== node.name.toUpperCase()) { + context.report({ + node, + message: "Capitalize this identifier", + fix: fixer => fixer.replaceText(node, node.name.toUpperCase()) + }); + } + } + }; + } + } + } + } + } + }, + { rules: { "test2/capitalize-identifiers": "error" } } + ]); + + configs.normalizeSync(); + + const fixResult = linter.verifyAndFix( + code, + configs, + { + + /* + * Apply a postprocessor that updates the locations of autofixes + * to make sure they correspond to locations in the original text. + */ + preprocess: input => input.split(" "), + postprocess(problemLists) { + return problemLists.reduce( + (combinedProblems, problemList, blockIndex) => + combinedProblems.concat( + problemList.map(problem => + Object.assign(problem, { + fix: { + text: problem.fix.text, + range: problem.fix.range.map( + rangeIndex => rangeIndex + blockIndex * 4 + ) + } + })) + ), + [] + ); + } + } + ); + + assert.strictEqual(fixResult.fixed, true); + assert.strictEqual(fixResult.messages.length, 0); + assert.strictEqual(fixResult.output, "FOO BAR BAZ"); + }); + }); + }); + +}); From a29a5579b45f2700cbf71c2de37d394e87a0d5b2 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 18 Oct 2021 17:22:08 -0700 Subject: [PATCH 02/37] Update lib/linter/linter.js Co-authored-by: Milos Djermanovic --- lib/linter/linter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 9dcc8d0f6f8..e6acfcee370 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -37,7 +37,6 @@ const SourceCodeFixer = require("./source-code-fixer"), timing = require("./timing"), ruleReplacements = require("../../conf/replacements.json"); -const { FlatConfigArray } = require("../config/flat-config-array"); const { getRuleFromConfig } = require("../config/flat-config-helpers"); const debug = require("debug")("eslint:linter"); From 38c0d16029323f8ba15e6832c58afd090ffd8d1d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 18 Oct 2021 17:22:14 -0700 Subject: [PATCH 03/37] Update lib/linter/linter.js Co-authored-by: Milos Djermanovic --- lib/linter/linter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index e6acfcee370..d448d8d4177 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -1460,7 +1460,7 @@ class Linter { ? getDirectiveComments( options.filename, sourceCode.ast, - ruleId => FlatConfigArray.getRuleFromConfig(ruleId, config), + ruleId => getRuleFromConfig(ruleId, config), options.warnInlineConfig ) : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] }; From f2a7458d43f483585b8578b0f712475fe5b3e9de Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 18 Oct 2021 18:12:46 -0700 Subject: [PATCH 04/37] Clean up FlatConfigArray detection --- lib/linter/linter.js | 4 ++-- tests/lib/linter/linter.js | 32 +++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index d448d8d4177..53175dd1095 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -1309,11 +1309,11 @@ class Linter { * compare directly to `FlatConfigArray` using `instanceof` * because it's not the same `FlatConfigArray` as in the tests. * So, we work around it by assuming an array is, in fact, a - * `FlatConfigArray` and otherwise we have to create one. + * `FlatConfigArray` if it has a `getConfig()` method. */ let configArray = config; - if (!Array.isArray(config)) { + if (!Array.isArray(config) || typeof config.getConfig !== "function") { configArray = new FlatConfigArray(config); configArray.normalizeSync(); } diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index cb93031ca3c..88416b3df11 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6265,7 +6265,7 @@ describe("Linter with FlatConfigArray", () => { describe("verify()", () => { - it("rule should run as warning when set to 1", () => { + it("rule should run as warning when set to 1 with a config array", () => { const ruleId = "semi", configs = createFlatConfigArray({ files: ["**/*.js"], @@ -6281,6 +6281,36 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(messages[0].ruleId, ruleId); }); + it("rule should run as warning when set to 1 with a plain array", () => { + const ruleId = "semi", + configs = [{ + files: ["**/*.js"], + rules: { + [ruleId]: 1 + } + }]; + + const messages = linter.verify("foo", configs, filename, true); + + assert.strictEqual(messages.length, 1, "Message length is wrong"); + assert.strictEqual(messages[0].ruleId, ruleId); + }); + + it("rule should run as warning when set to 1 with an object", () => { + const ruleId = "semi", + config = { + files: ["**/*.js"], + rules: { + [ruleId]: 1 + } + }; + + const messages = linter.verify("foo", config, filename, true); + + assert.strictEqual(messages.length, 1, "Message length is wrong"); + assert.strictEqual(messages[0].ruleId, ruleId); + }); + }); describe("defineRule()", () => { From 66cf8a3e8c95312a5d15d600429be7ac0295dd63 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 18 Oct 2021 18:13:48 -0700 Subject: [PATCH 05/37] Add back FlatConfigArray import --- lib/linter/linter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 53175dd1095..3267fbdbfc0 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -38,6 +38,7 @@ const timing = require("./timing"), ruleReplacements = require("../../conf/replacements.json"); const { getRuleFromConfig } = require("../config/flat-config-helpers"); +const { FlatConfigArray } = require("../config/flat-config-array"); const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; From dfdd4fad6531deb8c37601cd557b02f4dcc62e7e Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 20 Oct 2021 10:53:37 -0700 Subject: [PATCH 06/37] More flat config tests passing; originals failing --- lib/config/default-config.js | 1 + lib/linter/linter.js | 90 +++++++++++++------ lib/shared/types.js | 8 ++ tests/lib/linter/linter.js | 168 +++++++++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 26 deletions(-) diff --git a/lib/config/default-config.js b/lib/config/default-config.js index 51853ce4280..8529d455af8 100644 --- a/lib/config/default-config.js +++ b/lib/config/default-config.js @@ -46,6 +46,7 @@ exports.defaultConfig = [ ".git/**" ], languageOptions: { + sourceType: "script", parser: "@/espree", parserOptions: {} } diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 3267fbdbfc0..0e97b86476c 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -59,6 +59,7 @@ const parserSymbol = Symbol.for("eslint.RuleTester.parser"); /** @typedef {import("../shared/types").GlobalConf} GlobalConf */ /** @typedef {import("../shared/types").LintMessage} LintMessage */ /** @typedef {import("../shared/types").ParserOptions} ParserOptions */ +/** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */ /** @typedef {import("../shared/types").Processor} Processor */ /** @typedef {import("../shared/types").Rule} Rule */ @@ -574,6 +575,31 @@ function resolveParserOptions(parser, providedOptions, enabledEnvironments) { return mergedParserOptions; } +/** + * Converts parserOptions to languageOptions for backwards compatibility with eslintrc. + * @param {ConfigData} config Config object. + * @param {Object} config.globals Global variable definitions. + * @param {Parser} config.parser The parser to use. + * @param {ParserOptions} config.parserOptions The parserOptions to use. + * @returns {LanguageOptions} The languageOptions equivalent. + */ +function createLanguageOptions({ globals, parser, parserOptions}) { + + const { + ecmaVersion, + sourceType, + ...justParserOptions + } = parserOptions; + + return { + globals, + ecmaVersion, + sourceType, + parser, + ...justParserOptions + }; +} + /** * Combines the provided globals object with the globals from environments * @param {Record} providedGlobals The 'globals' key in a config @@ -622,20 +648,21 @@ function getRuleOptions(ruleConfig) { /** * Analyze scope of the given AST. * @param {ASTNode} ast The `Program` node to analyze. - * @param {ParserOptions} parserOptions The parser options. + * @param {LanguageOptions} languageOptions The parser options. * @param {Record} visitorKeys The visitor keys. * @returns {ScopeManager} The analysis result. */ -function analyzeScope(ast, parserOptions, visitorKeys) { +function analyzeScope(ast, languageOptions, visitorKeys) { + const parserOptions = languageOptions.parserOptions; const ecmaFeatures = parserOptions.ecmaFeatures || {}; - const ecmaVersion = parserOptions.ecmaVersion || DEFAULT_ECMA_VERSION; + 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: parserOptions.sourceType || "script", + sourceType: languageOptions.sourceType || "script", childVisitorKeys: visitorKeys || evk.KEYS, fallback: Traverser.getKeys }); @@ -646,16 +673,17 @@ function analyzeScope(ast, parserOptions, visitorKeys) { * optimization of functions, so it's best to keep the try-catch as isolated * as possible * @param {string} text The text to parse. - * @param {Parser} parser The parser to parse. - * @param {ParserOptions} providedParserOptions Options to pass to the parser + * @param {LanguageOptions} languageOptions Options to pass to the parser * @param {string} filePath The path to the file being parsed. * @returns {{success: false, error: Problem}|{success: true, sourceCode: SourceCode}} * An object containing the AST and parser services if parsing was successful, or the error if parsing failed * @private */ -function parse(text, parser, providedParserOptions, filePath) { +function parse(text, languageOptions, filePath) { const textToParse = stripUnicodeBOM(text).replace(astUtils.shebangPattern, (match, captured) => `//${captured}`); - const parserOptions = Object.assign({}, providedParserOptions, { + const parser = languageOptions.parser; + const parserIsEspree = parser === espree; + const parserOptions = Object.assign({}, languageOptions.parserOptions, { loc: true, range: true, raw: true, @@ -666,6 +694,11 @@ function parse(text, parser, providedParserOptions, filePath) { filePath }); + if (parserIsEspree) { + parserOptions.ecmaVersion = languageOptions.ecmaVersion; + parserOptions.sourceType = languageOptions.sourceType; + } + /* * Check for parsing errors first. If there's a parsing error, nothing * else can happen. However, a parsing error does not throw an error @@ -679,7 +712,7 @@ function parse(text, parser, providedParserOptions, filePath) { const ast = parseResult.ast; const parserServices = parseResult.services || {}; const visitorKeys = parseResult.visitorKeys || evk.KEYS; - const scopeManager = parseResult.scopeManager || analyzeScope(ast, parserOptions, visitorKeys); + const scopeManager = parseResult.scopeManager || analyzeScope(ast, languageOptions, visitorKeys); return { success: true, @@ -748,13 +781,14 @@ function getScope(scopeManager, currentNode) { * Marks a variable as used in the current scope * @param {ScopeManager} scopeManager The scope manager for this AST. The scope may be mutated by this function. * @param {ASTNode} currentNode The node currently being traversed - * @param {Object} parserOptions The options used to parse this text + * @param {LanguageOptions} languageOptions The options used to parse this text * @param {string} name The name of the variable that should be marked as used. * @returns {boolean} True if the variable was found and marked as used, false if not. */ -function markVariableAsUsed(scopeManager, currentNode, parserOptions, name) { +function markVariableAsUsed(scopeManager, currentNode, languageOptions, name) { + const parserOptions = languageOptions.parserOptions; const hasGlobalReturn = parserOptions.ecmaFeatures && parserOptions.ecmaFeatures.globalReturn; - const specialScope = hasGlobalReturn || parserOptions.sourceType === "module"; + const specialScope = hasGlobalReturn || languageOptions.sourceType === "module"; const currentScope = getScope(scopeManager, currentNode); // Special Node.js scope means we need to start one level deeper @@ -845,7 +879,7 @@ const BASE_TRAVERSAL_CONTEXT = Object.freeze( * @param {SourceCode} sourceCode A SourceCode object for the given text * @param {Object} configuredRules The rules configuration * @param {function(string): Rule} ruleMapper A mapper function from rule names to rules - * @param {Object} parserOptions The options that were passed to the parser + * @param {LanguageOptions} languageOptions The options for parsing the code. * @param {string} parserName The name of the parser in the config * @param {Object} settings The settings that were enabled in the config * @param {string} filename The reported filename of the code @@ -854,7 +888,7 @@ const BASE_TRAVERSAL_CONTEXT = Object.freeze( * @param {string} physicalFilename The full path of the file on disk without any code block information * @returns {Problem[]} An array of reported problems */ -function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd, physicalFilename) { +function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename) { const emitter = createEmitter(); const nodeQueue = []; let currentNode = sourceCode.ast; @@ -886,9 +920,10 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser getPhysicalFilename: () => physicalFilename || filename, getScope: () => getScope(sourceCode.scopeManager, currentNode), getSourceCode: () => sourceCode, - markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name), - parserOptions, + markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, languageOptions, name), + parserOptions: languageOptions.parserOptions, parserPath: parserName, + languageOptions, parserServices: sourceCode.parserServices, settings } @@ -1193,12 +1228,16 @@ class Linter { const parserOptions = resolveParserOptions(parser, config.parserOptions || {}, enabledEnvs); const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs); const settings = config.settings || {}; + const languageOptions = createLanguageOptions({ + globals: config.globals, + parser, + parserOptions + }); if (!slots.lastSourceCode) { const parseResult = parse( text, - parser, - parserOptions, + languageOptions, options.filename ); @@ -1219,7 +1258,7 @@ class Linter { ast: slots.lastSourceCode.ast, parserServices: slots.lastSourceCode.parserServices, visitorKeys: slots.lastSourceCode.visitorKeys, - scopeManager: analyzeScope(slots.lastSourceCode.ast, parserOptions) + scopeManager: analyzeScope(slots.lastSourceCode.ast, languageOptions) }); } } @@ -1245,8 +1284,8 @@ class Linter { sourceCode, configuredRules, ruleId => getRule(slots, ruleId), - parserOptions, parserName, + languageOptions, settings, options.filename, options.disableFixes, @@ -1315,7 +1354,7 @@ class Linter { let configArray = config; if (!Array.isArray(config) || typeof config.getConfig !== "function") { - configArray = new FlatConfigArray(config); + configArray = new FlatConfigArray(config || {}); configArray.normalizeSync(); } @@ -1429,8 +1468,7 @@ class Linter { if (!slots.lastSourceCode) { const parseResult = parse( text, - languageOptions.parser, - languageOptions.parserOptions, + languageOptions, options.filename ); @@ -1451,7 +1489,7 @@ class Linter { ast: slots.lastSourceCode.ast, parserServices: slots.lastSourceCode.parserServices, visitorKeys: slots.lastSourceCode.visitorKeys, - scopeManager: analyzeScope(slots.lastSourceCode.ast, languageOptions.parserOptions) + scopeManager: analyzeScope(slots.lastSourceCode.ast, languageOptions) }); } } @@ -1482,8 +1520,8 @@ class Linter { sourceCode, configuredRules, ruleId => getRuleFromConfig(ruleId, config), - languageOptions.parserOptions, - String(languageOptions.parser), + "TODO: Create backcompat parser name", + languageOptions, settings, options.filename, options.disableFixes, diff --git a/lib/shared/types.js b/lib/shared/types.js index c497f783be5..2fcb2d7aa3c 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -25,6 +25,14 @@ module.exports = {}; * @property {"script"|"module"} [sourceType] The source code type. */ +/** + * @typedef {Object} LanguageOptions + * @property {number|"latest"} [ecmaVersion] The ECMAScript version (or revision number). + * @property {"script"|"module"} [sourceType] The source code type. + * @property {string|Object} [parser] The parser to use. + * @property {Object} [parserOptions] The parser options to use. + */ + /** * @typedef {Object} ConfigData * @property {Record} [env] The environment settings. diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 88416b3df11..4f334ca4c4e 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6263,6 +6263,100 @@ describe("Linter with FlatConfigArray", () => { linter = new Linter({ configType: "flat" }); }); + describe("when using events", () => { + const code = TEST_CODE; + + it("an error should be thrown when an error occurs inside of an event handler", () => { + const config = { + plugins: { + test: { + rules: { + checker: () => ({ + Program() { + throw new Error("Intentional error."); + } + }) + } + } + }, + rules: { "test/checker": "error" } + }; + + assert.throws(() => { + linter.verify(code, config, filename); + }, `Intentional error.\nOccurred while linting ${filename}:1\nRule: "test/checker"`); + }); + + it("does not call rule listeners with a `this` value", () => { + const spy = sinon.spy(); + const config = { + plugins: { + test: { + rules: { + checker: () => ({ + Program: spy + }) + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config); + assert(spy.calledOnce); + assert.strictEqual(spy.firstCall.thisValue, void 0); + }); + + it("does not allow listeners to use special EventEmitter values", () => { + const spy = sinon.spy(); + const config = { + plugins: { + test: { + rules: { + checker: () => ({ + newListener: spy + }) + } + } + }, + rules: { + "test/checker": "error", + "no-undef": "error" + } + }; + + linter.verify("foo", config); + assert(spy.notCalled); + }); + + it("has all the `parent` properties on nodes when the rule listeners are created", () => { + const spy = sinon.spy(context => { + const ast = context.getSourceCode().ast; + + assert.strictEqual(ast.body[0].parent, ast); + assert.strictEqual(ast.body[0].expression.parent, ast.body[0]); + assert.strictEqual(ast.body[0].expression.left.parent, ast.body[0].expression); + assert.strictEqual(ast.body[0].expression.right.parent, ast.body[0].expression); + + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo + bar", config); + assert(spy.calledOnce); + }); + }); + describe("verify()", () => { it("rule should run as warning when set to 1 with a config array", () => { @@ -6620,4 +6714,78 @@ describe("Linter with FlatConfigArray", () => { }); }); + + describe("Edge cases", () => { + + describe("Modules", () => { + const moduleConfig = { + languageOptions: { + sourceType: "module", + ecmaVersion: 6 + } + }; + + it("should properly parse import statements when sourceType is module", () => { + const code = "import foo from 'foo';"; + const messages = linter.verify(code, moduleConfig); + + assert.strictEqual(messages.length, 0, "Unexpected linting error."); + }); + + it("should properly parse import all statements when sourceType is module", () => { + const code = "import * as foo from 'foo';"; + const messages = linter.verify(code, moduleConfig); + + assert.strictEqual(messages.length, 0, "Unexpected linting error."); + }); + + it("should properly parse default export statements when sourceType is module", () => { + const code = "export default function initialize() {}"; + const messages = linter.verify(code, moduleConfig); + + assert.strictEqual(messages.length, 0, "Unexpected linting error."); + }); + + }); + + + // https://github.com/eslint/eslint/issues/9687 + it("should report an error when invalid languageOptions found", () => { + let messages = linter.verify("", { languageOptions: { ecmaVersion: 222 } }); + + assert.deepStrictEqual(messages.length, 1); + assert.ok(messages[0].message.includes("Invalid ecmaVersion")); + + assert.throws(() => { + linter.verify("", { languageOptions: { sourceType: "foo" } }); + }, /Expected "script", "module", or "commonjs"./u); + + + messages = linter.verify("", { languageOptions: { ecmaVersion: 5, sourceType: "module" } }); + + assert.deepStrictEqual(messages.length, 1); + assert.ok(messages[0].message.includes("sourceType 'module' is not supported when ecmaVersion < 2015")); + }); + + it("should not crash when invalid parentheses syntax is encountered", () => { + linter.verify("left = (aSize.width/2) - ()"); + }); + + it("should not crash when let is used inside of switch case", () => { + linter.verify("switch(foo) { case 1: let bar=2; }", { languageOptions: { ecmaVersion: 6 } }); + }); + + it("should not crash when parsing destructured assignment", () => { + linter.verify("var { a='a' } = {};", { languageOptions: { ecmaVersion: 6 } }); + }); + + it("should report syntax error when a keyword exists in object property shorthand", () => { + const messages = linter.verify("let a = {this}", { languageOptions: { ecmaVersion: 6 } }); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].fatal, true); + }); + + }); + }); From 531be0c05b942814457fe46c57c99f90d09ebfe0 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 21 Oct 2021 12:08:41 -0700 Subject: [PATCH 07/37] Finish languageOptions features --- lib/linter/linter.js | 57 ++++++++++++--- tests/lib/linter/linter.js | 146 ++++++++++++++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 16 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 0e97b86476c..40b5cc8a024 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -47,6 +47,7 @@ const DEFAULT_ECMA_VERSION = 5; const commentParser = new ConfigCommentParser(); const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }; const parserSymbol = Symbol.for("eslint.RuleTester.parser"); +const globals = require("globals"); //------------------------------------------------------------------------------ // Typedefs @@ -577,13 +578,13 @@ function resolveParserOptions(parser, providedOptions, enabledEnvironments) { /** * Converts parserOptions to languageOptions for backwards compatibility with eslintrc. - * @param {ConfigData} config Config object. - * @param {Object} config.globals Global variable definitions. - * @param {Parser} config.parser The parser to use. + * @param {ConfigData} config Config object. + * @param {Object} config.globals Global variable definitions. + * @param {Parser} config.parser The parser to use. * @param {ParserOptions} config.parserOptions The parserOptions to use. * @returns {LanguageOptions} The languageOptions equivalent. */ -function createLanguageOptions({ globals, parser, parserOptions}) { +function createLanguageOptions({ globals: configuredGlobals, parser, parserOptions }) { const { ecmaVersion, @@ -592,11 +593,13 @@ function createLanguageOptions({ globals, parser, parserOptions}) { } = parserOptions; return { - globals, + globals: configuredGlobals, ecmaVersion, sourceType, parser, - ...justParserOptions + parserOptions: { + ...justParserOptions + } }; } @@ -694,9 +697,25 @@ function parse(text, languageOptions, filePath) { filePath }); + // Espree expects this information to be passed in if (parserIsEspree) { - parserOptions.ecmaVersion = languageOptions.ecmaVersion; - parserOptions.sourceType = languageOptions.sourceType; + if (languageOptions.ecmaVersion) { + parserOptions.ecmaVersion = languageOptions.ecmaVersion; + } + + if (languageOptions.sourceType) { + + // normalize for CommonJS + if (languageOptions.sourceType === "commonjs") { + parserOptions.sourceType = "script"; + parserOptions.ecmaFeatures = { + globalReturn: true, + ...parserOptions.ecmaFeatures + }; + } else { + parserOptions.sourceType = languageOptions.sourceType; + } + } } /* @@ -879,8 +898,8 @@ const BASE_TRAVERSAL_CONTEXT = Object.freeze( * @param {SourceCode} sourceCode A SourceCode object for the given text * @param {Object} configuredRules The rules configuration * @param {function(string): Rule} ruleMapper A mapper function from rule names to rules - * @param {LanguageOptions} languageOptions The options for parsing the code. * @param {string} parserName The name of the parser in the config + * @param {LanguageOptions} languageOptions The options for parsing the code. * @param {Object} settings The settings that were enabled in the config * @param {string} filename The reported filename of the code * @param {boolean} disableFixes If true, it doesn't make `fix` properties. @@ -921,7 +940,11 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO getScope: () => getScope(sourceCode.scopeManager, currentNode), getSourceCode: () => sourceCode, markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, languageOptions, name), - parserOptions: languageOptions.parserOptions, + parserOptions: { + ecmaVersion: languageOptions.ecmaVersion, + sourceType: languageOptions.sourceType, + ...languageOptions.parserOptions + }, parserPath: parserName, languageOptions, parserServices: sourceCode.parserServices, @@ -930,7 +953,6 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO ) ); - const lintingProblems = []; Object.keys(configuredRules).forEach(ruleId => { @@ -1462,7 +1484,18 @@ class Linter { } const languageOptions = config.languageOptions; - const configuredGlobals = languageOptions.globals || {}; + + languageOptions.ecmaVersion = normalizeEcmaVersion( + languageOptions.parser, + languageOptions.ecmaVersion + ); + + // add configured globals and language globals + const configuredGlobals = { + ...(globals[`es${languageOptions.ecmaVersion + 2009}`]), + ...(languageOptions.sourceType === "commonjs" ? globals.node : void 0), + ...languageOptions.globals + }; const settings = config.settings || {}; if (!slots.lastSourceCode) { diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 4f334ca4c4e..7f946c55249 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -96,8 +96,8 @@ describe("Linter", () => { linter.defineRule("checker", () => ({ Program: spy })); linter.verify("foo", { rules: { checker: "error" } }); - assert(spy.calledOnce); - assert.strictEqual(spy.firstCall.thisValue, void 0); + assert(spy.calledOnce, "Rule should have been called"); + assert.strictEqual(spy.firstCall.thisValue, void 0, "this value should be undefined"); }); it("does not allow listeners to use special EventEmitter values", () => { @@ -4117,7 +4117,20 @@ var a = "test2"; assert.strictEqual(messages.length, 0); }); - it("the 'latest' is equal to espree.lastEcmaVersion", () => { + it("the 'latest' is equal to espree.latestEcmaVersion", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; + + linter.defineRule("ecma-version", context => ({ + Program() { + ecmaVersion = context.parserOptions.ecmaVersion; + } + })); + linter.verify("", config); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion, "ecmaVersion should be 13"); + }); + + it("the 'latest' is equal to espree.latestEcmaVersion", () => { let ecmaVersion = null; const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; @@ -4127,7 +4140,20 @@ var a = "test2"; } })); linter.verify("", config); - assert.strictEqual(ecmaVersion, espree.latestEcmaVersion); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion, "ecmaVersion should be 13"); + }); + + it("the 'latest' is equal to espree.latestEcmaVersion on languageOptions", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; + + linter.defineRule("ecma-version", context => ({ + Program() { + ecmaVersion = context.languageOptions.ecmaVersion; + } + })); + linter.verify("", config); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion, "ecmaVersion should be 13"); }); it("should pass normalized ecmaVersion to eslint-scope", () => { @@ -6263,6 +6289,118 @@ describe("Linter with FlatConfigArray", () => { linter = new Linter({ configType: "flat" }); }); + describe("languageOptions", () => { + + describe("ecmaVersion", () => { + + it("should error when accessing a global that isn't available in given ecmaVersion", () => { + const messages = linter.verify("new Map()", { + languageOptions: { + ecmaVersion: 5 + }, + rules: { + "no-undef": "error" + } + }); + + assert.strictEqual(messages.length, 1, "There should be one linting error."); + assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); + }); + + it("should add globals for ES6 when ecmaVersion is 6", () => { + const messages = linter.verify("new Map()", { + languageOptions: { + ecmaVersion: 6 + }, + rules: { + "no-undef": "error" + } + }); + + assert.strictEqual(messages.length, 0, "There should be no linting errors."); + }); + }); + + describe("sourceType", () => { + + it("should error when import is used in a script", () => { + const messages = linter.verify("import foo from 'bar';", { + languageOptions: { + ecmaVersion: 6, + sourceType: "script" + } + }); + + assert.strictEqual(messages.length, 1, "There should be one parsing error."); + assert.strictEqual(messages[0].message, "Parsing error: 'import' and 'export' may appear only with 'sourceType: module'"); + }); + + it("should not error when import is used in a module", () => { + const messages = linter.verify("import foo from 'bar';", { + languageOptions: { + ecmaVersion: 6, + sourceType: "module" + } + }); + + assert.strictEqual(messages.length, 0, "There should no linting errors."); + }); + + it("should error when return is used at the top-level outside of commonjs", () => { + const messages = linter.verify("return", { + languageOptions: { + ecmaVersion: 6, + sourceType: "script" + } + }); + + assert.strictEqual(messages.length, 1, "There should be one parsing error."); + assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); + }); + + it("should not error when top-level return is used in commonjs", () => { + const messages = linter.verify("return", { + languageOptions: { + ecmaVersion: 6, + sourceType: "commonjs" + } + }); + + assert.strictEqual(messages.length, 0, "There should no linting errors."); + }); + + it("should error when accessing a Node.js global outside of commonjs", () => { + const messages = linter.verify("require()", { + languageOptions: { + ecmaVersion: 6 + }, + rules: { + "no-undef": "error" + } + }); + + assert.strictEqual(messages.length, 1, "There should be one linting error."); + assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); + }); + + it("should add globals for Node.js when sourceType is commonjs", () => { + const messages = linter.verify("require()", { + languageOptions: { + ecmaVersion: 6, + sourceType: "commonjs" + }, + rules: { + "no-undef": "error" + } + }); + + assert.strictEqual(messages.length, 0, "There should be no linting errors."); + }); + + }); + + }); + describe("when using events", () => { const code = TEST_CODE; From e3e9367babdc14cbc2358d5cf69525acfbb5d268 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 25 Oct 2021 10:14:03 -0700 Subject: [PATCH 08/37] Make sure to recognize RuleTester-wrapped Espree --- lib/linter/linter.js | 4 +++- tests/lib/linter/linter.js | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 40b5cc8a024..a777d71c27d 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -685,7 +685,9 @@ function analyzeScope(ast, languageOptions, visitorKeys) { function parse(text, languageOptions, filePath) { const textToParse = stripUnicodeBOM(text).replace(astUtils.shebangPattern, (match, captured) => `//${captured}`); const parser = languageOptions.parser; - const parserIsEspree = parser === espree; + + // RuleTester wraps Espree and places a symbol on the wrapper to identify it + const parserIsEspree = parser === espree || parser[parserSymbol] === espree; const parserOptions = Object.assign({}, languageOptions.parserOptions, { loc: true, range: true, diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 7f946c55249..5c52dd5f377 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6319,6 +6319,16 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(messages.length, 0, "There should be no linting errors."); }); + + it("should allow destructuring when ecmaVersion is 6", () => { + const messages = linter.verify("let {a} = b", { + languageOptions: { + ecmaVersion: 6 + } + }); + + assert.strictEqual(messages.length, 0, "There should be no linting errors."); + }); }); describe("sourceType", () => { From 4ffbc2ef704cde365c4ce2ada29914f233098cc6 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 25 Oct 2021 10:34:16 -0700 Subject: [PATCH 09/37] Move globals into local file --- conf/globals.js | 138 +++++++++++++++++++++++++++++++++++++ lib/linter/linter.js | 29 +++++++- tests/lib/linter/linter.js | 16 ++++- 3 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 conf/globals.js diff --git a/conf/globals.js b/conf/globals.js new file mode 100644 index 00000000000..ebd298d7385 --- /dev/null +++ b/conf/globals.js @@ -0,0 +1,138 @@ +/** + * @fileoverview Globals for ecmaVersion/sourceType + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Globals +//----------------------------------------------------------------------------- + +const commonjs = { + require: false, + exports: false, + module: false +}; + +const es3 = { + Array: false, + Boolean: false, + constructor: false, + Date: false, + decodeURI: false, + decodeURIComponent: false, + encodeURI: false, + encodeURIComponent: false, + Error: false, + escape: false, + eval: false, + EvalError: false, + Function: false, + hasOwnProperty: false, + Infinity: false, + isFinite: false, + isNaN: false, + isPrototypeOf: false, + Math: false, + NaN: false, + Number: false, + Object: false, + parseFloat: false, + parseInt: false, + propertyIsEnumerable: false, + RangeError: false, + ReferenceError: false, + RegExp: false, + String: false, + SyntaxError: false, + toLocaleString: false, + toString: false, + TypeError: false, + undefined: false, + unescape: false, + URIError: false, + valueOf: false +}; + +const es5 = { + ...es3, + JSON: false +}; + +const es2015 = { + ...es5, + ArrayBuffer: false, + DataView: false, + Float32Array: false, + Float64Array: false, + Int16Array: false, + Int32Array: false, + Int8Array: false, + Map: false, + Promise: false, + Proxy: false, + Reflect: false, + Set: false, + Symbol: false, + Uint16Array: false, + Uint32Array: false, + Uint8Array: false, + Uint8ClampedArray: false, + WeakMap: false, + WeakSet: false +}; + +// no new globals in ES2016 +const es2016 = { + ...es2015 +}; + +const es2017 = { + ...es2016, + Atomics: false, + SharedArrayBuffer: false +}; + +// no new globals in ES2018 +const es2018 = { + ...es2017 +}; + +// no new globals in ES2019 +const es2019 = { + ...es2018 +}; + +const es2020 = { + ...es2019, + BigInt: false, + BigInt64Array: false, + BigUint64Array: false, + globalThis: false +}; + +const es2021 = { + ...es2020, + AggregateError: false, + FinalizationRegistry: false, + WeakRef: false +}; + + +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- + +module.exports = { + commonjs, + es3, + es5, + es2015, + es2016, + es2017, + es2018, + es2019, + es2020, + es2021 +}; diff --git a/lib/linter/linter.js b/lib/linter/linter.js index a777d71c27d..b9c38e49633 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -47,7 +47,7 @@ const DEFAULT_ECMA_VERSION = 5; const commentParser = new ConfigCommentParser(); const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }; const parserSymbol = Symbol.for("eslint.RuleTester.parser"); -const globals = require("globals"); +const globals = require("../../conf/globals"); //------------------------------------------------------------------------------ // Typedefs @@ -130,6 +130,29 @@ const globals = require("globals"); // Helpers //------------------------------------------------------------------------------ +/** + * Retrieves globals for the given ecmaVersion. + * @param {number} ecmaVersion The version to retrieve globals for. + * @returns {Object} The globals for the given ecmaVersion. + */ +function getGlobalsForEcmaVersion(ecmaVersion) { + + switch (ecmaVersion) { + case 3: + return globals.es3; + + case 5: + return globals.es5; + + default: + if (ecmaVersion < 2015) { + return globals[`es${ecmaVersion + 2009}`]; + } + + return globals[`es${ecmaVersion}`]; + } +} + /** * Ensures that variables representing built-in properties of the Global Object, * and any globals declared by special block comments, are present in the global @@ -1494,8 +1517,8 @@ class Linter { // add configured globals and language globals const configuredGlobals = { - ...(globals[`es${languageOptions.ecmaVersion + 2009}`]), - ...(languageOptions.sourceType === "commonjs" ? globals.node : void 0), + ...(getGlobalsForEcmaVersion(languageOptions.ecmaVersion)), + ...(languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0), ...languageOptions.globals }; const settings = config.settings || {}; diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 5c52dd5f377..6a925574009 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6293,7 +6293,7 @@ describe("Linter with FlatConfigArray", () => { describe("ecmaVersion", () => { - it("should error when accessing a global that isn't available in given ecmaVersion", () => { + it("should error when accessing a global that isn't available in ecmaVersion 5", () => { const messages = linter.verify("new Map()", { languageOptions: { ecmaVersion: 5 @@ -6307,6 +6307,20 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); }); + it("should error when accessing a global that isn't available in ecmaVersion 3", () => { + const messages = linter.verify("JSON.stringify({})", { + languageOptions: { + ecmaVersion: 3 + }, + rules: { + "no-undef": "error" + } + }); + + assert.strictEqual(messages.length, 1, "There should be one linting error."); + assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); + }); + it("should add globals for ES6 when ecmaVersion is 6", () => { const messages = linter.verify("new Map()", { languageOptions: { From 3bc4832887d2b767927393411567d1573b590edf Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 25 Oct 2021 11:23:38 -0700 Subject: [PATCH 10/37] Normalize ecmaVersion to year on context.languageOptions --- lib/linter/linter.js | 33 +++++++++++++++++++++--- tests/lib/linter/linter.js | 51 +++++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index b9c38e49633..5d084aa6fd5 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -487,6 +487,33 @@ function normalizeEcmaVersion(parser, ecmaVersion) { return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion; } +/** + * Normalize ECMAScript version from the initial config into languageOptions (year) + * format. + * @param {Parser} parser The parser which uses this options. + * @param {number} ecmaVersion ECMAScript version from the initial config + * @returns {number} normalized ECMAScript version + */ +function normalizeEcmaVersionForLanguageOptions(parser, ecmaVersion) { + if ((parser[parserSymbol] || parser) === espree) { + if (ecmaVersion === "latest") { + return espree.latestEcmaVersion + 2009; + } + } + + switch (ecmaVersion) { + case 3: + return 3; + + case 5: + return 5; + + default: + return ecmaVersion >= 2015 ? ecmaVersion : ecmaVersion + 2009; + } + +} + const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)(?:\*\/|$)/gsu; /** @@ -617,7 +644,7 @@ function createLanguageOptions({ globals: configuredGlobals, parser, parserOptio return { globals: configuredGlobals, - ecmaVersion, + ecmaVersion: normalizeEcmaVersionForLanguageOptions(parser, ecmaVersion), sourceType, parser, parserOptions: { @@ -966,7 +993,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO getSourceCode: () => sourceCode, markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, languageOptions, name), parserOptions: { - ecmaVersion: languageOptions.ecmaVersion, + ecmaVersion: normalizeEcmaVersion(languageOptions.parser, languageOptions.ecmaVersion), sourceType: languageOptions.sourceType, ...languageOptions.parserOptions }, @@ -1510,7 +1537,7 @@ class Linter { const languageOptions = config.languageOptions; - languageOptions.ecmaVersion = normalizeEcmaVersion( + languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions( languageOptions.parser, languageOptions.ecmaVersion ); diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 6a925574009..e0083a96954 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -4153,7 +4153,7 @@ var a = "test2"; } })); linter.verify("", config); - assert.strictEqual(ecmaVersion, espree.latestEcmaVersion, "ecmaVersion should be 13"); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion + 2009, "ecmaVersion should be 2022"); }); it("should pass normalized ecmaVersion to eslint-scope", () => { @@ -6421,6 +6421,55 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(messages.length, 0, "There should be no linting errors."); }); + it("ecmaVersion should be normalized to year name for ES 6", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, 2015); + } + }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config, filename); + }); + + it("ecmaVersion should not be normalized to year name for ES 5", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, 5); + } + }; + } + } + } + }, + languageOptions: { + ecmaVersion: 5 + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config, filename); + }); + + }); }); From 80764256b227942989a5671eec3c93d8914e3e3d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 26 Oct 2021 12:06:21 -0700 Subject: [PATCH 11/37] Clean up ecmaVersion normalization --- lib/linter/linter.js | 37 +++++--- tests/lib/linter/linter.js | 130 ++++++++++++++++----------- tests/lib/rule-tester/rule-tester.js | 5 +- 3 files changed, 105 insertions(+), 67 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 5d084aa6fd5..a393ff86f97 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -130,6 +130,15 @@ const globals = require("../../conf/globals"); // Helpers //------------------------------------------------------------------------------ +/** + * 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); +} + /** * Retrieves globals for the given ecmaVersion. * @param {number} ecmaVersion The version to retrieve globals for. @@ -474,10 +483,13 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) { * @returns {number} normalized ECMAScript version */ function normalizeEcmaVersion(parser, ecmaVersion) { - if ((parser[parserSymbol] || parser) === espree) { + + if (isEspree(parser)) { if (ecmaVersion === "latest") { return espree.latestEcmaVersion; } + } else { + return ecmaVersion; } /* @@ -495,12 +507,16 @@ function normalizeEcmaVersion(parser, ecmaVersion) { * @returns {number} normalized ECMAScript version */ function normalizeEcmaVersionForLanguageOptions(parser, ecmaVersion) { - if ((parser[parserSymbol] || parser) === espree) { + if (isEspree(parser)) { if (ecmaVersion === "latest") { return espree.latestEcmaVersion + 2009; } } + if (isNaN(ecmaVersion) || !ecmaVersion) { + return void 0; + } + switch (ecmaVersion) { case 3: return 3; @@ -638,8 +654,7 @@ function createLanguageOptions({ globals: configuredGlobals, parser, parserOptio const { ecmaVersion, - sourceType, - ...justParserOptions + sourceType } = parserOptions; return { @@ -647,9 +662,7 @@ function createLanguageOptions({ globals: configuredGlobals, parser, parserOptio ecmaVersion: normalizeEcmaVersionForLanguageOptions(parser, ecmaVersion), sourceType, parser, - parserOptions: { - ...justParserOptions - } + parserOptions }; } @@ -735,9 +748,6 @@ function analyzeScope(ast, languageOptions, visitorKeys) { function parse(text, languageOptions, filePath) { const textToParse = stripUnicodeBOM(text).replace(astUtils.shebangPattern, (match, captured) => `//${captured}`); const parser = languageOptions.parser; - - // RuleTester wraps Espree and places a symbol on the wrapper to identify it - const parserIsEspree = parser === espree || parser[parserSymbol] === espree; const parserOptions = Object.assign({}, languageOptions.parserOptions, { loc: true, range: true, @@ -750,7 +760,7 @@ function parse(text, languageOptions, filePath) { }); // Espree expects this information to be passed in - if (parserIsEspree) { + if (isEspree(parser)) { if (languageOptions.ecmaVersion) { parserOptions.ecmaVersion = languageOptions.ecmaVersion; } @@ -993,8 +1003,6 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO getSourceCode: () => sourceCode, markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, languageOptions, name), parserOptions: { - ecmaVersion: normalizeEcmaVersion(languageOptions.parser, languageOptions.ecmaVersion), - sourceType: languageOptions.sourceType, ...languageOptions.parserOptions }, parserPath: parserName, @@ -1308,6 +1316,7 @@ class Linter { parserOptions }); + if (!slots.lastSourceCode) { const parseResult = parse( text, @@ -1605,7 +1614,7 @@ class Linter { sourceCode, configuredRules, ruleId => getRuleFromConfig(ruleId, config), - "TODO: Create backcompat parser name", + void 0, languageOptions, settings, options.filename, diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index e0083a96954..ed4bf6d511d 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -4130,17 +4130,18 @@ var a = "test2"; assert.strictEqual(ecmaVersion, espree.latestEcmaVersion, "ecmaVersion should be 13"); }); - it("the 'latest' is equal to espree.latestEcmaVersion", () => { + it("the 'latest' is not normalized for custom parsers", () => { let ecmaVersion = null; - const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; + const config = { rules: { "ecma-version": 2 }, parser: "custom-parser", parserOptions: { ecmaVersion: "latest" } }; + linter.defineParser("custom-parser", testParsers.enhancedParser); linter.defineRule("ecma-version", context => ({ Program() { ecmaVersion = context.parserOptions.ecmaVersion; } })); linter.verify("", config); - assert.strictEqual(ecmaVersion, espree.latestEcmaVersion, "ecmaVersion should be 13"); + assert.strictEqual(ecmaVersion, "latest", "ecmaVersion should be latest"); }); it("the 'latest' is equal to espree.latestEcmaVersion on languageOptions", () => { @@ -6343,6 +6344,80 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(messages.length, 0, "There should be no linting errors."); }); + + it("ecmaVersion should be normalized to year name for ES 6", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, 2015); + } + }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config, filename); + }); + + it("ecmaVersion should not be normalized to year name for ES 5", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, 5); + } + }; + } + } + } + }, + languageOptions: { + ecmaVersion: 5 + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config, filename); + }); + + it("ecmaVersion should be normalized to year name for 'latest'", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009); + } + }; + } + } + } + }, + languageOptions: { + ecmaVersion: "latest" + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config, filename); + }); + + }); describe("sourceType", () => { @@ -6421,54 +6496,6 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(messages.length, 0, "There should be no linting errors."); }); - it("ecmaVersion should be normalized to year name for ES 6", () => { - const config = { - plugins: { - test: { - rules: { - checker(context) { - return { - Program() { - assert.strictEqual(context.languageOptions.ecmaVersion, 2015); - } - }; - } - } - } - }, - languageOptions: { - ecmaVersion: 6 - }, - rules: { "test/checker": "error" } - }; - - linter.verify("foo", config, filename); - }); - - it("ecmaVersion should not be normalized to year name for ES 5", () => { - const config = { - plugins: { - test: { - rules: { - checker(context) { - return { - Program() { - assert.strictEqual(context.languageOptions.ecmaVersion, 5); - } - }; - } - } - } - }, - languageOptions: { - ecmaVersion: 5 - }, - rules: { "test/checker": "error" } - }; - - linter.verify("foo", config, filename); - }); - }); @@ -6925,7 +6952,6 @@ describe("Linter with FlatConfigArray", () => { }); }); - describe("Edge cases", () => { describe("Modules", () => { diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 6ebd82fb5ab..220c4d3aa53 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -1042,6 +1042,7 @@ describe("RuleTester", () => { }); assert.strictEqual(spy.args[1][1].parser, require.resolve("esprima")); }); + it("should pass normalized ecmaVersion to the rule", () => { const reportEcmaVersionRule = { meta: { @@ -1169,6 +1170,8 @@ describe("RuleTester", () => { parserOptions: { ecmaVersion: "latest" }, env: { es2020: true } }, + + // Non-Espree parsers should not have ecmaVersion normalized { code: "", errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], @@ -1194,7 +1197,7 @@ describe("RuleTester", () => { }, { code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: 2015 } }], parser: notEspree, parserOptions: { ecmaVersion: 2015 } }, From a20e642520a48d94dae5c79614734e95c18080d6 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 29 Oct 2021 10:18:51 -0700 Subject: [PATCH 12/37] Revert parserOptions.ecmaVersion behavior to original --- lib/linter/linter.js | 2 -- tests/lib/rule-tester/rule-tester.js | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index a393ff86f97..d51e3ab2600 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -488,8 +488,6 @@ function normalizeEcmaVersion(parser, ecmaVersion) { if (ecmaVersion === "latest") { return espree.latestEcmaVersion; } - } else { - return ecmaVersion; } /* diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 220c4d3aa53..5018a037b09 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -1171,7 +1171,7 @@ describe("RuleTester", () => { env: { es2020: true } }, - // Non-Espree parsers should not have ecmaVersion normalized + // Non-Espree parsers normalize ecmaVersion if it's not "latest" { code: "", errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], @@ -1197,7 +1197,7 @@ describe("RuleTester", () => { }, { code: "", - errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: 2015 } }], + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: 6 } }], parser: notEspree, parserOptions: { ecmaVersion: 2015 } }, From cee5632c1497ac45f6fa4b443277a40e8fd29924 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 29 Oct 2021 11:06:13 -0700 Subject: [PATCH 13/37] Add more tests --- lib/linter/linter.js | 46 +- tests/lib/linter/linter.js | 1301 ++++++++++++++++++++++++++++++++---- 2 files changed, 1214 insertions(+), 133 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index d51e3ab2600..d2625c11d6e 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -757,27 +757,6 @@ function parse(text, languageOptions, filePath) { filePath }); - // Espree expects this information to be passed in - if (isEspree(parser)) { - if (languageOptions.ecmaVersion) { - parserOptions.ecmaVersion = languageOptions.ecmaVersion; - } - - if (languageOptions.sourceType) { - - // normalize for CommonJS - if (languageOptions.sourceType === "commonjs") { - parserOptions.sourceType = "script"; - parserOptions.ecmaFeatures = { - globalReturn: true, - ...parserOptions.ecmaFeatures - }; - } else { - parserOptions.sourceType = languageOptions.sourceType; - } - } - } - /* * Check for parsing errors first. If there's a parsing error, nothing * else can happen. However, a parsing error does not throw an error @@ -1555,6 +1534,31 @@ class Linter { ...(languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0), ...languageOptions.globals }; + + // Espree expects this information to be passed in + if (isEspree(languageOptions.parser)) { + + const parserOptions = languageOptions.parserOptions; + + if (languageOptions.ecmaVersion) { + parserOptions.ecmaVersion = languageOptions.ecmaVersion; + } + + if (languageOptions.sourceType) { + + // normalize for CommonJS + if (languageOptions.sourceType === "commonjs") { + parserOptions.sourceType = "script"; + parserOptions.ecmaFeatures = { + globalReturn: true, + ...parserOptions.ecmaFeatures + }; + } else { + parserOptions.sourceType = languageOptions.sourceType; + } + } + } + const settings = config.settings || {}; if (!slots.lastSourceCode) { diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index ed4bf6d511d..f7d2f0cf26a 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6290,6 +6290,14 @@ describe("Linter with FlatConfigArray", () => { linter = new Linter({ configType: "flat" }); }); + describe("Static Members", () => { + describe("version", () => { + it("should return same version as instance property", () => { + assert.strictEqual(Linter.version, linter.version); + }); + }); + }); + describe("languageOptions", () => { describe("ecmaVersion", () => { @@ -6501,146 +6509,1215 @@ describe("Linter with FlatConfigArray", () => { }); - describe("when using events", () => { - const code = TEST_CODE; + describe("verify()", () => { - it("an error should be thrown when an error occurs inside of an event handler", () => { - const config = { - plugins: { - test: { - rules: { - checker: () => ({ - Program() { - throw new Error("Intentional error."); - } - }) + describe("inside rules", () => { + + const code = TEST_CODE; + + it("should throw an error when an error occurs inside of a rule visitor", () => { + const config = { + plugins: { + test: { + rules: { + checker: () => ({ + Program() { + throw new Error("Intentional error."); + } + }) + } } - } - }, - rules: { "test/checker": "error" } - }; + }, + rules: { "test/checker": "error" } + }; - assert.throws(() => { - linter.verify(code, config, filename); - }, `Intentional error.\nOccurred while linting ${filename}:1\nRule: "test/checker"`); - }); + assert.throws(() => { + linter.verify(code, config, filename); + }, `Intentional error.\nOccurred while linting ${filename}:1\nRule: "test/checker"`); + }); - it("does not call rule listeners with a `this` value", () => { - const spy = sinon.spy(); - const config = { - plugins: { - test: { - rules: { - checker: () => ({ - Program: spy - }) + it("should not call rule visitor with a `this` value", () => { + const spy = sinon.spy(); + const config = { + plugins: { + test: { + rules: { + checker: () => ({ + Program: spy + }) + } } - } - }, - rules: { "test/checker": "error" } - }; + }, + rules: { "test/checker": "error" } + }; - linter.verify("foo", config); - assert(spy.calledOnce); - assert.strictEqual(spy.firstCall.thisValue, void 0); - }); + linter.verify("foo", config); + assert(spy.calledOnce); + assert.strictEqual(spy.firstCall.thisValue, void 0); + }); - it("does not allow listeners to use special EventEmitter values", () => { - const spy = sinon.spy(); - const config = { - plugins: { - test: { - rules: { - checker: () => ({ - newListener: spy - }) + it("should not call unrecognized rule visitor when present in a rule", () => { + const spy = sinon.spy(); + const config = { + plugins: { + test: { + rules: { + checker: () => ({ + newListener: spy + }) + } } + }, + rules: { + "test/checker": "error", + "no-undef": "error" } - }, - rules: { - "test/checker": "error", - "no-undef": "error" - } - }; + }; - linter.verify("foo", config); - assert(spy.notCalled); - }); + linter.verify("foo", config); + assert(spy.notCalled); + }); - it("has all the `parent` properties on nodes when the rule listeners are created", () => { - const spy = sinon.spy(context => { - const ast = context.getSourceCode().ast; + it("should have all the `parent` properties on nodes when the rule visitors are created", () => { + const spy = sinon.spy(context => { + const ast = context.getSourceCode().ast; - assert.strictEqual(ast.body[0].parent, ast); - assert.strictEqual(ast.body[0].expression.parent, ast.body[0]); - assert.strictEqual(ast.body[0].expression.left.parent, ast.body[0].expression); - assert.strictEqual(ast.body[0].expression.right.parent, ast.body[0].expression); + assert.strictEqual(ast.body[0].parent, ast); + assert.strictEqual(ast.body[0].expression.parent, ast.body[0]); + assert.strictEqual(ast.body[0].expression.left.parent, ast.body[0].expression); + assert.strictEqual(ast.body[0].expression.right.parent, ast.body[0].expression); - return {}; - }); + return {}; + }); - const config = { - plugins: { - test: { - rules: { - checker: spy + const config = { + plugins: { + test: { + rules: { + checker: spy + } } - } - }, - rules: { "test/checker": "error" } - }; + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo + bar", config); + assert(spy.calledOnce); + }); - linter.verify("foo + bar", config); - assert(spy.calledOnce); }); - }); - describe("verify()", () => { + describe("Rule Context", () => { - it("rule should run as warning when set to 1 with a config array", () => { - const ruleId = "semi", - configs = createFlatConfigArray({ - files: ["**/*.js"], - rules: { - [ruleId]: 1 - } + describe("context.getSourceLines()", () => { + + it("should get proper lines when using \\n as a line break", () => { + const code = "a;\nb;"; + const spy = sinon.spy(context => { + assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); }); - configs.normalizeSync(); - const messages = linter.verify("foo", configs, filename, true); + it("should get proper lines when using \\r\\n as a line break", () => { + const code = "a;\r\nb;"; + const spy = sinon.spy(context => { + assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]); + return {}; + }); - assert.strictEqual(messages.length, 1, "Message length is wrong"); - assert.strictEqual(messages[0].ruleId, ruleId); - }); + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; - it("rule should run as warning when set to 1 with a plain array", () => { - const ruleId = "semi", - configs = [{ - files: ["**/*.js"], - rules: { - [ruleId]: 1 - } - }]; + linter.verify(code, config); + assert(spy.calledOnce); + }); - const messages = linter.verify("foo", configs, filename, true); + it("should get proper lines when using \\r as a line break", () => { + const code = "a;\rb;"; + const spy = sinon.spy(context => { + assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]); + return {}; + }); - assert.strictEqual(messages.length, 1, "Message length is wrong"); - assert.strictEqual(messages[0].ruleId, ruleId); - }); + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; - it("rule should run as warning when set to 1 with an object", () => { - const ruleId = "semi", - config = { - files: ["**/*.js"], - rules: { - [ruleId]: 1 - } - }; + linter.verify(code, config); + assert(spy.calledOnce); + }); - const messages = linter.verify("foo", config, filename, true); + it("should get proper lines when using \\u2028 as a line break", () => { + const code = "a;\u2028b;"; + const spy = sinon.spy(context => { + assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]); + return {}; + }); - assert.strictEqual(messages.length, 1, "Message length is wrong"); - assert.strictEqual(messages[0].ruleId, ruleId); + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + + it("should get proper lines when using \\u2029 as a line break", () => { + const code = "a;\u2029b;"; + const spy = sinon.spy(context => { + assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + + }); + + describe("context.getSource()", () => { + const code = TEST_CODE; + + it("should retrieve all text when used without parameters", () => { + + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + assert.strictEqual(context.getSource(), TEST_CODE); + }); + return { Program: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve all text for root node", () => { + + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(node => { + assert.strictEqual(context.getSource(node), TEST_CODE); + }); + return { Program: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should clamp to valid range when retrieving characters before start of source", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(node => { + assert.strictEqual(context.getSource(node, 2, 0), TEST_CODE); + }); + return { Program: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve all text for binary expression", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(node => { + assert.strictEqual(context.getSource(node), "6 * 7"); + }); + return { BinaryExpression: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve all text plus two characters before for binary expression", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(node => { + assert.strictEqual(context.getSource(node, 2), "= 6 * 7"); + }); + return { BinaryExpression: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve all text plus one character after for binary expression", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(node => { + assert.strictEqual(context.getSource(node, 0, 1), "6 * 7;"); + }); + return { BinaryExpression: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve all text plus two characters before and one character after for binary expression", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(node => { + assert.strictEqual(context.getSource(node, 2, 1), "= 6 * 7;"); + }); + return { BinaryExpression: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + }); + + describe("context.getAncestors()", () => { + const code = TEST_CODE; + + it("should retrieve all ancestors when used", () => { + + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const ancestors = context.getAncestors(); + + assert.strictEqual(ancestors.length, 3); + }); + return { BinaryExpression: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config, filename, true); + assert(spy && spy.calledOnce); + }); + + it("should retrieve empty ancestors for root node", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const ancestors = context.getAncestors(); + + assert.strictEqual(ancestors.length, 0); + }); + + return { Program: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + + describe("context.getNodeByRangeIndex()", () => { + const code = TEST_CODE; + + it("should retrieve a node starting at the given index", () => { + const spy = sinon.spy(context => { + assert.strictEqual(context.getNodeByRangeIndex(4).type, "Identifier"); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + + it("should retrieve a node containing the given index", () => { + const spy = sinon.spy(context => { + assert.strictEqual(context.getNodeByRangeIndex(6).type, "Identifier"); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + + it("should retrieve a node that is exactly the given index", () => { + const spy = sinon.spy(context => { + const node = context.getNodeByRangeIndex(13); + + assert.strictEqual(node.type, "Literal"); + assert.strictEqual(node.value, 6); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + + it("should retrieve a node ending with the given index", () => { + const spy = sinon.spy(context => { + assert.strictEqual(context.getNodeByRangeIndex(9).type, "Identifier"); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + + it("should retrieve the deepest node containing the given index", () => { + const spy = sinon.spy(context => { + const node1 = context.getNodeByRangeIndex(14); + + assert.strictEqual(node1.type, "BinaryExpression"); + + const node2 = context.getNodeByRangeIndex(3); + + assert.strictEqual(node2.type, "VariableDeclaration"); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + + it("should return null if the index is outside the range of any node", () => { + const spy = sinon.spy(context => { + const node1 = context.getNodeByRangeIndex(-1); + + assert.isNull(node1); + + const node2 = context.getNodeByRangeIndex(-99); + + assert.isNull(node2); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + }); + + + describe("context.getScope()", () => { + const code = "function foo() { q: for(;;) { break q; } } function bar () { var q = t; } var baz = (() => { return 1; });"; + + it("should retrieve the global scope correctly from a Program", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "global"); + }); + return { Program: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve the function scope correctly from a FunctionDeclaration", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "function"); + }); + return { FunctionDeclaration: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledTwice); + }); + + it("should retrieve the function scope correctly from a LabeledStatement", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block.id.name, "foo"); + }); + return { LabeledStatement: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve the function scope correctly from within an ArrowFunctionExpression", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block.type, "ArrowFunctionExpression"); + }); + + return { ReturnStatement: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve the function scope correctly from within an SwitchStatement", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "switch"); + assert.strictEqual(scope.block.type, "SwitchStatement"); + }); + + return { SwitchStatement: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + linter.verify("switch(foo){ case 'a': var b = 'foo'; }", config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve the function scope correctly from within a BlockStatement", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "block"); + assert.strictEqual(scope.block.type, "BlockStatement"); + }); + + return { BlockStatement: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + + linter.verify("var x; {let y = 1}", config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve the function scope correctly from within a nested block statement", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "block"); + assert.strictEqual(scope.block.type, "BlockStatement"); + }); + + return { BlockStatement: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + + linter.verify("if (true) { let x = 1 }", config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve the function scope correctly from within a FunctionDeclaration", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block.type, "FunctionDeclaration"); + }); + + return { FunctionDeclaration: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + + linter.verify("function foo() {}", config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve the function scope correctly from within a FunctionExpression", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block.type, "FunctionExpression"); + }); + + return { FunctionExpression: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + + linter.verify("(function foo() {})();", config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve the catch scope correctly from within a CatchClause", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "catch"); + assert.strictEqual(scope.block.type, "CatchClause"); + }); + + return { CatchClause: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + linter.verify("try {} catch (err) {}", config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve module scope correctly from an ES6 module", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "module"); + }); + + return { AssignmentExpression: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6, + sourceType: "module" + }, + rules: { "test/checker": "error" } + }; + + + linter.verify("var foo = {}; foo.bar = 1;", config); + assert(spy && spy.calledOnce); + }); + + it("should retrieve function scope correctly when sourceType is commonjs", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(scope.type, "function"); + }); + + return { AssignmentExpression: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6, + sourceType: "commonjs" + }, + rules: { "test/checker": "error" } + }; + + linter.verify("var foo = {}; foo.bar = 1;", config); + assert(spy && spy.calledOnce); + }); + }); + + describe("context.markVariableAsUsed()", () => { + it("should mark variables in current scope as used", () => { + const code = "var a = 1, b = 2;"; + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + assert.isTrue(context.markVariableAsUsed("a")); + + const scope = context.getScope(); + + assert.isTrue(getVariable(scope, "a").eslintUsed); + assert.notOk(getVariable(scope, "b").eslintUsed); + }); + + return { "Program:exit": spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should mark variables in function args as used", () => { + const code = "function abc(a, b) { return 1; }"; + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + assert.isTrue(context.markVariableAsUsed("a")); + + const scope = context.getScope(); + + assert.isTrue(getVariable(scope, "a").eslintUsed); + assert.notOk(getVariable(scope, "b").eslintUsed); + }); + + return { ReturnStatement: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should mark variables in higher scopes as used", () => { + const code = "var a, b; function abc() { return 1; }"; + let returnSpy, exitSpy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + returnSpy = sinon.spy(() => { + assert.isTrue(context.markVariableAsUsed("a")); + }); + exitSpy = sinon.spy(() => { + const scope = context.getScope(); + + assert.isTrue(getVariable(scope, "a").eslintUsed); + assert.notOk(getVariable(scope, "b").eslintUsed); + }); + + return { ReturnStatement: returnSpy, "Program:exit": exitSpy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(returnSpy && returnSpy.calledOnce); + assert(exitSpy && exitSpy.calledOnce); + }); + + it("should mark variables as used when sourceType is commonjs", () => { + const code = "var a = 1, b = 2;"; + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const globalScope = context.getScope(), + childScope = globalScope.childScopes[0]; + + assert.isTrue(context.markVariableAsUsed("a")); + + assert.isTrue(getVariable(childScope, "a").eslintUsed); + assert.isUndefined(getVariable(childScope, "b").eslintUsed); + }); + + return { "Program:exit": spy }; + } + } + } + }, + languageOptions: { + sourceType: "commonjs" + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should mark variables in modules as used", () => { + const code = "var a = 1, b = 2;"; + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const globalScope = context.getScope(), + childScope = globalScope.childScopes[0]; + + assert.isTrue(context.markVariableAsUsed("a")); + + assert.isTrue(getVariable(childScope, "a").eslintUsed); + assert.isUndefined(getVariable(childScope, "b").eslintUsed); + }); + + return { "Program:exit": spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6, + sourceType: "module" + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should return false if the given variable is not found", () => { + const code = "var a = 1, b = 2;"; + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + assert.isFalse(context.markVariableAsUsed("c")); + }); + + return { "Program:exit": spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + }); + + + it("rule should run as warning when set to 1 with a config array", () => { + const ruleId = "semi", + configs = createFlatConfigArray({ + files: ["**/*.js"], + rules: { + [ruleId]: 1 + } + }); + + configs.normalizeSync(); + const messages = linter.verify("foo", configs, filename, true); + + assert.strictEqual(messages.length, 1, "Message length is wrong"); + assert.strictEqual(messages[0].ruleId, ruleId); + }); + + it("rule should run as warning when set to 1 with a plain array", () => { + const ruleId = "semi", + configs = [{ + files: ["**/*.js"], + rules: { + [ruleId]: 1 + } + }]; + + const messages = linter.verify("foo", configs, filename, true); + + assert.strictEqual(messages.length, 1, "Message length is wrong"); + assert.strictEqual(messages[0].ruleId, ruleId); + }); + + it("rule should run as warning when set to 1 with an object", () => { + const ruleId = "semi", + config = { + files: ["**/*.js"], + rules: { + [ruleId]: 1 + } + }; + + const messages = linter.verify("foo", config, filename, true); + + assert.strictEqual(messages.length, 1, "Message length is wrong"); + assert.strictEqual(messages[0].ruleId, ruleId); + }); + + }); + + describe("getSourceCode()", () => { + const code = TEST_CODE; + + it("should retrieve SourceCode object after reset", () => { + linter.verify(code, {}, filename, true); + + const sourceCode = linter.getSourceCode(); + + assert.isObject(sourceCode); + assert.strictEqual(sourceCode.text, code); + assert.isObject(sourceCode.ast); + }); + + it("should retrieve SourceCode object without reset", () => { + linter.verify(code, {}, filename); + + const sourceCode = linter.getSourceCode(); + + assert.isObject(sourceCode); + assert.strictEqual(sourceCode.text, code); + assert.isObject(sourceCode.ast); }); }); From ac3f27036cfbcfa2df0c1ab0090c5c198af00ed0 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 29 Oct 2021 11:21:47 -0700 Subject: [PATCH 14/37] Update defaults for flat config (refs #14588) --- lib/config/default-config.js | 9 ++++- tests/lib/linter/linter.js | 77 +++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/lib/config/default-config.js b/lib/config/default-config.js index 8529d455af8..a655a6d83ca 100644 --- a/lib/config/default-config.js +++ b/lib/config/default-config.js @@ -46,9 +46,16 @@ exports.defaultConfig = [ ".git/**" ], languageOptions: { - sourceType: "script", + ecmaVersion: "latest", + sourceType: "module", parser: "@/espree", parserOptions: {} } + }, + { + files: ["**/*.cjs"], + languageOptions: { + sourceType: "commonjs" + } } ]; diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index f7d2f0cf26a..25a80672935 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6305,7 +6305,8 @@ describe("Linter with FlatConfigArray", () => { it("should error when accessing a global that isn't available in ecmaVersion 5", () => { const messages = linter.verify("new Map()", { languageOptions: { - ecmaVersion: 5 + ecmaVersion: 5, + sourceType: "script" }, rules: { "no-undef": "error" @@ -6319,7 +6320,8 @@ describe("Linter with FlatConfigArray", () => { it("should error when accessing a global that isn't available in ecmaVersion 3", () => { const messages = linter.verify("JSON.stringify({})", { languageOptions: { - ecmaVersion: 3 + ecmaVersion: 3, + sourceType: "script" }, rules: { "no-undef": "error" @@ -6377,6 +6379,27 @@ describe("Linter with FlatConfigArray", () => { linter.verify("foo", config, filename); }); + it("ecmaVersion should be normalized to to latest year by default", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009); + } + }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config, filename); + }); + it("ecmaVersion should not be normalized to year name for ES 5", () => { const config = { plugins: { @@ -6430,6 +6453,49 @@ describe("Linter with FlatConfigArray", () => { describe("sourceType", () => { + it("should be module by default", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.sourceType, "module"); + } + }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify("import foo from 'bar'", config, filename); + }); + + it("should default to commonjs when passed a .cjs filename", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.sourceType, "commonjs"); + } + }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify("import foo from 'bar'", config, `${filename}.cjs`); + }); + + it("should error when import is used in a script", () => { const messages = linter.verify("import foo from 'bar';", { languageOptions: { @@ -7460,6 +7526,7 @@ describe("Linter with FlatConfigArray", () => { }); describe("context.markVariableAsUsed()", () => { + it("should mark variables in current scope as used", () => { const code = "var a = 1, b = 2;"; let spy; @@ -7483,6 +7550,9 @@ describe("Linter with FlatConfigArray", () => { } } }, + languageOptions: { + sourceType: "script" + }, rules: { "test/checker": "error" } }; @@ -7544,6 +7614,9 @@ describe("Linter with FlatConfigArray", () => { } } }, + languageOptions: { + sourceType: "script" + }, rules: { "test/checker": "error" } }; From 7a03b48bca1d5593572cfa0441e4ce3eb2eb4cec Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 2 Nov 2021 17:49:35 -0700 Subject: [PATCH 15/37] More tests passing --- lib/linter/linter.js | 15 +- package.json | 2 +- tests/fixtures/parsers/throws-with-options.js | 2 +- tests/lib/config/flat-config-array.js | 23 ++ tests/lib/linter/linter.js | 354 ++++++++++++++++-- 5 files changed, 348 insertions(+), 48 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index d2625c11d6e..21811ce069a 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -500,19 +500,12 @@ function normalizeEcmaVersion(parser, ecmaVersion) { /** * Normalize ECMAScript version from the initial config into languageOptions (year) * format. - * @param {Parser} parser The parser which uses this options. * @param {number} ecmaVersion ECMAScript version from the initial config * @returns {number} normalized ECMAScript version */ -function normalizeEcmaVersionForLanguageOptions(parser, ecmaVersion) { - if (isEspree(parser)) { - if (ecmaVersion === "latest") { - return espree.latestEcmaVersion + 2009; - } - } - - if (isNaN(ecmaVersion) || !ecmaVersion) { - return void 0; +function normalizeEcmaVersionForLanguageOptions(ecmaVersion) { + if (ecmaVersion === "latest") { + return espree.latestEcmaVersion + 2009; } switch (ecmaVersion) { @@ -1524,7 +1517,6 @@ class Linter { const languageOptions = config.languageOptions; languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions( - languageOptions.parser, languageOptions.ecmaVersion ); @@ -1537,7 +1529,6 @@ class Linter { // Espree expects this information to be passed in if (isEspree(languageOptions.parser)) { - const parserOptions = languageOptions.parserOptions; if (languageOptions.ecmaVersion) { diff --git a/package.json b/package.json index d1515bd9c7e..d955a65a763 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "bugs": "https://github.com/eslint/eslint/issues/", "dependencies": { "@eslint/eslintrc": "^1.0.4", - "@humanwhocodes/config-array": "^0.9.1", + "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", diff --git a/tests/fixtures/parsers/throws-with-options.js b/tests/fixtures/parsers/throws-with-options.js index 95857751a75..0c701edbbe2 100644 --- a/tests/fixtures/parsers/throws-with-options.js +++ b/tests/fixtures/parsers/throws-with-options.js @@ -4,7 +4,7 @@ const espree = require("espree"); exports.parse = (sourceText, options) => { if (options.ecmaVersion) { - throw new Error("Expected no parserOptions to be used"); + throw new Error("Expected no parserOptions.ecmaVersion to be used"); } return espree.parse(sourceText, options); }; diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index 64ed2253476..1531f6dad5a 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -143,6 +143,29 @@ function normalizeRuleConfig(rulesConfig) { describe("FlatConfigArray", () => { + it("should not reuse languageOptions.parserOptions across configs", () => { + const base = [{ + languageOptions: { + parserOptions: { + foo: true + } + } + }]; + + const configs = new FlatConfigArray([], { + basePath: __dirname, + baseConfig: base + }); + + configs.normalizeSync(); + + const config = configs.getConfig("foo.js"); + + assert.notStrictEqual(base[0].languageOptions, config.languageOptions); + assert.notStrictEqual(base[0].languageOptions.parserOptions, config.languageOptions.parserOptions, "parserOptions should be new object"); + }); + + describe("Special configs", () => { it("eslint:recommended is replaced with an actual config", async () => { const configs = new FlatConfigArray(["eslint:recommended"], { basePath: __dirname }); diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 25a80672935..2e3e0aff071 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6575,9 +6575,9 @@ describe("Linter with FlatConfigArray", () => { }); - describe("verify()", () => { + describe.only("verify()", () => { - describe("inside rules", () => { + describe("Rule Internals", () => { const code = TEST_CODE; @@ -7721,53 +7721,339 @@ describe("Linter with FlatConfigArray", () => { }); }); + describe("Rule Severity", () => { + + it("rule should run as warning when set to 1 with a config array", () => { + const ruleId = "semi", + configs = createFlatConfigArray({ + files: ["**/*.js"], + rules: { + [ruleId]: 1 + } + }); + + configs.normalizeSync(); + const messages = linter.verify("foo", configs, filename, true); + + assert.strictEqual(messages.length, 1, "Message length is wrong"); + assert.strictEqual(messages[0].ruleId, ruleId); + }); + + it("rule should run as warning when set to 1 with a plain array", () => { + const ruleId = "semi", + configs = [{ + files: ["**/*.js"], + rules: { + [ruleId]: 1 + } + }]; + + const messages = linter.verify("foo", configs, filename, true); + + assert.strictEqual(messages.length, 1, "Message length is wrong"); + assert.strictEqual(messages[0].ruleId, ruleId); + }); + + it.only("rule should run as warning when set to 1 with an object", () => { + const ruleId = "semi", + config = { + files: ["**/*.js"], + rules: { + [ruleId]: 1 + } + }; + + const messages = linter.verify("foo", config, filename, true); + + assert.strictEqual(messages.length, 1, "Message length is wrong"); + assert.strictEqual(messages[0].ruleId, ruleId); + }); + }); - it("rule should run as warning when set to 1 with a config array", () => { - const ruleId = "semi", - configs = createFlatConfigArray({ - files: ["**/*.js"], - rules: { - [ruleId]: 1 + describe.only("Custom Parsers", () => { + + const errorPrefix = "Parsing error: "; + + it("should have file path passed to it", () => { + const code = "/* this is code */"; + const parseSpy = sinon.spy(testParsers.stubParser, "parse"); + const config = { + languageOptions: { + parser: testParsers.stubParser } - }); + }; - configs.normalizeSync(); - const messages = linter.verify("foo", configs, filename, true); + linter.verify(code, config, filename, true); - assert.strictEqual(messages.length, 1, "Message length is wrong"); - assert.strictEqual(messages[0].ruleId, ruleId); - }); + sinon.assert.calledWithMatch(parseSpy, "", { filePath: filename }); + }); - it("rule should run as warning when set to 1 with a plain array", () => { - const ruleId = "semi", - configs = [{ - files: ["**/*.js"], - rules: { - [ruleId]: 1 + it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { + const code = "var myDivElement =
;"; + const config = { + languageOptions: { + parser: esprima, + parserOptions: { + jsx: true + } } - }]; + }; + + const messages = linter.verify(code, config, filename); - const messages = linter.verify("foo", configs, filename, true); + assert.strictEqual(messages.length, 0); + }); - assert.strictEqual(messages.length, 1, "Message length is wrong"); - assert.strictEqual(messages[0].ruleId, ruleId); - }); + it("should not throw or report errors when the custom parser returns unrecognized operators (https://github.com/eslint/eslint/issues/10475)", () => { + const code = "null %% 'foo'"; + const config = { + languageOptions: { + parser: testParsers.unknownLogicalOperator + } + }; + + // This shouldn't throw + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not throw or report errors when the custom parser returns nested unrecognized operators (https://github.com/eslint/eslint/issues/10560)", () => { + const code = "foo && bar %% baz"; + const config = { + languageOptions: { + parser: testParsers.unknownLogicalOperatorNested + } + }; + + // This shouldn't throw + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); - it("rule should run as warning when set to 1 with an object", () => { - const ruleId = "semi", - config = { - files: ["**/*.js"], + it("should not throw or return errors when the custom parser returns unknown AST nodes", () => { + const code = "foo && bar %% baz"; + const nodes = []; + const config = { + plugins: { + test: { + rules: { + "collect-node-types": () => ({ + "*"(node) { + nodes.push(node.type); + } + }) + } + } + }, + languageOptions: { + parser: testParsers.nonJSParser + }, rules: { - [ruleId]: 1 + "test/collect-node-types": "error" } }; - const messages = linter.verify("foo", config, filename, true); + const messages = linter.verify(code, config, filename, true); - assert.strictEqual(messages.length, 1, "Message length is wrong"); - assert.strictEqual(messages[0].ruleId, ruleId); - }); + assert.strictEqual(messages.length, 0); + assert.isTrue(nodes.length > 0); + }); + + it("should strip leading line: prefix from parser error", () => { + const messages = linter.verify(";", { + languageOptions: { + parser: testParsers.lineError + } + }, "filename"); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, errorPrefix + testParsers.lineError.expectedError); + }); + + it("should not modify a parser error message without a leading line: prefix", () => { + const messages = linter.verify(";", { + languageOptions: { + parser: testParsers.noLineError + } + }, "filename"); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, errorPrefix + testParsers.noLineError.expectedError); + }); + + describe("if a parser provides 'visitorKeys'", () => { + let types = []; + let sourceCode; + let scopeManager; + let firstChildNodes = []; + + beforeEach(() => { + types = []; + firstChildNodes = []; + const config = { + plugins: { + test: { + rules: { + "collect-node-types": () => ({ + "*"(node) { + types.push(node.type); + } + }), + "save-scope-manager": context => { + scopeManager = context.getSourceCode().scopeManager; + + return {}; + }, + "esquery-option": () => ({ + ":first-child"(node) { + firstChildNodes.push(node); + } + }) + } + } + }, + languageOptions: { + parser: testParsers.enhancedParser2 + }, + rules: { + "test/collect-node-types": "error", + "test/save-scope-manager": "error", + "test/esquery-option": "error" + } + }; + + linter.verify("@foo class A {}", config); + + sourceCode = linter.getSourceCode(); + }); + + it("Traverser should use the visitorKeys (so 'types' includes 'Decorator')", () => { + assert.deepStrictEqual( + types, + ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] + ); + }); + + it("eslint-scope should use the visitorKeys (so 'childVisitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { + assert.deepStrictEqual( + scopeManager.__options.childVisitorKeys.ClassDeclaration, // eslint-disable-line no-underscore-dangle -- ScopeManager API + ["experimentalDecorators", "id", "superClass", "body"] + ); + }); + + it("should use the same visitorKeys if the source code object is reused", () => { + const types2 = []; + const config = { + plugins: { + test: { + rules: { + "collect-node-types": () => ({ + "*"(node) { + types2.push(node.type); + } + }), + } + } + }, + rules: { + "test/collect-node-types": "error", + } + }; + + linter.verify(sourceCode, config); + + assert.deepStrictEqual( + types2, + ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] + ); + }); + + it("esquery should use the visitorKeys (so 'visitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { + assert.deepStrictEqual( + firstChildNodes, + [sourceCode.ast.body[0], sourceCode.ast.body[0].experimentalDecorators[0]] + ); + }); + }); + + describe("if a parser provides 'scope'", () => { + let scope = null; + let sourceCode = null; + + beforeEach(() => { + const config = { + plugins: { + test: { + rules: { + "save-scope1": context => ({ + Program() { + scope = context.getScope(); + } + }), + } + } + }, + languageOptions: { + parser: testParsers.enhancedParser3 + }, + rules: { + "test/save-scope1": "error", + } + }; + + linter.verify("@foo class A {}", config); + + sourceCode = linter.getSourceCode(); + }); + + it("should use the scope (so the global scope has the reference of '@foo')", () => { + assert.strictEqual(scope.references.length, 1); + assert.deepStrictEqual( + scope.references[0].identifier.name, + "foo" + ); + }); + + it("should use the same scope if the source code object is reused", () => { + let scope2 = null; + const config = { + plugins: { + test: { + rules: { + "save-scope2": context => ({ + Program() { + scope2 = context.getScope(); + } + }), + } + } + }, + rules: { + "test/save-scope2": "error", + } + }; + + linter.verify(sourceCode, config, "test.js"); + + assert(scope2 !== null); + assert(scope2 === scope); + }); + }); + + it.only("should not pass any default parserOptions to the parser", () => { + const messages = linter.verify(";", { + languageOptions: { + parser: testParsers.throwsWithOptions + } + }, "filename"); + + assert.strictEqual(messages.length, 0); + }); + }); }); describe("getSourceCode()", () => { From e948144a5202452b750962c4c8c373a2734459fb Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 2 Nov 2021 18:09:02 -0700 Subject: [PATCH 16/37] Fix wrong ecmaVersion conversion --- lib/linter/linter.js | 2 +- tests/lib/config/flat-config-array.js | 2 +- tests/lib/linter/linter.js | 38 +++++++++++++-------------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 21811ce069a..7aa21a4d8fe 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -650,7 +650,7 @@ function createLanguageOptions({ globals: configuredGlobals, parser, parserOptio return { globals: configuredGlobals, - ecmaVersion: normalizeEcmaVersionForLanguageOptions(parser, ecmaVersion), + ecmaVersion: normalizeEcmaVersionForLanguageOptions(ecmaVersion), sourceType, parser, parserOptions diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index 1531f6dad5a..64cad9409cf 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -158,7 +158,7 @@ describe("FlatConfigArray", () => { }); configs.normalizeSync(); - + const config = configs.getConfig("foo.js"); assert.notStrictEqual(base[0].languageOptions, config.languageOptions); diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 2e3e0aff071..71523d5adc5 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6575,7 +6575,7 @@ describe("Linter with FlatConfigArray", () => { }); - describe.only("verify()", () => { + describe("verify()", () => { describe("Rule Internals", () => { @@ -7722,7 +7722,7 @@ describe("Linter with FlatConfigArray", () => { }); describe("Rule Severity", () => { - + it("rule should run as warning when set to 1 with a config array", () => { const ruleId = "semi", configs = createFlatConfigArray({ @@ -7731,14 +7731,14 @@ describe("Linter with FlatConfigArray", () => { [ruleId]: 1 } }); - + configs.normalizeSync(); const messages = linter.verify("foo", configs, filename, true); - + assert.strictEqual(messages.length, 1, "Message length is wrong"); assert.strictEqual(messages[0].ruleId, ruleId); }); - + it("rule should run as warning when set to 1 with a plain array", () => { const ruleId = "semi", configs = [{ @@ -7747,13 +7747,13 @@ describe("Linter with FlatConfigArray", () => { [ruleId]: 1 } }]; - + const messages = linter.verify("foo", configs, filename, true); - + assert.strictEqual(messages.length, 1, "Message length is wrong"); assert.strictEqual(messages[0].ruleId, ruleId); }); - + it.only("rule should run as warning when set to 1 with an object", () => { const ruleId = "semi", config = { @@ -7762,15 +7762,15 @@ describe("Linter with FlatConfigArray", () => { [ruleId]: 1 } }; - + const messages = linter.verify("foo", config, filename, true); - + assert.strictEqual(messages.length, 1, "Message length is wrong"); assert.strictEqual(messages[0].ruleId, ruleId); }); }); - describe.only("Custom Parsers", () => { + describe("Custom Parsers", () => { const errorPrefix = "Parsing error: "; @@ -7811,7 +7811,7 @@ describe("Linter with FlatConfigArray", () => { parser: testParsers.unknownLogicalOperator } }; - + // This shouldn't throw const messages = linter.verify(code, config, filename); @@ -7955,12 +7955,12 @@ describe("Linter with FlatConfigArray", () => { "*"(node) { types2.push(node.type); } - }), + }) } } }, rules: { - "test/collect-node-types": "error", + "test/collect-node-types": "error" } }; @@ -7993,7 +7993,7 @@ describe("Linter with FlatConfigArray", () => { Program() { scope = context.getScope(); } - }), + }) } } }, @@ -8001,7 +8001,7 @@ describe("Linter with FlatConfigArray", () => { parser: testParsers.enhancedParser3 }, rules: { - "test/save-scope1": "error", + "test/save-scope1": "error" } }; @@ -8028,12 +8028,12 @@ describe("Linter with FlatConfigArray", () => { Program() { scope2 = context.getScope(); } - }), + }) } } }, rules: { - "test/save-scope2": "error", + "test/save-scope2": "error" } }; @@ -8044,7 +8044,7 @@ describe("Linter with FlatConfigArray", () => { }); }); - it.only("should not pass any default parserOptions to the parser", () => { + it("should not pass any default parserOptions to the parser", () => { const messages = linter.verify(";", { languageOptions: { parser: testParsers.throwsWithOptions From 703e590883f78c9978510f0e7d81769d5f1dbae9 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 9 Nov 2021 09:11:26 -0800 Subject: [PATCH 17/37] Duplicated all tests for FlatConfig --- tests/lib/linter/linter.js | 5146 +++++++++++++++++++++++++++++++++--- 1 file changed, 4835 insertions(+), 311 deletions(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 71523d5adc5..492e9586f27 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -2357,7 +2357,7 @@ describe("Linter", () => { assert.strictEqual(messages[1].ruleId, "no-console"); }); - it("should not ignore violations if comment is of the type Shebang", () => { + it("should not ignore violations if comment is of the type hashbang", () => { const code = [ "#! eslint-disable-next-line no-alert", "alert('test');", @@ -2772,7 +2772,7 @@ var a = "test2"; }); }); - describe("when evaluating a file with a shebang", () => { + describe("when evaluating a file with a hashbang", () => { const code = "#!bin/program\n\nvar foo;;"; it("should preserve line numbers", () => { @@ -2785,13 +2785,13 @@ var a = "test2"; assert.strictEqual(messages[0].line, 3); }); - it("should have a comment with the shebang in it", () => { + it("should have a comment with the hashbang in it", () => { const config = { rules: { checker: "error" } }; const spy = sinon.spy(context => { const comments = context.getAllComments(); assert.strictEqual(comments.length, 1); - assert.strictEqual(comments[0].type, "Shebang"); + assert.strictEqual(comments[0].type, "hashbang"); return {}; }); @@ -2799,6 +2799,23 @@ var a = "test2"; linter.verify(code, config); assert(spy.calledOnce); }); + + it("should comment hashbang without breaking offset", () => { + const code = "#!/usr/bin/env node\n'123';"; + const config = { rules: { checker: "error" } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(node => { + assert.strictEqual(context.getSource(node), "'123';"); + }); + return { ExpressionStatement: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); describe("when evaluating broken code", () => { @@ -6271,7 +6288,7 @@ var a = "test2"; }); }); -describe("Linter with FlatConfigArray", () => { +describe.only("Linter with FlatConfigArray", () => { let linter; const filename = "filename.js"; @@ -6298,285 +6315,693 @@ describe("Linter with FlatConfigArray", () => { }); }); - describe("languageOptions", () => { - - describe("ecmaVersion", () => { - - it("should error when accessing a global that isn't available in ecmaVersion 5", () => { - const messages = linter.verify("new Map()", { - languageOptions: { - ecmaVersion: 5, - sourceType: "script" - }, - rules: { - "no-undef": "error" - } + describe("Config Options", () => { + + describe("languageOptions", () => { + + describe("ecmaVersion", () => { + + it("should error when accessing a global that isn't available in ecmaVersion 5", () => { + const messages = linter.verify("new Map()", { + languageOptions: { + ecmaVersion: 5, + sourceType: "script" + }, + rules: { + "no-undef": "error" + } + }); + + assert.strictEqual(messages.length, 1, "There should be one linting error."); + assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); }); - - assert.strictEqual(messages.length, 1, "There should be one linting error."); - assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); - }); - - it("should error when accessing a global that isn't available in ecmaVersion 3", () => { - const messages = linter.verify("JSON.stringify({})", { - languageOptions: { - ecmaVersion: 3, - sourceType: "script" - }, - rules: { - "no-undef": "error" - } + + it("should error when accessing a global that isn't available in ecmaVersion 3", () => { + const messages = linter.verify("JSON.stringify({})", { + languageOptions: { + ecmaVersion: 3, + sourceType: "script" + }, + rules: { + "no-undef": "error" + } + }); + + assert.strictEqual(messages.length, 1, "There should be one linting error."); + assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); }); - - assert.strictEqual(messages.length, 1, "There should be one linting error."); - assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); - }); - - it("should add globals for ES6 when ecmaVersion is 6", () => { - const messages = linter.verify("new Map()", { - languageOptions: { - ecmaVersion: 6 - }, - rules: { - "no-undef": "error" - } + + it("should add globals for ES6 when ecmaVersion is 6", () => { + const messages = linter.verify("new Map()", { + languageOptions: { + ecmaVersion: 6 + }, + rules: { + "no-undef": "error" + } + }); + + assert.strictEqual(messages.length, 0, "There should be no linting errors."); }); - - assert.strictEqual(messages.length, 0, "There should be no linting errors."); - }); - - it("should allow destructuring when ecmaVersion is 6", () => { - const messages = linter.verify("let {a} = b", { - languageOptions: { - ecmaVersion: 6 - } + + it("should allow destructuring when ecmaVersion is 6", () => { + const messages = linter.verify("let {a} = b", { + languageOptions: { + ecmaVersion: 6 + } + }); + + assert.strictEqual(messages.length, 0, "There should be no linting errors."); }); - - assert.strictEqual(messages.length, 0, "There should be no linting errors."); - }); - - it("ecmaVersion should be normalized to year name for ES 6", () => { - const config = { - plugins: { - test: { - rules: { - checker(context) { - return { - Program() { - assert.strictEqual(context.languageOptions.ecmaVersion, 2015); - } - }; + + it("ecmaVersion should be normalized to year name for ES 6", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, 2015); + } + }; + } } } - } - }, - languageOptions: { - ecmaVersion: 6 - }, - rules: { "test/checker": "error" } - }; - - linter.verify("foo", config, filename); - }); - - it("ecmaVersion should be normalized to to latest year by default", () => { - const config = { - plugins: { - test: { - rules: { - checker(context) { - return { - Program() { - assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009); - } - }; + }, + languageOptions: { + ecmaVersion: 6 + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config, filename); + }); + + it("ecmaVersion should be normalized to to latest year by default", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009); + } + }; + } } } - } - }, - rules: { "test/checker": "error" } - }; - - linter.verify("foo", config, filename); + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config, filename); + }); + + it("ecmaVersion should not be normalized to year name for ES 5", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, 5); + } + }; + } + } + } + }, + languageOptions: { + ecmaVersion: 5 + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config, filename); + }); + + it("ecmaVersion should be normalized to year name for 'latest'", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009); + } + }; + } + } + } + }, + languageOptions: { + ecmaVersion: "latest" + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo", config, filename); + }); + + }); - - it("ecmaVersion should not be normalized to year name for ES 5", () => { - const config = { - plugins: { - test: { - rules: { - checker(context) { - return { - Program() { - assert.strictEqual(context.languageOptions.ecmaVersion, 5); - } - }; + + describe("sourceType", () => { + + it("should be module by default", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.sourceType, "module"); + } + }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify("import foo from 'bar'", config, filename); + }); + + it("should default to commonjs when passed a .cjs filename", () => { + const config = { + plugins: { + test: { + rules: { + checker(context) { + return { + Program() { + assert.strictEqual(context.languageOptions.sourceType, "commonjs"); + } + }; + } } } + }, + rules: { "test/checker": "error" } + }; + + linter.verify("import foo from 'bar'", config, `${filename}.cjs`); + }); + + + it("should error when import is used in a script", () => { + const messages = linter.verify("import foo from 'bar';", { + languageOptions: { + ecmaVersion: 6, + sourceType: "script" } - }, - languageOptions: { - ecmaVersion: 5 - }, - rules: { "test/checker": "error" } - }; - - linter.verify("foo", config, filename); + }); + + assert.strictEqual(messages.length, 1, "There should be one parsing error."); + assert.strictEqual(messages[0].message, "Parsing error: 'import' and 'export' may appear only with 'sourceType: module'"); + }); + + it("should not error when import is used in a module", () => { + const messages = linter.verify("import foo from 'bar';", { + languageOptions: { + ecmaVersion: 6, + sourceType: "module" + } + }); + + assert.strictEqual(messages.length, 0, "There should no linting errors."); + }); + + it("should error when return is used at the top-level outside of commonjs", () => { + const messages = linter.verify("return", { + languageOptions: { + ecmaVersion: 6, + sourceType: "script" + } + }); + + assert.strictEqual(messages.length, 1, "There should be one parsing error."); + assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); + }); + + it("should not error when top-level return is used in commonjs", () => { + const messages = linter.verify("return", { + languageOptions: { + ecmaVersion: 6, + sourceType: "commonjs" + } + }); + + assert.strictEqual(messages.length, 0, "There should no linting errors."); + }); + + it("should error when accessing a Node.js global outside of commonjs", () => { + const messages = linter.verify("require()", { + languageOptions: { + ecmaVersion: 6 + }, + rules: { + "no-undef": "error" + } + }); + + assert.strictEqual(messages.length, 1, "There should be one linting error."); + assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); + }); + + it("should add globals for Node.js when sourceType is commonjs", () => { + const messages = linter.verify("require()", { + languageOptions: { + ecmaVersion: 6, + sourceType: "commonjs" + }, + rules: { + "no-undef": "error" + } + }); + + assert.strictEqual(messages.length, 0, "There should be no linting errors."); + }); + + }); - it("ecmaVersion should be normalized to year name for 'latest'", () => { - const config = { - plugins: { - test: { - rules: { - checker(context) { - return { - Program() { - assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009); + xdescribe("parser", () => { + + it("should be able to define a custom parser", () => { + const parser = { + parseForESLint: function parse(code, options) { + return { + ast: esprima.parse(code, options), + services: { + test: { + getMessage() { + return "Hi!"; } - }; + } } - } + }; } - }, - languageOptions: { - ecmaVersion: "latest" - }, - rules: { "test/checker": "error" } - }; + }; - linter.verify("foo", config, filename); - }); + linter.defineParser("test-parser", parser); + const config = { rules: {}, parser: "test-parser" }; + const messages = linter.verify("0", config, filename); + assert.strictEqual(messages.length, 0); + }); - }); - describe("sourceType", () => { + it("should pass parser as parserPath to all rules when provided on config", () => { - it("should be module by default", () => { - const config = { - plugins: { - test: { - rules: { - checker(context) { - return { - Program() { - assert.strictEqual(context.languageOptions.sourceType, "module"); - } - }; - } - } - } - }, - rules: { "test/checker": "error" } - }; + const alternateParser = "esprima"; - linter.verify("import foo from 'bar'", config, filename); - }); + linter.defineParser("esprima", esprima); + linter.defineRule("test-rule", sinon.mock().withArgs( + sinon.match({ parserPath: alternateParser }) + ).returns({})); - it("should default to commonjs when passed a .cjs filename", () => { - const config = { - plugins: { - test: { - rules: { - checker(context) { - return { - Program() { - assert.strictEqual(context.languageOptions.sourceType, "commonjs"); - } - }; - } - } + const config = { rules: { "test-rule": 2 }, parser: alternateParser }; + + linter.verify("0", config, filename); + }); + + it("should use parseForESLint() in custom parser when custom parser is specified", () => { + const config = { rules: {}, parser: "enhanced-parser" }; + + linter.defineParser("enhanced-parser", testParsers.enhancedParser); + const messages = linter.verify("0", config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should expose parser services when using parseForESLint() and services are specified", () => { + linter.defineParser("enhanced-parser", testParsers.enhancedParser); + linter.defineRule("test-service-rule", context => ({ + Literal(node) { + context.report({ + node, + message: context.parserServices.test.getMessage() + }); } - }, - rules: { "test/checker": "error" } - }; + })); - linter.verify("import foo from 'bar'", config, `${filename}.cjs`); - }); + const config = { rules: { "test-service-rule": 2 }, parser: "enhanced-parser" }; + const messages = linter.verify("0", config, filename); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hi!"); + }); - it("should error when import is used in a script", () => { - const messages = linter.verify("import foo from 'bar';", { - languageOptions: { - ecmaVersion: 6, - sourceType: "script" - } + it("should use the same parserServices if source code object is reused", () => { + linter.defineParser("enhanced-parser", testParsers.enhancedParser); + linter.defineRule("test-service-rule", context => ({ + Literal(node) { + context.report({ + node, + message: context.parserServices.test.getMessage() + }); + } + })); + + const config = { rules: { "test-service-rule": 2 }, parser: "enhanced-parser" }; + const messages = linter.verify("0", config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hi!"); + + const messages2 = linter.verify(linter.getSourceCode(), config, filename); + + assert.strictEqual(messages2.length, 1); + assert.strictEqual(messages2[0].message, "Hi!"); }); - assert.strictEqual(messages.length, 1, "There should be one parsing error."); - assert.strictEqual(messages[0].message, "Parsing error: 'import' and 'export' may appear only with 'sourceType: module'"); - }); + it("should pass parser as parserPath to all rules when default parser is used", () => { + linter.defineRule("test-rule", sinon.mock().withArgs( + sinon.match({ parserPath: "espree" }) + ).returns({})); - it("should not error when import is used in a module", () => { - const messages = linter.verify("import foo from 'bar';", { - languageOptions: { - ecmaVersion: 6, - sourceType: "module" - } + const config = { rules: { "test-rule": 2 } }; + + linter.verify("0", config, filename); }); - assert.strictEqual(messages.length, 0, "There should no linting errors."); }); - it("should error when return is used at the top-level outside of commonjs", () => { - const messages = linter.verify("return", { - languageOptions: { - ecmaVersion: 6, - sourceType: "script" - } + xdescribe("parseOptions", () => { + + it("should pass ecmaFeatures to all rules when provided on config", () => { + + const parserOptions = { + ecmaFeatures: { + jsx: true, + globalReturn: true + } + }; + + linter.defineRule("test-rule", sinon.mock().withArgs( + sinon.match({ parserOptions }) + ).returns({})); + + const config = { rules: { "test-rule": 2 }, parserOptions }; + + linter.verify("0", config, filename); }); - assert.strictEqual(messages.length, 1, "There should be one parsing error."); - assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); - }); + it("should pass parserOptions to all rules when default parserOptions is used", () => { - it("should not error when top-level return is used in commonjs", () => { - const messages = linter.verify("return", { - languageOptions: { - ecmaVersion: 6, - sourceType: "commonjs" - } + const parserOptions = {}; + + linter.defineRule("test-rule", sinon.mock().withArgs( + sinon.match({ parserOptions }) + ).returns({})); + + const config = { rules: { "test-rule": 2 } }; + + linter.verify("0", config, filename); + }); + + it("should properly parse object spread when ecmaVersion is 2018", () => { + + const messages = linter.verify("var x = { ...y };", { + parserOptions: { + ecmaVersion: 2018 + } + }, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should properly parse global return when passed ecmaFeatures", () => { + + const messages = linter.verify("return;", { + parserOptions: { + ecmaFeatures: { + globalReturn: true + } + } + }, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should properly parse global return when in Node.js environment", () => { + + const messages = linter.verify("return;", { + env: { + node: true + } + }, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not parse global return when in Node.js environment with globalReturn explicitly off", () => { + + const messages = linter.verify("return;", { + env: { + node: true + }, + parserOptions: { + ecmaFeatures: { + globalReturn: false + } + } + }, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); + }); + + it("should not parse global return when Node.js environment is false", () => { + + const messages = linter.verify("return;", {}, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); + }); + + it("should properly parse sloppy-mode code when impliedStrict is false", () => { + + const messages = linter.verify("var private;", {}, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not parse sloppy-mode code when impliedStrict is true", () => { + + const messages = linter.verify("var private;", { + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } + } + }, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Parsing error: The keyword 'private' is reserved"); + }); + + it("should properly parse valid code when impliedStrict is true", () => { + + const messages = linter.verify("var foo;", { + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } + } + }, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should properly parse JSX when passed ecmaFeatures", () => { + + const messages = linter.verify("var x =
;", { + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + }, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should report an error when JSX code is encountered and JSX is not enabled", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, {}, "filename"); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].line, 1); + assert.strictEqual(messages[0].column, 20); + assert.strictEqual(messages[0].message, "Parsing error: Unexpected token <"); + }); + + it("should not report an error when JSX code is encountered and JSX is enabled", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, { parserOptions: { ecmaFeatures: { jsx: true } } }, "filename"); + + assert.strictEqual(messages.length, 0); + }); + + it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }, "filename"); + + assert.strictEqual(messages.length, 0); + }); + + it("should allow 'await' as a property name in modules", () => { + const result = linter.verify( + "obj.await", + { parserOptions: { ecmaVersion: 6, sourceType: "module" } } + ); + + assert(result.length === 0); }); - assert.strictEqual(messages.length, 0, "There should no linting errors."); }); + + }); - it("should error when accessing a Node.js global outside of commonjs", () => { - const messages = linter.verify("require()", { - languageOptions: { - ecmaVersion: 6 - }, - rules: { - "no-undef": "error" + xdescribe("settings", () => { + const code = "test-rule"; + + it("should pass settings to all rules", () => { + linter.defineRule(code, context => ({ + Literal(node) { + context.report(node, context.settings.info); } - }); + })); + + const config = { rules: {}, settings: { info: "Hello" } }; - assert.strictEqual(messages.length, 1, "There should be one linting error."); - assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); + config.rules[code] = 1; + + const messages = linter.verify("0", config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hello"); }); - it("should add globals for Node.js when sourceType is commonjs", () => { - const messages = linter.verify("require()", { - languageOptions: { - ecmaVersion: 6, - sourceType: "commonjs" - }, - rules: { - "no-undef": "error" + it("should not have any settings if they were not passed in", () => { + linter.defineRule(code, context => ({ + Literal(node) { + if (Object.getOwnPropertyNames(context.settings).length !== 0) { + context.report(node, "Settings should be empty"); + } } - }); + })); + + const config = { rules: {} }; + + config.rules[code] = 1; + + const messages = linter.verify("0", config, filename); + + assert.strictEqual(messages.length, 0); + }); + }); + + describe("rules", () => { + const code = "var answer = 6 * 7"; + + it("should be configurable by only setting the integer value", () => { + const rule = "semi", + config = { rules: {} }; + + config.rules[rule] = 1; + + const messages = linter.verify(code, config, filename, true); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, rule); + }); + + it("should be configurable by only setting the string value", () => { + const rule = "semi", + config = { rules: {} }; + + config.rules[rule] = "warn"; - assert.strictEqual(messages.length, 0, "There should be no linting errors."); + const messages = linter.verify(code, config, filename, true); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 1); + assert.strictEqual(messages[0].ruleId, rule); + }); + + it("should be configurable by passing in values as an array", () => { + const rule = "semi", + config = { rules: {} }; + + config.rules[rule] = [1]; + + const messages = linter.verify(code, config, filename, true); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, rule); + }); + + it("should be configurable by passing in string value as an array", () => { + const rule = "semi", + config = { rules: {} }; + + config.rules[rule] = ["warn"]; + + const messages = linter.verify(code, config, filename, true); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 1); + assert.strictEqual(messages[0].ruleId, rule); + }); + + it("should not be configurable by setting other value", () => { + const rule = "semi", + config = { rules: {} }; + + config.rules[rule] = "1"; + + assert.throws(() => { + linter.verify(code, config, filename, true); + }, /Key "rules": Key "semi"/u); }); + it("should process empty config", () => { + const config = {}; + const messages = linter.verify(code, config, filename, true); + assert.strictEqual(messages.length, 0); + }); }); }); describe("verify()", () => { + it("should report warnings in order by line and column when called", () => { + + const code = "foo()\n alert('test')"; + const config = { rules: { "no-mixed-spaces-and-tabs": 1, "eol-last": 1, semi: [1, "always"] } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 3); + assert.strictEqual(messages[0].line, 1); + assert.strictEqual(messages[0].column, 6); + assert.strictEqual(messages[1].line, 2); + assert.strictEqual(messages[1].column, 18); + assert.strictEqual(messages[2].line, 2); + assert.strictEqual(messages[2].column, 18); + }); + describe("Rule Internals", () => { const code = TEST_CODE; @@ -6671,46 +7096,169 @@ describe("Linter with FlatConfigArray", () => { assert(spy.calledOnce); }); - }); - - describe("Rule Context", () => { - - describe("context.getSourceLines()", () => { + it("events for each node type should fire", () => { - it("should get proper lines when using \\n as a line break", () => { - const code = "a;\nb;"; - const spy = sinon.spy(context => { - assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]); - return {}; - }); + // spies for various AST node types + const spyLiteral = sinon.spy(), + spyVariableDeclarator = sinon.spy(), + spyVariableDeclaration = sinon.spy(), + spyIdentifier = sinon.spy(), + spyBinaryExpression = sinon.spy(); - const config = { - plugins: { - test: { - rules: { - checker: spy + const config = { + plugins: { + test: { + rules: { + checker() { + return { + Literal: spyLiteral, + VariableDeclarator: spyVariableDeclarator, + VariableDeclaration: spyVariableDeclaration, + Identifier: spyIdentifier, + BinaryExpression: spyBinaryExpression + }; } } - }, - rules: { "test/checker": "error" } - }; + } + }, + rules: { "test/checker": "error" } + }; - linter.verify(code, config); - assert(spy.calledOnce); - }); + const messages = linter.verify(code, config, filename, true); - it("should get proper lines when using \\r\\n as a line break", () => { - const code = "a;\r\nb;"; - const spy = sinon.spy(context => { - assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]); - return {}; - }); + assert.strictEqual(messages.length, 0); + sinon.assert.calledOnce(spyVariableDeclaration); + sinon.assert.calledOnce(spyVariableDeclarator); + sinon.assert.calledOnce(spyIdentifier); + sinon.assert.calledTwice(spyLiteral); + sinon.assert.calledOnce(spyBinaryExpression); + }); - const config = { - plugins: { - test: { - rules: { - checker: spy + it("should throw an error if a rule reports a problem without a message", () => { + + const config = { + plugins: { + test: { + rules: { + "invalid-report"(context) { + return { + Program(node) { + context.report({ node }); + } + }; + } + } + } + }, + rules: { "test/invalid-report": "error" } + }; + + assert.throws( + () => linter.verify("foo", config), + TypeError, + "Missing `message` property in report() call; add a message that describes the linting problem." + ); + }); + + + }); + + describe("Rule Context", () => { + + xdescribe("context.getFilename()", () => { + const ruleId = "filename-rule"; + + it("has access to the filename", () => { + linter.defineRule(ruleId, context => ({ + Literal(node) { + context.report(node, context.getFilename()); + } + })); + + const config = { rules: {} }; + + config.rules[ruleId] = 1; + + const messages = linter.verify("0", config, filename); + + assert.strictEqual(messages[0].message, filename); + }); + + it("defaults filename to ''", () => { + linter.defineRule(ruleId, context => ({ + Literal(node) { + context.report(node, context.getFilename()); + } + })); + + const config = { rules: {} }; + + config.rules[ruleId] = 1; + + const messages = linter.verify("0", config); + + assert.strictEqual(messages[0].message, ""); + }); + }); + + xdescribe("context.getPhysicalFilename()", () => { + + const ruleId = "filename-rule"; + + it("has access to the physicalFilename", () => { + linter.defineRule(ruleId, context => ({ + Literal(node) { + context.report(node, context.getPhysicalFilename()); + } + })); + + const config = { rules: {} }; + + config.rules[ruleId] = 1; + + const messages = linter.verify("0", config, filename); + + assert.strictEqual(messages[0].message, filename); + }); + + }); + + describe("context.getSourceLines()", () => { + + it("should get proper lines when using \\n as a line break", () => { + const code = "a;\nb;"; + const spy = sinon.spy(context => { + assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + + it("should get proper lines when using \\r\\n as a line break", () => { + const code = "a;\r\nb;"; + const spy = sinon.spy(context => { + assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy } } }, @@ -7158,7 +7706,6 @@ describe("Linter with FlatConfigArray", () => { }); }); - describe("context.getScope()", () => { const code = "function foo() { q: for(;;) { break q; } } function bar () { var q = t; } var baz = (() => { return 1; });"; @@ -7523,6 +8070,596 @@ describe("Linter with FlatConfigArray", () => { linter.verify("var foo = {}; foo.bar = 1;", config); assert(spy && spy.calledOnce); }); + + xdescribe("context.getScope()", () => { + + /** + * Get the scope on the node `astSelector` specified. + * @param {string} code The source code to verify. + * @param {string} astSelector The AST selector to get scope. + * @param {number} [ecmaVersion=5] The ECMAScript version. + * @returns {{node: ASTNode, scope: escope.Scope}} Gotten scope. + */ + function getScope(code, astSelector, ecmaVersion = 5) { + let node, scope; + + linter.defineRule("get-scope", context => ({ + [astSelector](node0) { + node = node0; + scope = context.getScope(); + } + })); + linter.verify( + code, + { + parserOptions: { ecmaVersion }, + rules: { "get-scope": 2 } + } + ); + + return { node, scope }; + } + + it("should return 'function' scope on FunctionDeclaration (ES5)", () => { + const { node, scope } = getScope("function f() {}", "FunctionDeclaration"); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node); + }); + + it("should return 'function' scope on FunctionExpression (ES5)", () => { + const { node, scope } = getScope("!function f() {}", "FunctionExpression"); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node); + }); + + it("should return 'function' scope on the body of FunctionDeclaration (ES5)", () => { + const { node, scope } = getScope("function f() {}", "BlockStatement"); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node.parent); + }); + + it("should return 'function' scope on the body of FunctionDeclaration (ES2015)", () => { + const { node, scope } = getScope("function f() {}", "BlockStatement", 2015); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node.parent); + }); + + it("should return 'function' scope on BlockStatement in functions (ES5)", () => { + const { node, scope } = getScope("function f() { { var b; } }", "BlockStatement > BlockStatement"); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node.parent.parent); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "b"]); + }); + + it("should return 'block' scope on BlockStatement in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { { let a; var b; } }", "BlockStatement > BlockStatement", 2015); + + assert.strictEqual(scope.type, "block"); + assert.strictEqual(scope.upper.type, "function"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["a"]); + assert.deepStrictEqual(scope.variableScope.variables.map(v => v.name), ["arguments", "b"]); + }); + + it("should return 'block' scope on nested BlockStatement in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { { let a; { let b; var c; } } }", "BlockStatement > BlockStatement > BlockStatement", 2015); + + assert.strictEqual(scope.type, "block"); + assert.strictEqual(scope.upper.type, "block"); + assert.strictEqual(scope.upper.upper.type, "function"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["b"]); + assert.deepStrictEqual(scope.upper.variables.map(v => v.name), ["a"]); + assert.deepStrictEqual(scope.variableScope.variables.map(v => v.name), ["arguments", "c"]); + }); + + it("should return 'function' scope on SwitchStatement in functions (ES5)", () => { + const { node, scope } = getScope("function f() { switch (a) { case 0: var b; } }", "SwitchStatement"); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node.parent.parent); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "b"]); + }); + + it("should return 'switch' scope on SwitchStatement in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { switch (a) { case 0: let b; } }", "SwitchStatement", 2015); + + assert.strictEqual(scope.type, "switch"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["b"]); + }); + + it("should return 'function' scope on SwitchCase in functions (ES5)", () => { + const { node, scope } = getScope("function f() { switch (a) { case 0: var b; } }", "SwitchCase"); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node.parent.parent.parent); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "b"]); + }); + + it("should return 'switch' scope on SwitchCase in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { switch (a) { case 0: let b; } }", "SwitchCase", 2015); + + assert.strictEqual(scope.type, "switch"); + assert.strictEqual(scope.block, node.parent); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["b"]); + }); + + it("should return 'catch' scope on CatchClause in functions (ES5)", () => { + const { node, scope } = getScope("function f() { try {} catch (e) { var a; } }", "CatchClause"); + + assert.strictEqual(scope.type, "catch"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["e"]); + }); + + it("should return 'catch' scope on CatchClause in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { try {} catch (e) { let a; } }", "CatchClause", 2015); + + assert.strictEqual(scope.type, "catch"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["e"]); + }); + + it("should return 'catch' scope on the block of CatchClause in functions (ES5)", () => { + const { node, scope } = getScope("function f() { try {} catch (e) { var a; } }", "CatchClause > BlockStatement"); + + assert.strictEqual(scope.type, "catch"); + assert.strictEqual(scope.block, node.parent); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["e"]); + }); + + it("should return 'block' scope on the block of CatchClause in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { try {} catch (e) { let a; } }", "CatchClause > BlockStatement", 2015); + + assert.strictEqual(scope.type, "block"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["a"]); + }); + + it("should return 'function' scope on ForStatement in functions (ES5)", () => { + const { node, scope } = getScope("function f() { for (var i = 0; i < 10; ++i) {} }", "ForStatement"); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node.parent.parent); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "i"]); + }); + + it("should return 'for' scope on ForStatement in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { for (let i = 0; i < 10; ++i) {} }", "ForStatement", 2015); + + assert.strictEqual(scope.type, "for"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["i"]); + }); + + it("should return 'function' scope on the block body of ForStatement in functions (ES5)", () => { + const { node, scope } = getScope("function f() { for (var i = 0; i < 10; ++i) {} }", "ForStatement > BlockStatement"); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node.parent.parent.parent); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "i"]); + }); + + it("should return 'block' scope on the block body of ForStatement in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { for (let i = 0; i < 10; ++i) {} }", "ForStatement > BlockStatement", 2015); + + assert.strictEqual(scope.type, "block"); + assert.strictEqual(scope.upper.type, "for"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), []); + assert.deepStrictEqual(scope.upper.variables.map(v => v.name), ["i"]); + }); + + it("should return 'function' scope on ForInStatement in functions (ES5)", () => { + const { node, scope } = getScope("function f() { for (var key in obj) {} }", "ForInStatement"); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node.parent.parent); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "key"]); + }); + + it("should return 'for' scope on ForInStatement in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { for (let key in obj) {} }", "ForInStatement", 2015); + + assert.strictEqual(scope.type, "for"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["key"]); + }); + + it("should return 'function' scope on the block body of ForInStatement in functions (ES5)", () => { + const { node, scope } = getScope("function f() { for (var key in obj) {} }", "ForInStatement > BlockStatement"); + + assert.strictEqual(scope.type, "function"); + assert.strictEqual(scope.block, node.parent.parent.parent); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "key"]); + }); + + it("should return 'block' scope on the block body of ForInStatement in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { for (let key in obj) {} }", "ForInStatement > BlockStatement", 2015); + + assert.strictEqual(scope.type, "block"); + assert.strictEqual(scope.upper.type, "for"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), []); + assert.deepStrictEqual(scope.upper.variables.map(v => v.name), ["key"]); + }); + + it("should return 'for' scope on ForOfStatement in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { for (let x of xs) {} }", "ForOfStatement", 2015); + + assert.strictEqual(scope.type, "for"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), ["x"]); + }); + + it("should return 'block' scope on the block body of ForOfStatement in functions (ES2015)", () => { + const { node, scope } = getScope("function f() { for (let x of xs) {} }", "ForOfStatement > BlockStatement", 2015); + + assert.strictEqual(scope.type, "block"); + assert.strictEqual(scope.upper.type, "for"); + assert.strictEqual(scope.block, node); + assert.deepStrictEqual(scope.variables.map(v => v.name), []); + assert.deepStrictEqual(scope.upper.variables.map(v => v.name), ["x"]); + }); + + it("should shadow the same name variable by the iteration variable.", () => { + const { node, scope } = getScope("let x; for (let x of x) {}", "ForOfStatement", 2015); + + assert.strictEqual(scope.type, "for"); + assert.strictEqual(scope.upper.type, "global"); + assert.strictEqual(scope.block, node); + assert.strictEqual(scope.upper.variables[0].references.length, 0); + assert.strictEqual(scope.references[0].identifier, node.left.declarations[0].id); + assert.strictEqual(scope.references[1].identifier, node.right); + assert.strictEqual(scope.references[1].resolved, scope.variables[0]); + }); + }); + + xdescribe("Variables and references", () => { + const code = [ + "a;", + "function foo() { b; }", + "Object;", + "foo;", + "var c;", + "c;", + "/* global d */", + "d;", + "e;", + "f;" + ].join("\n"); + let scope = null; + + beforeEach(() => { + let ok = false; + + linter.defineRules({ + test(context) { + return { + Program() { + scope = context.getScope(); + ok = true; + } + }; + } + }); + linter.verify(code, { rules: { test: 2 }, globals: { e: true, f: false } }); + assert(ok); + }); + + afterEach(() => { + scope = null; + }); + + it("Scope#through should contain references of undefined variables", () => { + assert.strictEqual(scope.through.length, 2); + assert.strictEqual(scope.through[0].identifier.name, "a"); + assert.strictEqual(scope.through[0].identifier.loc.start.line, 1); + assert.strictEqual(scope.through[0].resolved, null); + assert.strictEqual(scope.through[1].identifier.name, "b"); + assert.strictEqual(scope.through[1].identifier.loc.start.line, 2); + assert.strictEqual(scope.through[1].resolved, null); + }); + + it("Scope#variables should contain global variables", () => { + assert(scope.variables.some(v => v.name === "Object")); + assert(scope.variables.some(v => v.name === "foo")); + assert(scope.variables.some(v => v.name === "c")); + assert(scope.variables.some(v => v.name === "d")); + assert(scope.variables.some(v => v.name === "e")); + assert(scope.variables.some(v => v.name === "f")); + }); + + it("Scope#set should contain global variables", () => { + assert(scope.set.get("Object")); + assert(scope.set.get("foo")); + assert(scope.set.get("c")); + assert(scope.set.get("d")); + assert(scope.set.get("e")); + assert(scope.set.get("f")); + }); + + it("Variables#references should contain their references", () => { + assert.strictEqual(scope.set.get("Object").references.length, 1); + assert.strictEqual(scope.set.get("Object").references[0].identifier.name, "Object"); + assert.strictEqual(scope.set.get("Object").references[0].identifier.loc.start.line, 3); + assert.strictEqual(scope.set.get("Object").references[0].resolved, scope.set.get("Object")); + assert.strictEqual(scope.set.get("foo").references.length, 1); + assert.strictEqual(scope.set.get("foo").references[0].identifier.name, "foo"); + assert.strictEqual(scope.set.get("foo").references[0].identifier.loc.start.line, 4); + assert.strictEqual(scope.set.get("foo").references[0].resolved, scope.set.get("foo")); + assert.strictEqual(scope.set.get("c").references.length, 1); + assert.strictEqual(scope.set.get("c").references[0].identifier.name, "c"); + assert.strictEqual(scope.set.get("c").references[0].identifier.loc.start.line, 6); + assert.strictEqual(scope.set.get("c").references[0].resolved, scope.set.get("c")); + assert.strictEqual(scope.set.get("d").references.length, 1); + assert.strictEqual(scope.set.get("d").references[0].identifier.name, "d"); + assert.strictEqual(scope.set.get("d").references[0].identifier.loc.start.line, 8); + assert.strictEqual(scope.set.get("d").references[0].resolved, scope.set.get("d")); + assert.strictEqual(scope.set.get("e").references.length, 1); + assert.strictEqual(scope.set.get("e").references[0].identifier.name, "e"); + assert.strictEqual(scope.set.get("e").references[0].identifier.loc.start.line, 9); + assert.strictEqual(scope.set.get("e").references[0].resolved, scope.set.get("e")); + assert.strictEqual(scope.set.get("f").references.length, 1); + assert.strictEqual(scope.set.get("f").references[0].identifier.name, "f"); + assert.strictEqual(scope.set.get("f").references[0].identifier.loc.start.line, 10); + assert.strictEqual(scope.set.get("f").references[0].resolved, scope.set.get("f")); + }); + + it("Reference#resolved should be their variable", () => { + assert.strictEqual(scope.set.get("Object").references[0].resolved, scope.set.get("Object")); + assert.strictEqual(scope.set.get("foo").references[0].resolved, scope.set.get("foo")); + assert.strictEqual(scope.set.get("c").references[0].resolved, scope.set.get("c")); + assert.strictEqual(scope.set.get("d").references[0].resolved, scope.set.get("d")); + assert.strictEqual(scope.set.get("e").references[0].resolved, scope.set.get("e")); + assert.strictEqual(scope.set.get("f").references[0].resolved, scope.set.get("f")); + }); + }); + }); + + xdescribe("context.getDeclaredVariables(node)", () => { + + /** + * Assert `context.getDeclaredVariables(node)` is valid. + * @param {string} code A code to check. + * @param {string} type A type string of ASTNode. This method checks variables on the node of the type. + * @param {Array>} expectedNamesList An array of expected variable names. The expected variable names is an array of string. + * @returns {void} + */ + function verify(code, type, expectedNamesList) { + linter.defineRules({ + test(context) { + + /** + * Assert `context.getDeclaredVariables(node)` is empty. + * @param {ASTNode} node A node to check. + * @returns {void} + */ + function checkEmpty(node) { + assert.strictEqual(0, context.getDeclaredVariables(node).length); + } + const rule = { + Program: checkEmpty, + EmptyStatement: checkEmpty, + BlockStatement: checkEmpty, + ExpressionStatement: checkEmpty, + LabeledStatement: checkEmpty, + BreakStatement: checkEmpty, + ContinueStatement: checkEmpty, + WithStatement: checkEmpty, + SwitchStatement: checkEmpty, + ReturnStatement: checkEmpty, + ThrowStatement: checkEmpty, + TryStatement: checkEmpty, + WhileStatement: checkEmpty, + DoWhileStatement: checkEmpty, + ForStatement: checkEmpty, + ForInStatement: checkEmpty, + DebuggerStatement: checkEmpty, + ThisExpression: checkEmpty, + ArrayExpression: checkEmpty, + ObjectExpression: checkEmpty, + Property: checkEmpty, + SequenceExpression: checkEmpty, + UnaryExpression: checkEmpty, + BinaryExpression: checkEmpty, + AssignmentExpression: checkEmpty, + UpdateExpression: checkEmpty, + LogicalExpression: checkEmpty, + ConditionalExpression: checkEmpty, + CallExpression: checkEmpty, + NewExpression: checkEmpty, + MemberExpression: checkEmpty, + SwitchCase: checkEmpty, + Identifier: checkEmpty, + Literal: checkEmpty, + ForOfStatement: checkEmpty, + ArrowFunctionExpression: checkEmpty, + YieldExpression: checkEmpty, + TemplateLiteral: checkEmpty, + TaggedTemplateExpression: checkEmpty, + TemplateElement: checkEmpty, + ObjectPattern: checkEmpty, + ArrayPattern: checkEmpty, + RestElement: checkEmpty, + AssignmentPattern: checkEmpty, + ClassBody: checkEmpty, + MethodDefinition: checkEmpty, + MetaProperty: checkEmpty + }; + + rule[type] = function (node) { + const expectedNames = expectedNamesList.shift(); + const variables = context.getDeclaredVariables(node); + + assert(Array.isArray(expectedNames)); + assert(Array.isArray(variables)); + assert.strictEqual(expectedNames.length, variables.length); + for (let i = variables.length - 1; i >= 0; i--) { + assert.strictEqual(expectedNames[i], variables[i].name); + } + }; + return rule; + } + }); + linter.verify(code, { + rules: { test: 2 }, + parserOptions: { + ecmaVersion: 6, + sourceType: "module" + } + }); + + // Check all expected names are asserted. + assert.strictEqual(0, expectedNamesList.length); + } + + it("VariableDeclaration", () => { + const code = "\n var {a, x: [b], y: {c = 0}} = foo;\n let {d, x: [e], y: {f = 0}} = foo;\n const {g, x: [h], y: {i = 0}} = foo, {j, k = function(z) { let l; }} = bar;\n "; + const namesList = [ + ["a", "b", "c"], + ["d", "e", "f"], + ["g", "h", "i", "j", "k"], + ["l"] + ]; + + verify(code, "VariableDeclaration", namesList); + }); + + it("VariableDeclaration (on for-in/of loop)", () => { + + // TDZ scope is created here, so tests to exclude those. + const code = "\n for (var {a, x: [b], y: {c = 0}} in foo) {\n let g;\n }\n for (let {d, x: [e], y: {f = 0}} of foo) {\n let h;\n }\n "; + const namesList = [ + ["a", "b", "c"], + ["g"], + ["d", "e", "f"], + ["h"] + ]; + + verify(code, "VariableDeclaration", namesList); + }); + + it("VariableDeclarator", () => { + + // TDZ scope is created here, so tests to exclude those. + const code = "\n var {a, x: [b], y: {c = 0}} = foo;\n let {d, x: [e], y: {f = 0}} = foo;\n const {g, x: [h], y: {i = 0}} = foo, {j, k = function(z) { let l; }} = bar;\n "; + const namesList = [ + ["a", "b", "c"], + ["d", "e", "f"], + ["g", "h", "i"], + ["j", "k"], + ["l"] + ]; + + verify(code, "VariableDeclarator", namesList); + }); + + it("FunctionDeclaration", () => { + const code = "\n function foo({a, x: [b], y: {c = 0}}, [d, e]) {\n let z;\n }\n function bar({f, x: [g], y: {h = 0}}, [i, j = function(q) { let w; }]) {\n let z;\n }\n "; + const namesList = [ + ["foo", "a", "b", "c", "d", "e"], + ["bar", "f", "g", "h", "i", "j"] + ]; + + verify(code, "FunctionDeclaration", namesList); + }); + + it("FunctionExpression", () => { + const code = "\n (function foo({a, x: [b], y: {c = 0}}, [d, e]) {\n let z;\n });\n (function bar({f, x: [g], y: {h = 0}}, [i, j = function(q) { let w; }]) {\n let z;\n });\n "; + const namesList = [ + ["foo", "a", "b", "c", "d", "e"], + ["bar", "f", "g", "h", "i", "j"], + ["q"] + ]; + + verify(code, "FunctionExpression", namesList); + }); + + it("ArrowFunctionExpression", () => { + const code = "\n (({a, x: [b], y: {c = 0}}, [d, e]) => {\n let z;\n });\n (({f, x: [g], y: {h = 0}}, [i, j]) => {\n let z;\n });\n "; + const namesList = [ + ["a", "b", "c", "d", "e"], + ["f", "g", "h", "i", "j"] + ]; + + verify(code, "ArrowFunctionExpression", namesList); + }); + + it("ClassDeclaration", () => { + const code = "\n class A { foo(x) { let y; } }\n class B { foo(x) { let y; } }\n "; + const namesList = [ + ["A", "A"], // outer scope's and inner scope's. + ["B", "B"] + ]; + + verify(code, "ClassDeclaration", namesList); + }); + + it("ClassExpression", () => { + const code = "\n (class A { foo(x) { let y; } });\n (class B { foo(x) { let y; } });\n "; + const namesList = [ + ["A"], + ["B"] + ]; + + verify(code, "ClassExpression", namesList); + }); + + it("CatchClause", () => { + const code = "\n try {} catch ({a, b}) {\n let x;\n try {} catch ({c, d}) {\n let y;\n }\n }\n "; + const namesList = [ + ["a", "b"], + ["c", "d"] + ]; + + verify(code, "CatchClause", namesList); + }); + + it("ImportDeclaration", () => { + const code = "\n import \"aaa\";\n import * as a from \"bbb\";\n import b, {c, x as d} from \"ccc\";\n "; + const namesList = [ + [], + ["a"], + ["b", "c", "d"] + ]; + + verify(code, "ImportDeclaration", namesList); + }); + + it("ImportSpecifier", () => { + const code = "\n import \"aaa\";\n import * as a from \"bbb\";\n import b, {c, x as d} from \"ccc\";\n "; + const namesList = [ + ["c"], + ["d"] + ]; + + verify(code, "ImportSpecifier", namesList); + }); + + it("ImportDefaultSpecifier", () => { + const code = "\n import \"aaa\";\n import * as a from \"bbb\";\n import b, {c, x as d} from \"ccc\";\n "; + const namesList = [ + ["b"] + ]; + + verify(code, "ImportDefaultSpecifier", namesList); + }); + + it("ImportNamespaceSpecifier", () => { + const code = "\n import \"aaa\";\n import * as a from \"bbb\";\n import b, {c, x as d} from \"ccc\";\n "; + const namesList = [ + ["a"] + ]; + + verify(code, "ImportNamespaceSpecifier", namesList); + }); }); describe("context.markVariableAsUsed()", () => { @@ -7719,21 +8856,74 @@ describe("Linter with FlatConfigArray", () => { assert(spy && spy.calledOnce); }); }); - }); - describe("Rule Severity", () => { + xdescribe("context.getCwd()", () => { + const code = "a;\nb;"; + const config = { rules: { checker: "error" } }; - it("rule should run as warning when set to 1 with a config array", () => { - const ruleId = "semi", - configs = createFlatConfigArray({ - files: ["**/*.js"], - rules: { - [ruleId]: 1 - } - }); + it("should get cwd correctly in the context", () => { + const cwd = "cwd"; + const linterWithOption = new Linter({ cwd }); + let spy; - configs.normalizeSync(); - const messages = linter.verify("foo", configs, filename, true); + linterWithOption.defineRule("checker", context => { + spy = sinon.spy(() => { + assert.strictEqual(context.getCwd(), cwd); + }); + return { Program: spy }; + }); + + linterWithOption.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should assign process.cwd() to it if cwd is undefined", () => { + let spy; + const linterWithOption = new Linter({}); + + linterWithOption.defineRule("checker", context => { + + spy = sinon.spy(() => { + assert.strictEqual(context.getCwd(), process.cwd()); + }); + return { Program: spy }; + }); + + linterWithOption.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should assign process.cwd() to it if the option is undefined", () => { + let spy; + + linter.defineRule("checker", context => { + + spy = sinon.spy(() => { + assert.strictEqual(context.getCwd(), process.cwd()); + }); + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + + }); + + describe("Rule Severity", () => { + + it("rule should run as warning when set to 1 with a config array", () => { + const ruleId = "semi", + configs = createFlatConfigArray({ + files: ["**/*.js"], + rules: { + [ruleId]: 1 + } + }); + + configs.normalizeSync(); + const messages = linter.verify("foo", configs, filename, true); assert.strictEqual(messages.length, 1, "Message length is wrong"); assert.strictEqual(messages[0].ruleId, ruleId); @@ -7754,7 +8944,7 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(messages[0].ruleId, ruleId); }); - it.only("rule should run as warning when set to 1 with an object", () => { + it("rule should run as warning when set to 1 with an object", () => { const ruleId = "semi", config = { files: ["**/*.js"], @@ -8005,54 +9195,3165 @@ describe("Linter with FlatConfigArray", () => { } }; - linter.verify("@foo class A {}", config); + linter.verify("@foo class A {}", config); + + sourceCode = linter.getSourceCode(); + }); + + it("should use the scope (so the global scope has the reference of '@foo')", () => { + assert.strictEqual(scope.references.length, 1); + assert.deepStrictEqual( + scope.references[0].identifier.name, + "foo" + ); + }); + + it("should use the same scope if the source code object is reused", () => { + let scope2 = null; + const config = { + plugins: { + test: { + rules: { + "save-scope2": context => ({ + Program() { + scope2 = context.getScope(); + } + }) + } + } + }, + rules: { + "test/save-scope2": "error" + } + }; + + linter.verify(sourceCode, config, "test.js"); + + assert(scope2 !== null); + assert(scope2 === scope); + }); + }); + + it("should not pass any default parserOptions to the parser", () => { + const messages = linter.verify(";", { + languageOptions: { + parser: testParsers.throwsWithOptions + } + }, "filename"); + + assert.strictEqual(messages.length, 0); + }); + }); + + xdescribe("Code with a hashbang comment", () => { + const code = "#!bin/program\n\nvar foo;;"; + + it("should preserve line numbers", () => { + const config = { rules: { "no-extra-semi": 1 } }; + const messages = linter.verify(code, config); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-extra-semi"); + assert.strictEqual(messages[0].nodeType, "EmptyStatement"); + assert.strictEqual(messages[0].line, 3); + }); + + it("should have a comment with the hashbang in it", () => { + const config = { rules: { checker: "error" } }; + const spy = sinon.spy(context => { + const comments = context.getAllComments(); + + assert.strictEqual(comments.length, 1); + assert.strictEqual(comments[0].type, "hashbang"); + return {}; + }); + + linter.defineRule("checker", spy); + linter.verify(code, config); + assert(spy.calledOnce); + }); + }); + + xdescribe("Options", () => { + + describe("filename", () => { + it("should allow filename to be passed on options object", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.getFilename(), "foo.js"); + return {}; + }); + + linter.defineRule("checker", filenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); + assert(filenameChecker.calledOnce); + }); + + it("should allow filename to be passed as third argument", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.getFilename(), "bar.js"); + return {}; + }); + + linter.defineRule("checker", filenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }, "bar.js"); + assert(filenameChecker.calledOnce); + }); + + it("should default filename to when options object doesn't have filename", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.getFilename(), ""); + return {}; + }); + + linter.defineRule("checker", filenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }, {}); + assert(filenameChecker.calledOnce); + }); + + it("should default filename to when only two arguments are passed", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.getFilename(), ""); + return {}; + }); + + linter.defineRule("checker", filenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }); + assert(filenameChecker.calledOnce); + }); + }); + + describe("physicalFilename", () => { + it("should be same as `filename` passed on options object, if no processors are used", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), "foo.js"); + return {}; + }); + + linter.defineRule("checker", physicalFilenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); + assert(physicalFilenameChecker.calledOnce); + }); + + it("should default physicalFilename to when options object doesn't have filename", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), ""); + return {}; + }); + + linter.defineRule("checker", physicalFilenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }, {}); + assert(physicalFilenameChecker.calledOnce); + }); + + it("should default physicalFilename to when only two arguments are passed", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), ""); + return {}; + }); + + linter.defineRule("checker", physicalFilenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }); + assert(physicalFilenameChecker.calledOnce); + }); + }); + + }); + + describe("Inline Directives", () => { + + xdescribe("/*global*/ Comments", () => { + + describe("when evaluating code containing /*global */ and /*globals */ blocks", () => { + + it("variables should be available in global scope", () => { + const config = { rules: { checker: "error" }, globals: { Array: "off", ConfigGlobal: "writeable" } }; + const code = ` + /*global a b:true c:false d:readable e:writeable Math:off */ + function foo() {} + /*globals f:true*/ + /* global ConfigGlobal : readable */ + `; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + const a = getVariable(scope, "a"), + b = getVariable(scope, "b"), + c = getVariable(scope, "c"), + d = getVariable(scope, "d"), + e = getVariable(scope, "e"), + f = getVariable(scope, "f"), + mathGlobal = getVariable(scope, "Math"), + arrayGlobal = getVariable(scope, "Array"), + configGlobal = getVariable(scope, "ConfigGlobal"); + + assert.strictEqual(a.name, "a"); + assert.strictEqual(a.writeable, false); + assert.strictEqual(b.name, "b"); + assert.strictEqual(b.writeable, true); + assert.strictEqual(c.name, "c"); + assert.strictEqual(c.writeable, false); + assert.strictEqual(d.name, "d"); + assert.strictEqual(d.writeable, false); + assert.strictEqual(e.name, "e"); + assert.strictEqual(e.writeable, true); + assert.strictEqual(f.name, "f"); + assert.strictEqual(f.writeable, true); + assert.strictEqual(mathGlobal, null); + assert.strictEqual(arrayGlobal, null); + assert.strictEqual(configGlobal.name, "ConfigGlobal"); + assert.strictEqual(configGlobal.writeable, false); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + + describe("when evaluating code containing a /*global */ block with sloppy whitespace", () => { + const code = "/* global a b : true c: false*/"; + + it("variables should be available in global scope", () => { + const config = { rules: { checker: "error" } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + a = getVariable(scope, "a"), + b = getVariable(scope, "b"), + c = getVariable(scope, "c"); + + assert.strictEqual(a.name, "a"); + assert.strictEqual(a.writeable, false); + assert.strictEqual(b.name, "b"); + assert.strictEqual(b.writeable, true); + assert.strictEqual(c.name, "c"); + assert.strictEqual(c.writeable, false); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + + describe("when evaluating code containing a /*global */ block with specific variables", () => { + const code = "/* global toString hasOwnProperty valueOf: true */"; + + it("should not throw an error if comment block has global variables which are Object.prototype contains", () => { + const config = { rules: { checker: "error" } }; + + linter.verify(code, config); + }); + }); + + describe("when evaluating code containing a line comment", () => { + const code = "//global a \n function f() {}"; + + it("should not introduce a global variable", () => { + const config = { rules: { checker: "error" } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(getVariable(scope, "a"), null); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + + describe("when evaluating code containing normal block comments", () => { + const code = "/**/ /*a*/ /*b:true*/ /*foo c:false*/"; + + it("should not introduce a global variable", () => { + const config = { rules: { checker: "error" } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(getVariable(scope, "a"), null); + assert.strictEqual(getVariable(scope, "b"), null); + assert.strictEqual(getVariable(scope, "foo"), null); + assert.strictEqual(getVariable(scope, "c"), null); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + + it("should attach a \"/*global\" comment node to declared variables", () => { + const code = "/* global foo */\n/* global bar, baz */"; + let ok = false; + + linter.defineRules({ + test(context) { + return { + Program() { + const scope = context.getScope(); + const sourceCode = context.getSourceCode(); + const comments = sourceCode.getAllComments(); + + assert.strictEqual(2, comments.length); + + const foo = getVariable(scope, "foo"); + + assert.strictEqual(foo.eslintExplicitGlobal, true); + assert.strictEqual(foo.eslintExplicitGlobalComments[0], comments[0]); + + const bar = getVariable(scope, "bar"); + + assert.strictEqual(bar.eslintExplicitGlobal, true); + assert.strictEqual(bar.eslintExplicitGlobalComments[0], comments[1]); + + const baz = getVariable(scope, "baz"); + + assert.strictEqual(baz.eslintExplicitGlobal, true); + assert.strictEqual(baz.eslintExplicitGlobalComments[0], comments[1]); + + ok = true; + } + }; + } + }); + + linter.verify(code, { rules: { test: 2 } }); + assert(ok); + }); + + it("should report a linting error when a global is set to an invalid value", () => { + const results = linter.verify("/* global foo: AAAAA, bar: readonly */\nfoo;\nbar;", { rules: { "no-undef": "error" } }); + + assert.deepStrictEqual(results, [ + { + ruleId: null, + severity: 2, + message: "'AAAAA' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')", + line: 1, + column: 1, + endLine: 1, + endColumn: 39, + nodeType: null + }, + { + ruleId: "no-undef", + messageId: "undef", + severity: 2, + message: "'foo' is not defined.", + line: 2, + column: 1, + endLine: 2, + endColumn: 4, + nodeType: "Identifier" + } + ]); + }); + + }); + + xdescribe("/*exported*/ Comments", () => { + + it("we should behave nicely when no matching variable is found", () => { + const code = "/* exported horse */"; + const config = { rules: {} }; + + linter.verify(code, config, filename, true); + }); + + it("variables should be exported", () => { + const code = "/* exported horse */\n\nvar horse = 'circus'"; + const config = { rules: { checker: "error" } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); + + assert.strictEqual(horse.eslintUsed, true); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("undefined variables should not be exported", () => { + const code = "/* exported horse */\n\nhorse = 'circus'"; + const config = { rules: { checker: "error" } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); + + assert.strictEqual(horse, null); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("variables should be exported in strict mode", () => { + const code = "/* exported horse */\n'use strict';\nvar horse = 'circus'"; + const config = { rules: { checker: "error" } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); + + assert.strictEqual(horse.eslintUsed, true); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("variables should not be exported in the es6 module environment", () => { + const code = "/* exported horse */\nvar horse = 'circus'"; + const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6, sourceType: "module" } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); + + assert.strictEqual(horse, null); // there is no global scope at all + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("variables should not be exported when in the node environment", () => { + const code = "/* exported horse */\nvar horse = 'circus'"; + const config = { rules: { checker: "error" }, env: { node: true } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); + + assert.strictEqual(horse, null); // there is no global scope at all + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + + xdescribe("/*eslint*/ Comments", () => { + describe("when evaluating code with comments to enable rules", () => { + + it("should report a violation", () => { + const code = "/*eslint no-alert:1*/ alert('test');"; + const config = { rules: {} }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].message, "Unexpected alert."); + assert.include(messages[0].nodeType, "CallExpression"); + }); + + it("rules should not change initial config", () => { + const config = { rules: { strict: 2 } }; + const codeA = "/*eslint strict: 0*/ function bar() { return 2; }"; + const codeB = "function foo() { return 1; }"; + let messages = linter.verify(codeA, config, filename, false); + + assert.strictEqual(messages.length, 0); + + messages = linter.verify(codeB, config, filename, false); + assert.strictEqual(messages.length, 1); + }); + + it("rules should not change initial config", () => { + const config = { rules: { quotes: [2, "double"] } }; + const codeA = "/*eslint quotes: 0*/ function bar() { return '2'; }"; + const codeB = "function foo() { return '1'; }"; + let messages = linter.verify(codeA, config, filename, false); + + assert.strictEqual(messages.length, 0); + + messages = linter.verify(codeB, config, filename, false); + assert.strictEqual(messages.length, 1); + }); + + it("rules should not change initial config", () => { + const config = { rules: { quotes: [2, "double"] } }; + const codeA = "/*eslint quotes: [0, \"single\"]*/ function bar() { return '2'; }"; + const codeB = "function foo() { return '1'; }"; + let messages = linter.verify(codeA, config, filename, false); + + assert.strictEqual(messages.length, 0); + + messages = linter.verify(codeB, config, filename, false); + assert.strictEqual(messages.length, 1); + }); + + it("rules should not change initial config", () => { + const config = { rules: { "no-unused-vars": [2, { vars: "all" }] } }; + const codeA = "/*eslint no-unused-vars: [0, {\"vars\": \"local\"}]*/ var a = 44;"; + const codeB = "var b = 55;"; + let messages = linter.verify(codeA, config, filename, false); + + assert.strictEqual(messages.length, 0); + + messages = linter.verify(codeB, config, filename, false); + assert.strictEqual(messages.length, 1); + }); + }); + + describe("when evaluating code with invalid comments to enable rules", () => { + it("should report a violation when the config is not a valid rule configuration", () => { + assert.deepStrictEqual( + linter.verify("/*eslint no-alert:true*/ alert('test');", {}), + [ + { + severity: 2, + ruleId: "no-alert", + message: "Configuration for rule \"no-alert\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed 'true').\n", + line: 1, + column: 1, + endLine: 1, + endColumn: 25, + nodeType: null + } + ] + ); + }); + + it("should report a violation when the config violates a rule's schema", () => { + assert.deepStrictEqual( + linter.verify("/* eslint no-alert: [error, {nonExistentPropertyName: true}]*/", {}), + [ + { + severity: 2, + ruleId: "no-alert", + message: "Configuration for rule \"no-alert\" is invalid:\n\tValue [{\"nonExistentPropertyName\":true}] should NOT have more than 0 items.\n", + line: 1, + column: 1, + endLine: 1, + endColumn: 63, + nodeType: null + } + ] + ); + }); + }); + + describe("when evaluating code with comments to disable rules", () => { + const code = "/*eslint no-alert:0*/ alert('test');"; + + it("should not report a violation", () => { + const config = { rules: { "no-alert": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + }); + + describe("when evaluating code with comments to disable rules", () => { + let code, messages; + + it("should report an error when disabling a non-existent rule in inline comment", () => { + code = "/*eslint foo:0*/ ;"; + messages = linter.verify(code, {}, filename); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + + code = "/*eslint-disable foo*/ ;"; + messages = linter.verify(code, {}, filename); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + + code = "/*eslint-disable-line foo*/ ;"; + messages = linter.verify(code, {}, filename); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + + code = "/*eslint-disable-next-line foo*/ ;"; + messages = linter.verify(code, {}, filename); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + }); + + it("should not report an error, when disabling a non-existent rule in config", () => { + messages = linter.verify("", { rules: { foo: 0 } }, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should report an error, when config a non-existent rule in config", () => { + messages = linter.verify("", { rules: { foo: 1 } }, filename); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + + messages = linter.verify("", { rules: { foo: 2 } }, filename); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + }); + }); + + describe("when evaluating code with comments to enable multiple rules", () => { + const code = "/*eslint no-alert:1 no-console:1*/ alert('test'); console.log('test');"; + + it("should report a violation", () => { + const config = { rules: {} }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].message, "Unexpected alert."); + assert.include(messages[0].nodeType, "CallExpression"); + assert.strictEqual(messages[1].ruleId, "no-console"); + }); + }); + + describe("when evaluating code with comments to enable and disable multiple rules", () => { + const code = "/*eslint no-alert:1 no-console:0*/ alert('test'); console.log('test');"; + + it("should report a violation", () => { + const config = { rules: { "no-console": 1, "no-alert": 0 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].message, "Unexpected alert."); + assert.include(messages[0].nodeType, "CallExpression"); + }); + }); + + describe("when evaluating code with comments to disable and enable configurable rule as part of plugin", () => { + + let baseConfig; + + beforeEach(() => { + baseConfig = { + plugins: { + "test-plugin": { + rules: { + "test-rule"(context) { + return { + Literal(node) { + if (node.value === "trigger violation") { + context.report(node, "Reporting violation."); + } + } + }; + } + } + } + } + }; + + }); + + it("should not report a violation when inline comment enables plugin rule and there's no violation", () => { + const config = { ...baseConfig, rules: {} }; + const code = "/*eslint test-plugin/test-rule: 2*/ var a = \"no violation\";"; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not report a violation when inline comment disables plugin rule", () => { + const code = "/*eslint test-plugin/test-rule:0*/ var a = \"trigger violation\""; + const config = { ...baseConfig, rules: { "test-plugin/test-rule": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should report a violation when the report is right before the comment", () => { + const code = " /* eslint-disable */ "; + + linter.defineRule("checker", context => ({ + Program() { + context.report({ loc: { line: 1, column: 0 }, message: "foo" }); + } + })); + const problems = linter.verify(code, { rules: { checker: "error" } }); + + assert.strictEqual(problems.length, 1); + assert.strictEqual(problems[0].message, "foo"); + }); + + it("should not report a violation when the report is right at the start of the comment", () => { + const code = " /* eslint-disable */ "; + + linter.defineRule("checker", context => ({ + Program() { + context.report({ loc: { line: 1, column: 1 }, message: "foo" }); + } + })); + const problems = linter.verify(code, { rules: { checker: "error" } }); + + assert.strictEqual(problems.length, 0); + }); + + it("rules should not change initial config", () => { + const config = { rules: { "test-plugin/test-rule": 2 } }; + const codeA = "/*eslint test-plugin/test-rule: 0*/ var a = \"trigger violation\";"; + const codeB = "var a = \"trigger violation\";"; + let messages = linter.verify(codeA, config, filename, false); + + assert.strictEqual(messages.length, 0); + + messages = linter.verify(codeB, config, filename, false); + assert.strictEqual(messages.length, 1); + }); + }); + + describe("when evaluating code with comments to enable and disable all reporting", () => { + it("should report a violation", () => { + + const code = [ + "/*eslint-disable */", + "alert('test');", + "/*eslint-enable */", + "alert('test');" + ].join("\n"); + const config = { rules: { "no-alert": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].message, "Unexpected alert."); + assert.include(messages[0].nodeType, "CallExpression"); + assert.strictEqual(messages[0].line, 4); + }); + + it("should not report a violation", () => { + const code = [ + "/*eslint-disable */", + "alert('test');", + "alert('test');" + ].join("\n"); + const config = { rules: { "no-alert": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not report a violation", () => { + const code = [ + " alert('test1');/*eslint-disable */\n", + "alert('test');", + " alert('test');\n", + "/*eslint-enable */alert('test2');" + ].join(""); + const config = { rules: { "no-alert": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].column, 21); + assert.strictEqual(messages[1].column, 19); + }); + + it("should report a violation", () => { + + const code = [ + "/*eslint-disable */", + "alert('test');", + "/*eslint-disable */", + "alert('test');", + "/*eslint-enable*/", + "alert('test');", + "/*eslint-enable*/" + ].join("\n"); + + const config = { rules: { "no-alert": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + }); + + + it("should not report a violation", () => { + const code = [ + "/*eslint-disable */", + "(function(){ var b = 44;})()", + "/*eslint-enable */;any();" + ].join("\n"); + + const config = { rules: { "no-unused-vars": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not report a violation", () => { + const code = [ + "(function(){ /*eslint-disable */ var b = 44;})()", + "/*eslint-enable */;any();" + ].join("\n"); + + const config = { rules: { "no-unused-vars": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + }); + + describe("when evaluating code with comments to enable and disable multiple comma separated rules", () => { + const code = "/*eslint no-alert:1, no-console:0*/ alert('test'); console.log('test');"; + + it("should report a violation", () => { + const config = { rules: { "no-console": 1, "no-alert": 0 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].message, "Unexpected alert."); + assert.include(messages[0].nodeType, "CallExpression"); + }); + }); + + describe("when evaluating code with comments to enable configurable rule", () => { + const code = "/*eslint quotes:[2, \"double\"]*/ alert('test');"; + + it("should report a violation", () => { + const config = { rules: { quotes: [2, "single"] } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "quotes"); + assert.strictEqual(messages[0].message, "Strings must use doublequote."); + assert.include(messages[0].nodeType, "Literal"); + }); + }); + + describe("when evaluating code with comments to enable configurable rule using string severity", () => { + const code = "/*eslint quotes:[\"error\", \"double\"]*/ alert('test');"; + + it("should report a violation", () => { + const config = { rules: { quotes: [2, "single"] } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "quotes"); + assert.strictEqual(messages[0].message, "Strings must use doublequote."); + assert.include(messages[0].nodeType, "Literal"); + }); + }); + + describe("when evaluating code with incorrectly formatted comments to disable rule", () => { + it("should report a violation", () => { + const code = "/*eslint no-alert:'1'*/ alert('test');"; + + const config = { rules: { "no-alert": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + + /* + * Incorrectly formatted comment threw error; + * message from caught exception + * may differ amongst UAs, so verifying + * first part only as defined in the + * parseJsonConfig function in lib/eslint.js + */ + assert.match(messages[0].message, /^Failed to parse JSON from ' "no-alert":'1'':/u); + assert.strictEqual(messages[0].line, 1); + assert.strictEqual(messages[0].column, 1); + + assert.strictEqual(messages[1].ruleId, "no-alert"); + assert.strictEqual(messages[1].message, "Unexpected alert."); + assert.include(messages[1].nodeType, "CallExpression"); + }); + + it("should report a violation", () => { + const code = "/*eslint no-alert:abc*/ alert('test');"; + + const config = { rules: { "no-alert": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + + /* + * Incorrectly formatted comment threw error; + * message from caught exception + * may differ amongst UAs, so verifying + * first part only as defined in the + * parseJsonConfig function in lib/eslint.js + */ + assert.match(messages[0].message, /^Failed to parse JSON from ' "no-alert":abc':/u); + assert.strictEqual(messages[0].line, 1); + assert.strictEqual(messages[0].column, 1); + + assert.strictEqual(messages[1].ruleId, "no-alert"); + assert.strictEqual(messages[1].message, "Unexpected alert."); + assert.include(messages[1].nodeType, "CallExpression"); + }); + + it("should report a violation", () => { + const code = "/*eslint no-alert:0 2*/ alert('test');"; + + const config = { rules: { "no-alert": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + + /* + * Incorrectly formatted comment threw error; + * message from caught exception + * may differ amongst UAs, so verifying + * first part only as defined in the + * parseJsonConfig function in lib/eslint.js + */ + assert.match(messages[0].message, /^Failed to parse JSON from ' "no-alert":0 2':/u); + assert.strictEqual(messages[0].line, 1); + assert.strictEqual(messages[0].column, 1); + + assert.strictEqual(messages[1].ruleId, "no-alert"); + assert.strictEqual(messages[1].message, "Unexpected alert."); + assert.include(messages[1].nodeType, "CallExpression"); + }); + }); + + describe("when evaluating code with comments which have colon in its value", () => { + const code = String.raw` +/* eslint max-len: [2, 100, 2, {ignoreUrls: true, ignorePattern: "data:image\\/|\\s*require\\s*\\(|^\\s*loader\\.lazy|-\\*-"}] */ +alert('test'); +`; + + it("should not parse errors, should report a violation", () => { + const messages = linter.verify(code, {}, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "max-len"); + assert.strictEqual(messages[0].message, "This line has a length of 129. Maximum allowed is 100."); + assert.include(messages[0].nodeType, "Program"); + }); + }); + + describe("when evaluating code with comments that contain escape sequences", () => { + const code = String.raw` +/* eslint max-len: ["error", 1, { ignoreComments: true, ignorePattern: "console\\.log\\(" }] */ +console.log("test"); +consolexlog("test2"); +var a = "test2"; +`; + + it("should validate correctly", () => { + const config = { rules: {} }; + const messages = linter.verify(code, config, filename); + const [message1, message2] = messages; + + assert.strictEqual(messages.length, 2); + assert.strictEqual(message1.ruleId, "max-len"); + assert.strictEqual(message1.message, "This line has a length of 21. Maximum allowed is 1."); + assert.strictEqual(message1.line, 4); + assert.strictEqual(message1.column, 1); + assert.include(message1.nodeType, "Program"); + assert.strictEqual(message2.ruleId, "max-len"); + assert.strictEqual(message2.message, "This line has a length of 16. Maximum allowed is 1."); + assert.strictEqual(message2.line, 5); + assert.strictEqual(message2.column, 1); + assert.include(message2.nodeType, "Program"); + }); + }); + + }); + + describe("/*eslint-disable*/ and /*eslint-enable*/", () => { + it("should report a violation", () => { + const code = [ + "/*eslint-disable no-alert */", + "alert('test');", + "console.log('test');" // here + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + + assert.strictEqual(messages[0].ruleId, "no-console"); + }); + + it("should report no violation", () => { + const code = [ + "/* eslint-disable quotes */", + "console.log(\"foo\");", + "/* eslint-enable quotes */" + ].join("\n"); + const config = { rules: { quotes: 2 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should report a violation", () => { + const code = [ + "/*eslint-disable no-alert, no-console */", + "alert('test');", + "console.log('test');", + "/*eslint-enable*/", + + "alert('test');", // here + "console.log('test');" // here + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].line, 5); + assert.strictEqual(messages[1].ruleId, "no-console"); + assert.strictEqual(messages[1].line, 6); + }); + + it("should report a violation", () => { + const code = [ + "/*eslint-disable no-alert */", + "alert('test');", + "console.log('test');", + "/*eslint-enable no-console */", + + "alert('test');" // here + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + + assert.strictEqual(messages[0].ruleId, "no-console"); + }); + + + it("should report a violation", () => { + const code = [ + "/*eslint-disable no-alert, no-console */", + "alert('test');", + "console.log('test');", + "/*eslint-enable no-alert*/", + + "alert('test');", // here + "console.log('test');" + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].line, 5); + }); + + + it("should report a violation", () => { + const code = [ + "/*eslint-disable no-alert */", + + "/*eslint-disable no-console */", + "alert('test');", + "console.log('test');", + "/*eslint-enable */", + + "alert('test');", // here + "console.log('test');", // here + + "/*eslint-enable */", + + "alert('test');", // here + "console.log('test');", // here + + "/*eslint-enable*/" + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 4); + + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].line, 6); + + assert.strictEqual(messages[1].ruleId, "no-console"); + assert.strictEqual(messages[1].line, 7); + + assert.strictEqual(messages[2].ruleId, "no-alert"); + assert.strictEqual(messages[2].line, 9); + + assert.strictEqual(messages[3].ruleId, "no-console"); + assert.strictEqual(messages[3].line, 10); + + }); + + it("should report a violation", () => { + const code = [ + "/*eslint-disable no-alert, no-console */", + "alert('test');", + "console.log('test');", + + "/*eslint-enable no-alert */", + + "alert('test');", // here + "console.log('test');", + + "/*eslint-enable no-console */", + + "alert('test');", // here + "console.log('test');", // here + "/*eslint-enable no-console */" + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 3); + + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].line, 5); + + assert.strictEqual(messages[1].ruleId, "no-alert"); + assert.strictEqual(messages[1].line, 8); + + assert.strictEqual(messages[2].ruleId, "no-console"); + assert.strictEqual(messages[2].line, 9); + + }); + + it("should report a violation when severity is warn", () => { + const code = [ + "/*eslint-disable no-alert, no-console */", + "alert('test');", + "console.log('test');", + + "/*eslint-enable no-alert */", + + "alert('test');", // here + "console.log('test');", + + "/*eslint-enable no-console */", + + "alert('test');", // here + "console.log('test');", // here + "/*eslint-enable no-console */" + ].join("\n"); + const config = { rules: { "no-alert": "warn", "no-console": "warn" } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 3); + + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].line, 5); + + assert.strictEqual(messages[1].ruleId, "no-alert"); + assert.strictEqual(messages[1].line, 8); + + assert.strictEqual(messages[2].ruleId, "no-console"); + assert.strictEqual(messages[2].line, 9); + + }); + + it("should report no violation", () => { + const code = [ + "/*eslint-disable no-unused-vars */", + "var foo; // eslint-disable-line no-unused-vars", + "var bar;", + "/* eslint-enable no-unused-vars */" // here + ].join("\n"); + const config = { rules: { "no-unused-vars": 2 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + }); + + describe("/*eslint-disable-line*/", () => { + + it("should report a violation", () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert", + "console.log('test');" // here + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + + assert.strictEqual(messages[0].ruleId, "no-console"); + }); + + it("should report a violation", () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert", + "console.log('test'); // eslint-disable-line no-console", + "alert('test');" // here + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + + assert.strictEqual(messages[0].ruleId, "no-alert"); + }); + + it("should report a violation if eslint-disable-line in a block comment is not on a single line", () => { + const code = [ + "/* eslint-disable-line", + "*", + "*/ console.log('test');" // here + ].join("\n"); + const config = { + rules: { + "no-console": 1 + } + }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + + assert.strictEqual(messages[1].ruleId, "no-console"); + }); + + it("should not disable rule and add an extra report if eslint-disable-line in a block comment is not on a single line", () => { + const code = [ + "alert('test'); /* eslint-disable-line ", + "no-alert */" + ].join("\n"); + const config = { + rules: { + "no-alert": 1 + } + }; + + const messages = linter.verify(code, config); + + assert.deepStrictEqual(messages, [ + { + ruleId: "no-alert", + severity: 1, + line: 1, + column: 1, + endLine: 1, + endColumn: 14, + message: "Unexpected alert.", + messageId: "unexpected", + nodeType: "CallExpression" + }, + { + ruleId: null, + severity: 2, + message: "eslint-disable-line comment should not span multiple lines.", + line: 1, + column: 16, + endLine: 2, + endColumn: 12, + nodeType: null + } + ]); + }); + + it("should not report a violation for eslint-disable-line in block comment", () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert", + "alert('test'); /*eslint-disable-line no-alert*/" + ].join("\n"); + const config = { + rules: { + "no-alert": 1 + } + }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not report a violation", () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert", + "console.log('test'); // eslint-disable-line no-console" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not report a violation", () => { + const code = [ + "alert('test') // eslint-disable-line no-alert, quotes, semi", + "console.log('test'); // eslint-disable-line" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + quotes: [1, "double"], + semi: [1, "always"], + "no-console": 1 + } + }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not report a violation", () => { + const code = [ + "alert('test') /* eslint-disable-line no-alert, quotes, semi */", + "console.log('test'); /* eslint-disable-line */" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + quotes: [1, "double"], + semi: [1, "always"], + "no-console": 1 + } + }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should ignore violations of multiple rules when specified in mixed comments", () => { + const code = [ + " alert(\"test\"); /* eslint-disable-line no-alert */ // eslint-disable-line quotes" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + quotes: [1, "single"] + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should report no violation", () => { + const code = [ + "var foo1; // eslint-disable-line no-unused-vars", + "var foo2; // eslint-disable-line no-unused-vars", + "var foo3; // eslint-disable-line no-unused-vars", + "var foo4; // eslint-disable-line no-unused-vars", + "var foo5; // eslint-disable-line no-unused-vars" + ].join("\n"); + const config = { rules: { "no-unused-vars": 2 } }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + }); + + describe("/*eslint-disable-next-line*/", () => { + it("should ignore violation of specified rule on next line", () => { + const code = [ + "// eslint-disable-next-line no-alert", + "alert('test');", + "console.log('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + }); + + it("should ignore violation of specified rule if eslint-disable-next-line is a block comment", () => { + const code = [ + "/* eslint-disable-next-line no-alert */", + "alert('test');", + "console.log('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + }); + it("should ignore violation of specified rule if eslint-disable-next-line is a block comment", () => { + const code = [ + "/* eslint-disable-next-line no-alert */", + "alert('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not ignore violation if block comment is not on a single line", () => { + const code = [ + "/* eslint-disable-next-line", + "no-alert */alert('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[1].ruleId, "no-alert"); + }); + + it("should ignore violations only of specified rule", () => { + const code = [ + "// eslint-disable-next-line no-console", + "alert('test');", + "console.log('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[1].ruleId, "no-console"); + }); + + it("should ignore violations of multiple rules when specified", () => { + const code = [ + "// eslint-disable-next-line no-alert, quotes", + "alert(\"test\");", + "console.log('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + quotes: [1, "single"], + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + }); + + it("should ignore violations of multiple rules when specified in mixed comments", () => { + const code = [ + "/* eslint-disable-next-line no-alert */ // eslint-disable-next-line quotes", + "alert(\"test\");" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + quotes: [1, "single"] + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should ignore violations of only the specified rule on next line", () => { + const code = [ + "// eslint-disable-next-line quotes", + "alert(\"test\");", + "console.log('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + quotes: [1, "single"], + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[1].ruleId, "no-console"); + }); + + it("should ignore violations of specified rule on next line only", () => { + const code = [ + "alert('test');", + "// eslint-disable-next-line no-alert", + "alert('test');", + "console.log('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[1].ruleId, "no-console"); + }); + + it("should ignore all rule violations on next line if none specified", () => { + const code = [ + "// eslint-disable-next-line", + "alert(\"test\");", + "console.log('test')" + ].join("\n"); + const config = { + rules: { + semi: [1, "never"], + quotes: [1, "single"], + "no-alert": 1, + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + }); + + it("should ignore violations if eslint-disable-next-line is a block comment", () => { + const code = [ + "alert('test');", + "/* eslint-disable-next-line no-alert */", + "alert('test');", + "console.log('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[1].ruleId, "no-console"); + }); + + it("should report a violation", () => { + const code = [ + "/* eslint-disable-next-line", + "*", + "*/", + "console.log('test');" // here + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + + assert.strictEqual(messages[1].ruleId, "no-console"); + }); + + it("should not ignore violations if comment is of the type hashbang", () => { + const code = [ + "#! eslint-disable-next-line no-alert", + "alert('test');", + "console.log('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[1].ruleId, "no-console"); + }); + }); + + xdescribe("descriptions in directive comments", () => { + it("should ignore the part preceded by '--' in '/*eslint*/'.", () => { + const aaa = sinon.stub().returns({}); + const bbb = sinon.stub().returns({}); + + linter.defineRule("aaa", { create: aaa }); + linter.defineRule("bbb", { create: bbb }); + const messages = linter.verify(` + /*eslint aaa:error -- bbb:error */ + console.log("hello") + `, {}); + + // Don't include syntax error of the comment. + assert.deepStrictEqual(messages, []); + + // Use only `aaa`. + assert.strictEqual(aaa.callCount, 1); + assert.strictEqual(bbb.callCount, 0); + }); + + it("should ignore the part preceded by '--' in '/*eslint-env*/'.", () => { + const messages = linter.verify(` + /*eslint-env es2015 -- es2017 */ + var Promise = {} + var Atomics = {} + `, { rules: { "no-redeclare": "error" } }); + + // Don't include `Atomics` + assert.deepStrictEqual( + messages, + [{ + column: 25, + endColumn: 32, + endLine: 3, + line: 3, + message: "'Promise' is already defined as a built-in global variable.", + messageId: "redeclaredAsBuiltin", + nodeType: "Identifier", + ruleId: "no-redeclare", + severity: 2 + }] + ); + }); + + it("should ignore the part preceded by '--' in '/*global*/'.", () => { + const messages = linter.verify(` + /*global aaa -- bbb */ + var aaa = {} + var bbb = {} + `, { rules: { "no-redeclare": "error" } }); + + // Don't include `bbb` + assert.deepStrictEqual( + messages, + [{ + column: 30, + endColumn: 33, + line: 2, + endLine: 2, + message: "'aaa' is already defined by a variable declaration.", + messageId: "redeclaredBySyntax", + nodeType: "Block", + ruleId: "no-redeclare", + severity: 2 + }] + ); + }); + + it("should ignore the part preceded by '--' in '/*globals*/'.", () => { + const messages = linter.verify(` + /*globals aaa -- bbb */ + var aaa = {} + var bbb = {} + `, { rules: { "no-redeclare": "error" } }); + + // Don't include `bbb` + assert.deepStrictEqual( + messages, + [{ + column: 31, + endColumn: 34, + line: 2, + endLine: 2, + message: "'aaa' is already defined by a variable declaration.", + messageId: "redeclaredBySyntax", + nodeType: "Block", + ruleId: "no-redeclare", + severity: 2 + }] + ); + }); + + it("should ignore the part preceded by '--' in '/*exported*/'.", () => { + const messages = linter.verify(` + /*exported aaa -- bbb */ + var aaa = {} + var bbb = {} + `, { rules: { "no-unused-vars": "error" } }); + + // Don't include `aaa` + assert.deepStrictEqual( + messages, + [{ + column: 25, + endColumn: 28, + endLine: 4, + line: 4, + message: "'bbb' is assigned a value but never used.", + messageId: "unusedVar", + nodeType: "Identifier", + ruleId: "no-unused-vars", + severity: 2 + }] + ); + }); + + it("should ignore the part preceded by '--' in '/*eslint-disable*/'.", () => { + const messages = linter.verify(` + /*eslint-disable no-redeclare -- no-unused-vars */ + var aaa = {} + var aaa = {} + `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } }); + + // Do include `no-unused-vars` but not `no-redeclare` + assert.deepStrictEqual( + messages, + [{ + column: 25, + endLine: 4, + endColumn: 28, + line: 4, + message: "'aaa' is assigned a value but never used.", + messageId: "unusedVar", + nodeType: "Identifier", + ruleId: "no-unused-vars", + severity: 2 + }] + ); + }); + + it("should ignore the part preceded by '--' in '/*eslint-enable*/'.", () => { + const messages = linter.verify(` + /*eslint-disable no-redeclare, no-unused-vars */ + /*eslint-enable no-redeclare -- no-unused-vars */ + var aaa = {} + var aaa = {} + `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } }); + + // Do include `no-redeclare` but not `no-unused-vars` + assert.deepStrictEqual( + messages, + [{ + column: 25, + endLine: 5, + endColumn: 28, + line: 5, + message: "'aaa' is already defined.", + messageId: "redeclared", + nodeType: "Identifier", + ruleId: "no-redeclare", + severity: 2 + }] + ); + }); + + it("should ignore the part preceded by '--' in '//eslint-disable-line'.", () => { + const messages = linter.verify(` + var aaa = {} //eslint-disable-line no-redeclare -- no-unused-vars + var aaa = {} //eslint-disable-line no-redeclare -- no-unused-vars + `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } }); + + // Do include `no-unused-vars` but not `no-redeclare` + assert.deepStrictEqual( + messages, + [{ + column: 25, + endLine: 3, + endColumn: 28, + line: 3, + message: "'aaa' is assigned a value but never used.", + messageId: "unusedVar", + nodeType: "Identifier", + ruleId: "no-unused-vars", + severity: 2 + }] + ); + }); + + it("should ignore the part preceded by '--' in '/*eslint-disable-line*/'.", () => { + const messages = linter.verify(` + var aaa = {} /*eslint-disable-line no-redeclare -- no-unused-vars */ + var aaa = {} /*eslint-disable-line no-redeclare -- no-unused-vars */ + `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } }); + + // Do include `no-unused-vars` but not `no-redeclare` + assert.deepStrictEqual( + messages, + [{ + column: 25, + endLine: 3, + endColumn: 28, + line: 3, + message: "'aaa' is assigned a value but never used.", + messageId: "unusedVar", + nodeType: "Identifier", + ruleId: "no-unused-vars", + severity: 2 + }] + ); + }); + + it("should ignore the part preceded by '--' in '//eslint-disable-next-line'.", () => { + const messages = linter.verify(` + //eslint-disable-next-line no-redeclare -- no-unused-vars + var aaa = {} + //eslint-disable-next-line no-redeclare -- no-unused-vars + var aaa = {} + `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } }); + + // Do include `no-unused-vars` but not `no-redeclare` + assert.deepStrictEqual( + messages, + [{ + column: 25, + endLine: 5, + endColumn: 28, + line: 5, + message: "'aaa' is assigned a value but never used.", + messageId: "unusedVar", + nodeType: "Identifier", + ruleId: "no-unused-vars", + severity: 2 + }] + ); + }); + + it("should ignore the part preceded by '--' in '/*eslint-disable-next-line*/'.", () => { + const messages = linter.verify(` + /*eslint-disable-next-line no-redeclare -- no-unused-vars */ + var aaa = {} + /*eslint-disable-next-line no-redeclare -- no-unused-vars */ + var aaa = {} + `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } }); + + // Do include `no-unused-vars` but not `no-redeclare` + assert.deepStrictEqual( + messages, + [{ + column: 25, + endLine: 5, + endColumn: 28, + line: 5, + message: "'aaa' is assigned a value but never used.", + messageId: "unusedVar", + nodeType: "Identifier", + ruleId: "no-unused-vars", + severity: 2 + }] + ); + }); + + it("should not ignore the part preceded by '--' if the '--' is not surrounded by whitespaces.", () => { + const rule = sinon.stub().returns({}); + + linter.defineRule("a--rule", { create: rule }); + const messages = linter.verify(` + /*eslint a--rule:error */ + console.log("hello") + `, {}); + + // Don't include syntax error of the comment. + assert.deepStrictEqual(messages, []); + + // Use `a--rule`. + assert.strictEqual(rule.callCount, 1); + }); + + it("should ignore the part preceded by '--' even if the '--' is longer than 2.", () => { + const aaa = sinon.stub().returns({}); + const bbb = sinon.stub().returns({}); + + linter.defineRule("aaa", { create: aaa }); + linter.defineRule("bbb", { create: bbb }); + const messages = linter.verify(` + /*eslint aaa:error -------- bbb:error */ + console.log("hello") + `, {}); + + // Don't include syntax error of the comment. + assert.deepStrictEqual(messages, []); + + // Use only `aaa`. + assert.strictEqual(aaa.callCount, 1); + assert.strictEqual(bbb.callCount, 0); + }); + + it("should ignore the part preceded by '--' with line breaks.", () => { + const aaa = sinon.stub().returns({}); + const bbb = sinon.stub().returns({}); + + linter.defineRule("aaa", { create: aaa }); + linter.defineRule("bbb", { create: bbb }); + const messages = linter.verify(` + /*eslint aaa:error + -------- + bbb:error */ + console.log("hello") + `, {}); + + // Don't include syntax error of the comment. + assert.deepStrictEqual(messages, []); + + // Use only `aaa`. + assert.strictEqual(aaa.callCount, 1); + assert.strictEqual(bbb.callCount, 0); + }); + }); + + describe("allowInlineConfig option", () => { + xdescribe("when evaluating code with comments to change config when allowInlineConfig is enabled", () => { + it("should report a violation for disabling rules", () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + const config = { + rules: { + "no-alert": 1 + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + }); + + it("should report a violation for global variable declarations", () => { + const code = [ + "/* global foo */" + ].join("\n"); + const config = { + rules: { + test: 2 + } + }; + let ok = false; + + linter.defineRules({ + test(context) { + return { + Program() { + const scope = context.getScope(); + const sourceCode = context.getSourceCode(); + const comments = sourceCode.getAllComments(); + + assert.strictEqual(1, comments.length); + + const foo = getVariable(scope, "foo"); + + assert.notOk(foo); + + ok = true; + } + }; + } + }); + + linter.verify(code, config, { allowInlineConfig: false }); + assert(ok); + }); + + it("should report a violation for eslint-disable", () => { + const code = [ + "/* eslint-disable */", + "alert('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1 + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + }); + + it("should not report a violation for rule changes", () => { + const code = [ + "/*eslint no-alert:2*/", + "alert('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 0 + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); + + assert.strictEqual(messages.length, 0); + }); + + it("should report a violation for disable-line", () => { + const code = [ + "alert('test'); // eslint-disable-line" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + }); + + }); + + xdescribe("when evaluating code with 'noInlineConfig'", () => { + for (const directive of [ + "globals foo", + "global foo", + "exported foo", + "eslint eqeqeq: error", + "eslint-disable eqeqeq", + "eslint-disable-line eqeqeq", + "eslint-disable-next-line eqeqeq", + "eslint-enable eqeqeq", + ]) { + // eslint-disable-next-line no-loop-func -- No closures + it(`should warn '/* ${directive} */' if 'noInlineConfig' was given.`, () => { + const messages = linter.verify(`/* ${directive} */`, { noInlineConfig: true }); + + assert.deepStrictEqual(messages.length, 1); + assert.deepStrictEqual(messages[0].fatal, void 0); + assert.deepStrictEqual(messages[0].ruleId, null); + assert.deepStrictEqual(messages[0].severity, 1); + assert.deepStrictEqual(messages[0].message, `'/*${directive.split(" ")[0]}*/' has no effect because you have 'noInlineConfig' setting in your config.`); + }); + } + + for (const directive of [ + "eslint-disable-line eqeqeq", + "eslint-disable-next-line eqeqeq" + ]) { + // eslint-disable-next-line no-loop-func -- No closures + it(`should warn '// ${directive}' if 'noInlineConfig' was given.`, () => { + const messages = linter.verify(`// ${directive}`, { noInlineConfig: true }); + + assert.deepStrictEqual(messages.length, 1); + assert.deepStrictEqual(messages[0].fatal, void 0); + assert.deepStrictEqual(messages[0].ruleId, null); + assert.deepStrictEqual(messages[0].severity, 1); + assert.deepStrictEqual(messages[0].message, `'//${directive.split(" ")[0]}' has no effect because you have 'noInlineConfig' setting in your config.`); + }); + } + + it("should not warn if 'noInlineConfig' and '--no-inline-config' were given.", () => { + const messages = linter.verify("/* globals foo */", { noInlineConfig: true }, { allowInlineConfig: false }); + + assert.deepStrictEqual(messages.length, 0); + }); + }); + + describe("when evaluating code with comments to change config when allowInlineConfig is disabled", () => { + it("should not report a violation", () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + const config = { + rules: { + "no-alert": 1 + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); + + assert.strictEqual(messages.length, 0); + }); + }); + + }); + + xdescribe("reportUnusedDisableDirectives option", () => { + it("reports problems for unused eslint-disable comments", () => { + assert.deepStrictEqual( + linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: true }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("reports problems for unused eslint-disable comments (error)", () => { + assert.deepStrictEqual( + linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "error" }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("reports problems for unused eslint-disable comments (warn)", () => { + assert.deepStrictEqual( + linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "warn" }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 1, + nodeType: null + } + ] + ); + }); + + it("reports problems for unused eslint-disable comments (in config)", () => { + assert.deepStrictEqual( + linter.verify("/* eslint-disable */", { reportUnusedDisableDirectives: true }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 1, + nodeType: null + } + ] + ); + }); + + it("reports problems for partially unused eslint-disable comments (in config)", () => { + const code = "alert('test'); // eslint-disable-line no-alert, no-redeclare"; + const config = { + reportUnusedDisableDirectives: true, + rules: { + "no-alert": 1, + "no-redeclare": 1 + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); + + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'no-redeclare').", + line: 1, + column: 16, + fix: { + range: [46, 60], + text: "" + }, + severity: 1, + nodeType: null + } + ] + ); + }); + + describe("autofix", () => { + const alwaysReportsRule = { + create(context) { + return { + Program(node) { + context.report({ message: "bad code", loc: node.loc.end }); + } + }; + } + }; + + const neverReportsRule = { + create() { + return {}; + } + }; + + const ruleCount = 3; + const usedRules = Array.from( + { length: ruleCount }, + (_, index) => `used${index ? `-${index}` : ""}` // "used", "used-1", "used-2" + ); + const unusedRules = usedRules.map(name => `un${name}`); // "unused", "unused-1", "unused-2" + + const config = { + reportUnusedDisableDirectives: true, + rules: { + ...Object.fromEntries(usedRules.map(name => [name, "error"])), + ...Object.fromEntries(unusedRules.map(name => [name, "error"])) + } + }; + + beforeEach(() => { + linter.defineRules(Object.fromEntries(usedRules.map(name => [name, alwaysReportsRule]))); + linter.defineRules(Object.fromEntries(unusedRules.map(name => [name, neverReportsRule]))); + }); + + const tests = [ + + //----------------------------------------------- + // Removing the entire comment + //----------------------------------------------- + + { + code: "// eslint-disable-line unused", + output: " " + }, + { + code: "foo// eslint-disable-line unused", + output: "foo " + }, + { + code: "// eslint-disable-line ,unused,", + output: " " + }, + { + code: "// eslint-disable-line unused-1, unused-2", + output: " " + }, + { + code: "// eslint-disable-line ,unused-1,, unused-2,, -- comment", + output: " " + }, + { + code: "// eslint-disable-next-line unused\n", + output: " \n" + }, + { + code: "// eslint-disable-next-line unused\nfoo", + output: " \nfoo" + }, + { + code: "/* eslint-disable \nunused\n*/", + output: " " + }, + + //----------------------------------------------- + // Removing only individual rules + //----------------------------------------------- + + // content before the first rule should not be changed + { + code: "//eslint-disable-line unused, used", + output: "//eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used", + output: "// eslint-disable-line used" + }, + { + code: "/*\neslint-disable unused, used*/", + output: "/*\neslint-disable used*/" + }, + { + code: "/*\n eslint-disable unused, used*/", + output: "/*\n eslint-disable used*/" + }, + { + code: "/*\r\neslint-disable unused, used*/", + output: "/*\r\neslint-disable used*/" + }, + { + code: "/*\u2028eslint-disable unused, used*/", + output: "/*\u2028eslint-disable used*/" + }, + { + code: "/*\u00A0eslint-disable unused, used*/", + output: "/*\u00A0eslint-disable used*/" + }, + { + code: "// eslint-disable-line unused, used", + output: "// eslint-disable-line used" + }, + { + code: "/* eslint-disable\nunused, used*/", + output: "/* eslint-disable\nused*/" + }, + { + code: "/* eslint-disable\n unused, used*/", + output: "/* eslint-disable\n used*/" + }, + { + code: "/* eslint-disable\r\nunused, used*/", + output: "/* eslint-disable\r\nused*/" + }, + { + code: "/* eslint-disable\u2028unused, used*/", + output: "/* eslint-disable\u2028used*/" + }, + { + code: "/* eslint-disable\u00A0unused, used*/", + output: "/* eslint-disable\u00A0used*/" + }, + + // when removing the first rule, the comma and all whitespace up to the next rule (or next lone comma) should also be removed + { + code: "// eslint-disable-line unused,used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused , used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused ,used", + output: "// eslint-disable-line used" + }, + { + code: "/* eslint-disable unused\n,\nused */", + output: "/* eslint-disable used */" + }, + { + code: "/* eslint-disable unused \n \n,\n\n used */", + output: "/* eslint-disable used */" + }, + { + code: "/* eslint-disable unused\u2028,\u2028used */", + output: "/* eslint-disable used */" + }, + { + code: "// eslint-disable-line unused\u00A0,\u00A0used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused,,used", + output: "// eslint-disable-line ,used" + }, + { + code: "// eslint-disable-line unused, ,used", + output: "// eslint-disable-line ,used" + }, + { + code: "// eslint-disable-line unused,, used", + output: "// eslint-disable-line , used" + }, + { + code: "// eslint-disable-line unused,used ", + output: "// eslint-disable-line used " + }, + { + code: "// eslint-disable-next-line unused,used\n", + output: "// eslint-disable-next-line used\n" + }, + + // when removing a rule in the middle, one comma and all whitespace between commas should also be removed + { + code: "// eslint-disable-line used-1,unused,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + { + code: "// eslint-disable-line used-1, unused,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + { + code: "// eslint-disable-line used-1,unused ,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + { + code: "// eslint-disable-line used-1, unused ,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + { + code: "/* eslint-disable used-1,\nunused\n,used-2 */", + output: "/* eslint-disable used-1,used-2 */" + }, + { + code: "/* eslint-disable used-1,\n\n unused \n \n ,used-2 */", + output: "/* eslint-disable used-1,used-2 */" + }, + { + code: "/* eslint-disable used-1,\u2028unused\u2028,used-2 */", + output: "/* eslint-disable used-1,used-2 */" + }, + { + code: "// eslint-disable-line used-1,\u00A0unused\u00A0,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + + // when removing a rule in the middle, content around commas should not be changed + { + code: "// eslint-disable-line used-1, unused ,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + { + code: "// eslint-disable-line used-1,unused, used-2", + output: "// eslint-disable-line used-1, used-2" + }, + { + code: "// eslint-disable-line used-1 ,unused,used-2", + output: "// eslint-disable-line used-1 ,used-2" + }, + { + code: "// eslint-disable-line used-1 ,unused, used-2", + output: "// eslint-disable-line used-1 , used-2" + }, + { + code: "// eslint-disable-line used-1 , unused , used-2", + output: "// eslint-disable-line used-1 , used-2" + }, + { + code: "/* eslint-disable used-1\n,unused,\nused-2 */", + output: "/* eslint-disable used-1\n,\nused-2 */" + }, + { + code: "/* eslint-disable used-1\u2028,unused,\u2028used-2 */", + output: "/* eslint-disable used-1\u2028,\u2028used-2 */" + }, + { + code: "// eslint-disable-line used-1\u00A0,unused,\u00A0used-2", + output: "// eslint-disable-line used-1\u00A0,\u00A0used-2" + }, + { + code: "// eslint-disable-line , unused ,used", + output: "// eslint-disable-line ,used" + }, + { + code: "/* eslint-disable\n, unused ,used */", + output: "/* eslint-disable\n,used */" + }, + { + code: "/* eslint-disable used-1,\n,unused,used-2 */", + output: "/* eslint-disable used-1,\n,used-2 */" + }, + { + code: "/* eslint-disable used-1,unused,\n,used-2 */", + output: "/* eslint-disable used-1,\n,used-2 */" + }, + { + code: "/* eslint-disable used-1,\n,unused,\n,used-2 */", + output: "/* eslint-disable used-1,\n,\n,used-2 */" + }, + { + code: "// eslint-disable-line used, unused,", + output: "// eslint-disable-line used," + }, + { + code: "// eslint-disable-next-line used, unused,\n", + output: "// eslint-disable-next-line used,\n" + }, + { + code: "// eslint-disable-line used, unused, ", + output: "// eslint-disable-line used, " + }, + { + code: "// eslint-disable-line used, unused, -- comment", + output: "// eslint-disable-line used, -- comment" + }, + { + code: "/* eslint-disable used, unused,\n*/", + output: "/* eslint-disable used,\n*/" + }, + + // when removing the last rule, the comma and all whitespace up to the previous rule (or previous lone comma) should also be removed + { + code: "// eslint-disable-line used,unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used, unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used ,unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used , unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used, unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used ,unused", + output: "// eslint-disable-line used" + }, + { + code: "/* eslint-disable used\n,\nunused */", + output: "/* eslint-disable used */" + }, + { + code: "/* eslint-disable used \n \n,\n\n unused */", + output: "/* eslint-disable used */" + }, + { + code: "/* eslint-disable used\u2028,\u2028unused */", + output: "/* eslint-disable used */" + }, + { + code: "// eslint-disable-line used\u00A0,\u00A0unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used,,unused", + output: "// eslint-disable-line used," + }, + { + code: "// eslint-disable-line used, ,unused", + output: "// eslint-disable-line used," + }, + { + code: "/* eslint-disable used,\n,unused */", + output: "/* eslint-disable used, */" + }, + { + code: "/* eslint-disable used\n, ,unused */", + output: "/* eslint-disable used\n, */" + }, + + // content after the last rule should not be changed + { + code: "// eslint-disable-line used,unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used,unused ", + output: "// eslint-disable-line used " + }, + { + code: "// eslint-disable-line used,unused ", + output: "// eslint-disable-line used " + }, + { + code: "// eslint-disable-line used,unused -- comment", + output: "// eslint-disable-line used -- comment" + }, + { + code: "// eslint-disable-next-line used,unused\n", + output: "// eslint-disable-next-line used\n" + }, + { + code: "// eslint-disable-next-line used,unused \n", + output: "// eslint-disable-next-line used \n" + }, + { + code: "/* eslint-disable used,unused\u2028*/", + output: "/* eslint-disable used\u2028*/" + }, + { + code: "// eslint-disable-line used,unused\u00A0", + output: "// eslint-disable-line used\u00A0" + }, + + // multiply rules to remove + { + code: "// eslint-disable-line used, unused-1, unused-2", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused-1, used, unused-2", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused-1, unused-2, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used-1, unused-1, used-2, unused-2", + output: "// eslint-disable-line used-1, used-2" + }, + { + code: "// eslint-disable-line unused-1, used-1, unused-2, used-2", + output: "// eslint-disable-line used-1, used-2" + }, + { + code: ` + /* eslint-disable unused-1, + used-1, + unused-2, + used-2 + */ + `, + output: ` + /* eslint-disable used-1, + used-2 + */ + ` + }, + { + code: ` + /* eslint-disable + unused-1, + used-1, + unused-2, + used-2 + */ + `, + output: ` + /* eslint-disable + used-1, + used-2 + */ + ` + }, + { + code: ` + /* eslint-disable + used-1, + unused-1, + used-2, + unused-2 + */ + `, + output: ` + /* eslint-disable + used-1, + used-2 + */ + ` + }, + { + code: ` + /* eslint-disable + used-1, + unused-1, + used-2, + unused-2, + */ + `, + output: ` + /* eslint-disable + used-1, + used-2, + */ + ` + }, + { + code: ` + /* eslint-disable + ,unused-1 + ,used-1 + ,unused-2 + ,used-2 + */ + `, + output: ` + /* eslint-disable + ,used-1 + ,used-2 + */ + ` + }, + { + code: ` + /* eslint-disable + ,used-1 + ,unused-1 + ,used-2 + ,unused-2 + */ + `, + output: ` + /* eslint-disable + ,used-1 + ,used-2 + */ + ` + }, + { + code: ` + /* eslint-disable + used-1, + unused-1, + used-2, + unused-2 + + -- comment + */ + `, + output: ` + /* eslint-disable + used-1, + used-2 + + -- comment + */ + ` + }, + + // duplicates in the list + { + code: "// eslint-disable-line unused, unused, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used, unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used, unused, unused, used", + output: "// eslint-disable-line used, used" + } + ]; + + for (const { code, output } of tests) { + // eslint-disable-next-line no-loop-func -- `linter` is getting updated in beforeEach() + it(code, () => { + assert.strictEqual( + linter.verifyAndFix(code, config).output, + output + ); + }); + } + }); + }); + + }); + + xdescribe("Default Global Variables", () => { + const code = "x"; + + it("builtin global variables should be available in the global scope", () => { + const config = { rules: { checker: "error" } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.notStrictEqual(getVariable(scope, "Object"), null); + assert.notStrictEqual(getVariable(scope, "Array"), null); + assert.notStrictEqual(getVariable(scope, "undefined"), null); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("ES6 global variables should not be available by default", () => { + const config = { rules: { checker: "error" } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(getVariable(scope, "Promise"), null); + assert.strictEqual(getVariable(scope, "Symbol"), null); + assert.strictEqual(getVariable(scope, "WeakMap"), null); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("ES6 global variables should be available in the es6 environment", () => { + const config = { rules: { checker: "error" }, env: { es6: true } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.notStrictEqual(getVariable(scope, "Promise"), null); + assert.notStrictEqual(getVariable(scope, "Symbol"), null); + assert.notStrictEqual(getVariable(scope, "WeakMap"), null); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("ES6 global variables can be disabled when the es6 environment is enabled", () => { + const config = { rules: { checker: "error" }, globals: { Promise: "off", Symbol: "off", WeakMap: "off" }, env: { es6: true } }; + let spy; + + linter.defineRule("checker", context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(getVariable(scope, "Promise"), null); + assert.strictEqual(getVariable(scope, "Symbol"), null); + assert.strictEqual(getVariable(scope, "WeakMap"), null); + }); + + return { Program: spy }; + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + + xdescribe("Suggestions", () => { + it("provides suggestion information for tools to use", () => { + linter.defineRule("rule-with-suggestions", { + meta: { hasSuggestions: true }, + create: context => ({ + Program(node) { + context.report({ + node, + message: "Incorrect spacing", + suggest: [{ + desc: "Insert space at the beginning", + fix: fixer => fixer.insertTextBefore(node, " ") + }, { + desc: "Insert space at the end", + fix: fixer => fixer.insertTextAfter(node, " ") + }] + }); + } + }) + }); + + const messages = linter.verify("var a = 1;", { rules: { "rule-with-suggestions": "error" } }); + + assert.deepStrictEqual(messages[0].suggestions, [{ + desc: "Insert space at the beginning", + fix: { + range: [0, 0], + text: " " + } + }, { + desc: "Insert space at the end", + fix: { + range: [10, 10], + text: " " + } + }]); + }); + + it("supports messageIds for suggestions", () => { + linter.defineRule("rule-with-suggestions", { + meta: { + messages: { + suggestion1: "Insert space at the beginning", + suggestion2: "Insert space at the end" + }, + hasSuggestions: true + }, + create: context => ({ + Program(node) { + context.report({ + node, + message: "Incorrect spacing", + suggest: [{ + messageId: "suggestion1", + fix: fixer => fixer.insertTextBefore(node, " ") + }, { + messageId: "suggestion2", + fix: fixer => fixer.insertTextAfter(node, " ") + }] + }); + } + }) + }); + + const messages = linter.verify("var a = 1;", { rules: { "rule-with-suggestions": "error" } }); + + assert.deepStrictEqual(messages[0].suggestions, [{ + messageId: "suggestion1", + desc: "Insert space at the beginning", + fix: { + range: [0, 0], + text: " " + } + }, { + messageId: "suggestion2", + desc: "Insert space at the end", + fix: { + range: [10, 10], + text: " " + } + }]); + }); + + it("should throw an error if suggestion is passed but `meta.hasSuggestions` property is not enabled", () => { + linter.defineRule("rule-with-suggestions", { + meta: { docs: {}, schema: [] }, + create: context => ({ + Program(node) { + context.report({ + node, + message: "hello world", + suggest: [{ desc: "convert to foo", fix: fixer => fixer.insertTextBefore(node, " ") }] + }); + } + }) + }); + + assert.throws(() => { + linter.verify("0", { rules: { "rule-with-suggestions": "error" } }); + }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`."); + }); + + it("should throw an error if suggestion is passed but `meta.hasSuggestions` property is not enabled and the rule has the obsolete `meta.docs.suggestion` property", () => { + linter.defineRule("rule-with-meta-docs-suggestion", { + meta: { docs: { suggestion: true }, schema: [] }, + create: context => ({ + Program(node) { + context.report({ + node, + message: "hello world", + suggest: [{ desc: "convert to foo", fix: fixer => fixer.insertTextBefore(node, " ") }] + }); + } + }) + }); + + assert.throws(() => { + linter.verify("0", { rules: { "rule-with-meta-docs-suggestion": "error" } }); + }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint."); + }); + }); + + + describe("Error Conditions", () => { + describe("when evaluating broken code", () => { + const code = BROKEN_TEST_CODE; + + it("should report a violation with a useful parse error prefix", () => { + const messages = linter.verify(code); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.isNull(messages[0].ruleId); + assert.strictEqual(messages[0].line, 1); + assert.strictEqual(messages[0].column, 4); + assert.isTrue(messages[0].fatal); + assert.match(messages[0].message, /^Parsing error:/u); + }); + + it("should report source code where the issue is present", () => { + const inValidCode = [ + "var x = 20;", + "if (x ==4 {", + " x++;", + "}" + ]; + const messages = linter.verify(inValidCode.join("\n")); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.isTrue(messages[0].fatal); + assert.match(messages[0].message, /^Parsing error:/u); + }); + }); + + describe("when using an invalid (undefined) rule", () => { + linter = new Linter(); - sourceCode = linter.getSourceCode(); + const code = TEST_CODE; + const results = linter.verify(code, { rules: { foobar: 2 } }); + const result = results[0]; + const warningResult = linter.verify(code, { rules: { foobar: 1 } })[0]; + const arrayOptionResults = linter.verify(code, { rules: { foobar: [2, "always"] } }); + const objectOptionResults = linter.verify(code, { rules: { foobar: [1, { bar: false }] } }); + const resultsMultiple = linter.verify(code, { rules: { foobar: 2, barfoo: 1 } }); + + it("should report a problem", () => { + assert.isNotNull(result); + assert.isArray(results); + assert.isObject(result); + assert.property(result, "ruleId"); + assert.strictEqual(result.ruleId, "foobar"); }); - it("should use the scope (so the global scope has the reference of '@foo')", () => { - assert.strictEqual(scope.references.length, 1); - assert.deepStrictEqual( - scope.references[0].identifier.name, - "foo" - ); + it("should report that the rule does not exist", () => { + assert.property(result, "message"); + assert.strictEqual(result.message, "Definition for rule 'foobar' was not found."); }); - it("should use the same scope if the source code object is reused", () => { - let scope2 = null; - const config = { - plugins: { - test: { - rules: { - "save-scope2": context => ({ - Program() { - scope2 = context.getScope(); - } - }) - } - } - }, - rules: { - "test/save-scope2": "error" - } - }; + it("should report at the correct severity", () => { + assert.property(result, "severity"); + assert.strictEqual(result.severity, 2); + assert.strictEqual(warningResult.severity, 2); // this is 2, since the rulename is very likely to be wrong + }); - linter.verify(sourceCode, config, "test.js"); + it("should accept any valid rule configuration", () => { + assert.isObject(arrayOptionResults[0]); + assert.isObject(objectOptionResults[0]); + }); - assert(scope2 !== null); - assert(scope2 === scope); + it("should report multiple missing rules", () => { + assert.isArray(resultsMultiple); + + assert.deepStrictEqual( + resultsMultiple[1], + { + ruleId: "barfoo", + message: "Definition for rule 'barfoo' was not found.", + line: 1, + column: 1, + endLine: 1, + endColumn: 2, + severity: 2, + nodeType: null + } + ); }); }); - it("should not pass any default parserOptions to the parser", () => { - const messages = linter.verify(";", { - languageOptions: { - parser: testParsers.throwsWithOptions - } - }, "filename"); + describe("when using a rule which has been replaced", () => { + const code = TEST_CODE; + const results = linter.verify(code, { rules: { "no-comma-dangle": 2 } }); - assert.strictEqual(messages.length, 0); + it("should report the new rule", () => { + assert.strictEqual(results[0].ruleId, "no-comma-dangle"); + assert.strictEqual(results[0].message, "Rule 'no-comma-dangle' was removed and replaced by: comma-dangle"); + }); }); + }); }); @@ -8096,7 +12397,7 @@ describe("Linter with FlatConfigArray", () => { }, /This method cannot be used with flat config/u); }); }); - + describe("defineParser()", () => { it("should throw an error when called in flat config mode", () => { assert.throws(() => { @@ -8104,6 +12405,167 @@ describe("Linter with FlatConfigArray", () => { }, /This method cannot be used with flat config/u); }); }); + + describe("getRules()", () => { + it("should throw an error when called in flat config mode", () => { + assert.throws(() => { + linter.getRules(); + }, /This method cannot be used with flat config/u); + }); + }); + + describe("version", () => { + it("should return current version number", () => { + const version = linter.version; + + assert.isString(version); + assert.isTrue(parseInt(version[0], 10) >= 3); + }); + }); + + xdescribe("verifyAndFix()", () => { + it("Fixes the code", () => { + const messages = linter.verifyAndFix("var a", { + rules: { + semi: 2 + } + }, { filename: "test.js" }); + + assert.strictEqual(messages.output, "var a;", "Fixes were applied correctly"); + assert.isTrue(messages.fixed); + }); + + it("does not require a third argument", () => { + const fixResult = linter.verifyAndFix("var a", { + rules: { + semi: 2 + } + }); + + assert.deepStrictEqual(fixResult, { + fixed: true, + messages: [], + output: "var a;" + }); + }); + + it("does not include suggestions in autofix results", () => { + const fixResult = linter.verifyAndFix("var foo = /\\#/", { + rules: { + semi: 2, + "no-useless-escape": 2 + } + }); + + assert.strictEqual(fixResult.output, "var foo = /\\#/;"); + assert.strictEqual(fixResult.fixed, true); + assert.strictEqual(fixResult.messages[0].suggestions.length > 0, true); + }); + + it("does not apply autofixes when fix argument is `false`", () => { + const fixResult = linter.verifyAndFix("var a", { + rules: { + semi: 2 + } + }, { fix: false }); + + assert.strictEqual(fixResult.fixed, false); + }); + + it("stops fixing after 10 passes", () => { + + linter.defineRule("add-spaces", { + meta: { + fixable: "whitespace" + }, + create(context) { + return { + Program(node) { + context.report({ + node, + message: "Add a space before this node.", + fix: fixer => fixer.insertTextBefore(node, " ") + }); + } + }; + } + }); + + const fixResult = linter.verifyAndFix("a", { rules: { "add-spaces": "error" } }); + + assert.strictEqual(fixResult.fixed, true); + assert.strictEqual(fixResult.output, `${" ".repeat(10)}a`); + assert.strictEqual(fixResult.messages.length, 1); + }); + + it("should throw an error if fix is passed but meta has no `fixable` property", () => { + linter.defineRule("test-rule", { + meta: { + docs: {}, + schema: [] + }, + create: context => ({ + Program(node) { + context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + } + }) + }); + + assert.throws(() => { + linter.verify("0", { rules: { "test-rule": "error" } }); + }, /Fixable rules must set the `meta\.fixable` property to "code" or "whitespace".\nOccurred while linting :1\nRule: "test-rule"$/u); + }); + + it("should throw an error if fix is passed and there is no metadata", () => { + linter.defineRule("test-rule", { + create: context => ({ + Program(node) { + context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + } + }) + }); + + assert.throws(() => { + linter.verify("0", { rules: { "test-rule": "error" } }); + }, /Fixable rules must set the `meta\.fixable` property/u); + }); + + it("should throw an error if fix is passed from a legacy-format rule", () => { + linter.defineRule("test-rule", context => ({ + Program(node) { + context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + } + })); + + assert.throws(() => { + linter.verify("0", { rules: { "test-rule": "error" } }); + }, /Fixable rules must set the `meta\.fixable` property/u); + }); + }); + + describe("Mutability", () => { + let linter1 = null; + let linter2 = null; + + beforeEach(() => { + linter1 = new Linter(); + linter2 = new Linter(); + }); + + describe("rules", () => { + it("with no changes, same rules are loaded", () => { + assert.sameDeepMembers(Array.from(linter1.getRules().keys()), Array.from(linter2.getRules().keys())); + }); + + it("loading rule in one doesn't change the other", () => { + linter1.defineRule("mock-rule", () => ({})); + + assert.isTrue(linter1.getRules().has("mock-rule"), "mock rule is present"); + assert.isFalse(linter2.getRules().has("mock-rule"), "mock rule is not present"); + }); + }); + }); + describe("processors", () => { let receivedFilenames = []; @@ -8459,6 +12921,68 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(messages[0].fatal, true); }); + xit("should not crash when we reuse the SourceCode object", () => { + linter.verify("function render() { return
{hello}
}", { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }); + linter.verify(linter.getSourceCode(), { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }); + }); + + xit("should reuse the SourceCode object", () => { + let ast1 = null, + ast2 = null; + + linter.defineRule("save-ast1", () => ({ + Program(node) { + ast1 = node; + } + })); + linter.defineRule("save-ast2", () => ({ + Program(node) { + ast2 = node; + } + })); + + linter.verify("function render() { return
{hello}
}", { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, rules: { "save-ast1": 2 } }); + linter.verify(linter.getSourceCode(), { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, rules: { "save-ast2": 2 } }); + + assert(ast1 !== null); + assert(ast2 !== null); + assert(ast1 === ast2); + }); + + it("should not modify config object passed as argument", () => { + const config = {}; + + Object.freeze(config); + linter.verify("var", config); + }); + + xit("should pass 'id' to rule contexts with the rule id", () => { + const spy = sinon.spy(context => { + assert.strictEqual(context.id, "foo-bar-baz"); + return {}; + }); + + linter.defineRule("foo-bar-baz", spy); + linter.verify("x", { rules: { "foo-bar-baz": "error" } }); + assert(spy.calledOnce); + }); + + + xdescribe("when evaluating an empty string", () => { + it("runs rules", () => { + linter.defineRule("no-programs", context => ({ + Program(node) { + context.report({ node, message: "No programs allowed." }); + } + })); + + assert.strictEqual( + linter.verify("", { rules: { "no-programs": "error" } }).length, + 1 + ); + }); + }); + }); }); From 360c2d7b1b62235ba40e73e9f6e9e7f93b94cd88 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 10 Nov 2021 09:08:50 -0800 Subject: [PATCH 18/37] Finish languageOptions tests --- tests/lib/linter/linter.js | 1685 ++++++++++++++++++------------------ 1 file changed, 860 insertions(+), 825 deletions(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 492e9586f27..14140950d03 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -2773,9 +2773,9 @@ var a = "test2"; }); describe("when evaluating a file with a hashbang", () => { - const code = "#!bin/program\n\nvar foo;;"; it("should preserve line numbers", () => { + const code = "#!bin/program\n\nvar foo;;"; const config = { rules: { "no-extra-semi": 1 } }; const messages = linter.verify(code, config); @@ -2786,6 +2786,7 @@ var a = "test2"; }); it("should have a comment with the hashbang in it", () => { + const code = "#!bin/program\n\nvar foo;;"; const config = { rules: { checker: "error" } }; const spy = sinon.spy(context => { const comments = context.getAllComments(); @@ -2800,22 +2801,22 @@ var a = "test2"; assert(spy.calledOnce); }); - it("should comment hashbang without breaking offset", () => { - const code = "#!/usr/bin/env node\n'123';"; - const config = { rules: { checker: "error" } }; - let spy; + it("should comment hashbang without breaking offset", () => { + const code = "#!/usr/bin/env node\n'123';"; + const config = { rules: { checker: "error" } }; + let spy; - linter.defineRule("checker", context => { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node), "'123';"); - }); - return { ExpressionStatement: spy }; + linter.defineRule("checker", context => { + spy = sinon.spy(node => { + assert.strictEqual(context.getSource(node), "'123';"); }); - - linter.verify(code, config); - assert(spy && spy.calledOnce); + return { ExpressionStatement: spy }; }); + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); describe("when evaluating broken code", () => { @@ -6316,11 +6317,11 @@ describe.only("Linter with FlatConfigArray", () => { }); describe("Config Options", () => { - + describe("languageOptions", () => { - + describe("ecmaVersion", () => { - + it("should error when accessing a global that isn't available in ecmaVersion 5", () => { const messages = linter.verify("new Map()", { languageOptions: { @@ -6331,11 +6332,11 @@ describe.only("Linter with FlatConfigArray", () => { "no-undef": "error" } }); - + assert.strictEqual(messages.length, 1, "There should be one linting error."); assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); }); - + it("should error when accessing a global that isn't available in ecmaVersion 3", () => { const messages = linter.verify("JSON.stringify({})", { languageOptions: { @@ -6346,11 +6347,11 @@ describe.only("Linter with FlatConfigArray", () => { "no-undef": "error" } }); - + assert.strictEqual(messages.length, 1, "There should be one linting error."); assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); }); - + it("should add globals for ES6 when ecmaVersion is 6", () => { const messages = linter.verify("new Map()", { languageOptions: { @@ -6360,20 +6361,20 @@ describe.only("Linter with FlatConfigArray", () => { "no-undef": "error" } }); - + assert.strictEqual(messages.length, 0, "There should be no linting errors."); }); - + it("should allow destructuring when ecmaVersion is 6", () => { const messages = linter.verify("let {a} = b", { languageOptions: { ecmaVersion: 6 } }); - + assert.strictEqual(messages.length, 0, "There should be no linting errors."); }); - + it("ecmaVersion should be normalized to year name for ES 6", () => { const config = { plugins: { @@ -6394,10 +6395,10 @@ describe.only("Linter with FlatConfigArray", () => { }, rules: { "test/checker": "error" } }; - + linter.verify("foo", config, filename); }); - + it("ecmaVersion should be normalized to to latest year by default", () => { const config = { plugins: { @@ -6415,10 +6416,10 @@ describe.only("Linter with FlatConfigArray", () => { }, rules: { "test/checker": "error" } }; - + linter.verify("foo", config, filename); }); - + it("ecmaVersion should not be normalized to year name for ES 5", () => { const config = { plugins: { @@ -6439,10 +6440,10 @@ describe.only("Linter with FlatConfigArray", () => { }, rules: { "test/checker": "error" } }; - + linter.verify("foo", config, filename); }); - + it("ecmaVersion should be normalized to year name for 'latest'", () => { const config = { plugins: { @@ -6463,15 +6464,15 @@ describe.only("Linter with FlatConfigArray", () => { }, rules: { "test/checker": "error" } }; - + linter.verify("foo", config, filename); }); - - + + }); - + describe("sourceType", () => { - + it("should be module by default", () => { const config = { plugins: { @@ -6489,10 +6490,10 @@ describe.only("Linter with FlatConfigArray", () => { }, rules: { "test/checker": "error" } }; - + linter.verify("import foo from 'bar'", config, filename); }); - + it("should default to commonjs when passed a .cjs filename", () => { const config = { plugins: { @@ -6510,11 +6511,11 @@ describe.only("Linter with FlatConfigArray", () => { }, rules: { "test/checker": "error" } }; - + linter.verify("import foo from 'bar'", config, `${filename}.cjs`); }); - - + + it("should error when import is used in a script", () => { const messages = linter.verify("import foo from 'bar';", { languageOptions: { @@ -6522,11 +6523,11 @@ describe.only("Linter with FlatConfigArray", () => { sourceType: "script" } }); - + assert.strictEqual(messages.length, 1, "There should be one parsing error."); assert.strictEqual(messages[0].message, "Parsing error: 'import' and 'export' may appear only with 'sourceType: module'"); }); - + it("should not error when import is used in a module", () => { const messages = linter.verify("import foo from 'bar';", { languageOptions: { @@ -6534,10 +6535,10 @@ describe.only("Linter with FlatConfigArray", () => { sourceType: "module" } }); - + assert.strictEqual(messages.length, 0, "There should no linting errors."); }); - + it("should error when return is used at the top-level outside of commonjs", () => { const messages = linter.verify("return", { languageOptions: { @@ -6545,11 +6546,11 @@ describe.only("Linter with FlatConfigArray", () => { sourceType: "script" } }); - + assert.strictEqual(messages.length, 1, "There should be one parsing error."); assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); }); - + it("should not error when top-level return is used in commonjs", () => { const messages = linter.verify("return", { languageOptions: { @@ -6557,10 +6558,10 @@ describe.only("Linter with FlatConfigArray", () => { sourceType: "commonjs" } }); - + assert.strictEqual(messages.length, 0, "There should no linting errors."); }); - + it("should error when accessing a Node.js global outside of commonjs", () => { const messages = linter.verify("require()", { languageOptions: { @@ -6570,11 +6571,11 @@ describe.only("Linter with FlatConfigArray", () => { "no-undef": "error" } }); - + assert.strictEqual(messages.length, 1, "There should be one linting error."); assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef."); }); - + it("should add globals for Node.js when sourceType is commonjs", () => { const messages = linter.verify("require()", { languageOptions: { @@ -6585,14 +6586,27 @@ describe.only("Linter with FlatConfigArray", () => { "no-undef": "error" } }); - + assert.strictEqual(messages.length, 0, "There should be no linting errors."); }); - - + + it("should allow 'await' as a property name in modules", () => { + const result = linter.verify( + "obj.await", + { + languageOptions: { + ecmaVersion: 6, + sourceType: "module" + } + } + ); + + assert(result.length === 0); + }); + }); - xdescribe("parser", () => { + describe("parser", () => { it("should be able to define a custom parser", () => { const parser = { @@ -6610,49 +6624,96 @@ describe.only("Linter with FlatConfigArray", () => { } }; - linter.defineParser("test-parser", parser); - const config = { rules: {}, parser: "test-parser" }; + const config = { + plugins: { + test: { + parsers: { + "test-parser": parser + } + } + }, + languageOptions: { + parser: "test/test-parser" + } + }; + + const messages = linter.verify("0", config, filename); assert.strictEqual(messages.length, 0); }); - it("should pass parser as parserPath to all rules when provided on config", () => { - - const alternateParser = "esprima"; - - linter.defineParser("esprima", esprima); - linter.defineRule("test-rule", sinon.mock().withArgs( - sinon.match({ parserPath: alternateParser }) - ).returns({})); + it("should pass parser as context.languageOptions.parser to all rules when provided on config", () => { - const config = { rules: { "test-rule": 2 }, parser: alternateParser }; + const config = { + plugins: { + test: { + rules: { + "test-rule": sinon.mock().withArgs( + sinon.match({ languageOptions: { parser: esprima } }) + ).returns({}) + } + } + }, + languageOptions: { + parser: esprima + }, + rules: { + "test/test-rule": 2 + } + }; linter.verify("0", config, filename); }); it("should use parseForESLint() in custom parser when custom parser is specified", () => { - const config = { rules: {}, parser: "enhanced-parser" }; + const config = { + plugins: { + test: { + parsers: { + "enhanced-parser": testParsers.enhancedParser + } + } + }, + languageOptions: { + parser: "test/enhanced-parser" + } + }; - linter.defineParser("enhanced-parser", testParsers.enhancedParser); const messages = linter.verify("0", config, filename); assert.strictEqual(messages.length, 0); }); it("should expose parser services when using parseForESLint() and services are specified", () => { - linter.defineParser("enhanced-parser", testParsers.enhancedParser); - linter.defineRule("test-service-rule", context => ({ - Literal(node) { - context.report({ - node, - message: context.parserServices.test.getMessage() - }); + + const config = { + plugins: { + test: { + parsers: { + "enhanced-parser": testParsers.enhancedParser + }, + rules: { + "test-service-rule": context => ({ + Literal(node) { + context.report({ + node, + message: context.parserServices.test.getMessage() + }); + } + }) + } + } + }, + languageOptions: { + parser: "test/enhanced-parser" + }, + rules: { + "test/test-service-rule": 2 } - })); + }; - const config = { rules: { "test-service-rule": 2 }, parser: "enhanced-parser" }; const messages = linter.verify("0", config, filename); assert.strictEqual(messages.length, 1); @@ -6660,17 +6721,33 @@ describe.only("Linter with FlatConfigArray", () => { }); it("should use the same parserServices if source code object is reused", () => { - linter.defineParser("enhanced-parser", testParsers.enhancedParser); - linter.defineRule("test-service-rule", context => ({ - Literal(node) { - context.report({ - node, - message: context.parserServices.test.getMessage() - }); + + const config = { + plugins: { + test: { + parsers: { + "enhanced-parser": testParsers.enhancedParser + }, + rules: { + "test-service-rule": context => ({ + Literal(node) { + context.report({ + node, + message: context.parserServices.test.getMessage() + }); + } + }) + } + } + }, + languageOptions: { + parser: "test/enhanced-parser" + }, + rules: { + "test/test-service-rule": 2 } - })); + }; - const config = { rules: { "test-service-rule": 2 }, parser: "enhanced-parser" }; const messages = linter.verify("0", config, filename); assert.strictEqual(messages.length, 1); @@ -6682,19 +6759,29 @@ describe.only("Linter with FlatConfigArray", () => { assert.strictEqual(messages2[0].message, "Hi!"); }); - it("should pass parser as parserPath to all rules when default parser is used", () => { - linter.defineRule("test-rule", sinon.mock().withArgs( - sinon.match({ parserPath: "espree" }) - ).returns({})); + it("should pass parser as context.languageOptions.parser to all rules when default parser is used", () => { - const config = { rules: { "test-rule": 2 } }; + const config = { + plugins: { + test: { + rules: { + "test-rule": sinon.mock().withArgs( + sinon.match({ languageOptions: { parser: espree } }) + ).returns({}) + } + } + }, + rules: { + "test/test-rule": 2 + } + }; linter.verify("0", config, filename); }); }); - xdescribe("parseOptions", () => { + describe("parseOptions", () => { it("should pass ecmaFeatures to all rules when provided on config", () => { @@ -6705,101 +6792,35 @@ describe.only("Linter with FlatConfigArray", () => { } }; - linter.defineRule("test-rule", sinon.mock().withArgs( - sinon.match({ parserOptions }) - ).returns({})); - - const config = { rules: { "test-rule": 2 }, parserOptions }; - - linter.verify("0", config, filename); - }); - - it("should pass parserOptions to all rules when default parserOptions is used", () => { - - const parserOptions = {}; - - linter.defineRule("test-rule", sinon.mock().withArgs( - sinon.match({ parserOptions }) - ).returns({})); - - const config = { rules: { "test-rule": 2 } }; - - linter.verify("0", config, filename); - }); - - it("should properly parse object spread when ecmaVersion is 2018", () => { - - const messages = linter.verify("var x = { ...y };", { - parserOptions: { - ecmaVersion: 2018 - } - }, filename); - - assert.strictEqual(messages.length, 0); - }); - - it("should properly parse global return when passed ecmaFeatures", () => { - - const messages = linter.verify("return;", { - parserOptions: { - ecmaFeatures: { - globalReturn: true + const config = { + plugins: { + test: { + rules: { + "test-rule": sinon.mock().withArgs( + sinon.match({ languageOptions: { parserOptions } }) + ).returns({}) + } } - } - }, filename); - - assert.strictEqual(messages.length, 0); - }); - - it("should properly parse global return when in Node.js environment", () => { - - const messages = linter.verify("return;", { - env: { - node: true - } - }, filename); - - assert.strictEqual(messages.length, 0); - }); - - it("should not parse global return when in Node.js environment with globalReturn explicitly off", () => { - - const messages = linter.verify("return;", { - env: { - node: true }, - parserOptions: { - ecmaFeatures: { - globalReturn: false - } + languageOptions: { + parserOptions + }, + rules: { + "test/test-rule": 2 } - }, filename); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); - }); - - it("should not parse global return when Node.js environment is false", () => { - - const messages = linter.verify("return;", {}, filename); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); - }); - - it("should properly parse sloppy-mode code when impliedStrict is false", () => { - - const messages = linter.verify("var private;", {}, filename); + }; - assert.strictEqual(messages.length, 0); + linter.verify("0", config, filename); }); it("should not parse sloppy-mode code when impliedStrict is true", () => { const messages = linter.verify("var private;", { - parserOptions: { - ecmaFeatures: { - impliedStrict: true + languageOptions: { + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } } } }, filename); @@ -6811,9 +6832,11 @@ describe.only("Linter with FlatConfigArray", () => { it("should properly parse valid code when impliedStrict is true", () => { const messages = linter.verify("var foo;", { - parserOptions: { - ecmaFeatures: { - impliedStrict: true + languageOptions: { + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } } } }, filename); @@ -6824,9 +6847,11 @@ describe.only("Linter with FlatConfigArray", () => { it("should properly parse JSX when passed ecmaFeatures", () => { const messages = linter.verify("var x =
;", { - parserOptions: { - ecmaFeatures: { - jsx: true + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true + } } } }, filename); @@ -6846,29 +6871,39 @@ describe.only("Linter with FlatConfigArray", () => { it("should not report an error when JSX code is encountered and JSX is enabled", () => { const code = "var myDivElement =
;"; - const messages = linter.verify(code, { parserOptions: { ecmaFeatures: { jsx: true } } }, "filename"); + const messages = linter.verify(code, { + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } + + }, "filename"); assert.strictEqual(messages.length, 0); }); it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { const code = "var myDivElement =
;"; - const messages = linter.verify(code, { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }, "filename"); - - assert.strictEqual(messages.length, 0); - }); + const messages = linter.verify(code, { + languageOptions: { + ecmaVersion: 6, + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } - it("should allow 'await' as a property name in modules", () => { - const result = linter.verify( - "obj.await", - { parserOptions: { ecmaVersion: 6, sourceType: "module" } } - ); + }, "filename"); - assert(result.length === 0); + assert.strictEqual(messages.length, 0); }); }); - + }); xdescribe("settings", () => { @@ -7707,7 +7742,7 @@ describe.only("Linter with FlatConfigArray", () => { }); describe("context.getScope()", () => { - const code = "function foo() { q: for(;;) { break q; } } function bar () { var q = t; } var baz = (() => { return 1; });"; + const codeToTestScope = "function foo() { q: for(;;) { break q; } } function bar () { var q = t; } var baz = (() => { return 1; });"; it("should retrieve the global scope correctly from a Program", () => { let spy; @@ -7733,7 +7768,7 @@ describe.only("Linter with FlatConfigArray", () => { rules: { "test/checker": "error" } }; - linter.verify(code, config); + linter.verify(codeToTestScope, config); assert(spy && spy.calledOnce); }); @@ -7761,7 +7796,7 @@ describe.only("Linter with FlatConfigArray", () => { rules: { "test/checker": "error" } }; - linter.verify(code, config); + linter.verify(codeToTestScope, config); assert(spy && spy.calledTwice); }); @@ -7791,7 +7826,7 @@ describe.only("Linter with FlatConfigArray", () => { }; - linter.verify(code, config); + linter.verify(codeToTestScope, config); assert(spy && spy.calledOnce); }); @@ -7822,7 +7857,7 @@ describe.only("Linter with FlatConfigArray", () => { }; - linter.verify(code, config); + linter.verify(codeToTestScope, config); assert(spy && spy.calledOnce); }); @@ -8075,12 +8110,12 @@ describe.only("Linter with FlatConfigArray", () => { /** * Get the scope on the node `astSelector` specified. - * @param {string} code The source code to verify. + * @param {string} codeToEvaluate The source code to verify. * @param {string} astSelector The AST selector to get scope. * @param {number} [ecmaVersion=5] The ECMAScript version. * @returns {{node: ASTNode, scope: escope.Scope}} Gotten scope. */ - function getScope(code, astSelector, ecmaVersion = 5) { + function getScope(codeToEvaluate, astSelector, ecmaVersion = 5) { let node, scope; linter.defineRule("get-scope", context => ({ @@ -8090,7 +8125,7 @@ describe.only("Linter with FlatConfigArray", () => { } })); linter.verify( - code, + codeToEvaluate, { parserOptions: { ecmaVersion }, rules: { "get-scope": 2 } @@ -8420,7 +8455,7 @@ describe.only("Linter with FlatConfigArray", () => { assert.strictEqual(scope.set.get("e").references[0].resolved, scope.set.get("e")); assert.strictEqual(scope.set.get("f").references[0].resolved, scope.set.get("f")); }); - }); + }); }); xdescribe("context.getDeclaredVariables(node)", () => { @@ -8494,7 +8529,7 @@ describe.only("Linter with FlatConfigArray", () => { MetaProperty: checkEmpty }; - rule[type] = function (node) { + rule[type] = function(node) { const expectedNames = expectedNamesList.shift(); const variables = context.getDeclaredVariables(node); @@ -9275,7 +9310,7 @@ describe.only("Linter with FlatConfigArray", () => { }); xdescribe("Options", () => { - + describe("filename", () => { it("should allow filename to be passed on options object", () => { const filenameChecker = sinon.spy(context => { @@ -9360,11 +9395,11 @@ describe.only("Linter with FlatConfigArray", () => { }); describe("Inline Directives", () => { - + xdescribe("/*global*/ Comments", () => { - + describe("when evaluating code containing /*global */ and /*globals */ blocks", () => { - + it("variables should be available in global scope", () => { const config = { rules: { checker: "error" }, globals: { Array: "off", ConfigGlobal: "writeable" } }; const code = ` @@ -9374,7 +9409,7 @@ describe.only("Linter with FlatConfigArray", () => { /* global ConfigGlobal : readable */ `; let spy; - + linter.defineRule("checker", context => { spy = sinon.spy(() => { const scope = context.getScope(); @@ -9387,7 +9422,7 @@ describe.only("Linter with FlatConfigArray", () => { mathGlobal = getVariable(scope, "Math"), arrayGlobal = getVariable(scope, "Array"), configGlobal = getVariable(scope, "ConfigGlobal"); - + assert.strictEqual(a.name, "a"); assert.strictEqual(a.writeable, false); assert.strictEqual(b.name, "b"); @@ -9405,29 +9440,29 @@ describe.only("Linter with FlatConfigArray", () => { assert.strictEqual(configGlobal.name, "ConfigGlobal"); assert.strictEqual(configGlobal.writeable, false); }); - + return { Program: spy }; }); - + linter.verify(code, config); assert(spy && spy.calledOnce); }); }); - + describe("when evaluating code containing a /*global */ block with sloppy whitespace", () => { const code = "/* global a b : true c: false*/"; - + it("variables should be available in global scope", () => { const config = { rules: { checker: "error" } }; let spy; - + linter.defineRule("checker", context => { spy = sinon.spy(() => { const scope = context.getScope(), a = getVariable(scope, "a"), b = getVariable(scope, "b"), c = getVariable(scope, "c"); - + assert.strictEqual(a.name, "a"); assert.strictEqual(a.writeable, false); assert.strictEqual(b.name, "b"); @@ -9435,67 +9470,67 @@ describe.only("Linter with FlatConfigArray", () => { assert.strictEqual(c.name, "c"); assert.strictEqual(c.writeable, false); }); - + return { Program: spy }; }); - + linter.verify(code, config); assert(spy && spy.calledOnce); }); }); - + describe("when evaluating code containing a /*global */ block with specific variables", () => { const code = "/* global toString hasOwnProperty valueOf: true */"; - + it("should not throw an error if comment block has global variables which are Object.prototype contains", () => { const config = { rules: { checker: "error" } }; - + linter.verify(code, config); }); }); - + describe("when evaluating code containing a line comment", () => { const code = "//global a \n function f() {}"; - + it("should not introduce a global variable", () => { const config = { rules: { checker: "error" } }; let spy; - + linter.defineRule("checker", context => { spy = sinon.spy(() => { const scope = context.getScope(); - + assert.strictEqual(getVariable(scope, "a"), null); }); - + return { Program: spy }; }); - + linter.verify(code, config); assert(spy && spy.calledOnce); }); }); - + describe("when evaluating code containing normal block comments", () => { const code = "/**/ /*a*/ /*b:true*/ /*foo c:false*/"; - + it("should not introduce a global variable", () => { const config = { rules: { checker: "error" } }; let spy; - + linter.defineRule("checker", context => { spy = sinon.spy(() => { const scope = context.getScope(); - + assert.strictEqual(getVariable(scope, "a"), null); assert.strictEqual(getVariable(scope, "b"), null); assert.strictEqual(getVariable(scope, "foo"), null); assert.strictEqual(getVariable(scope, "c"), null); }); - + return { Program: spy }; }); - + linter.verify(code, config); assert(spy && spy.calledOnce); }); @@ -9569,117 +9604,117 @@ describe.only("Linter with FlatConfigArray", () => { }); }); - + xdescribe("/*exported*/ Comments", () => { - + it("we should behave nicely when no matching variable is found", () => { const code = "/* exported horse */"; const config = { rules: {} }; - + linter.verify(code, config, filename, true); }); - + it("variables should be exported", () => { const code = "/* exported horse */\n\nvar horse = 'circus'"; const config = { rules: { checker: "error" } }; let spy; - + linter.defineRule("checker", context => { spy = sinon.spy(() => { const scope = context.getScope(), horse = getVariable(scope, "horse"); - + assert.strictEqual(horse.eslintUsed, true); }); - + return { Program: spy }; }); - + linter.verify(code, config); assert(spy && spy.calledOnce); }); - + it("undefined variables should not be exported", () => { const code = "/* exported horse */\n\nhorse = 'circus'"; const config = { rules: { checker: "error" } }; let spy; - + linter.defineRule("checker", context => { spy = sinon.spy(() => { const scope = context.getScope(), horse = getVariable(scope, "horse"); - + assert.strictEqual(horse, null); }); - + return { Program: spy }; }); - + linter.verify(code, config); assert(spy && spy.calledOnce); }); - + it("variables should be exported in strict mode", () => { const code = "/* exported horse */\n'use strict';\nvar horse = 'circus'"; const config = { rules: { checker: "error" } }; let spy; - + linter.defineRule("checker", context => { spy = sinon.spy(() => { const scope = context.getScope(), horse = getVariable(scope, "horse"); - + assert.strictEqual(horse.eslintUsed, true); }); - + return { Program: spy }; }); - + linter.verify(code, config); assert(spy && spy.calledOnce); }); - + it("variables should not be exported in the es6 module environment", () => { const code = "/* exported horse */\nvar horse = 'circus'"; const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6, sourceType: "module" } }; let spy; - + linter.defineRule("checker", context => { spy = sinon.spy(() => { const scope = context.getScope(), horse = getVariable(scope, "horse"); - + assert.strictEqual(horse, null); // there is no global scope at all }); - + return { Program: spy }; }); - + linter.verify(code, config); assert(spy && spy.calledOnce); }); - + it("variables should not be exported when in the node environment", () => { const code = "/* exported horse */\nvar horse = 'circus'"; const config = { rules: { checker: "error" }, env: { node: true } }; let spy; - + linter.defineRule("checker", context => { spy = sinon.spy(() => { const scope = context.getScope(), horse = getVariable(scope, "horse"); - + assert.strictEqual(horse, null); // there is no global scope at all }); - + return { Program: spy }; }); - + linter.verify(code, config); assert(spy && spy.calledOnce); }); }); - + xdescribe("/*eslint*/ Comments", () => { describe("when evaluating code with comments to enable rules", () => { @@ -11316,7 +11351,7 @@ var a = "test2"; "eslint-disable eqeqeq", "eslint-disable-line eqeqeq", "eslint-disable-next-line eqeqeq", - "eslint-enable eqeqeq", + "eslint-enable eqeqeq" ]) { // eslint-disable-next-line no-loop-func -- No closures it(`should warn '/* ${directive} */' if 'noInlineConfig' was given.`, () => { @@ -11376,553 +11411,553 @@ var a = "test2"; }); xdescribe("reportUnusedDisableDirectives option", () => { - it("reports problems for unused eslint-disable comments", () => { - assert.deepStrictEqual( - linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: true }), - [ - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", - line: 1, - column: 1, - fix: { - range: [0, 20], - text: " " - }, - severity: 2, - nodeType: null - } - ] - ); - }); + it("reports problems for unused eslint-disable comments", () => { + assert.deepStrictEqual( + linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: true }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); - it("reports problems for unused eslint-disable comments (error)", () => { - assert.deepStrictEqual( - linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "error" }), - [ - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", - line: 1, - column: 1, - fix: { - range: [0, 20], - text: " " - }, - severity: 2, - nodeType: null - } - ] - ); - }); + it("reports problems for unused eslint-disable comments (error)", () => { + assert.deepStrictEqual( + linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "error" }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); - it("reports problems for unused eslint-disable comments (warn)", () => { - assert.deepStrictEqual( - linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "warn" }), - [ - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", - line: 1, - column: 1, - fix: { - range: [0, 20], - text: " " - }, - severity: 1, - nodeType: null - } - ] - ); - }); + it("reports problems for unused eslint-disable comments (warn)", () => { + assert.deepStrictEqual( + linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "warn" }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 1, + nodeType: null + } + ] + ); + }); - it("reports problems for unused eslint-disable comments (in config)", () => { - assert.deepStrictEqual( - linter.verify("/* eslint-disable */", { reportUnusedDisableDirectives: true }), - [ - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", - line: 1, - column: 1, - fix: { - range: [0, 20], - text: " " - }, - severity: 1, - nodeType: null - } - ] - ); - }); + it("reports problems for unused eslint-disable comments (in config)", () => { + assert.deepStrictEqual( + linter.verify("/* eslint-disable */", { reportUnusedDisableDirectives: true }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 1, + nodeType: null + } + ] + ); + }); - it("reports problems for partially unused eslint-disable comments (in config)", () => { - const code = "alert('test'); // eslint-disable-line no-alert, no-redeclare"; - const config = { - reportUnusedDisableDirectives: true, - rules: { - "no-alert": 1, - "no-redeclare": 1 - } - }; + it("reports problems for partially unused eslint-disable comments (in config)", () => { + const code = "alert('test'); // eslint-disable-line no-alert, no-redeclare"; + const config = { + reportUnusedDisableDirectives: true, + rules: { + "no-alert": 1, + "no-redeclare": 1 + } + }; - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: true - }); + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'no-redeclare').", - line: 1, - column: 16, - fix: { - range: [46, 60], - text: "" - }, - severity: 1, - nodeType: null - } - ] - ); - }); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'no-redeclare').", + line: 1, + column: 16, + fix: { + range: [46, 60], + text: "" + }, + severity: 1, + nodeType: null + } + ] + ); + }); - describe("autofix", () => { - const alwaysReportsRule = { - create(context) { - return { - Program(node) { - context.report({ message: "bad code", loc: node.loc.end }); + describe("autofix", () => { + const alwaysReportsRule = { + create(context) { + return { + Program(node) { + context.report({ message: "bad code", loc: node.loc.end }); + } + }; } }; - } - }; - const neverReportsRule = { - create() { - return {}; - } - }; + const neverReportsRule = { + create() { + return {}; + } + }; - const ruleCount = 3; - const usedRules = Array.from( - { length: ruleCount }, - (_, index) => `used${index ? `-${index}` : ""}` // "used", "used-1", "used-2" - ); - const unusedRules = usedRules.map(name => `un${name}`); // "unused", "unused-1", "unused-2" + const ruleCount = 3; + const usedRules = Array.from( + { length: ruleCount }, + (_, index) => `used${index ? `-${index}` : ""}` // "used", "used-1", "used-2" + ); + const unusedRules = usedRules.map(name => `un${name}`); // "unused", "unused-1", "unused-2" - const config = { - reportUnusedDisableDirectives: true, - rules: { - ...Object.fromEntries(usedRules.map(name => [name, "error"])), - ...Object.fromEntries(unusedRules.map(name => [name, "error"])) - } - }; + const config = { + reportUnusedDisableDirectives: true, + rules: { + ...Object.fromEntries(usedRules.map(name => [name, "error"])), + ...Object.fromEntries(unusedRules.map(name => [name, "error"])) + } + }; - beforeEach(() => { - linter.defineRules(Object.fromEntries(usedRules.map(name => [name, alwaysReportsRule]))); - linter.defineRules(Object.fromEntries(unusedRules.map(name => [name, neverReportsRule]))); - }); + beforeEach(() => { + linter.defineRules(Object.fromEntries(usedRules.map(name => [name, alwaysReportsRule]))); + linter.defineRules(Object.fromEntries(unusedRules.map(name => [name, neverReportsRule]))); + }); - const tests = [ + const tests = [ - //----------------------------------------------- - // Removing the entire comment - //----------------------------------------------- + //----------------------------------------------- + // Removing the entire comment + //----------------------------------------------- - { - code: "// eslint-disable-line unused", - output: " " - }, - { - code: "foo// eslint-disable-line unused", - output: "foo " - }, - { - code: "// eslint-disable-line ,unused,", - output: " " - }, - { - code: "// eslint-disable-line unused-1, unused-2", - output: " " - }, - { - code: "// eslint-disable-line ,unused-1,, unused-2,, -- comment", - output: " " - }, - { - code: "// eslint-disable-next-line unused\n", - output: " \n" - }, - { - code: "// eslint-disable-next-line unused\nfoo", - output: " \nfoo" - }, - { - code: "/* eslint-disable \nunused\n*/", - output: " " - }, + { + code: "// eslint-disable-line unused", + output: " " + }, + { + code: "foo// eslint-disable-line unused", + output: "foo " + }, + { + code: "// eslint-disable-line ,unused,", + output: " " + }, + { + code: "// eslint-disable-line unused-1, unused-2", + output: " " + }, + { + code: "// eslint-disable-line ,unused-1,, unused-2,, -- comment", + output: " " + }, + { + code: "// eslint-disable-next-line unused\n", + output: " \n" + }, + { + code: "// eslint-disable-next-line unused\nfoo", + output: " \nfoo" + }, + { + code: "/* eslint-disable \nunused\n*/", + output: " " + }, - //----------------------------------------------- - // Removing only individual rules - //----------------------------------------------- + //----------------------------------------------- + // Removing only individual rules + //----------------------------------------------- - // content before the first rule should not be changed - { - code: "//eslint-disable-line unused, used", - output: "//eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused, used", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused, used", - output: "// eslint-disable-line used" - }, - { - code: "/*\neslint-disable unused, used*/", - output: "/*\neslint-disable used*/" - }, - { - code: "/*\n eslint-disable unused, used*/", - output: "/*\n eslint-disable used*/" - }, - { - code: "/*\r\neslint-disable unused, used*/", - output: "/*\r\neslint-disable used*/" - }, - { - code: "/*\u2028eslint-disable unused, used*/", - output: "/*\u2028eslint-disable used*/" - }, - { - code: "/*\u00A0eslint-disable unused, used*/", - output: "/*\u00A0eslint-disable used*/" - }, - { - code: "// eslint-disable-line unused, used", - output: "// eslint-disable-line used" - }, - { - code: "/* eslint-disable\nunused, used*/", - output: "/* eslint-disable\nused*/" - }, - { - code: "/* eslint-disable\n unused, used*/", - output: "/* eslint-disable\n used*/" - }, - { - code: "/* eslint-disable\r\nunused, used*/", - output: "/* eslint-disable\r\nused*/" - }, - { - code: "/* eslint-disable\u2028unused, used*/", - output: "/* eslint-disable\u2028used*/" - }, - { - code: "/* eslint-disable\u00A0unused, used*/", - output: "/* eslint-disable\u00A0used*/" - }, + // content before the first rule should not be changed + { + code: "//eslint-disable-line unused, used", + output: "//eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used", + output: "// eslint-disable-line used" + }, + { + code: "/*\neslint-disable unused, used*/", + output: "/*\neslint-disable used*/" + }, + { + code: "/*\n eslint-disable unused, used*/", + output: "/*\n eslint-disable used*/" + }, + { + code: "/*\r\neslint-disable unused, used*/", + output: "/*\r\neslint-disable used*/" + }, + { + code: "/*\u2028eslint-disable unused, used*/", + output: "/*\u2028eslint-disable used*/" + }, + { + code: "/*\u00A0eslint-disable unused, used*/", + output: "/*\u00A0eslint-disable used*/" + }, + { + code: "// eslint-disable-line unused, used", + output: "// eslint-disable-line used" + }, + { + code: "/* eslint-disable\nunused, used*/", + output: "/* eslint-disable\nused*/" + }, + { + code: "/* eslint-disable\n unused, used*/", + output: "/* eslint-disable\n used*/" + }, + { + code: "/* eslint-disable\r\nunused, used*/", + output: "/* eslint-disable\r\nused*/" + }, + { + code: "/* eslint-disable\u2028unused, used*/", + output: "/* eslint-disable\u2028used*/" + }, + { + code: "/* eslint-disable\u00A0unused, used*/", + output: "/* eslint-disable\u00A0used*/" + }, - // when removing the first rule, the comma and all whitespace up to the next rule (or next lone comma) should also be removed - { - code: "// eslint-disable-line unused,used", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused, used", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused , used", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused, used", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused ,used", - output: "// eslint-disable-line used" - }, - { - code: "/* eslint-disable unused\n,\nused */", - output: "/* eslint-disable used */" - }, - { - code: "/* eslint-disable unused \n \n,\n\n used */", - output: "/* eslint-disable used */" - }, - { - code: "/* eslint-disable unused\u2028,\u2028used */", - output: "/* eslint-disable used */" - }, - { - code: "// eslint-disable-line unused\u00A0,\u00A0used", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused,,used", - output: "// eslint-disable-line ,used" - }, - { - code: "// eslint-disable-line unused, ,used", - output: "// eslint-disable-line ,used" - }, - { - code: "// eslint-disable-line unused,, used", - output: "// eslint-disable-line , used" - }, - { - code: "// eslint-disable-line unused,used ", - output: "// eslint-disable-line used " - }, - { - code: "// eslint-disable-next-line unused,used\n", - output: "// eslint-disable-next-line used\n" - }, + // when removing the first rule, the comma and all whitespace up to the next rule (or next lone comma) should also be removed + { + code: "// eslint-disable-line unused,used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused , used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused ,used", + output: "// eslint-disable-line used" + }, + { + code: "/* eslint-disable unused\n,\nused */", + output: "/* eslint-disable used */" + }, + { + code: "/* eslint-disable unused \n \n,\n\n used */", + output: "/* eslint-disable used */" + }, + { + code: "/* eslint-disable unused\u2028,\u2028used */", + output: "/* eslint-disable used */" + }, + { + code: "// eslint-disable-line unused\u00A0,\u00A0used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused,,used", + output: "// eslint-disable-line ,used" + }, + { + code: "// eslint-disable-line unused, ,used", + output: "// eslint-disable-line ,used" + }, + { + code: "// eslint-disable-line unused,, used", + output: "// eslint-disable-line , used" + }, + { + code: "// eslint-disable-line unused,used ", + output: "// eslint-disable-line used " + }, + { + code: "// eslint-disable-next-line unused,used\n", + output: "// eslint-disable-next-line used\n" + }, - // when removing a rule in the middle, one comma and all whitespace between commas should also be removed - { - code: "// eslint-disable-line used-1,unused,used-2", - output: "// eslint-disable-line used-1,used-2" - }, - { - code: "// eslint-disable-line used-1, unused,used-2", - output: "// eslint-disable-line used-1,used-2" - }, - { - code: "// eslint-disable-line used-1,unused ,used-2", - output: "// eslint-disable-line used-1,used-2" - }, - { - code: "// eslint-disable-line used-1, unused ,used-2", - output: "// eslint-disable-line used-1,used-2" - }, - { - code: "/* eslint-disable used-1,\nunused\n,used-2 */", - output: "/* eslint-disable used-1,used-2 */" - }, - { - code: "/* eslint-disable used-1,\n\n unused \n \n ,used-2 */", - output: "/* eslint-disable used-1,used-2 */" - }, - { - code: "/* eslint-disable used-1,\u2028unused\u2028,used-2 */", - output: "/* eslint-disable used-1,used-2 */" - }, - { - code: "// eslint-disable-line used-1,\u00A0unused\u00A0,used-2", - output: "// eslint-disable-line used-1,used-2" - }, + // when removing a rule in the middle, one comma and all whitespace between commas should also be removed + { + code: "// eslint-disable-line used-1,unused,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + { + code: "// eslint-disable-line used-1, unused,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + { + code: "// eslint-disable-line used-1,unused ,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + { + code: "// eslint-disable-line used-1, unused ,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + { + code: "/* eslint-disable used-1,\nunused\n,used-2 */", + output: "/* eslint-disable used-1,used-2 */" + }, + { + code: "/* eslint-disable used-1,\n\n unused \n \n ,used-2 */", + output: "/* eslint-disable used-1,used-2 */" + }, + { + code: "/* eslint-disable used-1,\u2028unused\u2028,used-2 */", + output: "/* eslint-disable used-1,used-2 */" + }, + { + code: "// eslint-disable-line used-1,\u00A0unused\u00A0,used-2", + output: "// eslint-disable-line used-1,used-2" + }, - // when removing a rule in the middle, content around commas should not be changed - { - code: "// eslint-disable-line used-1, unused ,used-2", - output: "// eslint-disable-line used-1,used-2" - }, - { - code: "// eslint-disable-line used-1,unused, used-2", - output: "// eslint-disable-line used-1, used-2" - }, - { - code: "// eslint-disable-line used-1 ,unused,used-2", - output: "// eslint-disable-line used-1 ,used-2" - }, - { - code: "// eslint-disable-line used-1 ,unused, used-2", - output: "// eslint-disable-line used-1 , used-2" - }, - { - code: "// eslint-disable-line used-1 , unused , used-2", - output: "// eslint-disable-line used-1 , used-2" - }, - { - code: "/* eslint-disable used-1\n,unused,\nused-2 */", - output: "/* eslint-disable used-1\n,\nused-2 */" - }, - { - code: "/* eslint-disable used-1\u2028,unused,\u2028used-2 */", - output: "/* eslint-disable used-1\u2028,\u2028used-2 */" - }, - { - code: "// eslint-disable-line used-1\u00A0,unused,\u00A0used-2", - output: "// eslint-disable-line used-1\u00A0,\u00A0used-2" - }, - { - code: "// eslint-disable-line , unused ,used", - output: "// eslint-disable-line ,used" - }, - { - code: "/* eslint-disable\n, unused ,used */", - output: "/* eslint-disable\n,used */" - }, - { - code: "/* eslint-disable used-1,\n,unused,used-2 */", - output: "/* eslint-disable used-1,\n,used-2 */" - }, - { - code: "/* eslint-disable used-1,unused,\n,used-2 */", - output: "/* eslint-disable used-1,\n,used-2 */" - }, - { - code: "/* eslint-disable used-1,\n,unused,\n,used-2 */", - output: "/* eslint-disable used-1,\n,\n,used-2 */" - }, - { - code: "// eslint-disable-line used, unused,", - output: "// eslint-disable-line used," - }, - { - code: "// eslint-disable-next-line used, unused,\n", - output: "// eslint-disable-next-line used,\n" - }, - { - code: "// eslint-disable-line used, unused, ", - output: "// eslint-disable-line used, " - }, - { - code: "// eslint-disable-line used, unused, -- comment", - output: "// eslint-disable-line used, -- comment" - }, - { - code: "/* eslint-disable used, unused,\n*/", - output: "/* eslint-disable used,\n*/" - }, + // when removing a rule in the middle, content around commas should not be changed + { + code: "// eslint-disable-line used-1, unused ,used-2", + output: "// eslint-disable-line used-1,used-2" + }, + { + code: "// eslint-disable-line used-1,unused, used-2", + output: "// eslint-disable-line used-1, used-2" + }, + { + code: "// eslint-disable-line used-1 ,unused,used-2", + output: "// eslint-disable-line used-1 ,used-2" + }, + { + code: "// eslint-disable-line used-1 ,unused, used-2", + output: "// eslint-disable-line used-1 , used-2" + }, + { + code: "// eslint-disable-line used-1 , unused , used-2", + output: "// eslint-disable-line used-1 , used-2" + }, + { + code: "/* eslint-disable used-1\n,unused,\nused-2 */", + output: "/* eslint-disable used-1\n,\nused-2 */" + }, + { + code: "/* eslint-disable used-1\u2028,unused,\u2028used-2 */", + output: "/* eslint-disable used-1\u2028,\u2028used-2 */" + }, + { + code: "// eslint-disable-line used-1\u00A0,unused,\u00A0used-2", + output: "// eslint-disable-line used-1\u00A0,\u00A0used-2" + }, + { + code: "// eslint-disable-line , unused ,used", + output: "// eslint-disable-line ,used" + }, + { + code: "/* eslint-disable\n, unused ,used */", + output: "/* eslint-disable\n,used */" + }, + { + code: "/* eslint-disable used-1,\n,unused,used-2 */", + output: "/* eslint-disable used-1,\n,used-2 */" + }, + { + code: "/* eslint-disable used-1,unused,\n,used-2 */", + output: "/* eslint-disable used-1,\n,used-2 */" + }, + { + code: "/* eslint-disable used-1,\n,unused,\n,used-2 */", + output: "/* eslint-disable used-1,\n,\n,used-2 */" + }, + { + code: "// eslint-disable-line used, unused,", + output: "// eslint-disable-line used," + }, + { + code: "// eslint-disable-next-line used, unused,\n", + output: "// eslint-disable-next-line used,\n" + }, + { + code: "// eslint-disable-line used, unused, ", + output: "// eslint-disable-line used, " + }, + { + code: "// eslint-disable-line used, unused, -- comment", + output: "// eslint-disable-line used, -- comment" + }, + { + code: "/* eslint-disable used, unused,\n*/", + output: "/* eslint-disable used,\n*/" + }, - // when removing the last rule, the comma and all whitespace up to the previous rule (or previous lone comma) should also be removed - { - code: "// eslint-disable-line used,unused", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line used, unused", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line used ,unused", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line used , unused", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line used, unused", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line used ,unused", - output: "// eslint-disable-line used" - }, - { - code: "/* eslint-disable used\n,\nunused */", - output: "/* eslint-disable used */" - }, - { - code: "/* eslint-disable used \n \n,\n\n unused */", - output: "/* eslint-disable used */" - }, - { - code: "/* eslint-disable used\u2028,\u2028unused */", - output: "/* eslint-disable used */" - }, - { - code: "// eslint-disable-line used\u00A0,\u00A0unused", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line used,,unused", - output: "// eslint-disable-line used," - }, - { - code: "// eslint-disable-line used, ,unused", - output: "// eslint-disable-line used," - }, - { - code: "/* eslint-disable used,\n,unused */", - output: "/* eslint-disable used, */" - }, - { - code: "/* eslint-disable used\n, ,unused */", - output: "/* eslint-disable used\n, */" - }, + // when removing the last rule, the comma and all whitespace up to the previous rule (or previous lone comma) should also be removed + { + code: "// eslint-disable-line used,unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used, unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used ,unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used , unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used, unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used ,unused", + output: "// eslint-disable-line used" + }, + { + code: "/* eslint-disable used\n,\nunused */", + output: "/* eslint-disable used */" + }, + { + code: "/* eslint-disable used \n \n,\n\n unused */", + output: "/* eslint-disable used */" + }, + { + code: "/* eslint-disable used\u2028,\u2028unused */", + output: "/* eslint-disable used */" + }, + { + code: "// eslint-disable-line used\u00A0,\u00A0unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used,,unused", + output: "// eslint-disable-line used," + }, + { + code: "// eslint-disable-line used, ,unused", + output: "// eslint-disable-line used," + }, + { + code: "/* eslint-disable used,\n,unused */", + output: "/* eslint-disable used, */" + }, + { + code: "/* eslint-disable used\n, ,unused */", + output: "/* eslint-disable used\n, */" + }, - // content after the last rule should not be changed - { - code: "// eslint-disable-line used,unused", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line used,unused ", - output: "// eslint-disable-line used " - }, - { - code: "// eslint-disable-line used,unused ", - output: "// eslint-disable-line used " - }, - { - code: "// eslint-disable-line used,unused -- comment", - output: "// eslint-disable-line used -- comment" - }, - { - code: "// eslint-disable-next-line used,unused\n", - output: "// eslint-disable-next-line used\n" - }, - { - code: "// eslint-disable-next-line used,unused \n", - output: "// eslint-disable-next-line used \n" - }, - { - code: "/* eslint-disable used,unused\u2028*/", - output: "/* eslint-disable used\u2028*/" - }, - { - code: "// eslint-disable-line used,unused\u00A0", - output: "// eslint-disable-line used\u00A0" - }, + // content after the last rule should not be changed + { + code: "// eslint-disable-line used,unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used,unused ", + output: "// eslint-disable-line used " + }, + { + code: "// eslint-disable-line used,unused ", + output: "// eslint-disable-line used " + }, + { + code: "// eslint-disable-line used,unused -- comment", + output: "// eslint-disable-line used -- comment" + }, + { + code: "// eslint-disable-next-line used,unused\n", + output: "// eslint-disable-next-line used\n" + }, + { + code: "// eslint-disable-next-line used,unused \n", + output: "// eslint-disable-next-line used \n" + }, + { + code: "/* eslint-disable used,unused\u2028*/", + output: "/* eslint-disable used\u2028*/" + }, + { + code: "// eslint-disable-line used,unused\u00A0", + output: "// eslint-disable-line used\u00A0" + }, - // multiply rules to remove - { - code: "// eslint-disable-line used, unused-1, unused-2", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused-1, used, unused-2", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused-1, unused-2, used", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line used-1, unused-1, used-2, unused-2", - output: "// eslint-disable-line used-1, used-2" - }, - { - code: "// eslint-disable-line unused-1, used-1, unused-2, used-2", - output: "// eslint-disable-line used-1, used-2" - }, - { - code: ` + // multiply rules to remove + { + code: "// eslint-disable-line used, unused-1, unused-2", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused-1, used, unused-2", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused-1, unused-2, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used-1, unused-1, used-2, unused-2", + output: "// eslint-disable-line used-1, used-2" + }, + { + code: "// eslint-disable-line unused-1, used-1, unused-2, used-2", + output: "// eslint-disable-line used-1, used-2" + }, + { + code: ` /* eslint-disable unused-1, used-1, unused-2, used-2 */ `, - output: ` + output: ` /* eslint-disable used-1, used-2 */ ` - }, - { - code: ` + }, + { + code: ` /* eslint-disable unused-1, used-1, @@ -11930,15 +11965,15 @@ var a = "test2"; used-2 */ `, - output: ` + output: ` /* eslint-disable used-1, used-2 */ ` - }, - { - code: ` + }, + { + code: ` /* eslint-disable used-1, unused-1, @@ -11946,15 +11981,15 @@ var a = "test2"; unused-2 */ `, - output: ` + output: ` /* eslint-disable used-1, used-2 */ ` - }, - { - code: ` + }, + { + code: ` /* eslint-disable used-1, unused-1, @@ -11962,15 +11997,15 @@ var a = "test2"; unused-2, */ `, - output: ` + output: ` /* eslint-disable used-1, used-2, */ ` - }, - { - code: ` + }, + { + code: ` /* eslint-disable ,unused-1 ,used-1 @@ -11978,15 +12013,15 @@ var a = "test2"; ,used-2 */ `, - output: ` + output: ` /* eslint-disable ,used-1 ,used-2 */ ` - }, - { - code: ` + }, + { + code: ` /* eslint-disable ,used-1 ,unused-1 @@ -11994,15 +12029,15 @@ var a = "test2"; ,unused-2 */ `, - output: ` + output: ` /* eslint-disable ,used-1 ,used-2 */ ` - }, - { - code: ` + }, + { + code: ` /* eslint-disable used-1, unused-1, @@ -12012,7 +12047,7 @@ var a = "test2"; -- comment */ `, - output: ` + output: ` /* eslint-disable used-1, used-2 @@ -12020,34 +12055,34 @@ var a = "test2"; -- comment */ ` - }, + }, - // duplicates in the list - { - code: "// eslint-disable-line unused, unused, used", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused, used, unused", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line used, unused, unused, used", - output: "// eslint-disable-line used, used" - } - ]; + // duplicates in the list + { + code: "// eslint-disable-line unused, unused, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used, unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used, unused, unused, used", + output: "// eslint-disable-line used, used" + } + ]; - for (const { code, output } of tests) { - // eslint-disable-next-line no-loop-func -- `linter` is getting updated in beforeEach() - it(code, () => { - assert.strictEqual( - linter.verifyAndFix(code, config).output, - output - ); + for (const { code, output } of tests) { + // eslint-disable-next-line no-loop-func -- `linter` is getting updated in beforeEach() + it(code, () => { + assert.strictEqual( + linter.verifyAndFix(code, config).output, + output + ); + }); + } }); - } - }); - }); + }); }); @@ -12397,7 +12432,7 @@ var a = "test2"; }, /This method cannot be used with flat config/u); }); }); - + describe("defineParser()", () => { it("should throw an error when called in flat config mode", () => { assert.throws(() => { @@ -12405,7 +12440,7 @@ var a = "test2"; }, /This method cannot be used with flat config/u); }); }); - + describe("getRules()", () => { it("should throw an error when called in flat config mode", () => { assert.throws(() => { From aaab04e49ed8959b87309f90a3381ab9edaa9dc8 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 10 Nov 2021 09:14:17 -0800 Subject: [PATCH 19/37] Settings tests working --- tests/lib/linter/linter.js | 59 ++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 14140950d03..3b7991f84f3 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6906,19 +6906,30 @@ describe.only("Linter with FlatConfigArray", () => { }); - xdescribe("settings", () => { - const code = "test-rule"; + describe("settings", () => { + const ruleId = "test-rule"; it("should pass settings to all rules", () => { - linter.defineRule(code, context => ({ - Literal(node) { - context.report(node, context.settings.info); - } - })); - const config = { rules: {}, settings: { info: "Hello" } }; - - config.rules[code] = 1; + const config = { + plugins: { + test: { + rules: { + [ruleId]: context => ({ + Literal(node) { + context.report(node, context.settings.info); + } + }) + } + } + }, + settings: { + info: "Hello" + }, + rules: { + [`test/${ruleId}`]: 1 + } + }; const messages = linter.verify("0", config, filename); @@ -6927,17 +6938,27 @@ describe.only("Linter with FlatConfigArray", () => { }); it("should not have any settings if they were not passed in", () => { - linter.defineRule(code, context => ({ - Literal(node) { - if (Object.getOwnPropertyNames(context.settings).length !== 0) { - context.report(node, "Settings should be empty"); + + const config = { + plugins: { + test: { + rules: { + [ruleId]: context => ({ + Literal(node) { + if (Object.getOwnPropertyNames(context.settings).length !== 0) { + context.report(node, "Settings should be empty"); + } + } + }) + } } + }, + settings: { + }, + rules: { + [`test/${ruleId}`]: 1 } - })); - - const config = { rules: {} }; - - config.rules[code] = 1; + }; const messages = linter.verify("0", config, filename); From f25b1397478e0025dcd026edf4d3884af12f7202 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 10 Nov 2021 09:37:58 -0800 Subject: [PATCH 20/37] Rule context tests working --- tests/lib/linter/linter.js | 380 +++++++++++++++++++++++-------------- 1 file changed, 233 insertions(+), 147 deletions(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 3b7991f84f3..4926bba028e 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -7221,19 +7221,27 @@ describe.only("Linter with FlatConfigArray", () => { describe("Rule Context", () => { - xdescribe("context.getFilename()", () => { + describe("context.getFilename()", () => { const ruleId = "filename-rule"; it("has access to the filename", () => { - linter.defineRule(ruleId, context => ({ - Literal(node) { - context.report(node, context.getFilename()); - } - })); - - const config = { rules: {} }; - config.rules[ruleId] = 1; + const config = { + plugins: { + test: { + rules: { + [ruleId]: context => ({ + Literal(node) { + context.report(node, context.getFilename()); + } + }) + } + } + }, + rules: { + [`test/${ruleId}`]: 1 + } + }; const messages = linter.verify("0", config, filename); @@ -7241,15 +7249,24 @@ describe.only("Linter with FlatConfigArray", () => { }); it("defaults filename to ''", () => { - linter.defineRule(ruleId, context => ({ - Literal(node) { - context.report(node, context.getFilename()); - } - })); - const config = { rules: {} }; + const config = { + plugins: { + test: { + rules: { + [ruleId]: context => ({ + Literal(node) { + context.report(node, context.getFilename()); + } + }) + } + } + }, + rules: { + [`test/${ruleId}`]: 1 + } + }; - config.rules[ruleId] = 1; const messages = linter.verify("0", config); @@ -7257,20 +7274,28 @@ describe.only("Linter with FlatConfigArray", () => { }); }); - xdescribe("context.getPhysicalFilename()", () => { + describe("context.getPhysicalFilename()", () => { const ruleId = "filename-rule"; it("has access to the physicalFilename", () => { - linter.defineRule(ruleId, context => ({ - Literal(node) { - context.report(node, context.getPhysicalFilename()); - } - })); - const config = { rules: {} }; - - config.rules[ruleId] = 1; + const config = { + plugins: { + test: { + rules: { + [ruleId]: context => ({ + Literal(node) { + context.report(node, context.getPhysicalFilename()); + } + }) + } + } + }, + rules: { + [`test/${ruleId}`]: 1 + } + }; const messages = linter.verify("0", config, filename); @@ -8127,7 +8152,7 @@ describe.only("Linter with FlatConfigArray", () => { assert(spy && spy.calledOnce); }); - xdescribe("context.getScope()", () => { + describe("Scope Internals", () => { /** * Get the scope on the node `astSelector` specified. @@ -8139,18 +8164,29 @@ describe.only("Linter with FlatConfigArray", () => { function getScope(codeToEvaluate, astSelector, ecmaVersion = 5) { let node, scope; - linter.defineRule("get-scope", context => ({ - [astSelector](node0) { - node = node0; - scope = context.getScope(); - } - })); + const config = { + plugins: { + test: { + rules: { + "get-scope": context => ({ + [astSelector](node0) { + node = node0; + scope = context.getScope(); + } + }) + } + } + }, + languageOptions: { + ecmaVersion, + sourceType: "script" + }, + rules: { "test/get-scope": "error" } + }; + linter.verify( codeToEvaluate, - { - parserOptions: { ecmaVersion }, - rules: { "get-scope": 2 } - } + config ); return { node, scope }; @@ -8377,7 +8413,7 @@ describe.only("Linter with FlatConfigArray", () => { }); }); - xdescribe("Variables and references", () => { + describe("Variables and references", () => { const code = [ "a;", "function foo() { b; }", @@ -8395,17 +8431,32 @@ describe.only("Linter with FlatConfigArray", () => { beforeEach(() => { let ok = false; - linter.defineRules({ - test(context) { - return { - Program() { - scope = context.getScope(); - ok = true; + const config = { + plugins: { + test: { + rules: { + test(context) { + return { + Program() { + scope = context.getScope(); + ok = true; + } + }; + } } - }; + } + }, + languageOptions: { + globals: { e: true, f: false }, + sourceType: "script", + ecmaVersion: 5 + }, + rules: { + "test/test": 2 } - }); - linter.verify(code, { rules: { test: 2 }, globals: { e: true, f: false } }); + }; + + linter.verify(code, config); assert(ok); }); @@ -8479,7 +8530,7 @@ describe.only("Linter with FlatConfigArray", () => { }); }); - xdescribe("context.getDeclaredVariables(node)", () => { + describe("context.getDeclaredVariables(node)", () => { /** * Assert `context.getDeclaredVariables(node)` is valid. @@ -8489,88 +8540,98 @@ describe.only("Linter with FlatConfigArray", () => { * @returns {void} */ function verify(code, type, expectedNamesList) { - linter.defineRules({ - test(context) { + const config = { + plugins: { + test: { - /** - * Assert `context.getDeclaredVariables(node)` is empty. - * @param {ASTNode} node A node to check. - * @returns {void} - */ - function checkEmpty(node) { - assert.strictEqual(0, context.getDeclaredVariables(node).length); - } - const rule = { - Program: checkEmpty, - EmptyStatement: checkEmpty, - BlockStatement: checkEmpty, - ExpressionStatement: checkEmpty, - LabeledStatement: checkEmpty, - BreakStatement: checkEmpty, - ContinueStatement: checkEmpty, - WithStatement: checkEmpty, - SwitchStatement: checkEmpty, - ReturnStatement: checkEmpty, - ThrowStatement: checkEmpty, - TryStatement: checkEmpty, - WhileStatement: checkEmpty, - DoWhileStatement: checkEmpty, - ForStatement: checkEmpty, - ForInStatement: checkEmpty, - DebuggerStatement: checkEmpty, - ThisExpression: checkEmpty, - ArrayExpression: checkEmpty, - ObjectExpression: checkEmpty, - Property: checkEmpty, - SequenceExpression: checkEmpty, - UnaryExpression: checkEmpty, - BinaryExpression: checkEmpty, - AssignmentExpression: checkEmpty, - UpdateExpression: checkEmpty, - LogicalExpression: checkEmpty, - ConditionalExpression: checkEmpty, - CallExpression: checkEmpty, - NewExpression: checkEmpty, - MemberExpression: checkEmpty, - SwitchCase: checkEmpty, - Identifier: checkEmpty, - Literal: checkEmpty, - ForOfStatement: checkEmpty, - ArrowFunctionExpression: checkEmpty, - YieldExpression: checkEmpty, - TemplateLiteral: checkEmpty, - TaggedTemplateExpression: checkEmpty, - TemplateElement: checkEmpty, - ObjectPattern: checkEmpty, - ArrayPattern: checkEmpty, - RestElement: checkEmpty, - AssignmentPattern: checkEmpty, - ClassBody: checkEmpty, - MethodDefinition: checkEmpty, - MetaProperty: checkEmpty - }; + rules: { + test(context) { + + /** + * Assert `context.getDeclaredVariables(node)` is empty. + * @param {ASTNode} node A node to check. + * @returns {void} + */ + function checkEmpty(node) { + assert.strictEqual(0, context.getDeclaredVariables(node).length); + } + const rule = { + Program: checkEmpty, + EmptyStatement: checkEmpty, + BlockStatement: checkEmpty, + ExpressionStatement: checkEmpty, + LabeledStatement: checkEmpty, + BreakStatement: checkEmpty, + ContinueStatement: checkEmpty, + WithStatement: checkEmpty, + SwitchStatement: checkEmpty, + ReturnStatement: checkEmpty, + ThrowStatement: checkEmpty, + TryStatement: checkEmpty, + WhileStatement: checkEmpty, + DoWhileStatement: checkEmpty, + ForStatement: checkEmpty, + ForInStatement: checkEmpty, + DebuggerStatement: checkEmpty, + ThisExpression: checkEmpty, + ArrayExpression: checkEmpty, + ObjectExpression: checkEmpty, + Property: checkEmpty, + SequenceExpression: checkEmpty, + UnaryExpression: checkEmpty, + BinaryExpression: checkEmpty, + AssignmentExpression: checkEmpty, + UpdateExpression: checkEmpty, + LogicalExpression: checkEmpty, + ConditionalExpression: checkEmpty, + CallExpression: checkEmpty, + NewExpression: checkEmpty, + MemberExpression: checkEmpty, + SwitchCase: checkEmpty, + Identifier: checkEmpty, + Literal: checkEmpty, + ForOfStatement: checkEmpty, + ArrowFunctionExpression: checkEmpty, + YieldExpression: checkEmpty, + TemplateLiteral: checkEmpty, + TaggedTemplateExpression: checkEmpty, + TemplateElement: checkEmpty, + ObjectPattern: checkEmpty, + ArrayPattern: checkEmpty, + RestElement: checkEmpty, + AssignmentPattern: checkEmpty, + ClassBody: checkEmpty, + MethodDefinition: checkEmpty, + MetaProperty: checkEmpty + }; - rule[type] = function(node) { - const expectedNames = expectedNamesList.shift(); - const variables = context.getDeclaredVariables(node); + rule[type] = function(node) { + const expectedNames = expectedNamesList.shift(); + const variables = context.getDeclaredVariables(node); - assert(Array.isArray(expectedNames)); - assert(Array.isArray(variables)); - assert.strictEqual(expectedNames.length, variables.length); - for (let i = variables.length - 1; i >= 0; i--) { - assert.strictEqual(expectedNames[i], variables[i].name); + assert(Array.isArray(expectedNames)); + assert(Array.isArray(variables)); + assert.strictEqual(expectedNames.length, variables.length); + for (let i = variables.length - 1; i >= 0; i--) { + assert.strictEqual(expectedNames[i], variables[i].name); + } + }; + return rule; + } } - }; - return rule; - } - }); - linter.verify(code, { - rules: { test: 2 }, - parserOptions: { + + } + }, + languageOptions: { ecmaVersion: 6, sourceType: "module" + }, + rules: { + "test/test": 2 } - }); + }; + + linter.verify(code, config); // Check all expected names are asserted. assert.strictEqual(0, expectedNamesList.length); @@ -8913,37 +8974,54 @@ describe.only("Linter with FlatConfigArray", () => { }); }); - xdescribe("context.getCwd()", () => { + describe("context.getCwd()", () => { const code = "a;\nb;"; - const config = { rules: { checker: "error" } }; + const baseConfig = { rules: { "test/checker": "error" } }; it("should get cwd correctly in the context", () => { const cwd = "cwd"; - const linterWithOption = new Linter({ cwd }); + const linterWithOption = new Linter({ cwd, configType: "flat" }); let spy; - - linterWithOption.defineRule("checker", context => { - spy = sinon.spy(() => { - assert.strictEqual(context.getCwd(), cwd); - }); - return { Program: spy }; - }); + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + assert.strictEqual(context.getCwd(), cwd); + }); + return { Program: spy }; + } + } + } + }, + ...baseConfig + }; linterWithOption.verify(code, config); assert(spy && spy.calledOnce); }); it("should assign process.cwd() to it if cwd is undefined", () => { - let spy; - const linterWithOption = new Linter({}); - linterWithOption.defineRule("checker", context => { + const linterWithOption = new Linter({ configType: "flat" }); + let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { - spy = sinon.spy(() => { - assert.strictEqual(context.getCwd(), process.cwd()); - }); - return { Program: spy }; - }); + spy = sinon.spy(() => { + assert.strictEqual(context.getCwd(), process.cwd()); + }); + return { Program: spy }; + } + } + } + }, + ...baseConfig + }; linterWithOption.verify(code, config); assert(spy && spy.calledOnce); @@ -8951,14 +9029,22 @@ describe.only("Linter with FlatConfigArray", () => { it("should assign process.cwd() to it if the option is undefined", () => { let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { - linter.defineRule("checker", context => { - - spy = sinon.spy(() => { - assert.strictEqual(context.getCwd(), process.cwd()); - }); - return { Program: spy }; - }); + spy = sinon.spy(() => { + assert.strictEqual(context.getCwd(), process.cwd()); + }); + return { Program: spy }; + } + } + } + }, + ...baseConfig + }; linter.verify(code, config); assert(spy && spy.calledOnce); From f6ea3f7a7ad7565455fa6666c272197054bf63f4 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 10 Nov 2021 09:50:57 -0800 Subject: [PATCH 21/37] Options tests working --- tests/lib/linter/linter.js | 724 ++++++++++++++++++++++--------------- 1 file changed, 423 insertions(+), 301 deletions(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 4926bba028e..5c233461afc 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6643,7 +6643,6 @@ describe.only("Linter with FlatConfigArray", () => { assert.strictEqual(messages.length, 0); }); - it("should pass parser as context.languageOptions.parser to all rules when provided on config", () => { const config = { @@ -6779,6 +6778,293 @@ describe.only("Linter with FlatConfigArray", () => { linter.verify("0", config, filename); }); + + describe("Custom Parsers", () => { + + const errorPrefix = "Parsing error: "; + + it("should have file path passed to it", () => { + const code = "/* this is code */"; + const parseSpy = sinon.spy(testParsers.stubParser, "parse"); + const config = { + languageOptions: { + parser: testParsers.stubParser + } + }; + + linter.verify(code, config, filename, true); + + sinon.assert.calledWithMatch(parseSpy, "", { filePath: filename }); + }); + + it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { + const code = "var myDivElement =
;"; + const config = { + languageOptions: { + parser: esprima, + parserOptions: { + jsx: true + } + } + }; + + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not throw or report errors when the custom parser returns unrecognized operators (https://github.com/eslint/eslint/issues/10475)", () => { + const code = "null %% 'foo'"; + const config = { + languageOptions: { + parser: testParsers.unknownLogicalOperator + } + }; + + // This shouldn't throw + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not throw or report errors when the custom parser returns nested unrecognized operators (https://github.com/eslint/eslint/issues/10560)", () => { + const code = "foo && bar %% baz"; + const config = { + languageOptions: { + parser: testParsers.unknownLogicalOperatorNested + } + }; + + // This shouldn't throw + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should not throw or return errors when the custom parser returns unknown AST nodes", () => { + const code = "foo && bar %% baz"; + const nodes = []; + const config = { + plugins: { + test: { + rules: { + "collect-node-types": () => ({ + "*"(node) { + nodes.push(node.type); + } + }) + } + } + }, + languageOptions: { + parser: testParsers.nonJSParser + }, + rules: { + "test/collect-node-types": "error" + } + }; + + const messages = linter.verify(code, config, filename, true); + + assert.strictEqual(messages.length, 0); + assert.isTrue(nodes.length > 0); + }); + + it("should strip leading line: prefix from parser error", () => { + const messages = linter.verify(";", { + languageOptions: { + parser: testParsers.lineError + } + }, "filename"); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, errorPrefix + testParsers.lineError.expectedError); + }); + + it("should not modify a parser error message without a leading line: prefix", () => { + const messages = linter.verify(";", { + languageOptions: { + parser: testParsers.noLineError + } + }, "filename"); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, errorPrefix + testParsers.noLineError.expectedError); + }); + + describe("if a parser provides 'visitorKeys'", () => { + let types = []; + let sourceCode; + let scopeManager; + let firstChildNodes = []; + + beforeEach(() => { + types = []; + firstChildNodes = []; + const config = { + plugins: { + test: { + rules: { + "collect-node-types": () => ({ + "*"(node) { + types.push(node.type); + } + }), + "save-scope-manager": context => { + scopeManager = context.getSourceCode().scopeManager; + + return {}; + }, + "esquery-option": () => ({ + ":first-child"(node) { + firstChildNodes.push(node); + } + }) + } + } + }, + languageOptions: { + parser: testParsers.enhancedParser2 + }, + rules: { + "test/collect-node-types": "error", + "test/save-scope-manager": "error", + "test/esquery-option": "error" + } + }; + + linter.verify("@foo class A {}", config); + + sourceCode = linter.getSourceCode(); + }); + + it("Traverser should use the visitorKeys (so 'types' includes 'Decorator')", () => { + assert.deepStrictEqual( + types, + ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] + ); + }); + + it("eslint-scope should use the visitorKeys (so 'childVisitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { + assert.deepStrictEqual( + scopeManager.__options.childVisitorKeys.ClassDeclaration, // eslint-disable-line no-underscore-dangle -- ScopeManager API + ["experimentalDecorators", "id", "superClass", "body"] + ); + }); + + it("should use the same visitorKeys if the source code object is reused", () => { + const types2 = []; + const config = { + plugins: { + test: { + rules: { + "collect-node-types": () => ({ + "*"(node) { + types2.push(node.type); + } + }) + } + } + }, + rules: { + "test/collect-node-types": "error" + } + }; + + linter.verify(sourceCode, config); + + assert.deepStrictEqual( + types2, + ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] + ); + }); + + it("esquery should use the visitorKeys (so 'visitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { + assert.deepStrictEqual( + firstChildNodes, + [sourceCode.ast.body[0], sourceCode.ast.body[0].experimentalDecorators[0]] + ); + }); + }); + + describe("if a parser provides 'scope'", () => { + let scope = null; + let sourceCode = null; + + beforeEach(() => { + const config = { + plugins: { + test: { + rules: { + "save-scope1": context => ({ + Program() { + scope = context.getScope(); + } + }) + } + } + }, + languageOptions: { + parser: testParsers.enhancedParser3 + }, + rules: { + "test/save-scope1": "error" + } + }; + + linter.verify("@foo class A {}", config); + + sourceCode = linter.getSourceCode(); + }); + + it("should use the scope (so the global scope has the reference of '@foo')", () => { + assert.strictEqual(scope.references.length, 1); + assert.deepStrictEqual( + scope.references[0].identifier.name, + "foo" + ); + }); + + it("should use the same scope if the source code object is reused", () => { + let scope2 = null; + const config = { + plugins: { + test: { + rules: { + "save-scope2": context => ({ + Program() { + scope2 = context.getScope(); + } + }) + } + } + }, + rules: { + "test/save-scope2": "error" + } + }; + + linter.verify(sourceCode, config, "test.js"); + + assert(scope2 !== null); + assert(scope2 === scope); + }); + }); + + it("should not pass any default parserOptions to the parser", () => { + const messages = linter.verify(";", { + languageOptions: { + parser: testParsers.throwsWithOptions + } + }, "filename"); + + assert.strictEqual(messages.length, 0); + }); + }); + + }); describe("parseOptions", () => { @@ -9102,364 +9388,138 @@ describe.only("Linter with FlatConfigArray", () => { }); }); - describe("Custom Parsers", () => { - - const errorPrefix = "Parsing error: "; - - it("should have file path passed to it", () => { - const code = "/* this is code */"; - const parseSpy = sinon.spy(testParsers.stubParser, "parse"); - const config = { - languageOptions: { - parser: testParsers.stubParser - } - }; - - linter.verify(code, config, filename, true); - - sinon.assert.calledWithMatch(parseSpy, "", { filePath: filename }); - }); - - it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { - const code = "var myDivElement =
;"; - const config = { - languageOptions: { - parser: esprima, - parserOptions: { - jsx: true - } - } - }; - - const messages = linter.verify(code, config, filename); - - assert.strictEqual(messages.length, 0); - }); - - it("should not throw or report errors when the custom parser returns unrecognized operators (https://github.com/eslint/eslint/issues/10475)", () => { - const code = "null %% 'foo'"; - const config = { - languageOptions: { - parser: testParsers.unknownLogicalOperator - } - }; + describe("Code with a hashbang comment", () => { + const code = "#!bin/program\n\nvar foo;;"; - // This shouldn't throw - const messages = linter.verify(code, config, filename); + it("should preserve line numbers", () => { + const config = { rules: { "no-extra-semi": 1 } }; + const messages = linter.verify(code, config); - assert.strictEqual(messages.length, 0); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-extra-semi"); + assert.strictEqual(messages[0].nodeType, "EmptyStatement"); + assert.strictEqual(messages[0].line, 3); }); - it("should not throw or report errors when the custom parser returns nested unrecognized operators (https://github.com/eslint/eslint/issues/10560)", () => { - const code = "foo && bar %% baz"; - const config = { - languageOptions: { - parser: testParsers.unknownLogicalOperatorNested - } - }; - - // This shouldn't throw - const messages = linter.verify(code, config, filename); + it("should have a comment with the hashbang in it", () => { + const spy = sinon.spy(context => { + const comments = context.getAllComments(); - assert.strictEqual(messages.length, 0); - }); + assert.strictEqual(comments.length, 1); + assert.strictEqual(comments[0].type, "Shebang"); + return {}; + }); - it("should not throw or return errors when the custom parser returns unknown AST nodes", () => { - const code = "foo && bar %% baz"; - const nodes = []; const config = { plugins: { test: { rules: { - "collect-node-types": () => ({ - "*"(node) { - nodes.push(node.type); - } - }) + checker: spy } } }, - languageOptions: { - parser: testParsers.nonJSParser - }, rules: { - "test/collect-node-types": "error" + "test/checker": "error" } }; - const messages = linter.verify(code, config, filename, true); - - assert.strictEqual(messages.length, 0); - assert.isTrue(nodes.length > 0); - }); - - it("should strip leading line: prefix from parser error", () => { - const messages = linter.verify(";", { - languageOptions: { - parser: testParsers.lineError - } - }, "filename"); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, errorPrefix + testParsers.lineError.expectedError); + linter.verify(code, config); + assert(spy.calledOnce); }); + }); - it("should not modify a parser error message without a leading line: prefix", () => { - const messages = linter.verify(";", { - languageOptions: { - parser: testParsers.noLineError - } - }, "filename"); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, errorPrefix + testParsers.noLineError.expectedError); - }); + describe("Options", () => { - describe("if a parser provides 'visitorKeys'", () => { - let types = []; - let sourceCode; - let scopeManager; - let firstChildNodes = []; + describe("filename", () => { + it("should allow filename to be passed on options object", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.getFilename(), "foo.js"); + return {}; + }); - beforeEach(() => { - types = []; - firstChildNodes = []; const config = { plugins: { test: { rules: { - "collect-node-types": () => ({ - "*"(node) { - types.push(node.type); - } - }), - "save-scope-manager": context => { - scopeManager = context.getSourceCode().scopeManager; - - return {}; - }, - "esquery-option": () => ({ - ":first-child"(node) { - firstChildNodes.push(node); - } - }) + checker: filenameChecker } } }, - languageOptions: { - parser: testParsers.enhancedParser2 - }, rules: { - "test/collect-node-types": "error", - "test/save-scope-manager": "error", - "test/esquery-option": "error" + "test/checker": "error" } }; - linter.verify("@foo class A {}", config); - - sourceCode = linter.getSourceCode(); - }); - - it("Traverser should use the visitorKeys (so 'types' includes 'Decorator')", () => { - assert.deepStrictEqual( - types, - ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] - ); + linter.verify("foo;", config, { filename: "foo.js" }); + assert(filenameChecker.calledOnce); }); - it("eslint-scope should use the visitorKeys (so 'childVisitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { - assert.deepStrictEqual( - scopeManager.__options.childVisitorKeys.ClassDeclaration, // eslint-disable-line no-underscore-dangle -- ScopeManager API - ["experimentalDecorators", "id", "superClass", "body"] - ); - }); + it("should allow filename to be passed as third argument", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.getFilename(), "bar.js"); + return {}; + }); - it("should use the same visitorKeys if the source code object is reused", () => { - const types2 = []; const config = { plugins: { test: { rules: { - "collect-node-types": () => ({ - "*"(node) { - types2.push(node.type); - } - }) + checker: filenameChecker } } }, rules: { - "test/collect-node-types": "error" + "test/checker": "error" } }; - linter.verify(sourceCode, config); - - assert.deepStrictEqual( - types2, - ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] - ); - }); - - it("esquery should use the visitorKeys (so 'visitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { - assert.deepStrictEqual( - firstChildNodes, - [sourceCode.ast.body[0], sourceCode.ast.body[0].experimentalDecorators[0]] - ); + linter.verify("foo;", config, "bar.js"); + assert(filenameChecker.calledOnce); }); - }); - describe("if a parser provides 'scope'", () => { - let scope = null; - let sourceCode = null; + it("should default filename to when options object doesn't have filename", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.getFilename(), ""); + return {}; + }); - beforeEach(() => { const config = { plugins: { test: { rules: { - "save-scope1": context => ({ - Program() { - scope = context.getScope(); - } - }) + checker: filenameChecker } } }, - languageOptions: { - parser: testParsers.enhancedParser3 - }, rules: { - "test/save-scope1": "error" + "test/checker": "error" } }; - linter.verify("@foo class A {}", config); - - sourceCode = linter.getSourceCode(); + linter.verify("foo;", config, {}); + assert(filenameChecker.calledOnce); }); - it("should use the scope (so the global scope has the reference of '@foo')", () => { - assert.strictEqual(scope.references.length, 1); - assert.deepStrictEqual( - scope.references[0].identifier.name, - "foo" - ); - }); + it("should default filename to when only two arguments are passed", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.getFilename(), ""); + return {}; + }); - it("should use the same scope if the source code object is reused", () => { - let scope2 = null; const config = { plugins: { test: { rules: { - "save-scope2": context => ({ - Program() { - scope2 = context.getScope(); - } - }) + checker: filenameChecker } } }, rules: { - "test/save-scope2": "error" + "test/checker": "error" } }; - linter.verify(sourceCode, config, "test.js"); - - assert(scope2 !== null); - assert(scope2 === scope); - }); - }); - - it("should not pass any default parserOptions to the parser", () => { - const messages = linter.verify(";", { - languageOptions: { - parser: testParsers.throwsWithOptions - } - }, "filename"); - - assert.strictEqual(messages.length, 0); - }); - }); - - xdescribe("Code with a hashbang comment", () => { - const code = "#!bin/program\n\nvar foo;;"; - - it("should preserve line numbers", () => { - const config = { rules: { "no-extra-semi": 1 } }; - const messages = linter.verify(code, config); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-extra-semi"); - assert.strictEqual(messages[0].nodeType, "EmptyStatement"); - assert.strictEqual(messages[0].line, 3); - }); - - it("should have a comment with the hashbang in it", () => { - const config = { rules: { checker: "error" } }; - const spy = sinon.spy(context => { - const comments = context.getAllComments(); - - assert.strictEqual(comments.length, 1); - assert.strictEqual(comments[0].type, "hashbang"); - return {}; - }); - - linter.defineRule("checker", spy); - linter.verify(code, config); - assert(spy.calledOnce); - }); - }); - - xdescribe("Options", () => { - - describe("filename", () => { - it("should allow filename to be passed on options object", () => { - const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), "foo.js"); - return {}; - }); - - linter.defineRule("checker", filenameChecker); - linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); - assert(filenameChecker.calledOnce); - }); - - it("should allow filename to be passed as third argument", () => { - const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), "bar.js"); - return {}; - }); - - linter.defineRule("checker", filenameChecker); - linter.verify("foo;", { rules: { checker: "error" } }, "bar.js"); - assert(filenameChecker.calledOnce); - }); - - it("should default filename to when options object doesn't have filename", () => { - const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), ""); - return {}; - }); - - linter.defineRule("checker", filenameChecker); - linter.verify("foo;", { rules: { checker: "error" } }, {}); - assert(filenameChecker.calledOnce); - }); - - it("should default filename to when only two arguments are passed", () => { - const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), ""); - return {}; - }); - - linter.defineRule("checker", filenameChecker); - linter.verify("foo;", { rules: { checker: "error" } }); + linter.verify("foo;", config); assert(filenameChecker.calledOnce); }); }); @@ -9471,8 +9531,20 @@ describe.only("Linter with FlatConfigArray", () => { return {}; }); - linter.defineRule("checker", physicalFilenameChecker); - linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); + const config = { + plugins: { + test: { + rules: { + checker: physicalFilenameChecker + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify("foo;", config, { filename: "foo.js" }); assert(physicalFilenameChecker.calledOnce); }); @@ -9482,8 +9554,20 @@ describe.only("Linter with FlatConfigArray", () => { return {}; }); - linter.defineRule("checker", physicalFilenameChecker); - linter.verify("foo;", { rules: { checker: "error" } }, {}); + const config = { + plugins: { + test: { + rules: { + checker: physicalFilenameChecker + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify("foo;", config, {}); assert(physicalFilenameChecker.calledOnce); }); @@ -9493,8 +9577,20 @@ describe.only("Linter with FlatConfigArray", () => { return {}; }); - linter.defineRule("checker", physicalFilenameChecker); - linter.verify("foo;", { rules: { checker: "error" } }); + const config = { + plugins: { + test: { + rules: { + checker: physicalFilenameChecker + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify("foo;", config); assert(physicalFilenameChecker.calledOnce); }); }); @@ -13098,28 +13194,54 @@ var a = "test2"; linter.verify("var", config); }); - xit("should pass 'id' to rule contexts with the rule id", () => { + it("should pass 'id' to rule contexts with the rule id", () => { + const spy = sinon.spy(context => { - assert.strictEqual(context.id, "foo-bar-baz"); + assert.strictEqual(context.id, "test/foo-bar-baz"); return {}; }); - linter.defineRule("foo-bar-baz", spy); - linter.verify("x", { rules: { "foo-bar-baz": "error" } }); + const config = { + plugins: { + test: { + rules: { + "foo-bar-baz": spy + } + } + }, + rules: { + "test/foo-bar-baz": "error" + } + }; + + + linter.verify("x", config); assert(spy.calledOnce); }); - xdescribe("when evaluating an empty string", () => { + describe("when evaluating an empty string", () => { it("runs rules", () => { - linter.defineRule("no-programs", context => ({ - Program(node) { - context.report({ node, message: "No programs allowed." }); + + const config = { + plugins: { + test: { + rules: { + "no-programs": context => ({ + Program(node) { + context.report({ node, message: "No programs allowed." }); + } + }) + } + } + }, + rules: { + "test/no-programs": "error" } - })); + }; assert.strictEqual( - linter.verify("", { rules: { "no-programs": "error" } }).length, + linter.verify("", config).length, 1 ); }); From c0a1eb4602e31af825203c3a37af5c6185b36395 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 11 Nov 2021 11:41:55 -0800 Subject: [PATCH 22/37] Directives tests working --- lib/linter/linter.js | 4 +- tests/lib/linter/linter.js | 643 ++++++++++++++++++++++--------------- 2 files changed, 380 insertions(+), 267 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 7aa21a4d8fe..827ed82153c 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -304,7 +304,7 @@ function createDisableDirectives(options) { for (const ruleId of directiveRules) { // push to directives, if the rule is defined(including null, e.g. /*eslint enable*/) - if (ruleId === null || ruleMapper(ruleId) !== null) { + if (ruleId === null || !!ruleMapper(ruleId)) { result.directives.push({ parentComment, type, line: commentToken.loc.start.line, column: commentToken.loc.start.column + 1, ruleId }); } else { result.directiveProblems.push(createLintingProblem({ ruleId, loc: commentToken.loc })); @@ -436,7 +436,7 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) { const rule = ruleMapper(name); const ruleValue = parseResult.config[name]; - if (rule === null) { + if (!rule) { problems.push(createLintingProblem({ ruleId: name, loc: comment.loc })); return; } diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 5c233461afc..c0ae4af669c 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -9599,53 +9599,64 @@ describe.only("Linter with FlatConfigArray", () => { describe("Inline Directives", () => { - xdescribe("/*global*/ Comments", () => { + describe("/*global*/ Comments", () => { describe("when evaluating code containing /*global */ and /*globals */ blocks", () => { it("variables should be available in global scope", () => { - const config = { rules: { checker: "error" }, globals: { Array: "off", ConfigGlobal: "writeable" } }; const code = ` - /*global a b:true c:false d:readable e:writeable Math:off */ - function foo() {} - /*globals f:true*/ - /* global ConfigGlobal : readable */ - `; + /*global a b:true c:false d:readable e:writeable Math:off */ + function foo() {} + /*globals f:true*/ + /* global ConfigGlobal : readable */ + `; let spy; - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(); - const a = getVariable(scope, "a"), - b = getVariable(scope, "b"), - c = getVariable(scope, "c"), - d = getVariable(scope, "d"), - e = getVariable(scope, "e"), - f = getVariable(scope, "f"), - mathGlobal = getVariable(scope, "Math"), - arrayGlobal = getVariable(scope, "Array"), - configGlobal = getVariable(scope, "ConfigGlobal"); - - assert.strictEqual(a.name, "a"); - assert.strictEqual(a.writeable, false); - assert.strictEqual(b.name, "b"); - assert.strictEqual(b.writeable, true); - assert.strictEqual(c.name, "c"); - assert.strictEqual(c.writeable, false); - assert.strictEqual(d.name, "d"); - assert.strictEqual(d.writeable, false); - assert.strictEqual(e.name, "e"); - assert.strictEqual(e.writeable, true); - assert.strictEqual(f.name, "f"); - assert.strictEqual(f.writeable, true); - assert.strictEqual(mathGlobal, null); - assert.strictEqual(arrayGlobal, null); - assert.strictEqual(configGlobal.name, "ConfigGlobal"); - assert.strictEqual(configGlobal.writeable, false); - }); + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + const a = getVariable(scope, "a"), + b = getVariable(scope, "b"), + c = getVariable(scope, "c"), + d = getVariable(scope, "d"), + e = getVariable(scope, "e"), + f = getVariable(scope, "f"), + mathGlobal = getVariable(scope, "Math"), + arrayGlobal = getVariable(scope, "Array"), + configGlobal = getVariable(scope, "ConfigGlobal"); + + assert.strictEqual(a.name, "a"); + assert.strictEqual(a.writeable, false); + assert.strictEqual(b.name, "b"); + assert.strictEqual(b.writeable, true); + assert.strictEqual(c.name, "c"); + assert.strictEqual(c.writeable, false); + assert.strictEqual(d.name, "d"); + assert.strictEqual(d.writeable, false); + assert.strictEqual(e.name, "e"); + assert.strictEqual(e.writeable, true); + assert.strictEqual(f.name, "f"); + assert.strictEqual(f.writeable, true); + assert.strictEqual(mathGlobal, null); + assert.strictEqual(arrayGlobal, null); + assert.strictEqual(configGlobal.name, "ConfigGlobal"); + assert.strictEqual(configGlobal.writeable, false); + }); - return { Program: spy }; - }); + return { Program: spy }; + } + } + } + }, + rules: { "test/checker": "error" }, + languageOptions: { + globals: { Array: "off", ConfigGlobal: "writeable" } + } + }; linter.verify(code, config); assert(spy && spy.calledOnce); @@ -9656,58 +9667,65 @@ describe.only("Linter with FlatConfigArray", () => { const code = "/* global a b : true c: false*/"; it("variables should be available in global scope", () => { - const config = { rules: { checker: "error" } }; - let spy; - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(), - a = getVariable(scope, "a"), - b = getVariable(scope, "b"), - c = getVariable(scope, "c"); - - assert.strictEqual(a.name, "a"); - assert.strictEqual(a.writeable, false); - assert.strictEqual(b.name, "b"); - assert.strictEqual(b.writeable, true); - assert.strictEqual(c.name, "c"); - assert.strictEqual(c.writeable, false); - }); + let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + a = getVariable(scope, "a"), + b = getVariable(scope, "b"), + c = getVariable(scope, "c"); + + assert.strictEqual(a.name, "a"); + assert.strictEqual(a.writeable, false); + assert.strictEqual(b.name, "b"); + assert.strictEqual(b.writeable, true); + assert.strictEqual(c.name, "c"); + assert.strictEqual(c.writeable, false); + }); - return { Program: spy }; - }); + return { Program: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; linter.verify(code, config); assert(spy && spy.calledOnce); }); }); - describe("when evaluating code containing a /*global */ block with specific variables", () => { - const code = "/* global toString hasOwnProperty valueOf: true */"; - - it("should not throw an error if comment block has global variables which are Object.prototype contains", () => { - const config = { rules: { checker: "error" } }; - - linter.verify(code, config); - }); - }); - describe("when evaluating code containing a line comment", () => { const code = "//global a \n function f() {}"; it("should not introduce a global variable", () => { - const config = { rules: { checker: "error" } }; let spy; - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(); + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); - assert.strictEqual(getVariable(scope, "a"), null); - }); + assert.strictEqual(getVariable(scope, "a"), null); + }); + + return { Program: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; - return { Program: spy }; - }); linter.verify(code, config); assert(spy && spy.calledOnce); @@ -9718,21 +9736,30 @@ describe.only("Linter with FlatConfigArray", () => { const code = "/**/ /*a*/ /*b:true*/ /*foo c:false*/"; it("should not introduce a global variable", () => { - const config = { rules: { checker: "error" } }; let spy; - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(); + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(getVariable(scope, "a"), null); + assert.strictEqual(getVariable(scope, "b"), null); + assert.strictEqual(getVariable(scope, "foo"), null); + assert.strictEqual(getVariable(scope, "c"), null); + }); - assert.strictEqual(getVariable(scope, "a"), null); - assert.strictEqual(getVariable(scope, "b"), null); - assert.strictEqual(getVariable(scope, "foo"), null); - assert.strictEqual(getVariable(scope, "c"), null); - }); + return { Program: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; - return { Program: spy }; - }); linter.verify(code, config); assert(spy && spy.calledOnce); @@ -9742,39 +9769,46 @@ describe.only("Linter with FlatConfigArray", () => { it("should attach a \"/*global\" comment node to declared variables", () => { const code = "/* global foo */\n/* global bar, baz */"; let ok = false; + const config = { + plugins: { + test: { + rules: { + test(context) { + return { + Program() { + const scope = context.getScope(); + const sourceCode = context.getSourceCode(); + const comments = sourceCode.getAllComments(); - linter.defineRules({ - test(context) { - return { - Program() { - const scope = context.getScope(); - const sourceCode = context.getSourceCode(); - const comments = sourceCode.getAllComments(); - - assert.strictEqual(2, comments.length); + assert.strictEqual(2, comments.length); - const foo = getVariable(scope, "foo"); + const foo = getVariable(scope, "foo"); - assert.strictEqual(foo.eslintExplicitGlobal, true); - assert.strictEqual(foo.eslintExplicitGlobalComments[0], comments[0]); + assert.strictEqual(foo.eslintExplicitGlobal, true); + assert.strictEqual(foo.eslintExplicitGlobalComments[0], comments[0]); - const bar = getVariable(scope, "bar"); + const bar = getVariable(scope, "bar"); - assert.strictEqual(bar.eslintExplicitGlobal, true); - assert.strictEqual(bar.eslintExplicitGlobalComments[0], comments[1]); + assert.strictEqual(bar.eslintExplicitGlobal, true); + assert.strictEqual(bar.eslintExplicitGlobalComments[0], comments[1]); - const baz = getVariable(scope, "baz"); + const baz = getVariable(scope, "baz"); - assert.strictEqual(baz.eslintExplicitGlobal, true); - assert.strictEqual(baz.eslintExplicitGlobalComments[0], comments[1]); + assert.strictEqual(baz.eslintExplicitGlobal, true); + assert.strictEqual(baz.eslintExplicitGlobalComments[0], comments[1]); - ok = true; + ok = true; + } + }; + } } - }; - } - }); + } + }, + rules: { "test/test": "error" } + }; + - linter.verify(code, { rules: { test: 2 } }); + linter.verify(code, config); assert(ok); }); @@ -9808,7 +9842,7 @@ describe.only("Linter with FlatConfigArray", () => { }); - xdescribe("/*exported*/ Comments", () => { + describe("/*exported*/ Comments", () => { it("we should behave nicely when no matching variable is found", () => { const code = "/* exported horse */"; @@ -9819,19 +9853,30 @@ describe.only("Linter with FlatConfigArray", () => { it("variables should be exported", () => { const code = "/* exported horse */\n\nvar horse = 'circus'"; - const config = { rules: { checker: "error" } }; let spy; - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(), - horse = getVariable(scope, "horse"); + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); - assert.strictEqual(horse.eslintUsed, true); - }); + assert.strictEqual(horse.eslintUsed, true); + }); - return { Program: spy }; - }); + return { Program: spy }; + } + } + } + }, + languageOptions: { + sourceType: "script" + }, + rules: { "test/checker": "error" } + }; linter.verify(code, config); assert(spy && spy.calledOnce); @@ -9839,19 +9884,29 @@ describe.only("Linter with FlatConfigArray", () => { it("undefined variables should not be exported", () => { const code = "/* exported horse */\n\nhorse = 'circus'"; - const config = { rules: { checker: "error" } }; let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(), - horse = getVariable(scope, "horse"); - - assert.strictEqual(horse, null); - }); + assert.strictEqual(horse, null); + }); - return { Program: spy }; - }); + return { Program: spy }; + } + } + } + }, + languageOptions: { + sourceType: "script" + }, + rules: { "test/checker": "error" } + }; linter.verify(code, config); assert(spy && spy.calledOnce); @@ -9859,19 +9914,29 @@ describe.only("Linter with FlatConfigArray", () => { it("variables should be exported in strict mode", () => { const code = "/* exported horse */\n'use strict';\nvar horse = 'circus'"; - const config = { rules: { checker: "error" } }; let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(), - horse = getVariable(scope, "horse"); - - assert.strictEqual(horse.eslintUsed, true); - }); + assert.strictEqual(horse.eslintUsed, true); + }); - return { Program: spy }; - }); + return { Program: spy }; + } + } + } + }, + languageOptions: { + sourceType: "script" + }, + rules: { "test/checker": "error" } + }; linter.verify(code, config); assert(spy && spy.calledOnce); @@ -9879,46 +9944,67 @@ describe.only("Linter with FlatConfigArray", () => { it("variables should not be exported in the es6 module environment", () => { const code = "/* exported horse */\nvar horse = 'circus'"; - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6, sourceType: "module" } }; let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(), - horse = getVariable(scope, "horse"); - - assert.strictEqual(horse, null); // there is no global scope at all - }); + assert.strictEqual(horse, null); // there is no global scope at all + }); - return { Program: spy }; - }); + return { Program: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 6, + sourceType: "module" + }, + rules: { "test/checker": "error" } + }; linter.verify(code, config); assert(spy && spy.calledOnce); }); - it("variables should not be exported when in the node environment", () => { + it("variables should not be exported when in a commonjs file", () => { const code = "/* exported horse */\nvar horse = 'circus'"; - const config = { rules: { checker: "error" }, env: { node: true } }; let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(), - horse = getVariable(scope, "horse"); - - assert.strictEqual(horse, null); // there is no global scope at all - }); + assert.strictEqual(horse, null); // there is no global scope at all + }); - return { Program: spy }; - }); + return { Program: spy }; + } + } + } + }, + languageOptions: { + sourceType: "commonjs" + }, + rules: { "test/checker": "error" } + }; linter.verify(code, config); assert(spy && spy.calledOnce); }); }); - xdescribe("/*eslint*/ Comments", () => { + describe("/*eslint*/ Comments", () => { describe("when evaluating code with comments to enable rules", () => { it("should report a violation", () => { @@ -9934,7 +10020,12 @@ describe.only("Linter with FlatConfigArray", () => { }); it("rules should not change initial config", () => { - const config = { rules: { strict: 2 } }; + const config = { + languageOptions: { + sourceType: "script" + }, + rules: { strict: 2 } + }; const codeA = "/*eslint strict: 0*/ function bar() { return 2; }"; const codeB = "function foo() { return 1; }"; let messages = linter.verify(codeA, config, filename, false); @@ -9946,7 +10037,12 @@ describe.only("Linter with FlatConfigArray", () => { }); it("rules should not change initial config", () => { - const config = { rules: { quotes: [2, "double"] } }; + const config = { + languageOptions: { + sourceType: "script" + }, + rules: { quotes: [2, "double"] } + }; const codeA = "/*eslint quotes: 0*/ function bar() { return '2'; }"; const codeB = "function foo() { return '1'; }"; let messages = linter.verify(codeA, config, filename, false); @@ -9970,7 +10066,12 @@ describe.only("Linter with FlatConfigArray", () => { }); it("rules should not change initial config", () => { - const config = { rules: { "no-unused-vars": [2, { vars: "all" }] } }; + const config = { + languageOptions: { + sourceType: "script" + }, + rules: { "no-unused-vars": [2, { vars: "all" }] } + }; const codeA = "/*eslint no-unused-vars: [0, {\"vars\": \"local\"}]*/ var a = 44;"; const codeB = "var b = 55;"; let messages = linter.verify(codeA, config, filename, false); @@ -10021,58 +10122,52 @@ describe.only("Linter with FlatConfigArray", () => { }); describe("when evaluating code with comments to disable rules", () => { - const code = "/*eslint no-alert:0*/ alert('test');"; it("should not report a violation", () => { const config = { rules: { "no-alert": 1 } }; - - const messages = linter.verify(code, config, filename); + const messages = linter.verify("/*eslint no-alert:0*/ alert('test');", config, filename); assert.strictEqual(messages.length, 0); }); - }); - - describe("when evaluating code with comments to disable rules", () => { - let code, messages; it("should report an error when disabling a non-existent rule in inline comment", () => { - code = "/*eslint foo:0*/ ;"; - messages = linter.verify(code, {}, filename); - assert.strictEqual(messages.length, 1); + let code = "/*eslint foo:0*/ ;"; + let messages = linter.verify(code, {}, filename); + + assert.strictEqual(messages.length, 1, "/*eslint*/ comment should report problem."); assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); code = "/*eslint-disable foo*/ ;"; messages = linter.verify(code, {}, filename); - assert.strictEqual(messages.length, 1); + assert.strictEqual(messages.length, 1, "/*eslint-disable*/ comment should report problem."); assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); code = "/*eslint-disable-line foo*/ ;"; messages = linter.verify(code, {}, filename); - assert.strictEqual(messages.length, 1); + assert.strictEqual(messages.length, 1, "/*eslint-disable-line*/ comment should report problem."); assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); code = "/*eslint-disable-next-line foo*/ ;"; messages = linter.verify(code, {}, filename); - assert.strictEqual(messages.length, 1); + assert.strictEqual(messages.length, 1, "/*eslint-disable-next-line*/ comment should report problem."); assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); }); it("should not report an error, when disabling a non-existent rule in config", () => { - messages = linter.verify("", { rules: { foo: 0 } }, filename); + const messages = linter.verify("", { rules: { foo: 0 } }, filename); assert.strictEqual(messages.length, 0); }); - it("should report an error, when config a non-existent rule in config", () => { - messages = linter.verify("", { rules: { foo: 1 } }, filename); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + it("should throw an error when a non-existent rule in config", () => { + assert.throws(() => { + linter.verify("", { rules: { foo: 1 } }, filename); + }, /Key "rules": Key "foo":/u); + + assert.throws(() => { + linter.verify("", { rules: { foo: 2 } }, filename); + }, /Key "rules": Key "foo":/u); - messages = linter.verify("", { rules: { foo: 2 } }, filename); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); }); }); @@ -10153,12 +10248,24 @@ describe.only("Linter with FlatConfigArray", () => { it("should report a violation when the report is right before the comment", () => { const code = " /* eslint-disable */ "; - linter.defineRule("checker", context => ({ - Program() { - context.report({ loc: { line: 1, column: 0 }, message: "foo" }); + const config = { + plugins: { + test: { + rules: { + checker: context => ({ + Program() { + context.report({ loc: { line: 1, column: 0 }, message: "foo" }); + } + }) + } + } + }, + rules: { + "test/checker": "error" } - })); - const problems = linter.verify(code, { rules: { checker: "error" } }); + }; + + const problems = linter.verify(code, config); assert.strictEqual(problems.length, 1); assert.strictEqual(problems[0].message, "foo"); @@ -10167,18 +10274,30 @@ describe.only("Linter with FlatConfigArray", () => { it("should not report a violation when the report is right at the start of the comment", () => { const code = " /* eslint-disable */ "; - linter.defineRule("checker", context => ({ - Program() { - context.report({ loc: { line: 1, column: 1 }, message: "foo" }); + const config = { + plugins: { + test: { + rules: { + checker: context => ({ + Program() { + context.report({ loc: { line: 1, column: 1 }, message: "foo" }); + } + }) + } + } + }, + rules: { + "test/checker": "error" } - })); - const problems = linter.verify(code, { rules: { checker: "error" } }); + }; + + const problems = linter.verify(code, config); assert.strictEqual(problems.length, 0); }); it("rules should not change initial config", () => { - const config = { rules: { "test-plugin/test-rule": 2 } }; + const config = { ...baseConfig, rules: { "test-plugin/test-rule": 2 } }; const codeA = "/*eslint test-plugin/test-rule: 0*/ var a = \"trigger violation\";"; const codeB = "var a = \"trigger violation\";"; let messages = linter.verify(codeA, config, filename, false); @@ -11111,17 +11230,25 @@ var a = "test2"; }); }); - xdescribe("descriptions in directive comments", () => { + describe("descriptions in directive comments", () => { it("should ignore the part preceded by '--' in '/*eslint*/'.", () => { const aaa = sinon.stub().returns({}); const bbb = sinon.stub().returns({}); + const config = { + plugins: { + test: { + rules: { + aaa: { create: aaa }, + bbb: { create: bbb } + } + } + } + }; - linter.defineRule("aaa", { create: aaa }); - linter.defineRule("bbb", { create: bbb }); const messages = linter.verify(` - /*eslint aaa:error -- bbb:error */ + /*eslint test/aaa:error -- test/bbb:error */ console.log("hello") - `, {}); + `, config); // Don't include syntax error of the comment. assert.deepStrictEqual(messages, []); @@ -11131,60 +11258,17 @@ var a = "test2"; assert.strictEqual(bbb.callCount, 0); }); - it("should ignore the part preceded by '--' in '/*eslint-env*/'.", () => { - const messages = linter.verify(` - /*eslint-env es2015 -- es2017 */ - var Promise = {} - var Atomics = {} - `, { rules: { "no-redeclare": "error" } }); - - // Don't include `Atomics` - assert.deepStrictEqual( - messages, - [{ - column: 25, - endColumn: 32, - endLine: 3, - line: 3, - message: "'Promise' is already defined as a built-in global variable.", - messageId: "redeclaredAsBuiltin", - nodeType: "Identifier", - ruleId: "no-redeclare", - severity: 2 - }] - ); - }); - - it("should ignore the part preceded by '--' in '/*global*/'.", () => { - const messages = linter.verify(` - /*global aaa -- bbb */ - var aaa = {} - var bbb = {} - `, { rules: { "no-redeclare": "error" } }); - - // Don't include `bbb` - assert.deepStrictEqual( - messages, - [{ - column: 30, - endColumn: 33, - line: 2, - endLine: 2, - message: "'aaa' is already defined by a variable declaration.", - messageId: "redeclaredBySyntax", - nodeType: "Block", - ruleId: "no-redeclare", - severity: 2 - }] - ); - }); - it("should ignore the part preceded by '--' in '/*globals*/'.", () => { const messages = linter.verify(` /*globals aaa -- bbb */ var aaa = {} var bbb = {} - `, { rules: { "no-redeclare": "error" } }); + `, { + languageOptions: { + sourceType: "script" + }, + rules: { "no-redeclare": "error" } + }); // Don't include `bbb` assert.deepStrictEqual( @@ -11208,7 +11292,12 @@ var a = "test2"; /*exported aaa -- bbb */ var aaa = {} var bbb = {} - `, { rules: { "no-unused-vars": "error" } }); + `, { + languageOptions: { + sourceType: "script" + }, + rules: { "no-unused-vars": "error" } + }); // Don't include `aaa` assert.deepStrictEqual( @@ -11374,12 +11463,20 @@ var a = "test2"; it("should not ignore the part preceded by '--' if the '--' is not surrounded by whitespaces.", () => { const rule = sinon.stub().returns({}); + const config = { + plugins: { + test: { + rules: { + "a--rule": { create: rule } + } + } + } + }; - linter.defineRule("a--rule", { create: rule }); const messages = linter.verify(` - /*eslint a--rule:error */ + /*eslint test/a--rule:error */ console.log("hello") - `, {}); + `, config); // Don't include syntax error of the comment. assert.deepStrictEqual(messages, []); @@ -11391,13 +11488,21 @@ var a = "test2"; it("should ignore the part preceded by '--' even if the '--' is longer than 2.", () => { const aaa = sinon.stub().returns({}); const bbb = sinon.stub().returns({}); + const config = { + plugins: { + test: { + rules: { + aaa: { create: aaa }, + bbb: { create: bbb } + } + } + } + }; - linter.defineRule("aaa", { create: aaa }); - linter.defineRule("bbb", { create: bbb }); const messages = linter.verify(` - /*eslint aaa:error -------- bbb:error */ + /*eslint test/aaa:error -------- test/bbb:error */ console.log("hello") - `, {}); + `, config); // Don't include syntax error of the comment. assert.deepStrictEqual(messages, []); @@ -11410,15 +11515,23 @@ var a = "test2"; it("should ignore the part preceded by '--' with line breaks.", () => { const aaa = sinon.stub().returns({}); const bbb = sinon.stub().returns({}); + const config = { + plugins: { + test: { + rules: { + aaa: { create: aaa }, + bbb: { create: bbb } + } + } + } + }; - linter.defineRule("aaa", { create: aaa }); - linter.defineRule("bbb", { create: bbb }); const messages = linter.verify(` - /*eslint aaa:error + /*eslint test/aaa:error -------- - bbb:error */ + test/bbb:error */ console.log("hello") - `, {}); + `, config); // Don't include syntax error of the comment. assert.deepStrictEqual(messages, []); From f6b12075ed5a359337cf04890894876bd027b07d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 11 Nov 2021 12:31:34 -0800 Subject: [PATCH 23/37] reportUnusedDisableDirectives tests working --- tests/lib/linter/linter.js | 530 ++++++++++++++++++++----------------- 1 file changed, 280 insertions(+), 250 deletions(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index c0ae4af669c..ad7a4d3ba0d 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -11543,7 +11543,7 @@ var a = "test2"; }); describe("allowInlineConfig option", () => { - xdescribe("when evaluating code with comments to change config when allowInlineConfig is enabled", () => { + describe("when evaluating code with comments to change config when allowInlineConfig is enabled", () => { it("should report a violation for disabling rules", () => { const code = [ "alert('test'); // eslint-disable-line no-alert" @@ -11564,35 +11564,38 @@ var a = "test2"; }); it("should report a violation for global variable declarations", () => { + let ok = false; const code = [ "/* global foo */" ].join("\n"); const config = { - rules: { - test: 2 - } - }; - let ok = false; - - linter.defineRules({ - test(context) { - return { - Program() { - const scope = context.getScope(); - const sourceCode = context.getSourceCode(); - const comments = sourceCode.getAllComments(); + plugins: { + test: { + rules: { + test(context) { + return { + Program() { + const scope = context.getScope(); + const sourceCode = context.getSourceCode(); + const comments = sourceCode.getAllComments(); - assert.strictEqual(1, comments.length); + assert.strictEqual(1, comments.length); - const foo = getVariable(scope, "foo"); + const foo = getVariable(scope, "foo"); - assert.notOk(foo); + assert.notOk(foo); - ok = true; + ok = true; + } + }; + } } - }; + } + }, + rules: { + "test/test": 2 } - }); + }; linter.verify(code, config, { allowInlineConfig: false }); assert(ok); @@ -11658,7 +11661,7 @@ var a = "test2"; }); - xdescribe("when evaluating code with 'noInlineConfig'", () => { + describe("when evaluating code with 'noInlineConfig'", () => { for (const directive of [ "globals foo", "global foo", @@ -11671,7 +11674,11 @@ var a = "test2"; ]) { // eslint-disable-next-line no-loop-func -- No closures it(`should warn '/* ${directive} */' if 'noInlineConfig' was given.`, () => { - const messages = linter.verify(`/* ${directive} */`, { noInlineConfig: true }); + const messages = linter.verify(`/* ${directive} */`, { + linterOptions: { + noInlineConfig: true + } + }); assert.deepStrictEqual(messages.length, 1); assert.deepStrictEqual(messages[0].fatal, void 0); @@ -11687,7 +11694,11 @@ var a = "test2"; ]) { // eslint-disable-next-line no-loop-func -- No closures it(`should warn '// ${directive}' if 'noInlineConfig' was given.`, () => { - const messages = linter.verify(`// ${directive}`, { noInlineConfig: true }); + const messages = linter.verify(`// ${directive}`, { + linterOptions: { + noInlineConfig: true + } + }); assert.deepStrictEqual(messages.length, 1); assert.deepStrictEqual(messages[0].fatal, void 0); @@ -11698,7 +11709,11 @@ var a = "test2"; } it("should not warn if 'noInlineConfig' and '--no-inline-config' were given.", () => { - const messages = linter.verify("/* globals foo */", { noInlineConfig: true }, { allowInlineConfig: false }); + const messages = linter.verify("/* globals foo */", { + linterOptions: { + noInlineConfig: true + } + }, { allowInlineConfig: false }); assert.deepStrictEqual(messages.length, 0); }); @@ -11726,7 +11741,7 @@ var a = "test2"; }); - xdescribe("reportUnusedDisableDirectives option", () => { + describe("reportUnusedDisableDirectives option", () => { it("reports problems for unused eslint-disable comments", () => { assert.deepStrictEqual( linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: true }), @@ -11789,7 +11804,11 @@ var a = "test2"; it("reports problems for unused eslint-disable comments (in config)", () => { assert.deepStrictEqual( - linter.verify("/* eslint-disable */", { reportUnusedDisableDirectives: true }), + linter.verify("/* eslint-disable */", { + linterOptions: { + reportUnusedDisableDirectives: true + } + }), [ { ruleId: null, @@ -11810,7 +11829,9 @@ var a = "test2"; it("reports problems for partially unused eslint-disable comments (in config)", () => { const code = "alert('test'); // eslint-disable-line no-alert, no-redeclare"; const config = { - reportUnusedDisableDirectives: true, + linterOptions: { + reportUnusedDisableDirectives: true + }, rules: { "no-alert": 1, "no-redeclare": 1 @@ -11866,16 +11887,25 @@ var a = "test2"; const unusedRules = usedRules.map(name => `un${name}`); // "unused", "unused-1", "unused-2" const config = { - reportUnusedDisableDirectives: true, + plugins: { + test: { + rules: {} + } + }, + linterOptions: { + reportUnusedDisableDirectives: true + }, rules: { - ...Object.fromEntries(usedRules.map(name => [name, "error"])), - ...Object.fromEntries(unusedRules.map(name => [name, "error"])) + ...Object.fromEntries(usedRules.map(name => [`test/${name}`, "error"])), + ...Object.fromEntries(unusedRules.map(name => [`test/${name}`, "error"])) } }; beforeEach(() => { - linter.defineRules(Object.fromEntries(usedRules.map(name => [name, alwaysReportsRule]))); - linter.defineRules(Object.fromEntries(unusedRules.map(name => [name, neverReportsRule]))); + config.plugins.test.rules = { + ...Object.fromEntries(usedRules.map(name => [name, alwaysReportsRule])), + ...Object.fromEntries(unusedRules.map(name => [name, neverReportsRule])) + }; }); const tests = [ @@ -11885,35 +11915,35 @@ var a = "test2"; //----------------------------------------------- { - code: "// eslint-disable-line unused", + code: "// eslint-disable-line test/unused", output: " " }, { - code: "foo// eslint-disable-line unused", + code: "foo// eslint-disable-line test/unused", output: "foo " }, { - code: "// eslint-disable-line ,unused,", + code: "// eslint-disable-line ,test/unused,", output: " " }, { - code: "// eslint-disable-line unused-1, unused-2", + code: "// eslint-disable-line test/unused-1, test/unused-2", output: " " }, { - code: "// eslint-disable-line ,unused-1,, unused-2,, -- comment", + code: "// eslint-disable-line ,test/unused-1,, test/unused-2,, -- comment", output: " " }, { - code: "// eslint-disable-next-line unused\n", + code: "// eslint-disable-next-line test/unused\n", output: " \n" }, { - code: "// eslint-disable-next-line unused\nfoo", + code: "// eslint-disable-next-line test/unused\nfoo", output: " \nfoo" }, { - code: "/* eslint-disable \nunused\n*/", + code: "/* eslint-disable \ntest/unused\n*/", output: " " }, @@ -11923,450 +11953,450 @@ var a = "test2"; // content before the first rule should not be changed { - code: "//eslint-disable-line unused, used", - output: "//eslint-disable-line used" + code: "//eslint-disable-line test/unused, test/used", + output: "//eslint-disable-line test/used" }, { - code: "// eslint-disable-line unused, used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused, test/used", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line unused, used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused, test/used", + output: "// eslint-disable-line test/used" }, { - code: "/*\neslint-disable unused, used*/", - output: "/*\neslint-disable used*/" + code: "/*\neslint-disable test/unused, test/used*/", + output: "/*\neslint-disable test/used*/" }, { - code: "/*\n eslint-disable unused, used*/", - output: "/*\n eslint-disable used*/" + code: "/*\n eslint-disable test/unused, test/used*/", + output: "/*\n eslint-disable test/used*/" }, { - code: "/*\r\neslint-disable unused, used*/", - output: "/*\r\neslint-disable used*/" + code: "/*\r\neslint-disable test/unused, test/used*/", + output: "/*\r\neslint-disable test/used*/" }, { - code: "/*\u2028eslint-disable unused, used*/", - output: "/*\u2028eslint-disable used*/" + code: "/*\u2028eslint-disable test/unused, test/used*/", + output: "/*\u2028eslint-disable test/used*/" }, { - code: "/*\u00A0eslint-disable unused, used*/", - output: "/*\u00A0eslint-disable used*/" + code: "/*\u00A0eslint-disable test/unused, test/used*/", + output: "/*\u00A0eslint-disable test/used*/" }, { - code: "// eslint-disable-line unused, used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused, test/used", + output: "// eslint-disable-line test/used" }, { - code: "/* eslint-disable\nunused, used*/", - output: "/* eslint-disable\nused*/" + code: "/* eslint-disable\ntest/unused, test/used*/", + output: "/* eslint-disable\ntest/used*/" }, { - code: "/* eslint-disable\n unused, used*/", - output: "/* eslint-disable\n used*/" + code: "/* eslint-disable\n test/unused, test/used*/", + output: "/* eslint-disable\n test/used*/" }, { - code: "/* eslint-disable\r\nunused, used*/", - output: "/* eslint-disable\r\nused*/" + code: "/* eslint-disable\r\ntest/unused, test/used*/", + output: "/* eslint-disable\r\ntest/used*/" }, { - code: "/* eslint-disable\u2028unused, used*/", - output: "/* eslint-disable\u2028used*/" + code: "/* eslint-disable\u2028test/unused, test/used*/", + output: "/* eslint-disable\u2028test/used*/" }, { - code: "/* eslint-disable\u00A0unused, used*/", - output: "/* eslint-disable\u00A0used*/" + code: "/* eslint-disable\u00A0test/unused, test/used*/", + output: "/* eslint-disable\u00A0test/used*/" }, // when removing the first rule, the comma and all whitespace up to the next rule (or next lone comma) should also be removed { - code: "// eslint-disable-line unused,used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused,test/used", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line unused, used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused, test/used", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line unused , used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused , test/used", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line unused, used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused, test/used", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line unused ,used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused ,test/used", + output: "// eslint-disable-line test/used" }, { - code: "/* eslint-disable unused\n,\nused */", - output: "/* eslint-disable used */" + code: "/* eslint-disable test/unused\n,\ntest/used */", + output: "/* eslint-disable test/used */" }, { - code: "/* eslint-disable unused \n \n,\n\n used */", - output: "/* eslint-disable used */" + code: "/* eslint-disable test/unused \n \n,\n\n test/used */", + output: "/* eslint-disable test/used */" }, { - code: "/* eslint-disable unused\u2028,\u2028used */", - output: "/* eslint-disable used */" + code: "/* eslint-disable test/unused\u2028,\u2028test/used */", + output: "/* eslint-disable test/used */" }, { - code: "// eslint-disable-line unused\u00A0,\u00A0used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused\u00A0,\u00A0test/used", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line unused,,used", - output: "// eslint-disable-line ,used" + code: "// eslint-disable-line test/unused,,test/used", + output: "// eslint-disable-line ,test/used" }, { - code: "// eslint-disable-line unused, ,used", - output: "// eslint-disable-line ,used" + code: "// eslint-disable-line test/unused, ,test/used", + output: "// eslint-disable-line ,test/used" }, { - code: "// eslint-disable-line unused,, used", - output: "// eslint-disable-line , used" + code: "// eslint-disable-line test/unused,, test/used", + output: "// eslint-disable-line , test/used" }, { - code: "// eslint-disable-line unused,used ", - output: "// eslint-disable-line used " + code: "// eslint-disable-line test/unused,test/used ", + output: "// eslint-disable-line test/used " }, { - code: "// eslint-disable-next-line unused,used\n", - output: "// eslint-disable-next-line used\n" + code: "// eslint-disable-next-line test/unused,test/used\n", + output: "// eslint-disable-next-line test/used\n" }, // when removing a rule in the middle, one comma and all whitespace between commas should also be removed { - code: "// eslint-disable-line used-1,unused,used-2", - output: "// eslint-disable-line used-1,used-2" + code: "// eslint-disable-line test/used-1,test/unused,test/used-2", + output: "// eslint-disable-line test/used-1,test/used-2" }, { - code: "// eslint-disable-line used-1, unused,used-2", - output: "// eslint-disable-line used-1,used-2" + code: "// eslint-disable-line test/used-1, test/unused,test/used-2", + output: "// eslint-disable-line test/used-1,test/used-2" }, { - code: "// eslint-disable-line used-1,unused ,used-2", - output: "// eslint-disable-line used-1,used-2" + code: "// eslint-disable-line test/used-1,test/unused ,test/used-2", + output: "// eslint-disable-line test/used-1,test/used-2" }, { - code: "// eslint-disable-line used-1, unused ,used-2", - output: "// eslint-disable-line used-1,used-2" + code: "// eslint-disable-line test/used-1, test/unused ,test/used-2", + output: "// eslint-disable-line test/used-1,test/used-2" }, { - code: "/* eslint-disable used-1,\nunused\n,used-2 */", - output: "/* eslint-disable used-1,used-2 */" + code: "/* eslint-disable test/used-1,\ntest/unused\n,test/used-2 */", + output: "/* eslint-disable test/used-1,test/used-2 */" }, { - code: "/* eslint-disable used-1,\n\n unused \n \n ,used-2 */", - output: "/* eslint-disable used-1,used-2 */" + code: "/* eslint-disable test/used-1,\n\n test/unused \n \n ,test/used-2 */", + output: "/* eslint-disable test/used-1,test/used-2 */" }, { - code: "/* eslint-disable used-1,\u2028unused\u2028,used-2 */", - output: "/* eslint-disable used-1,used-2 */" + code: "/* eslint-disable test/used-1,\u2028test/unused\u2028,test/used-2 */", + output: "/* eslint-disable test/used-1,test/used-2 */" }, { - code: "// eslint-disable-line used-1,\u00A0unused\u00A0,used-2", - output: "// eslint-disable-line used-1,used-2" + code: "// eslint-disable-line test/used-1,\u00A0test/unused\u00A0,test/used-2", + output: "// eslint-disable-line test/used-1,test/used-2" }, // when removing a rule in the middle, content around commas should not be changed { - code: "// eslint-disable-line used-1, unused ,used-2", - output: "// eslint-disable-line used-1,used-2" + code: "// eslint-disable-line test/used-1, test/unused ,test/used-2", + output: "// eslint-disable-line test/used-1,test/used-2" }, { - code: "// eslint-disable-line used-1,unused, used-2", - output: "// eslint-disable-line used-1, used-2" + code: "// eslint-disable-line test/used-1,test/unused, test/used-2", + output: "// eslint-disable-line test/used-1, test/used-2" }, { - code: "// eslint-disable-line used-1 ,unused,used-2", - output: "// eslint-disable-line used-1 ,used-2" + code: "// eslint-disable-line test/used-1 ,test/unused,test/used-2", + output: "// eslint-disable-line test/used-1 ,test/used-2" }, { - code: "// eslint-disable-line used-1 ,unused, used-2", - output: "// eslint-disable-line used-1 , used-2" + code: "// eslint-disable-line test/used-1 ,test/unused, test/used-2", + output: "// eslint-disable-line test/used-1 , test/used-2" }, { - code: "// eslint-disable-line used-1 , unused , used-2", - output: "// eslint-disable-line used-1 , used-2" + code: "// eslint-disable-line test/used-1 , test/unused , test/used-2", + output: "// eslint-disable-line test/used-1 , test/used-2" }, { - code: "/* eslint-disable used-1\n,unused,\nused-2 */", - output: "/* eslint-disable used-1\n,\nused-2 */" + code: "/* eslint-disable test/used-1\n,test/unused,\ntest/used-2 */", + output: "/* eslint-disable test/used-1\n,\ntest/used-2 */" }, { - code: "/* eslint-disable used-1\u2028,unused,\u2028used-2 */", - output: "/* eslint-disable used-1\u2028,\u2028used-2 */" + code: "/* eslint-disable test/used-1\u2028,test/unused,\u2028test/used-2 */", + output: "/* eslint-disable test/used-1\u2028,\u2028test/used-2 */" }, { - code: "// eslint-disable-line used-1\u00A0,unused,\u00A0used-2", - output: "// eslint-disable-line used-1\u00A0,\u00A0used-2" + code: "// eslint-disable-line test/used-1\u00A0,test/unused,\u00A0test/used-2", + output: "// eslint-disable-line test/used-1\u00A0,\u00A0test/used-2" }, { - code: "// eslint-disable-line , unused ,used", - output: "// eslint-disable-line ,used" + code: "// eslint-disable-line , test/unused ,test/used", + output: "// eslint-disable-line ,test/used" }, { - code: "/* eslint-disable\n, unused ,used */", - output: "/* eslint-disable\n,used */" + code: "/* eslint-disable\n, test/unused ,test/used */", + output: "/* eslint-disable\n,test/used */" }, { - code: "/* eslint-disable used-1,\n,unused,used-2 */", - output: "/* eslint-disable used-1,\n,used-2 */" + code: "/* eslint-disable test/used-1,\n,test/unused,test/used-2 */", + output: "/* eslint-disable test/used-1,\n,test/used-2 */" }, { - code: "/* eslint-disable used-1,unused,\n,used-2 */", - output: "/* eslint-disable used-1,\n,used-2 */" + code: "/* eslint-disable test/used-1,test/unused,\n,test/used-2 */", + output: "/* eslint-disable test/used-1,\n,test/used-2 */" }, { - code: "/* eslint-disable used-1,\n,unused,\n,used-2 */", - output: "/* eslint-disable used-1,\n,\n,used-2 */" + code: "/* eslint-disable test/used-1,\n,test/unused,\n,test/used-2 */", + output: "/* eslint-disable test/used-1,\n,\n,test/used-2 */" }, { - code: "// eslint-disable-line used, unused,", - output: "// eslint-disable-line used," + code: "// eslint-disable-line test/used, test/unused,", + output: "// eslint-disable-line test/used," }, { - code: "// eslint-disable-next-line used, unused,\n", - output: "// eslint-disable-next-line used,\n" + code: "// eslint-disable-next-line test/used, test/unused,\n", + output: "// eslint-disable-next-line test/used,\n" }, { - code: "// eslint-disable-line used, unused, ", - output: "// eslint-disable-line used, " + code: "// eslint-disable-line test/used, test/unused, ", + output: "// eslint-disable-line test/used, " }, { - code: "// eslint-disable-line used, unused, -- comment", - output: "// eslint-disable-line used, -- comment" + code: "// eslint-disable-line test/used, test/unused, -- comment", + output: "// eslint-disable-line test/used, -- comment" }, { - code: "/* eslint-disable used, unused,\n*/", - output: "/* eslint-disable used,\n*/" + code: "/* eslint-disable test/used, test/unused,\n*/", + output: "/* eslint-disable test/used,\n*/" }, // when removing the last rule, the comma and all whitespace up to the previous rule (or previous lone comma) should also be removed { - code: "// eslint-disable-line used,unused", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/used,test/unused", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line used, unused", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/used, test/unused", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line used ,unused", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/used ,test/unused", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line used , unused", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/used , test/unused", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line used, unused", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/used, test/unused", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line used ,unused", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/used ,test/unused", + output: "// eslint-disable-line test/used" }, { - code: "/* eslint-disable used\n,\nunused */", - output: "/* eslint-disable used */" + code: "/* eslint-disable test/used\n,\ntest/unused */", + output: "/* eslint-disable test/used */" }, { - code: "/* eslint-disable used \n \n,\n\n unused */", - output: "/* eslint-disable used */" + code: "/* eslint-disable test/used \n \n,\n\n test/unused */", + output: "/* eslint-disable test/used */" }, { - code: "/* eslint-disable used\u2028,\u2028unused */", - output: "/* eslint-disable used */" + code: "/* eslint-disable test/used\u2028,\u2028test/unused */", + output: "/* eslint-disable test/used */" }, { - code: "// eslint-disable-line used\u00A0,\u00A0unused", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/used\u00A0,\u00A0test/unused", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line used,,unused", - output: "// eslint-disable-line used," + code: "// eslint-disable-line test/used,,test/unused", + output: "// eslint-disable-line test/used," }, { - code: "// eslint-disable-line used, ,unused", - output: "// eslint-disable-line used," + code: "// eslint-disable-line test/used, ,test/unused", + output: "// eslint-disable-line test/used," }, { - code: "/* eslint-disable used,\n,unused */", - output: "/* eslint-disable used, */" + code: "/* eslint-disable test/used,\n,test/unused */", + output: "/* eslint-disable test/used, */" }, { - code: "/* eslint-disable used\n, ,unused */", - output: "/* eslint-disable used\n, */" + code: "/* eslint-disable test/used\n, ,test/unused */", + output: "/* eslint-disable test/used\n, */" }, // content after the last rule should not be changed { - code: "// eslint-disable-line used,unused", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/used,test/unused", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line used,unused ", - output: "// eslint-disable-line used " + code: "// eslint-disable-line test/used,test/unused ", + output: "// eslint-disable-line test/used " }, { - code: "// eslint-disable-line used,unused ", - output: "// eslint-disable-line used " + code: "// eslint-disable-line test/used,test/unused ", + output: "// eslint-disable-line test/used " }, { - code: "// eslint-disable-line used,unused -- comment", - output: "// eslint-disable-line used -- comment" + code: "// eslint-disable-line test/used,test/unused -- comment", + output: "// eslint-disable-line test/used -- comment" }, { - code: "// eslint-disable-next-line used,unused\n", - output: "// eslint-disable-next-line used\n" + code: "// eslint-disable-next-line test/used,test/unused\n", + output: "// eslint-disable-next-line test/used\n" }, { - code: "// eslint-disable-next-line used,unused \n", - output: "// eslint-disable-next-line used \n" + code: "// eslint-disable-next-line test/used,test/unused \n", + output: "// eslint-disable-next-line test/used \n" }, { - code: "/* eslint-disable used,unused\u2028*/", - output: "/* eslint-disable used\u2028*/" + code: "/* eslint-disable test/used,test/unused\u2028*/", + output: "/* eslint-disable test/used\u2028*/" }, { - code: "// eslint-disable-line used,unused\u00A0", - output: "// eslint-disable-line used\u00A0" + code: "// eslint-disable-line test/used,test/unused\u00A0", + output: "// eslint-disable-line test/used\u00A0" }, // multiply rules to remove { - code: "// eslint-disable-line used, unused-1, unused-2", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/used, test/unused-1, test/unused-2", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line unused-1, used, unused-2", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused-1, test/used, test/unused-2", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line unused-1, unused-2, used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused-1, test/unused-2, test/used", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line used-1, unused-1, used-2, unused-2", - output: "// eslint-disable-line used-1, used-2" + code: "// eslint-disable-line test/used-1, test/unused-1, test/used-2, test/unused-2", + output: "// eslint-disable-line test/used-1, test/used-2" }, { - code: "// eslint-disable-line unused-1, used-1, unused-2, used-2", - output: "// eslint-disable-line used-1, used-2" + code: "// eslint-disable-line test/unused-1, test/used-1, test/unused-2, test/used-2", + output: "// eslint-disable-line test/used-1, test/used-2" }, { code: ` - /* eslint-disable unused-1, - used-1, - unused-2, - used-2 + /* eslint-disable test/unused-1, + test/used-1, + test/unused-2, + test/used-2 */ `, output: ` - /* eslint-disable used-1, - used-2 + /* eslint-disable test/used-1, + test/used-2 */ ` }, { code: ` /* eslint-disable - unused-1, - used-1, - unused-2, - used-2 + test/unused-1, + test/used-1, + test/unused-2, + test/used-2 */ `, output: ` /* eslint-disable - used-1, - used-2 + test/used-1, + test/used-2 */ ` }, { code: ` /* eslint-disable - used-1, - unused-1, - used-2, - unused-2 + test/used-1, + test/unused-1, + test/used-2, + test/unused-2 */ `, output: ` /* eslint-disable - used-1, - used-2 + test/used-1, + test/used-2 */ ` }, { code: ` /* eslint-disable - used-1, - unused-1, - used-2, - unused-2, + test/used-1, + test/unused-1, + test/used-2, + test/unused-2, */ `, output: ` /* eslint-disable - used-1, - used-2, + test/used-1, + test/used-2, */ ` }, { code: ` /* eslint-disable - ,unused-1 - ,used-1 - ,unused-2 - ,used-2 + ,test/unused-1 + ,test/used-1 + ,test/unused-2 + ,test/used-2 */ `, output: ` /* eslint-disable - ,used-1 - ,used-2 + ,test/used-1 + ,test/used-2 */ ` }, { code: ` /* eslint-disable - ,used-1 - ,unused-1 - ,used-2 - ,unused-2 + ,test/used-1 + ,test/unused-1 + ,test/used-2 + ,test/unused-2 */ `, output: ` /* eslint-disable - ,used-1 - ,used-2 + ,test/used-1 + ,test/used-2 */ ` }, { code: ` /* eslint-disable - used-1, - unused-1, - used-2, - unused-2 + test/used-1, + test/unused-1, + test/used-2, + test/unused-2 -- comment */ `, output: ` /* eslint-disable - used-1, - used-2 + test/used-1, + test/used-2 -- comment */ @@ -12375,16 +12405,16 @@ var a = "test2"; // duplicates in the list { - code: "// eslint-disable-line unused, unused, used", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused, test/unused, test/used", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line unused, used, unused", - output: "// eslint-disable-line used" + code: "// eslint-disable-line test/unused, test/used, test/unused", + output: "// eslint-disable-line test/used" }, { - code: "// eslint-disable-line used, unused, unused, used", - output: "// eslint-disable-line used, used" + code: "// eslint-disable-line test/used, test/unused, test/unused, test/used", + output: "// eslint-disable-line test/used, test/used" } ]; From 90c32149ce379165cd2a5f3e0d8b02382fba5336 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 12 Nov 2021 09:12:56 -0800 Subject: [PATCH 24/37] All original tests passing --- conf/globals.js | 7 +- lib/config/rule-validator.js | 24 +- lib/linter/linter.js | 2 +- tests/lib/linter/linter.js | 562 +++++++++++++++++++---------------- 4 files changed, 337 insertions(+), 258 deletions(-) diff --git a/conf/globals.js b/conf/globals.js index ebd298d7385..423aa975d83 100644 --- a/conf/globals.js +++ b/conf/globals.js @@ -119,6 +119,10 @@ const es2021 = { WeakRef: false }; +const es2022 = { + ...es2021 +}; + //----------------------------------------------------------------------------- // Exports @@ -134,5 +138,6 @@ module.exports = { es2018, es2019, es2020, - es2021 + es2021, + es2022 }; diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js index 706b4fb03cc..eb7a3677250 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -11,6 +11,7 @@ const ajv = require("../shared/ajv")(); const { parseRuleId, getRuleFromConfig } = require("./flat-config-helpers"); +const ruleReplacements = require("../../conf/replacements.json"); //----------------------------------------------------------------------------- // Helpers @@ -34,15 +35,26 @@ function throwRuleNotFoundError({ pluginName, ruleName }, config) { // if the plugin exists then we need to check if the rule exists if (config.plugins && config.plugins[pluginName]) { + if (pluginName === "@") { - errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`; + const replacementRuleName = ruleReplacements.rules[ruleName]; - // otherwise, let's see if we can find the rule name elsewhere - for (const [otherPluginName, otherPlugin] of Object.entries(config.plugins)) { - if (otherPlugin.rules && otherPlugin.rules[ruleName]) { - errorMessage += ` Did you mean "${otherPluginName}/${ruleName}"?`; - break; + if (replacementRuleName) { + errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`; } + + } else { + + errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`; + + // otherwise, let's see if we can find the rule name elsewhere + for (const [otherPluginName, otherPlugin] of Object.entries(config.plugins)) { + if (otherPlugin.rules && otherPlugin.rules[ruleName]) { + errorMessage += ` Did you mean "${otherPluginName}/${ruleName}"?`; + break; + } + } + } // falls through to throw error diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 827ed82153c..624d7e86f64 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -995,7 +995,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO const rule = ruleMapper(ruleId); - if (rule === null) { + if (!rule) { lintingProblems.push(createLintingProblem({ ruleId })); return; } diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index ad7a4d3ba0d..9f8053ef1e5 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -2854,12 +2854,16 @@ var a = "test2"; linter = new Linter(); const code = TEST_CODE; - const results = linter.verify(code, { rules: { foobar: 2 } }); - const result = results[0]; - const warningResult = linter.verify(code, { rules: { foobar: 1 } })[0]; - const arrayOptionResults = linter.verify(code, { rules: { foobar: [2, "always"] } }); - const objectOptionResults = linter.verify(code, { rules: { foobar: [1, { bar: false }] } }); - const resultsMultiple = linter.verify(code, { rules: { foobar: 2, barfoo: 1 } }); + let results, result, warningResult, arrayOptionResults, objectOptionResults, resultsMultiple; + + beforeEach(() => { + results = linter.verify(code, { rules: { foobar: 2 } }); + result = results[0]; + warningResult = linter.verify(code, { rules: { foobar: 1 } })[0]; + arrayOptionResults = linter.verify(code, { rules: { foobar: [2, "always"] } }); + objectOptionResults = linter.verify(code, { rules: { foobar: [1, { bar: false }] } }); + resultsMultiple = linter.verify(code, { rules: { foobar: 2, barfoo: 1 } }); + }); it("should report a problem", () => { assert.isNotNull(result); @@ -2906,9 +2910,10 @@ var a = "test2"; describe("when using a rule which has been replaced", () => { const code = TEST_CODE; - const results = linter.verify(code, { rules: { "no-comma-dangle": 2 } }); it("should report the new rule", () => { + const results = linter.verify(code, { rules: { "no-comma-dangle": 2 } }); + assert.strictEqual(results[0].ruleId, "no-comma-dangle"); assert.strictEqual(results[0].message, "Rule 'no-comma-dangle' was removed and replaced by: comma-dangle"); }); @@ -12432,112 +12437,110 @@ var a = "test2"; }); - xdescribe("Default Global Variables", () => { + describe("Default Global Variables", () => { const code = "x"; it("builtin global variables should be available in the global scope", () => { - const config = { rules: { checker: "error" } }; - let spy; - - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(); - - assert.notStrictEqual(getVariable(scope, "Object"), null); - assert.notStrictEqual(getVariable(scope, "Array"), null); - assert.notStrictEqual(getVariable(scope, "undefined"), null); - }); - - return { Program: spy }; - }); - - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); - - it("ES6 global variables should not be available by default", () => { - const config = { rules: { checker: "error" } }; let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(); - - assert.strictEqual(getVariable(scope, "Promise"), null); - assert.strictEqual(getVariable(scope, "Symbol"), null); - assert.strictEqual(getVariable(scope, "WeakMap"), null); - }); + assert.notStrictEqual(getVariable(scope, "Object"), null); + assert.notStrictEqual(getVariable(scope, "Array"), null); + assert.notStrictEqual(getVariable(scope, "undefined"), null); + }); - return { Program: spy }; - }); + return { Program: spy }; + } + } + } + }, + languageOptions: { + ecmaVersion: 5, + sourceType: "script" + }, + rules: { + "test/checker": "error" + } + }; linter.verify(code, config); - assert(spy && spy.calledOnce); + assert(spy && spy.calledOnce, "Rule should have been called."); }); - it("ES6 global variables should be available in the es6 environment", () => { - const config = { rules: { checker: "error" }, env: { es6: true } }; + it("ES6 global variables should be available by default", () => { let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(); - - assert.notStrictEqual(getVariable(scope, "Promise"), null); - assert.notStrictEqual(getVariable(scope, "Symbol"), null); - assert.notStrictEqual(getVariable(scope, "WeakMap"), null); - }); + assert.notStrictEqual(getVariable(scope, "Promise"), null); + assert.notStrictEqual(getVariable(scope, "Symbol"), null); + assert.notStrictEqual(getVariable(scope, "WeakMap"), null); + }); - return { Program: spy }; - }); + return { Program: spy }; + } + } + } + }, + languageOptions: { + sourceType: "script" + }, + rules: { + "test/checker": "error" + } + }; linter.verify(code, config); assert(spy && spy.calledOnce); }); - it("ES6 global variables can be disabled when the es6 environment is enabled", () => { - const config = { rules: { checker: "error" }, globals: { Promise: "off", Symbol: "off", WeakMap: "off" }, env: { es6: true } }; - let spy; - - linter.defineRule("checker", context => { - spy = sinon.spy(() => { - const scope = context.getScope(); - - assert.strictEqual(getVariable(scope, "Promise"), null); - assert.strictEqual(getVariable(scope, "Symbol"), null); - assert.strictEqual(getVariable(scope, "WeakMap"), null); - }); - - return { Program: spy }; - }); - - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); }); - xdescribe("Suggestions", () => { + describe("Suggestions", () => { it("provides suggestion information for tools to use", () => { - linter.defineRule("rule-with-suggestions", { - meta: { hasSuggestions: true }, - create: context => ({ - Program(node) { - context.report({ - node, - message: "Incorrect spacing", - suggest: [{ - desc: "Insert space at the beginning", - fix: fixer => fixer.insertTextBefore(node, " ") - }, { - desc: "Insert space at the end", - fix: fixer => fixer.insertTextAfter(node, " ") - }] - }); + + const config = { + plugins: { + test: { + rules: { + "rule-with-suggestions": { + meta: { hasSuggestions: true }, + create: context => ({ + Program(node) { + context.report({ + node, + message: "Incorrect spacing", + suggest: [{ + desc: "Insert space at the beginning", + fix: fixer => fixer.insertTextBefore(node, " ") + }, { + desc: "Insert space at the end", + fix: fixer => fixer.insertTextAfter(node, " ") + }] + }); + } + }) + } + } } - }) - }); + }, + rules: { + "test/rule-with-suggestions": "error" + } + }; - const messages = linter.verify("var a = 1;", { rules: { "rule-with-suggestions": "error" } }); + const messages = linter.verify("var a = 1;", config); assert.deepStrictEqual(messages[0].suggestions, [{ desc: "Insert space at the beginning", @@ -12555,32 +12558,44 @@ var a = "test2"; }); it("supports messageIds for suggestions", () => { - linter.defineRule("rule-with-suggestions", { - meta: { - messages: { - suggestion1: "Insert space at the beginning", - suggestion2: "Insert space at the end" - }, - hasSuggestions: true - }, - create: context => ({ - Program(node) { - context.report({ - node, - message: "Incorrect spacing", - suggest: [{ - messageId: "suggestion1", - fix: fixer => fixer.insertTextBefore(node, " ") - }, { - messageId: "suggestion2", - fix: fixer => fixer.insertTextAfter(node, " ") - }] - }); + + const config = { + plugins: { + test: { + rules: { + "rule-with-suggestions": { + meta: { + messages: { + suggestion1: "Insert space at the beginning", + suggestion2: "Insert space at the end" + }, + hasSuggestions: true + }, + create: context => ({ + Program(node) { + context.report({ + node, + message: "Incorrect spacing", + suggest: [{ + messageId: "suggestion1", + fix: fixer => fixer.insertTextBefore(node, " ") + }, { + messageId: "suggestion2", + fix: fixer => fixer.insertTextAfter(node, " ") + }] + }); + } + }) + } + } } - }) - }); + }, + rules: { + "test/rule-with-suggestions": "error" + } + }; - const messages = linter.verify("var a = 1;", { rules: { "rule-with-suggestions": "error" } }); + const messages = linter.verify("var a = 1;", config); assert.deepStrictEqual(messages[0].suggestions, [{ messageId: "suggestion1", @@ -12600,40 +12615,64 @@ var a = "test2"; }); it("should throw an error if suggestion is passed but `meta.hasSuggestions` property is not enabled", () => { - linter.defineRule("rule-with-suggestions", { - meta: { docs: {}, schema: [] }, - create: context => ({ - Program(node) { - context.report({ - node, - message: "hello world", - suggest: [{ desc: "convert to foo", fix: fixer => fixer.insertTextBefore(node, " ") }] - }); + + const config = { + plugins: { + test: { + rules: { + "rule-with-suggestions": { + meta: { docs: {}, schema: [] }, + create: context => ({ + Program(node) { + context.report({ + node, + message: "hello world", + suggest: [{ desc: "convert to foo", fix: fixer => fixer.insertTextBefore(node, " ") }] + }); + } + }) + } + } } - }) - }); + }, + rules: { + "test/rule-with-suggestions": "error" + } + }; assert.throws(() => { - linter.verify("0", { rules: { "rule-with-suggestions": "error" } }); + linter.verify("0", config); }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`."); }); it("should throw an error if suggestion is passed but `meta.hasSuggestions` property is not enabled and the rule has the obsolete `meta.docs.suggestion` property", () => { - linter.defineRule("rule-with-meta-docs-suggestion", { - meta: { docs: { suggestion: true }, schema: [] }, - create: context => ({ - Program(node) { - context.report({ - node, - message: "hello world", - suggest: [{ desc: "convert to foo", fix: fixer => fixer.insertTextBefore(node, " ") }] - }); + + const config = { + plugins: { + test: { + rules: { + "rule-with-meta-docs-suggestion": { + meta: { docs: { suggestion: true }, schema: [] }, + create: context => ({ + Program(node) { + context.report({ + node, + message: "hello world", + suggest: [{ desc: "convert to foo", fix: fixer => fixer.insertTextBefore(node, " ") }] + }); + } + }) + } + } } - }) - }); + }, + rules: { + "test/rule-with-meta-docs-suggestion": "error" + } + }; assert.throws(() => { - linter.verify("0", { rules: { "rule-with-meta-docs-suggestion": "error" } }); + linter.verify("0", config); }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint."); }); }); @@ -12671,67 +12710,15 @@ var a = "test2"; }); }); - describe("when using an invalid (undefined) rule", () => { - linter = new Linter(); - - const code = TEST_CODE; - const results = linter.verify(code, { rules: { foobar: 2 } }); - const result = results[0]; - const warningResult = linter.verify(code, { rules: { foobar: 1 } })[0]; - const arrayOptionResults = linter.verify(code, { rules: { foobar: [2, "always"] } }); - const objectOptionResults = linter.verify(code, { rules: { foobar: [1, { bar: false }] } }); - const resultsMultiple = linter.verify(code, { rules: { foobar: 2, barfoo: 1 } }); - - it("should report a problem", () => { - assert.isNotNull(result); - assert.isArray(results); - assert.isObject(result); - assert.property(result, "ruleId"); - assert.strictEqual(result.ruleId, "foobar"); - }); - - it("should report that the rule does not exist", () => { - assert.property(result, "message"); - assert.strictEqual(result.message, "Definition for rule 'foobar' was not found."); - }); - - it("should report at the correct severity", () => { - assert.property(result, "severity"); - assert.strictEqual(result.severity, 2); - assert.strictEqual(warningResult.severity, 2); // this is 2, since the rulename is very likely to be wrong - }); - - it("should accept any valid rule configuration", () => { - assert.isObject(arrayOptionResults[0]); - assert.isObject(objectOptionResults[0]); - }); - - it("should report multiple missing rules", () => { - assert.isArray(resultsMultiple); - - assert.deepStrictEqual( - resultsMultiple[1], - { - ruleId: "barfoo", - message: "Definition for rule 'barfoo' was not found.", - line: 1, - column: 1, - endLine: 1, - endColumn: 2, - severity: 2, - nodeType: null - } - ); - }); - }); - describe("when using a rule which has been replaced", () => { const code = TEST_CODE; - const results = linter.verify(code, { rules: { "no-comma-dangle": 2 } }); it("should report the new rule", () => { - assert.strictEqual(results[0].ruleId, "no-comma-dangle"); - assert.strictEqual(results[0].message, "Rule 'no-comma-dangle' was removed and replaced by: comma-dangle"); + + assert.throws(() => { + linter.verify(code, { rules: { "no-comma-dangle": 2 } }); + }, /Key "rules": Key "no-comma-dangle": Rule "no-comma-dangle" was removed and replaced by "comma-dangle"/u); + }); }); @@ -12804,7 +12791,7 @@ var a = "test2"; }); }); - xdescribe("verifyAndFix()", () => { + describe("verifyAndFix()", () => { it("Fixes the code", () => { const messages = linter.verifyAndFix("var a", { rules: { @@ -12855,24 +12842,35 @@ var a = "test2"; it("stops fixing after 10 passes", () => { - linter.defineRule("add-spaces", { - meta: { - fixable: "whitespace" - }, - create(context) { - return { - Program(node) { - context.report({ - node, - message: "Add a space before this node.", - fix: fixer => fixer.insertTextBefore(node, " ") - }); + const config = { + plugins: { + test: { + rules: { + "add-spaces": { + meta: { + fixable: "whitespace" + }, + create(context) { + return { + Program(node) { + context.report({ + node, + message: "Add a space before this node.", + fix: fixer => fixer.insertTextBefore(node, " ") + }); + } + }; + } + } } - }; + } + }, + rules: { + "test/add-spaces": "error" } - }); + }; - const fixResult = linter.verifyAndFix("a", { rules: { "add-spaces": "error" } }); + const fixResult = linter.verifyAndFix("a", config); assert.strictEqual(fixResult.fixed, true); assert.strictEqual(fixResult.output, `${" ".repeat(10)}a`); @@ -12880,46 +12878,84 @@ var a = "test2"; }); it("should throw an error if fix is passed but meta has no `fixable` property", () => { - linter.defineRule("test-rule", { - meta: { - docs: {}, - schema: [] - }, - create: context => ({ - Program(node) { - context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + + const config = { + plugins: { + test: { + rules: { + "test-rule": { + meta: { + docs: {}, + schema: [] + }, + create: context => ({ + Program(node) { + context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + } + }) + } + } } - }) - }); + }, + rules: { + "test/test-rule": "error" + } + }; + assert.throws(() => { - linter.verify("0", { rules: { "test-rule": "error" } }); - }, /Fixable rules must set the `meta\.fixable` property to "code" or "whitespace".\nOccurred while linting :1\nRule: "test-rule"$/u); + linter.verify("0", config); + }, /Fixable rules must set the `meta\.fixable` property to "code" or "whitespace".\nOccurred while linting :1\nRule: "test\/test-rule"$/u); }); it("should throw an error if fix is passed and there is no metadata", () => { - linter.defineRule("test-rule", { - create: context => ({ - Program(node) { - context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + + const config = { + plugins: { + test: { + rules: { + "test-rule": { + create: context => ({ + Program(node) { + context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + } + }) + } + } } - }) - }); + }, + rules: { + "test/test-rule": "error" + } + }; assert.throws(() => { - linter.verify("0", { rules: { "test-rule": "error" } }); + linter.verify("0", config); }, /Fixable rules must set the `meta\.fixable` property/u); }); it("should throw an error if fix is passed from a legacy-format rule", () => { - linter.defineRule("test-rule", context => ({ - Program(node) { - context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + + const config = { + plugins: { + test: { + rules: { + "test-rule": context => ({ + Program(node) { + context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + } + }) + } + } + }, + rules: { + "test/test-rule": "error" } - })); + }; + assert.throws(() => { - linter.verify("0", { rules: { "test-rule": "error" } }); + linter.verify("0", config); }, /Fixable rules must set the `meta\.fixable` property/u); }); }); @@ -13302,28 +13338,54 @@ var a = "test2"; assert.strictEqual(messages[0].fatal, true); }); - xit("should not crash when we reuse the SourceCode object", () => { - linter.verify("function render() { return
{hello}
}", { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }); - linter.verify(linter.getSourceCode(), { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }); + it("should not crash when we reuse the SourceCode object", () => { + const config = { + languageOptions: { + ecmaVersion: 6, + parserOptions: { + ecmaFeatures: { jsx: true } + } + } + }; + + linter.verify("function render() { return
{hello}
}", config); + linter.verify(linter.getSourceCode(), config); }); - xit("should reuse the SourceCode object", () => { + it("should reuse the SourceCode object", () => { let ast1 = null, ast2 = null; - linter.defineRule("save-ast1", () => ({ - Program(node) { - ast1 = node; - } - })); - linter.defineRule("save-ast2", () => ({ - Program(node) { - ast2 = node; + const config = { + plugins: { + test: { + rules: { + "save-ast1": () => ({ + Program(node) { + ast1 = node; + } + }), + + "save-ast2": () => ({ + Program(node) { + ast2 = node; + } + }) + + } + } + }, + languageOptions: { + ecmaVersion: 6, + parserOptions: { + ecmaFeatures: { jsx: true } + } } - })); + }; - linter.verify("function render() { return
{hello}
}", { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, rules: { "save-ast1": 2 } }); - linter.verify(linter.getSourceCode(), { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, rules: { "save-ast2": 2 } }); + + linter.verify("function render() { return
{hello}
}", { ...config, rules: { "test/save-ast1": "error" } }); + linter.verify(linter.getSourceCode(), { ...config, rules: { "test/save-ast2": "error" } }); assert(ast1 !== null); assert(ast2 !== null); From 7130a274a089cfdddb1f8a43664db7133de77c42 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 12 Nov 2021 09:33:03 -0800 Subject: [PATCH 25/37] Add test to verify lazy loading of rules (fixes #14862) --- lib/linter/linter.js | 32 ++++++++++++++-------------- tests/lib/linter/linter.js | 43 +++++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 624d7e86f64..47e2f6ac4e3 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -738,17 +738,21 @@ function analyzeScope(ast, languageOptions, visitorKeys) { */ function parse(text, languageOptions, filePath) { const textToParse = stripUnicodeBOM(text).replace(astUtils.shebangPattern, (match, captured) => `//${captured}`); - const parser = languageOptions.parser; - const parserOptions = Object.assign({}, languageOptions.parserOptions, { - loc: true, - range: true, - raw: true, - tokens: true, - comment: true, - eslintVisitorKeys: true, - eslintScopeManager: true, - filePath - }); + 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 @@ -1531,10 +1535,6 @@ class Linter { if (isEspree(languageOptions.parser)) { const parserOptions = languageOptions.parserOptions; - if (languageOptions.ecmaVersion) { - parserOptions.ecmaVersion = languageOptions.ecmaVersion; - } - if (languageOptions.sourceType) { // normalize for CommonJS @@ -1693,7 +1693,7 @@ class Linter { debug("With flat config: %s", options.filename); // we need a filename to match configs against - const filename = options.filename || "code.js"; + const filename = options.filename || ""; // Store the config array in order to get plugin envs and rules later. internalSlotsMap.get(this).lastConfigArray = configArray; diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 9f8053ef1e5..82ad0bbde65 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -2792,7 +2792,7 @@ var a = "test2"; const comments = context.getAllComments(); assert.strictEqual(comments.length, 1); - assert.strictEqual(comments[0].type, "hashbang"); + assert.strictEqual(comments[0].type, "Shebang"); return {}; }); @@ -6294,7 +6294,7 @@ var a = "test2"; }); }); -describe.only("Linter with FlatConfigArray", () => { +describe("Linter with FlatConfigArray", () => { let linter; const filename = "filename.js"; @@ -7058,14 +7058,22 @@ describe.only("Linter with FlatConfigArray", () => { }); }); - it("should not pass any default parserOptions to the parser", () => { - const messages = linter.verify(";", { + it("should pass default languageOptions to the parser", () => { + + const spy = sinon.spy((code, options) => espree.parse(code, options)); + + linter.verify(";", { languageOptions: { - parser: testParsers.throwsWithOptions + parser: { + parse: spy + } } - }, "filename"); + }, "filename.js"); - assert.strictEqual(messages.length, 0); + assert(spy.calledWithMatch(";", { + ecmaVersion: espree.latestEcmaVersion + 2009, + sourceType: "module" + })); }); }); @@ -7349,6 +7357,27 @@ describe.only("Linter with FlatConfigArray", () => { assert.strictEqual(messages[2].column, 18); }); + describe("Plugins", () => { + + it("should not load rule definition when rule isn't used", () => { + + const spy = sinon.spy(); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + } + }; + + linter.verify("code", config, filename); + assert.sTrue(spy.notCalled, "Rule should not have been called"); + }); + }); + describe("Rule Internals", () => { const code = TEST_CODE; From 165e59baa01d4adef4925e8a6882c84fafb0219b Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 12 Nov 2021 09:47:32 -0800 Subject: [PATCH 26/37] Fix failing tests --- lib/config/rule-validator.js | 8 +++----- tests/lib/linter/linter.js | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js index eb7a3677250..9172f935688 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -35,13 +35,11 @@ function throwRuleNotFoundError({ pluginName, ruleName }, config) { // if the plugin exists then we need to check if the rule exists if (config.plugins && config.plugins[pluginName]) { - if (pluginName === "@") { + const replacementRuleName = ruleReplacements.rules[ruleName]; - const replacementRuleName = ruleReplacements.rules[ruleName]; + if (pluginName === "@" && replacementRuleName) { - if (replacementRuleName) { - errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`; - } + errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`; } else { diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 82ad0bbde65..43a96c87ff1 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -7374,7 +7374,7 @@ describe("Linter with FlatConfigArray", () => { }; linter.verify("code", config, filename); - assert.sTrue(spy.notCalled, "Rule should not have been called"); + assert.isTrue(spy.notCalled, "Rule should not have been called"); }); }); From 2536a6b0ebe9370c6d9cfbd8ddfb1b4601bcbf93 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 12 Nov 2021 10:01:19 -0800 Subject: [PATCH 27/37] Fix failing browser test --- tests/lib/linter/linter.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 43a96c87ff1..a1d521c7bd7 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6765,22 +6765,34 @@ describe("Linter with FlatConfigArray", () => { it("should pass parser as context.languageOptions.parser to all rules when default parser is used", () => { + // references to Espree get messed up in a browser context, so wrap it + const fakeParser = { + parse: espree.parse + }; + + const spy = sinon.spy(context => { + assert.strictEqual(context.languageOptions.parser, fakeParser); + return {}; + }); + const config = { plugins: { test: { rules: { - "test-rule": sinon.mock().withArgs( - sinon.match({ languageOptions: { parser: espree } }) - ).returns({}) + "test-rule": spy } } }, + languageOptions: { + parser: fakeParser + }, rules: { "test/test-rule": 2 } }; linter.verify("0", config, filename); + assert.isTrue(spy.calledOnce); }); From e0e1618f69453f2a74dcf0e2e7f59c496d8fd34f Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 23 Nov 2021 13:31:48 -0800 Subject: [PATCH 28/37] Fix ecmaVersion edge cases --- conf/globals.js | 7 ++++--- lib/linter/linter.js | 19 +++++++++++++------ lib/shared/types.js | 1 + tests/lib/linter/linter.js | 28 ++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/conf/globals.js b/conf/globals.js index 423aa975d83..076ffb2af94 100644 --- a/conf/globals.js +++ b/conf/globals.js @@ -10,9 +10,10 @@ //----------------------------------------------------------------------------- const commonjs = { - require: false, - exports: false, - module: false + exports: true, + global: false, + module: false, + require: false }; const es3 = { diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 47e2f6ac4e3..8e1182577c1 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -504,21 +504,29 @@ function normalizeEcmaVersion(parser, ecmaVersion) { * @returns {number} normalized ECMAScript version */ function normalizeEcmaVersionForLanguageOptions(ecmaVersion) { - if (ecmaVersion === "latest") { - return espree.latestEcmaVersion + 2009; - } switch (ecmaVersion) { case 3: return 3; + // void 0 = now ecmaVersion specified so use the default case 5: + case void 0: return 5; default: - return ecmaVersion >= 2015 ? ecmaVersion : ecmaVersion + 2009; + 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 espree.latestEcmaVersion + 2009; } const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)(?:\*\/|$)/gsu; @@ -1290,7 +1298,6 @@ class Linter { parserOptions }); - if (!slots.lastSourceCode) { const parseResult = parse( text, @@ -1411,7 +1418,7 @@ class Linter { let configArray = config; if (!Array.isArray(config) || typeof config.getConfig !== "function") { - configArray = new FlatConfigArray(config || {}); + configArray = new FlatConfigArray(config); configArray.normalizeSync(); } diff --git a/lib/shared/types.js b/lib/shared/types.js index 2fcb2d7aa3c..94c173edb1b 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -28,6 +28,7 @@ module.exports = {}; /** * @typedef {Object} LanguageOptions * @property {number|"latest"} [ecmaVersion] The ECMAScript version (or revision number). + * @property {Record} [globals] The global variable settings. * @property {"script"|"module"} [sourceType] The source code type. * @property {string|Object} [parser] The parser to use. * @property {Object} [parserOptions] The parser options to use. diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index a1d521c7bd7..f94f1ecd764 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -4180,6 +4180,34 @@ var a = "test2"; assert.strictEqual(ecmaVersion, espree.latestEcmaVersion + 2009, "ecmaVersion should be 2022"); }); + it("the 'next' is equal to espree.latestEcmaVersion on languageOptions with custom parser", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 }, parser: "custom-parser", parserOptions: { ecmaVersion: "next" } }; + + linter.defineParser("custom-parser", testParsers.stubParser); + linter.defineRule("ecma-version", context => ({ + Program() { + ecmaVersion = context.languageOptions.ecmaVersion; + } + })); + linter.verify("", config); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion + 2009, "ecmaVersion should be 2022"); + }); + + it("missing ecmaVersion is equal to 5 on languageOptions with custom parser", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 }, parser: "custom-parser" }; + + linter.defineParser("custom-parser", testParsers.enhancedParser); + linter.defineRule("ecma-version", context => ({ + Program() { + ecmaVersion = context.languageOptions.ecmaVersion; + } + })); + linter.verify("", config); + assert.strictEqual(ecmaVersion, 5, "ecmaVersion should be 2022"); + }); + it("should pass normalized ecmaVersion to eslint-scope", () => { let blockScope = null; From 240f0ad6afc1d2e6b40daea8cc8d5add9aec9ee3 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 23 Nov 2021 13:44:40 -0800 Subject: [PATCH 29/37] Switch globalReturn to false if sourceType is module --- lib/linter/linter.js | 8 ++++++++ tests/lib/linter/linter.js | 39 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 8e1182577c1..c7943dd81cc 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -1554,6 +1554,14 @@ class Linter { } else { parserOptions.sourceType = languageOptions.sourceType; } + + if ( + parserOptions.sourceType === "module" && + parserOptions.ecmaFeatures && + parserOptions.ecmaFeatures.globalReturn + ) { + parserOptions.ecmaFeatures.globalReturn = false; + } } } diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index f94f1ecd764..f083950b6ca 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -7126,8 +7126,7 @@ describe("Linter with FlatConfigArray", () => { const parserOptions = { ecmaFeatures: { - jsx: true, - globalReturn: true + jsx: true } }; @@ -7152,6 +7151,42 @@ describe("Linter with FlatConfigArray", () => { linter.verify("0", config, filename); }); + it("should switch globalReturn to false if sourceType is module", () => { + + const config = { + plugins: { + test: { + rules: { + "test-rule": sinon.mock().withArgs( + sinon.match({ + languageOptions: { + parserOptions: { + ecmaFeatures: { + globalReturn: false + } + } + } + }) + ).returns({}) + } + } + }, + languageOptions: { + sourceType: "module", + parserOptions: { + ecmaFeatures: { + globalReturn: true + } + } + }, + rules: { + "test/test-rule": 2 + } + }; + + linter.verify("0", config, filename); + }); + it("should not parse sloppy-mode code when impliedStrict is true", () => { const messages = linter.verify("var private;", { From a351eecbce22f1817691e743b33c9ad06b6a3940 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 23 Nov 2021 18:07:38 -0800 Subject: [PATCH 30/37] Update Espree and eslint-scope to support sourceType:commonjs --- lib/linter/linter.js | 18 ++++++------------ package.json | 4 ++-- tests/lib/linter/linter.js | 8 ++++---- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index c7943dd81cc..0df18863ce2 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -850,8 +850,11 @@ function getScope(scopeManager, currentNode) { */ function markVariableAsUsed(scopeManager, currentNode, languageOptions, name) { const parserOptions = languageOptions.parserOptions; - const hasGlobalReturn = parserOptions.ecmaFeatures && parserOptions.ecmaFeatures.globalReturn; - const specialScope = hasGlobalReturn || languageOptions.sourceType === "module"; + const sourceType = languageOptions.sourceType; + const hasGlobalReturn = + (parserOptions.ecmaFeatures && parserOptions.ecmaFeatures.globalReturn) || + sourceType === "commonjs"; + const specialScope = hasGlobalReturn || sourceType === "module"; const currentScope = getScope(scopeManager, currentNode); // Special Node.js scope means we need to start one level deeper @@ -1544,16 +1547,7 @@ class Linter { if (languageOptions.sourceType) { - // normalize for CommonJS - if (languageOptions.sourceType === "commonjs") { - parserOptions.sourceType = "script"; - parserOptions.ecmaFeatures = { - globalReturn: true, - ...parserOptions.ecmaFeatures - }; - } else { - parserOptions.sourceType = languageOptions.sourceType; - } + parserOptions.sourceType = languageOptions.sourceType; if ( parserOptions.sourceType === "module" && diff --git a/package.json b/package.json index d955a65a763..5132217befb 100644 --- a/package.json +++ b/package.json @@ -56,10 +56,10 @@ "doctrine": "^3.0.0", "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^6.0.0", + "eslint-scope": "^7.1.0", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.1.0", - "espree": "^9.0.0", + "espree": "^9.1.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index f083950b6ca..590bd045079 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -9287,10 +9287,10 @@ describe("Linter with FlatConfigArray", () => { const globalScope = context.getScope(), childScope = globalScope.childScopes[0]; - assert.isTrue(context.markVariableAsUsed("a")); + assert.isTrue(context.markVariableAsUsed("a"), "Call to markVariableAsUsed should return true"); - assert.isTrue(getVariable(childScope, "a").eslintUsed); - assert.isUndefined(getVariable(childScope, "b").eslintUsed); + assert.isTrue(getVariable(childScope, "a").eslintUsed, "'a' should be marked as used."); + assert.isUndefined(getVariable(childScope, "b").eslintUsed, "'b' should be marked as used."); }); return { "Program:exit": spy }; @@ -9305,7 +9305,7 @@ describe("Linter with FlatConfigArray", () => { }; linter.verify(code, config); - assert(spy && spy.calledOnce); + assert(spy && spy.calledOnce, "Spy wasn't called."); }); it("should mark variables in modules as used", () => { From 4cd44a463df0ae38b1f8fd74788aaa90706ff614 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 26 Nov 2021 10:09:52 -0800 Subject: [PATCH 31/37] Update lib/linter/linter.js Co-authored-by: Milos Djermanovic --- lib/linter/linter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 0df18863ce2..785ec40eddc 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -509,7 +509,7 @@ function normalizeEcmaVersionForLanguageOptions(ecmaVersion) { case 3: return 3; - // void 0 = now ecmaVersion specified so use the default + // void 0 = no ecmaVersion specified so use the default case 5: case void 0: return 5; From 8cffdc84e0bab04b95deb4e47e0f454cbe4188d2 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 26 Nov 2021 10:10:11 -0800 Subject: [PATCH 32/37] Update lib/linter/linter.js Co-authored-by: Milos Djermanovic --- lib/linter/linter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 785ec40eddc..f51df327511 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -500,7 +500,7 @@ function normalizeEcmaVersion(parser, ecmaVersion) { /** * Normalize ECMAScript version from the initial config into languageOptions (year) * format. - * @param {number} ecmaVersion ECMAScript version from the initial config + * @param {any} [ecmaVersion] ECMAScript version from the initial config * @returns {number} normalized ECMAScript version */ function normalizeEcmaVersionForLanguageOptions(ecmaVersion) { From 2c2e5d7787184f2782fbdbef5616871e332273d9 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 29 Nov 2021 13:08:06 -0800 Subject: [PATCH 33/37] Fix processors functionality --- lib/linter/linter.js | 9 +++++---- tests/lib/linter/linter.js | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index f51df327511..813639b340e 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -1425,7 +1425,7 @@ class Linter { configArray.normalizeSync(); } - return this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options); + return this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options, true); } if (typeof config.extractConfig === "function") { @@ -1696,9 +1696,11 @@ class Linter { * @param {string|SourceCode} textOrSourceCode The source code. * @param {FlatConfigArray} configArray The config array. * @param {VerifyOptions&ProcessorOptions} options The options. + * @param {boolean} [firstCall=false] Indicates if this is being called directly + * from verify(). (TODO: Remove once eslintrc is removed.) * @returns {LintMessage[]} The found problems. */ - _verifyWithFlatConfigArray(textOrSourceCode, configArray, options) { + _verifyWithFlatConfigArray(textOrSourceCode, configArray, options, firstCall = false) { debug("With flat config: %s", options.filename); // we need a filename to match configs against @@ -1722,9 +1724,8 @@ class Linter { ); } - // check for options-based processing - if (options.preprocess || options.postprocess) { + if (firstCall && (options.preprocess || options.postprocess)) { return this._verifyWithFlatConfigArrayAndProcessor(textOrSourceCode, config, options); } diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 590bd045079..dd2d06de38e 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -13133,6 +13133,29 @@ var a = "test2"; assert.deepStrictEqual(preprocess.args[0], [code, filename], "preprocess was called with the wrong arguments"); }); + it("should run preprocess only once", () => { + const logs = []; + const config = { + files: ["*.md"], + processor: { + preprocess(text, filenameForText) { + logs.push({ + text, + filename: filenameForText + }); + + return [{ text: "bar", filename: "0.js" }]; + }, + postprocess() { + return []; + } + } + }; + + linter.verify("foo", config, "a.md"); + assert.strictEqual(logs.length, 1, "preprocess() should only be called once."); + }); + it("should apply a preprocessor to the code, and lint each code sample separately", () => { const code = "foo bar baz"; const configs = createFlatConfigArray([ From d40a473d7929459e4eb5295db4bbedb568200ff5 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 3 Dec 2021 17:13:22 -0800 Subject: [PATCH 34/37] Update lib/shared/types.js Co-authored-by: Brandon Mills --- lib/shared/types.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/shared/types.js b/lib/shared/types.js index 94c173edb1b..04fc2886735 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -29,7 +29,7 @@ module.exports = {}; * @typedef {Object} LanguageOptions * @property {number|"latest"} [ecmaVersion] The ECMAScript version (or revision number). * @property {Record} [globals] The global variable settings. - * @property {"script"|"module"} [sourceType] The source code type. + * @property {"script"|"module"|"commonjs"} [sourceType] The source code type. * @property {string|Object} [parser] The parser to use. * @property {Object} [parserOptions] The parser options to use. */ From 78237f2e8cc4fe9a890546b8b5a8405517ab58df Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 3 Dec 2021 17:15:09 -0800 Subject: [PATCH 35/37] Update tests/lib/linter/linter.js Co-authored-by: Brandon Mills --- tests/lib/linter/linter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index dd2d06de38e..090588763e3 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -4205,7 +4205,7 @@ var a = "test2"; } })); linter.verify("", config); - assert.strictEqual(ecmaVersion, 5, "ecmaVersion should be 2022"); + assert.strictEqual(ecmaVersion, 5, "ecmaVersion should be 5"); }); it("should pass normalized ecmaVersion to eslint-scope", () => { From b042303354a88542409bb0be982eb82d3f753dc7 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 3 Dec 2021 17:15:30 -0800 Subject: [PATCH 36/37] Update tests/lib/linter/linter.js Co-authored-by: Brandon Mills --- tests/lib/linter/linter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 090588763e3..96d129262d3 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6432,7 +6432,7 @@ describe("Linter with FlatConfigArray", () => { linter.verify("foo", config, filename); }); - it("ecmaVersion should be normalized to to latest year by default", () => { + it("ecmaVersion should be normalized to latest year by default", () => { const config = { plugins: { test: { From 9ad40d05be619321251046244f1db565747624f3 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 3 Dec 2021 17:15:46 -0800 Subject: [PATCH 37/37] Update lib/linter/linter.js Co-authored-by: Brandon Mills --- lib/linter/linter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 813639b340e..f897b8ddb8c 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -945,7 +945,7 @@ const BASE_TRAVERSAL_CONTEXT = Object.freeze( * @param {SourceCode} sourceCode A SourceCode object for the given text * @param {Object} configuredRules The rules configuration * @param {function(string): Rule} ruleMapper A mapper function from rule names to rules - * @param {string} parserName The name of the parser in the config + * @param {string | undefined} parserName The name of the parser in the config * @param {LanguageOptions} languageOptions The options for parsing the code. * @param {Object} settings The settings that were enabled in the config * @param {string} filename The reported filename of the code