From 32ac37a76b2e009a8f106229bc7732671d358189 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 3 Dec 2021 17:31:11 -0800 Subject: [PATCH] feat: Flat config support in Linter (refs #13481) (#15185) * Update: Flat config support in Linter (refs #13481) * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic * Clean up FlatConfigArray detection * Add back FlatConfigArray import * More flat config tests passing; originals failing * Finish languageOptions features * Make sure to recognize RuleTester-wrapped Espree * Move globals into local file * Normalize ecmaVersion to year on context.languageOptions * Clean up ecmaVersion normalization * Revert parserOptions.ecmaVersion behavior to original * Add more tests * Update defaults for flat config (refs #14588) * More tests passing * Fix wrong ecmaVersion conversion * Duplicated all tests for FlatConfig * Finish languageOptions tests * Settings tests working * Rule context tests working * Options tests working * Directives tests working * reportUnusedDisableDirectives tests working * All original tests passing * Add test to verify lazy loading of rules (fixes #14862) * Fix failing tests * Fix failing browser test * Fix ecmaVersion edge cases * Switch globalReturn to false if sourceType is module * Update Espree and eslint-scope to support sourceType:commonjs * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic * Fix processors functionality * Update lib/shared/types.js Co-authored-by: Brandon Mills * Update tests/lib/linter/linter.js Co-authored-by: Brandon Mills * Update tests/lib/linter/linter.js Co-authored-by: Brandon Mills * Update lib/linter/linter.js Co-authored-by: Brandon Mills Co-authored-by: Milos Djermanovic Co-authored-by: Brandon Mills --- conf/globals.js | 144 + karma.conf.js | 9 + lib/config/default-config.js | 13 +- 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 | 55 +- lib/linter/linter.js | 500 +- lib/shared/types.js | 9 + package.json | 2 +- tests/fixtures/parsers/throws-with-options.js | 2 +- tests/lib/config/flat-config-array.js | 27 +- tests/lib/linter/linter.js | 7370 ++++++++++++++++- tests/lib/rule-tester/rule-tester.js | 5 +- 14 files changed, 8125 insertions(+), 103 deletions(-) create mode 100644 conf/globals.js create mode 100644 lib/config/flat-config-helpers.js diff --git a/conf/globals.js b/conf/globals.js new file mode 100644 index 00000000000..076ffb2af94 --- /dev/null +++ b/conf/globals.js @@ -0,0 +1,144 @@ +/** + * @fileoverview Globals for ecmaVersion/sourceType + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Globals +//----------------------------------------------------------------------------- + +const commonjs = { + exports: true, + global: false, + module: false, + require: 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 +}; + +const es2022 = { + ...es2021 +}; + + +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- + +module.exports = { + commonjs, + es3, + es5, + es2015, + es2016, + es2017, + es2018, + es2019, + es2020, + es2021, + es2022 +}; diff --git a/karma.conf.js b/karma.conf.js index 2c4c1d11ded..606d13f88f6 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..a655a6d83ca 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,16 @@ exports.defaultConfig = [ ".git/**" ], languageOptions: { - parser: "@/espree" + ecmaVersion: "latest", + sourceType: "module", + parser: "@/espree", + parserOptions: {} + } + }, + { + files: ["**/*.cjs"], + languageOptions: { + sourceType: "commonjs" } } ]; 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..9172f935688 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -10,52 +10,49 @@ //----------------------------------------------------------------------------- const ajv = require("../shared/ajv")(); +const { parseRuleId, getRuleFromConfig } = require("./flat-config-helpers"); +const ruleReplacements = require("../../conf/replacements.json"); //----------------------------------------------------------------------------- // 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}".`; // if the plugin exists then we need to check if the rule exists if (config.plugins && config.plugins[pluginName]) { + const replacementRuleName = ruleReplacements.rules[ruleName]; - const plugin = config.plugins[pluginName]; + if (pluginName === "@" && replacementRuleName) { - // first check for exact rule match - if (plugin.rules && plugin.rules[ruleName]) { - return config.plugins[pluginName].rules[ruleName]; - } + errorMessage = `${errorMessageHeader}: Rule "${ruleName}" was removed and replaced by "${replacementRuleName}".`; + + } else { - errorMessage = `${errorMessageHeader}: Could not find "${ruleName}" in plugin "${pluginName}".`; + 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; + // 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 @@ -154,7 +151,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..f897b8ddb8c 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 { getRuleFromConfig } = require("../config/flat-config-helpers"); +const { FlatConfigArray } = require("../config/flat-config-array"); const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; @@ -45,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("../../conf/globals"); //------------------------------------------------------------------------------ // Typedefs @@ -57,6 +60,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 */ @@ -126,6 +130,38 @@ const parserSymbol = Symbol.for("eslint.RuleTester.parser"); // 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. + * @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 @@ -268,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 })); @@ -400,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; } @@ -447,7 +483,8 @@ 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; } @@ -460,6 +497,38 @@ function normalizeEcmaVersion(parser, ecmaVersion) { return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion; } +/** + * Normalize ECMAScript version from the initial config into languageOptions (year) + * format. + * @param {any} [ecmaVersion] ECMAScript version from the initial config + * @returns {number} normalized ECMAScript version + */ +function normalizeEcmaVersionForLanguageOptions(ecmaVersion) { + + switch (ecmaVersion) { + case 3: + return 3; + + // void 0 = no ecmaVersion specified so use the default + case 5: + case void 0: + return 5; + + default: + if (typeof ecmaVersion === "number") { + return ecmaVersion >= 2015 ? ecmaVersion : ecmaVersion + 2009; + } + } + + /* + * We default to the latest supported ecmaVersion for everything else. + * Remember, this is for languageOptions.ecmaVersion, which sets the version + * that is used for a number of processes inside of ESLint. It's normally + * safe to assume people want the latest unless otherwise specified. + */ + return espree.latestEcmaVersion + 2009; +} + const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)(?:\*\/|$)/gsu; /** @@ -511,7 +580,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 +596,9 @@ function normalizeVerifyOptions(providedOptions, config) { reportUnusedDisableDirectives = reportUnusedDisableDirectives ? "error" : "off"; } if (typeof reportUnusedDisableDirectives !== "string") { - reportUnusedDisableDirectives = config.reportUnusedDisableDirectives ? "warn" : "off"; + reportUnusedDisableDirectives = + linterOptions.reportUnusedDisableDirectives + ? "warn" : "off"; } return { @@ -566,6 +641,30 @@ 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: configuredGlobals, parser, parserOptions }) { + + const { + ecmaVersion, + sourceType + } = parserOptions; + + return { + globals: configuredGlobals, + ecmaVersion: normalizeEcmaVersionForLanguageOptions(ecmaVersion), + sourceType, + parser, + parserOptions + }; +} + /** * Combines the provided globals object with the globals from environments * @param {Record} providedGlobals The 'globals' key in a config @@ -614,20 +713,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 }); @@ -638,25 +738,29 @@ 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, { - 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 @@ -671,7 +775,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, @@ -740,13 +844,17 @@ 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) { - const hasGlobalReturn = parserOptions.ecmaFeatures && parserOptions.ecmaFeatures.globalReturn; - const specialScope = hasGlobalReturn || parserOptions.sourceType === "module"; +function markVariableAsUsed(scopeManager, currentNode, languageOptions, name) { + const parserOptions = languageOptions.parserOptions; + 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 @@ -837,8 +945,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 {Object} parserOptions The options that were passed to the parser - * @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 * @param {boolean} disableFixes If true, it doesn't make `fix` properties. @@ -846,7 +954,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; @@ -878,16 +986,18 @@ 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 } ) ); - const lintingProblems = []; Object.keys(configuredRules).forEach(ruleId => { @@ -900,7 +1010,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser const rule = ruleMapper(ruleId); - if (rule === null) { + if (!rule) { lintingProblems.push(createLintingProblem({ ruleId })); return; } @@ -1074,13 +1184,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 +1213,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() }); @@ -1168,12 +1295,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 ); @@ -1194,7 +1325,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) }); } } @@ -1220,8 +1351,8 @@ class Linter { sourceCode, configuredRules, ruleId => getRule(slots, ruleId), - parserOptions, parserName, + languageOptions, settings, options.filename, options.disableFixes, @@ -1270,15 +1401,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` if it has a `getConfig()` method. + */ + let configArray = config; + + if (!Array.isArray(config) || typeof config.getConfig !== "function") { + configArray = new FlatConfigArray(config); + configArray.normalizeSync(); + } + + return this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options, true); + } + + 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 +1448,214 @@ 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; + + languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions( + languageOptions.ecmaVersion + ); + + // add configured globals and language globals + const configuredGlobals = { + ...(getGlobalsForEcmaVersion(languageOptions.ecmaVersion)), + ...(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.sourceType) { + + parserOptions.sourceType = languageOptions.sourceType; + + if ( + parserOptions.sourceType === "module" && + parserOptions.ecmaFeatures && + parserOptions.ecmaFeatures.globalReturn + ) { + parserOptions.ecmaFeatures.globalReturn = false; + } + } + } + + const settings = config.settings || {}; + + if (!slots.lastSourceCode) { + const parseResult = parse( + text, + languageOptions, + 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) + }); + } + } + + const sourceCode = slots.lastSourceCode; + const commentDirectives = options.allowInlineConfig + ? getDirectiveComments( + options.filename, + sourceCode.ast, + ruleId => 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), + void 0, + languageOptions, + 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 +1691,47 @@ 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. + * @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, firstCall = false) { + debug("With flat config: %s", options.filename); + + // we need a filename to match configs against + const filename = options.filename || ""; + + // 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 (firstCall && (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 +1805,7 @@ class Linter { * @returns {void} */ defineRule(ruleId, ruleModule) { + assertEslintrcConfig(this); internalSlotsMap.get(this).ruleMap.define(ruleId, ruleModule); } @@ -1406,6 +1815,7 @@ class Linter { * @returns {void} */ defineRules(rulesToDefine) { + assertEslintrcConfig(this); Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => { this.defineRule(ruleId, rulesToDefine[ruleId]); }); @@ -1416,6 +1826,7 @@ class Linter { * @returns {Map} All loaded rules */ getRules() { + assertEslintrcConfig(this); const { lastConfigArray, ruleMap } = internalSlotsMap.get(this); return new Map(function *() { @@ -1434,6 +1845,7 @@ class Linter { * @returns {void} */ defineParser(parserId, parserModule) { + assertEslintrcConfig(this); internalSlotsMap.get(this).parserMap.set(parserId, parserModule); } @@ -1441,7 +1853,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/lib/shared/types.js b/lib/shared/types.js index c497f783be5..04fc2886735 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -25,6 +25,15 @@ module.exports = {}; * @property {"script"|"module"} [sourceType] The source code type. */ +/** + * @typedef {Object} LanguageOptions + * @property {number|"latest"} [ecmaVersion] The ECMAScript version (or revision number). + * @property {Record} [globals] The global variable settings. + * @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. + */ + /** * @typedef {Object} ConfigData * @property {Record} [env] The environment settings. diff --git a/package.json b/package.json index 6fc8d28770e..4fabd01f944 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.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 f6b099096e1..64cad9409cf 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] }); } @@ -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 }); @@ -638,7 +661,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..96d129262d3 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 @@ -95,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", () => { @@ -2356,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');", @@ -2771,10 +2772,10 @@ var a = "test2"; }); }); - describe("when evaluating a file with a shebang", () => { - const code = "#!bin/program\n\nvar foo;;"; + describe("when evaluating a file with a hashbang", () => { 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); @@ -2784,7 +2785,8 @@ 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 code = "#!bin/program\n\nvar foo;;"; const config = { rules: { checker: "error" } }; const spy = sinon.spy(context => { const comments = context.getAllComments(); @@ -2798,6 +2800,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", () => { @@ -2835,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); @@ -2887,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"); }); @@ -4116,7 +4140,7 @@ 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" } }; @@ -4126,7 +4150,62 @@ var a = "test2"; } })); linter.verify("", config); - assert.strictEqual(ecmaVersion, espree.latestEcmaVersion); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion, "ecmaVersion should be 13"); + }); + + it("the 'latest' is not normalized for custom parsers", () => { + let ecmaVersion = null; + 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, "latest", "ecmaVersion should be latest"); + }); + + 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 + 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 5"); }); it("should pass normalized ecmaVersion to eslint-scope", () => { @@ -6242,3 +6321,7264 @@ 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("Static Members", () => { + describe("version", () => { + it("should return same version as instance property", () => { + assert.strictEqual(Linter.version, linter.version); + }); + }); + }); + + 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."); + }); + + 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."); + }); + + 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."); + }); + + 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: { + 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 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: { + 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", () => { + + 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" + } + }); + + 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("should allow 'await' as a property name in modules", () => { + const result = linter.verify( + "obj.await", + { + languageOptions: { + ecmaVersion: 6, + sourceType: "module" + } + } + ); + + assert(result.length === 0); + }); + + }); + + describe("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!"; + } + } + } + }; + } + }; + + 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 context.languageOptions.parser to all rules when provided on config", () => { + + 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 = { + plugins: { + test: { + parsers: { + "enhanced-parser": testParsers.enhancedParser + } + } + }, + languageOptions: { + parser: "test/enhanced-parser" + } + }; + + const messages = linter.verify("0", config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should expose parser services when using parseForESLint() and services are specified", () => { + + 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 messages = linter.verify("0", config, filename); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hi!"); + }); + + it("should use the same parserServices if source code object is reused", () => { + + 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 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!"); + }); + + 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": spy + } + } + }, + languageOptions: { + parser: fakeParser + }, + rules: { + "test/test-rule": 2 + } + }; + + linter.verify("0", config, filename); + assert.isTrue(spy.calledOnce); + }); + + + 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 pass default languageOptions to the parser", () => { + + const spy = sinon.spy((code, options) => espree.parse(code, options)); + + linter.verify(";", { + languageOptions: { + parser: { + parse: spy + } + } + }, "filename.js"); + + assert(spy.calledWithMatch(";", { + ecmaVersion: espree.latestEcmaVersion + 2009, + sourceType: "module" + })); + }); + }); + + + }); + + describe("parseOptions", () => { + + it("should pass ecmaFeatures to all rules when provided on config", () => { + + const parserOptions = { + ecmaFeatures: { + jsx: true + } + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": sinon.mock().withArgs( + sinon.match({ languageOptions: { parserOptions } }) + ).returns({}) + } + } + }, + languageOptions: { + parserOptions + }, + rules: { + "test/test-rule": 2 + } + }; + + 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;", { + languageOptions: { + 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;", { + languageOptions: { + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } + } + } + }, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should properly parse JSX when passed ecmaFeatures", () => { + + const messages = linter.verify("var x =
;", { + languageOptions: { + 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, { + 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, { + languageOptions: { + ecmaVersion: 6, + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } + + }, "filename"); + + assert.strictEqual(messages.length, 0); + }); + + }); + + }); + + describe("settings", () => { + const ruleId = "test-rule"; + + it("should pass settings to all rules", () => { + + 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); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hello"); + }); + + it("should not have any settings if they were not passed in", () => { + + 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 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"; + + 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("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.isTrue(spy.notCalled, "Rule should not have been called"); + }); + }); + + describe("Rule Internals", () => { + + 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" } + }; + + assert.throws(() => { + linter.verify(code, config, filename); + }, `Intentional error.\nOccurred while linting ${filename}:1\nRule: "test/checker"`); + }); + + 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" } + }; + + linter.verify("foo", config); + assert(spy.calledOnce); + assert.strictEqual(spy.firstCall.thisValue, void 0); + }); + + 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" + } + }; + + linter.verify("foo", config); + assert(spy.notCalled); + }); + + 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); + + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify("foo + bar", config); + assert(spy.calledOnce); + }); + + it("events for each node type should fire", () => { + + // 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() { + return { + Literal: spyLiteral, + VariableDeclarator: spyVariableDeclarator, + VariableDeclaration: spyVariableDeclaration, + Identifier: spyIdentifier, + BinaryExpression: spyBinaryExpression + }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + const messages = linter.verify(code, config, filename, true); + + 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); + }); + + 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", () => { + + describe("context.getFilename()", () => { + const ruleId = "filename-rule"; + + it("has access to the filename", () => { + + 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); + + assert.strictEqual(messages[0].message, filename); + }); + + it("defaults filename to ''", () => { + + const config = { + plugins: { + test: { + rules: { + [ruleId]: context => ({ + Literal(node) { + context.report(node, context.getFilename()); + } + }) + } + } + }, + rules: { + [`test/${ruleId}`]: 1 + } + }; + + + const messages = linter.verify("0", config); + + assert.strictEqual(messages[0].message, ""); + }); + }); + + describe("context.getPhysicalFilename()", () => { + + const ruleId = "filename-rule"; + + it("has access to the physicalFilename", () => { + + 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); + + 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 + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + + 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 {}; + }); + + 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 \\u2028 as a line break", () => { + const code = "a;\u2028b;"; + 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 \\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 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; + + 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(codeToTestScope, 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(codeToTestScope, 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(codeToTestScope, 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(codeToTestScope, 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("Scope Internals", () => { + + /** + * Get the scope on the node `astSelector` specified. + * @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(codeToEvaluate, astSelector, ecmaVersion = 5) { + let node, scope; + + 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, + config + ); + + 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]); + }); + }); + + describe("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; + + 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, config); + 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")); + }); + }); + }); + + describe("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) { + const config = { + plugins: { + test: { + + 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); + + 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; + } + } + + } + }, + languageOptions: { + ecmaVersion: 6, + sourceType: "module" + }, + rules: { + "test/test": 2 + } + }; + + linter.verify(code, config); + + // 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()", () => { + + 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 }; + } + } + } + }, + languageOptions: { + sourceType: "script" + }, + 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 }; + } + } + } + }, + languageOptions: { + sourceType: "script" + }, + 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"), "Call to markVariableAsUsed should return true"); + + 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 }; + } + } + } + }, + languageOptions: { + sourceType: "commonjs" + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce, "Spy wasn't called."); + }); + + 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); + }); + }); + + describe("context.getCwd()", () => { + const code = "a;\nb;"; + const baseConfig = { rules: { "test/checker": "error" } }; + + it("should get cwd correctly in the context", () => { + const cwd = "cwd"; + const linterWithOption = new Linter({ cwd, configType: "flat" }); + let 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", () => { + + 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 }; + } + } + } + }, + ...baseConfig + }; + + linterWithOption.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("should assign process.cwd() to it if the option is undefined", () => { + let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { + + spy = sinon.spy(() => { + assert.strictEqual(context.getCwd(), process.cwd()); + }); + return { Program: spy }; + } + } + } + }, + ...baseConfig + }; + + 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); + }); + + 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("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 spy = sinon.spy(context => { + const comments = context.getAllComments(); + + assert.strictEqual(comments.length, 1); + assert.strictEqual(comments[0].type, "Shebang"); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: spy + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify(code, config); + assert(spy.calledOnce); + }); + }); + + describe("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 {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: filenameChecker + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify("foo;", config, { 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 {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: filenameChecker + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify("foo;", config, "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 {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: filenameChecker + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify("foo;", config, {}); + assert(filenameChecker.calledOnce); + }); + + it("should default filename to when only two arguments are passed", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.getFilename(), ""); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: filenameChecker + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify("foo;", config); + 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 {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: physicalFilenameChecker + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify("foo;", config, { 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 {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: physicalFilenameChecker + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify("foo;", config, {}); + assert(physicalFilenameChecker.calledOnce); + }); + + it("should default physicalFilename to when only two arguments are passed", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), ""); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + checker: physicalFilenameChecker + } + } + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify("foo;", config); + assert(physicalFilenameChecker.calledOnce); + }); + }); + + }); + + describe("Inline Directives", () => { + + describe("/*global*/ Comments", () => { + + describe("when evaluating code containing /*global */ and /*globals */ blocks", () => { + + it("variables should be available in global scope", () => { + const code = ` + /*global a b:true c:false d:readable e:writeable Math:off */ + function foo() {} + /*globals f:true*/ + /* global ConfigGlobal : readable */ + `; + let spy; + + 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 }; + } + } + } + }, + rules: { "test/checker": "error" }, + languageOptions: { + globals: { Array: "off", ConfigGlobal: "writeable" } + } + }; + + 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", () => { + + 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 }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + + describe("when evaluating code containing a line comment", () => { + const code = "//global a \n function f() {}"; + + it("should not introduce a global variable", () => { + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(); + + assert.strictEqual(getVariable(scope, "a"), null); + }); + + return { Program: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + + 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", () => { + let spy; + + 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); + }); + + return { Program: spy }; + } + } + } + }, + rules: { "test/checker": "error" } + }; + + + 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; + const config = { + plugins: { + test: { + rules: { + 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; + } + }; + } + } + } + }, + rules: { "test/test": "error" } + }; + + + linter.verify(code, config); + 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" + } + ]); + }); + + }); + + describe("/*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'"; + let spy; + + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); + + assert.strictEqual(horse.eslintUsed, true); + }); + + return { Program: spy }; + } + } + } + }, + languageOptions: { + sourceType: "script" + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + it("undefined variables should not be exported", () => { + const code = "/* exported horse */\n\nhorse = 'circus'"; + let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); + + assert.strictEqual(horse, null); + }); + + return { Program: spy }; + } + } + } + }, + languageOptions: { + sourceType: "script" + }, + rules: { "test/checker": "error" } + }; + + 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'"; + let spy; + const config = { + plugins: { + test: { + rules: { + checker: context => { + spy = sinon.spy(() => { + const scope = context.getScope(), + horse = getVariable(scope, "horse"); + + assert.strictEqual(horse.eslintUsed, true); + }); + + return { Program: spy }; + } + } + } + }, + languageOptions: { + sourceType: "script" + }, + rules: { "test/checker": "error" } + }; + + 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'"; + let spy; + const config = { + plugins: { + test: { + rules: { + 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 }; + } + } + } + }, + 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 a commonjs file", () => { + const code = "/* exported horse */\nvar horse = 'circus'"; + let spy; + const config = { + plugins: { + test: { + rules: { + 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 }; + } + } + } + }, + languageOptions: { + sourceType: "commonjs" + }, + rules: { "test/checker": "error" } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + }); + + describe("/*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 = { + 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); + + 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 = { + 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); + + 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 = { + 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); + + 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", () => { + + it("should not report a violation", () => { + const config = { rules: { "no-alert": 1 } }; + const messages = linter.verify("/*eslint no-alert:0*/ alert('test');", config, filename); + + assert.strictEqual(messages.length, 0); + }); + + it("should report an error when disabling a non-existent rule in inline comment", () => { + 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, "/*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, "/*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, "/*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", () => { + const messages = linter.verify("", { rules: { foo: 0 } }, filename); + + assert.strictEqual(messages.length, 0); + }); + + 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); + + }); + }); + + 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 */ "; + + 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, config); + + 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 */ "; + + 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, config); + + assert.strictEqual(problems.length, 0); + }); + + it("rules should not change initial config", () => { + 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); + + 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"); + }); + }); + + 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 } + } + } + } + }; + + const messages = linter.verify(` + /*eslint test/aaa:error -- test/bbb:error */ + console.log("hello") + `, config); + + // 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 '/*globals*/'.", () => { + const messages = linter.verify(` + /*globals aaa -- bbb */ + var aaa = {} + var bbb = {} + `, { + languageOptions: { + sourceType: "script" + }, + 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 = {} + `, { + languageOptions: { + sourceType: "script" + }, + 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({}); + const config = { + plugins: { + test: { + rules: { + "a--rule": { create: rule } + } + } + } + }; + + const messages = linter.verify(` + /*eslint test/a--rule:error */ + console.log("hello") + `, config); + + // 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({}); + const config = { + plugins: { + test: { + rules: { + aaa: { create: aaa }, + bbb: { create: bbb } + } + } + } + }; + + const messages = linter.verify(` + /*eslint test/aaa:error -------- test/bbb:error */ + console.log("hello") + `, config); + + // 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({}); + const config = { + plugins: { + test: { + rules: { + aaa: { create: aaa }, + bbb: { create: bbb } + } + } + } + }; + + const messages = linter.verify(` + /*eslint test/aaa:error + -------- + test/bbb:error */ + console.log("hello") + `, config); + + // 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", () => { + 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" + ].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", () => { + let ok = false; + const code = [ + "/* global foo */" + ].join("\n"); + const config = { + plugins: { + test: { + rules: { + 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; + } + }; + } + } + } + }, + rules: { + "test/test": 2 + } + }; + + 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"); + }); + + }); + + describe("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} */`, { + linterOptions: { + 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}`, { + linterOptions: { + 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 */", { + linterOptions: { + 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); + }); + }); + + }); + + describe("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 */", { + linterOptions: { + 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 = { + linterOptions: { + 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 = { + plugins: { + test: { + rules: {} + } + }, + linterOptions: { + reportUnusedDisableDirectives: true + }, + rules: { + ...Object.fromEntries(usedRules.map(name => [`test/${name}`, "error"])), + ...Object.fromEntries(unusedRules.map(name => [`test/${name}`, "error"])) + } + }; + + beforeEach(() => { + config.plugins.test.rules = { + ...Object.fromEntries(usedRules.map(name => [name, alwaysReportsRule])), + ...Object.fromEntries(unusedRules.map(name => [name, neverReportsRule])) + }; + }); + + const tests = [ + + //----------------------------------------------- + // Removing the entire comment + //----------------------------------------------- + + { + code: "// eslint-disable-line test/unused", + output: " " + }, + { + code: "foo// eslint-disable-line test/unused", + output: "foo " + }, + { + code: "// eslint-disable-line ,test/unused,", + output: " " + }, + { + code: "// eslint-disable-line test/unused-1, test/unused-2", + output: " " + }, + { + code: "// eslint-disable-line ,test/unused-1,, test/unused-2,, -- comment", + output: " " + }, + { + code: "// eslint-disable-next-line test/unused\n", + output: " \n" + }, + { + code: "// eslint-disable-next-line test/unused\nfoo", + output: " \nfoo" + }, + { + code: "/* eslint-disable \ntest/unused\n*/", + output: " " + }, + + //----------------------------------------------- + // Removing only individual rules + //----------------------------------------------- + + // content before the first rule should not be changed + { + code: "//eslint-disable-line test/unused, test/used", + output: "//eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/unused, test/used", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/unused, test/used", + output: "// eslint-disable-line test/used" + }, + { + code: "/*\neslint-disable test/unused, test/used*/", + output: "/*\neslint-disable test/used*/" + }, + { + code: "/*\n eslint-disable test/unused, test/used*/", + output: "/*\n eslint-disable test/used*/" + }, + { + code: "/*\r\neslint-disable test/unused, test/used*/", + output: "/*\r\neslint-disable test/used*/" + }, + { + code: "/*\u2028eslint-disable test/unused, test/used*/", + output: "/*\u2028eslint-disable test/used*/" + }, + { + code: "/*\u00A0eslint-disable test/unused, test/used*/", + output: "/*\u00A0eslint-disable test/used*/" + }, + { + code: "// eslint-disable-line test/unused, test/used", + output: "// eslint-disable-line test/used" + }, + { + code: "/* eslint-disable\ntest/unused, test/used*/", + output: "/* eslint-disable\ntest/used*/" + }, + { + code: "/* eslint-disable\n test/unused, test/used*/", + output: "/* eslint-disable\n test/used*/" + }, + { + code: "/* eslint-disable\r\ntest/unused, test/used*/", + output: "/* eslint-disable\r\ntest/used*/" + }, + { + code: "/* eslint-disable\u2028test/unused, test/used*/", + output: "/* eslint-disable\u2028test/used*/" + }, + { + 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 test/unused,test/used", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/unused, test/used", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/unused , test/used", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/unused, test/used", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/unused ,test/used", + output: "// eslint-disable-line test/used" + }, + { + code: "/* eslint-disable test/unused\n,\ntest/used */", + output: "/* eslint-disable test/used */" + }, + { + code: "/* eslint-disable test/unused \n \n,\n\n test/used */", + output: "/* eslint-disable test/used */" + }, + { + code: "/* eslint-disable test/unused\u2028,\u2028test/used */", + output: "/* eslint-disable test/used */" + }, + { + code: "// eslint-disable-line test/unused\u00A0,\u00A0test/used", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/unused,,test/used", + output: "// eslint-disable-line ,test/used" + }, + { + code: "// eslint-disable-line test/unused, ,test/used", + output: "// eslint-disable-line ,test/used" + }, + { + code: "// eslint-disable-line test/unused,, test/used", + output: "// eslint-disable-line , test/used" + }, + { + code: "// eslint-disable-line test/unused,test/used ", + output: "// eslint-disable-line test/used " + }, + { + 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 test/used-1,test/unused,test/used-2", + output: "// eslint-disable-line test/used-1,test/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 test/used-1,test/unused ,test/used-2", + output: "// eslint-disable-line test/used-1,test/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 test/used-1,\ntest/unused\n,test/used-2 */", + output: "/* eslint-disable test/used-1,test/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 test/used-1,\u2028test/unused\u2028,test/used-2 */", + output: "/* eslint-disable test/used-1,test/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 test/used-1, test/unused ,test/used-2", + output: "// eslint-disable-line test/used-1,test/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 test/used-1 ,test/unused,test/used-2", + output: "// eslint-disable-line test/used-1 ,test/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 test/used-1 , test/unused , test/used-2", + output: "// eslint-disable-line test/used-1 , test/used-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 test/used-1\u2028,test/unused,\u2028test/used-2 */", + output: "/* eslint-disable test/used-1\u2028,\u2028test/used-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 , test/unused ,test/used", + output: "// eslint-disable-line ,test/used" + }, + { + code: "/* eslint-disable\n, test/unused ,test/used */", + output: "/* eslint-disable\n,test/used */" + }, + { + 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 test/used-1,test/unused,\n,test/used-2 */", + output: "/* eslint-disable test/used-1,\n,test/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 test/used, test/unused,", + output: "// eslint-disable-line test/used," + }, + { + code: "// eslint-disable-next-line test/used, test/unused,\n", + output: "// eslint-disable-next-line test/used,\n" + }, + { + code: "// eslint-disable-line test/used, test/unused, ", + output: "// eslint-disable-line test/used, " + }, + { + code: "// eslint-disable-line test/used, test/unused, -- comment", + output: "// eslint-disable-line test/used, -- comment" + }, + { + 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 test/used,test/unused", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/used, test/unused", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/used ,test/unused", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/used , test/unused", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/used, test/unused", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/used ,test/unused", + output: "// eslint-disable-line test/used" + }, + { + code: "/* eslint-disable test/used\n,\ntest/unused */", + output: "/* eslint-disable test/used */" + }, + { + code: "/* eslint-disable test/used \n \n,\n\n test/unused */", + output: "/* eslint-disable test/used */" + }, + { + code: "/* eslint-disable test/used\u2028,\u2028test/unused */", + output: "/* eslint-disable test/used */" + }, + { + code: "// eslint-disable-line test/used\u00A0,\u00A0test/unused", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/used,,test/unused", + output: "// eslint-disable-line test/used," + }, + { + code: "// eslint-disable-line test/used, ,test/unused", + output: "// eslint-disable-line test/used," + }, + { + code: "/* eslint-disable test/used,\n,test/unused */", + output: "/* eslint-disable test/used, */" + }, + { + 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 test/used,test/unused", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/used,test/unused ", + output: "// eslint-disable-line test/used " + }, + { + code: "// eslint-disable-line test/used,test/unused ", + output: "// eslint-disable-line test/used " + }, + { + code: "// eslint-disable-line test/used,test/unused -- comment", + output: "// eslint-disable-line test/used -- comment" + }, + { + code: "// eslint-disable-next-line test/used,test/unused\n", + output: "// eslint-disable-next-line test/used\n" + }, + { + code: "// eslint-disable-next-line test/used,test/unused \n", + output: "// eslint-disable-next-line test/used \n" + }, + { + code: "/* eslint-disable test/used,test/unused\u2028*/", + output: "/* eslint-disable test/used\u2028*/" + }, + { + code: "// eslint-disable-line test/used,test/unused\u00A0", + output: "// eslint-disable-line test/used\u00A0" + }, + + // multiply rules to remove + { + code: "// eslint-disable-line test/used, test/unused-1, test/unused-2", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/unused-1, test/used, test/unused-2", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/unused-1, test/unused-2, test/used", + output: "// eslint-disable-line test/used" + }, + { + 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 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 test/unused-1, + test/used-1, + test/unused-2, + test/used-2 + */ + `, + output: ` + /* eslint-disable test/used-1, + test/used-2 + */ + ` + }, + { + code: ` + /* eslint-disable + test/unused-1, + test/used-1, + test/unused-2, + test/used-2 + */ + `, + output: ` + /* eslint-disable + test/used-1, + test/used-2 + */ + ` + }, + { + code: ` + /* eslint-disable + test/used-1, + test/unused-1, + test/used-2, + test/unused-2 + */ + `, + output: ` + /* eslint-disable + test/used-1, + test/used-2 + */ + ` + }, + { + code: ` + /* eslint-disable + test/used-1, + test/unused-1, + test/used-2, + test/unused-2, + */ + `, + output: ` + /* eslint-disable + test/used-1, + test/used-2, + */ + ` + }, + { + code: ` + /* eslint-disable + ,test/unused-1 + ,test/used-1 + ,test/unused-2 + ,test/used-2 + */ + `, + output: ` + /* eslint-disable + ,test/used-1 + ,test/used-2 + */ + ` + }, + { + code: ` + /* eslint-disable + ,test/used-1 + ,test/unused-1 + ,test/used-2 + ,test/unused-2 + */ + `, + output: ` + /* eslint-disable + ,test/used-1 + ,test/used-2 + */ + ` + }, + { + code: ` + /* eslint-disable + test/used-1, + test/unused-1, + test/used-2, + test/unused-2 + + -- comment + */ + `, + output: ` + /* eslint-disable + test/used-1, + test/used-2 + + -- comment + */ + ` + }, + + // duplicates in the list + { + code: "// eslint-disable-line test/unused, test/unused, test/used", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/unused, test/used, test/unused", + output: "// eslint-disable-line test/used" + }, + { + code: "// eslint-disable-line test/used, test/unused, test/unused, test/used", + output: "// eslint-disable-line test/used, test/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 + ); + }); + } + }); + }); + + }); + + describe("Default Global Variables", () => { + const code = "x"; + + it("builtin global variables should be available in the global scope", () => { + let spy; + const config = { + plugins: { + test: { + rules: { + 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 }; + } + } + } + }, + languageOptions: { + ecmaVersion: 5, + sourceType: "script" + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce, "Rule should have been called."); + }); + + 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(); + + assert.notStrictEqual(getVariable(scope, "Promise"), null); + assert.notStrictEqual(getVariable(scope, "Symbol"), null); + assert.notStrictEqual(getVariable(scope, "WeakMap"), null); + }); + + return { Program: spy }; + } + } + } + }, + languageOptions: { + sourceType: "script" + }, + rules: { + "test/checker": "error" + } + }; + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); + + }); + + describe("Suggestions", () => { + it("provides suggestion information for tools to use", () => { + + 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;", config); + + 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", () => { + + 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;", config); + + 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", () => { + + 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", 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", () => { + + 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", config); + }, "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 a rule which has been replaced", () => { + const code = TEST_CODE; + + it("should report the new rule", () => { + + 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); + + }); + }); + + }); + }); + + 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); + }); + + }); + + 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("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); + }); + }); + + describe("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", () => { + + 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", config); + + 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", () => { + + 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", 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", () => { + + 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", config); + }, /Fixable rules must set the `meta\.fixable` property/u); + }); + + it("should throw an error if fix is passed from a legacy-format rule", () => { + + 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", config); + }, /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 = []; + 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 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([ + 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"); + }); + }); + }); + + 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); + }); + + 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); + }); + + it("should reuse the SourceCode object", () => { + let ast1 = null, + ast2 = null; + + 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}
}", { ...config, rules: { "test/save-ast1": "error" } }); + linter.verify(linter.getSourceCode(), { ...config, rules: { "test/save-ast2": "error" } }); + + 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); + }); + + it("should pass 'id' to rule contexts with the rule id", () => { + + const spy = sinon.spy(context => { + assert.strictEqual(context.id, "test/foo-bar-baz"); + return {}; + }); + + const config = { + plugins: { + test: { + rules: { + "foo-bar-baz": spy + } + } + }, + rules: { + "test/foo-bar-baz": "error" + } + }; + + + linter.verify("x", config); + assert(spy.calledOnce); + }); + + + describe("when evaluating an empty string", () => { + it("runs rules", () => { + + 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("", config).length, + 1 + ); + }); + }); + + }); + +}); diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 6ebd82fb5ab..5018a037b09 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 normalize ecmaVersion if it's not "latest" { 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: 6 } }], parser: notEspree, parserOptions: { ecmaVersion: 2015 } },