diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md index d189d53ad4f..c49863501ae 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -169,6 +169,13 @@ This method returns the scope which has the following types: **※2** Only if the `for` statement defines the iteration variable as a block-scoped variable (E.g., `for (let i = 0;;) {}`).
**※3** The scope of the closest ancestor node which has own scope. If the closest ancestor node has multiple scopes then it chooses the innermost scope (E.g., the `Program` node has a `global` scope and a `module` scope if `Program#sourceType` is `"module"`. The innermost scope is the `module` scope.). +The returned value is a [`Scope` object](scope-manager-interface.md) defined by the `eslint-scope` package. The `Variable` objects of global variables have some additional properties. + +* `variable.writeable` (`boolean | undefined`) ... If `true`, this global variable can be assigned arbitrary value. If `false`, this global variable is read-only. +* `variable.eslintExplicitGlobal` (`boolean | undefined`) ... If `true`, this global variable was defined by a `/* globals */` directive comment in the source code file. +* `variable.eslintExplicitGlobalComments` (`Comment[] | undefined`) ... The array of `/* globals */` directive comments which defined this global variable in the source code file. This property is `undefined` if there are no `/* globals */` directive comments. +* `variable.eslintImplicitGlobalSetting` (`"readonly" | "writable" | undefined`) ... The configured value in config files. This can be different from `variable.writeable` if there are `/* globals */` directive comments. + ### context.report() The main method you'll use is `context.report()`, which publishes a warning or error (depending on the configuration being used). This method accepts a single argument, which is an object containing the following properties: diff --git a/docs/rules/no-redeclare.md b/docs/rules/no-redeclare.md index d0d71e5dee5..c3b8422cf3f 100644 --- a/docs/rules/no-redeclare.md +++ b/docs/rules/no-redeclare.md @@ -27,7 +27,7 @@ a = 10; ## Options -This rule takes one optional argument, an object with a boolean property `"builtinGlobals"`. It defaults to `false`. +This rule takes one optional argument, an object with a boolean property `"builtinGlobals"`. It defaults to `true`. If set to `true`, this rule also checks redeclaration of built-in globals, such as `Object`, `Array`, `Number`... ### builtinGlobals diff --git a/lib/config/config-ops.js b/lib/config/config-ops.js index 48a8302d91f..beca8ca2c31 100644 --- a/lib/config/config-ops.js +++ b/lib/config/config-ops.js @@ -385,14 +385,14 @@ module.exports = { case "true": case "writeable": case "writable": - return "writeable"; + return "writable"; case null: case false: case "false": case "readable": case "readonly": - return "readable"; + return "readonly"; default: throw new Error(`'${configuredValue}' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')`); diff --git a/lib/linter.js b/lib/linter.js index bd3d8b23553..f2297529bb8 100644 --- a/lib/linter.js +++ b/lib/linter.js @@ -70,39 +70,41 @@ const commentParser = new ConfigCommentParser(); * @param {{exportedVariables: Object, enabledGlobals: Object}} commentDirectives Directives from comment configuration * @returns {void} */ -function addDeclaredGlobals(globalScope, configGlobals, commentDirectives) { - const mergedGlobalsInfo = Object.assign( - {}, +function addDeclaredGlobals(globalScope, configGlobals, { exportedVariables, enabledGlobals }) { + + // Define configured global variables. + for (const id of new Set([...Object.keys(configGlobals), ...Object.keys(enabledGlobals)])) { /* * `ConfigOps.normalizeConfigGlobal` will throw an error if a configured global value is invalid. However, these errors would * typically be caught when validating a config anyway (validity for inline global comments is checked separately). */ - lodash.mapValues(configGlobals, value => ({ sourceComment: null, value: ConfigOps.normalizeConfigGlobal(value) })), - lodash.mapValues(commentDirectives.enabledGlobals, ({ comment, value }) => ({ sourceComment: comment, value })) - ); + const configValue = configGlobals[id] === void 0 ? void 0 : ConfigOps.normalizeConfigGlobal(configGlobals[id]); + const commentValue = enabledGlobals[id] && enabledGlobals[id].value; + const value = commentValue || configValue; + const sourceComments = enabledGlobals[id] && enabledGlobals[id].comments; - Object.keys(mergedGlobalsInfo) - .filter(name => mergedGlobalsInfo[name].value !== "off") - .forEach(name => { - let variable = globalScope.set.get(name); - - if (!variable) { - variable = new eslintScope.Variable(name, globalScope); - if (mergedGlobalsInfo[name].sourceComment === null) { - variable.eslintExplicitGlobal = false; - } else { - variable.eslintExplicitGlobal = true; - variable.eslintExplicitGlobalComment = mergedGlobalsInfo[name].sourceComment; - } - globalScope.variables.push(variable); - globalScope.set.set(name, variable); - } - variable.writeable = (mergedGlobalsInfo[name].value === "writeable"); - }); + if (value === "off") { + continue; + } + + let variable = globalScope.set.get(id); + + if (!variable) { + variable = new eslintScope.Variable(id, globalScope); + + globalScope.variables.push(variable); + globalScope.set.set(id, variable); + } + + variable.eslintImplicitGlobalSetting = configValue; + variable.eslintExplicitGlobal = sourceComments !== void 0; + variable.eslintExplicitGlobalComments = sourceComments; + variable.writeable = (value === "writable"); + } // mark all exported variables as such - Object.keys(commentDirectives.exportedVariables).forEach(name => { + Object.keys(exportedVariables).forEach(name => { const variable = globalScope.set.get(name); if (variable) { @@ -157,7 +159,7 @@ function createDisableDirectives(type, loc, value) { * @param {string} filename The file being checked. * @param {ASTNode} ast The top node of the AST. * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules - * @returns {{configuredRules: Object, enabledGlobals: Object, exportedVariables: Object, problems: Problem[], disableDirectives: DisableDirective[]}} + * @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: Problem[], disableDirectives: DisableDirective[]}} * A collection of the directive comments that were found, along with any problems that occurred when parsing */ function getDirectiveComments(filename, ast, ruleMapper) { @@ -201,11 +203,8 @@ function getDirectiveComments(filename, ast, ruleMapper) { break; case "globals": - case "global": { - const updatedGlobals = commentParser.parseStringConfig(directiveValue, comment); - - Object.keys(updatedGlobals).forEach(globalName => { - const { value } = updatedGlobals[globalName]; + case "global": + for (const [id, { value }] of Object.entries(commentParser.parseStringConfig(directiveValue, comment))) { let normalizedValue; try { @@ -221,13 +220,21 @@ function getDirectiveComments(filename, ast, ruleMapper) { endColumn: comment.loc.end.column + 1, nodeType: null }); - return; + continue; } - enabledGlobals[globalName] = { comment, value: normalizedValue }; - }); + if (enabledGlobals[id]) { + enabledGlobals[id].comments.push(comment); + enabledGlobals[id].value = normalizedValue; + } else { + enabledGlobals[id] = { + comments: [comment], + value: normalizedValue + }; + } + } break; - } + case "eslint-disable": disableDirectives.push(...createDisableDirectives("disable", comment.loc.start, directiveValue)); break; diff --git a/lib/rules/no-redeclare.js b/lib/rules/no-redeclare.js index 4d689cc6138..6adde0f97a5 100644 --- a/lib/rules/no-redeclare.js +++ b/lib/rules/no-redeclare.js @@ -5,6 +5,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../util/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -20,11 +26,17 @@ module.exports = { url: "https://eslint.org/docs/rules/no-redeclare" }, + messages: { + redeclared: "'{{id}}' is already defined.", + redeclaredAsBuiltin: "'{{id}}' is already defined as a built-in global variable.", + redeclaredBySyntax: "'{{id}}' is already defined by a variable declaration." + }, + schema: [ { type: "object", properties: { - builtinGlobals: { type: "boolean", default: false } + builtinGlobals: { type: "boolean", default: true } }, additionalProperties: false } @@ -33,72 +45,128 @@ module.exports = { create(context) { const options = { - builtinGlobals: context.options[0] && context.options[0].builtinGlobals + builtinGlobals: Boolean( + context.options.length === 0 || + context.options[0].builtinGlobals + ) }; + const sourceCode = context.getSourceCode(); /** - * Find variables in a given scope and flag redeclared ones. - * @param {Scope} scope - An eslint-scope scope object. - * @returns {void} - * @private + * Iterate declarations of a given variable. + * @param {escope.variable} variable The variable object to iterate declarations. + * @returns {IterableIterator<{type:string,node:ASTNode,loc:SourceLocation}>} The declarations. */ - function findVariablesInScope(scope) { - scope.variables.forEach(variable => { - const hasBuiltin = options.builtinGlobals && "writeable" in variable; - const count = (hasBuiltin ? 1 : 0) + variable.identifiers.length; + function *iterateDeclarations(variable) { + if (options.builtinGlobals && ( + variable.eslintImplicitGlobalSetting === "readonly" || + variable.eslintImplicitGlobalSetting === "writable" + )) { + yield { type: "builtin" }; + } - if (count >= 2) { - variable.identifiers.sort((a, b) => a.range[1] - b.range[1]); + for (const id of variable.identifiers) { + yield { type: "syntax", node: id, loc: id.loc }; + } - for (let i = (hasBuiltin ? 0 : 1), l = variable.identifiers.length; i < l; i++) { - context.report({ node: variable.identifiers[i], message: "'{{a}}' is already defined.", data: { a: variable.name } }); - } + if (variable.eslintExplicitGlobalComments) { + for (const comment of variable.eslintExplicitGlobalComments) { + yield { + type: "comment", + node: comment, + loc: astUtils.getNameLocationInGlobalDirectiveComment( + sourceCode, + comment, + variable.name + ) + }; } - }); - + } } /** - * Find variables in the current scope. - * @param {ASTNode} node - The Program node. + * Find variables in a given scope and flag redeclared ones. + * @param {Scope} scope - An eslint-scope scope object. * @returns {void} * @private */ - function checkForGlobal(node) { - const scope = context.getScope(), - parserOptions = context.parserOptions, - ecmaFeatures = parserOptions.ecmaFeatures || {}; - - // Nodejs env or modules has a special scope. - if (ecmaFeatures.globalReturn || node.sourceType === "module") { - findVariablesInScope(scope.childScopes[0]); - } else { - findVariablesInScope(scope); + function findVariablesInScope(scope) { + for (const variable of scope.variables) { + const [ + declaration, + ...extraDeclarations + ] = iterateDeclarations(variable); + + if (extraDeclarations.length === 0) { + continue; + } + + /* + * If the type of a declaration is different from the type of + * the first declaration, it shows the location of the first + * declaration. + */ + const detailMessageId = declaration.type === "builtin" + ? "redeclaredAsBuiltin" + : "redeclaredBySyntax"; + const data = { id: variable.name }; + + // Report extra declarations. + for (const { type, node, loc } of extraDeclarations) { + const messageId = type === declaration.type + ? "redeclared" + : detailMessageId; + + context.report({ node, loc, messageId, data }); + } } } /** * Find variables in the current scope. + * @param {ASTNode} node The node of the current scope. * @returns {void} * @private */ - function checkForBlock() { - findVariablesInScope(context.getScope()); - } + function checkForBlock(node) { + const scope = context.getScope(); - if (context.parserOptions.ecmaVersion >= 6) { - return { - Program: checkForGlobal, - BlockStatement: checkForBlock, - SwitchStatement: checkForBlock - }; + /* + * In ES5, some node type such as `BlockStatement` doesn't have that scope. + * `scope.block` is a different node in such a case. + */ + if (scope.block === node) { + findVariablesInScope(scope); + } } + return { - Program: checkForGlobal, + Program() { + const scope = context.getScope(); + + findVariablesInScope(scope); + + // Node.js or ES modules has a special scope. + if ( + scope.type === "global" && + scope.childScopes[0] && + + // The special scope's block is the Program node. + scope.block === scope.childScopes[0].block + ) { + findVariablesInScope(scope.childScopes[0]); + } + }, + FunctionDeclaration: checkForBlock, FunctionExpression: checkForBlock, - ArrowFunctionExpression: checkForBlock - }; + ArrowFunctionExpression: checkForBlock, + BlockStatement: checkForBlock, + ForStatement: checkForBlock, + ForInStatement: checkForBlock, + ForOfStatement: checkForBlock, + SwitchStatement: checkForBlock + }; } }; diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js index 1482a0238ca..8e32951bdae 100644 --- a/lib/rules/no-unused-vars.js +++ b/lib/rules/no-unused-vars.js @@ -9,7 +9,6 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); const astUtils = require("../util/ast-utils"); //------------------------------------------------------------------------------ @@ -588,39 +587,6 @@ module.exports = { return unusedVars; } - /** - * Gets the index of a given variable name in a given comment. - * @param {eslint-scope.Variable} variable - A variable to get. - * @param {ASTNode} comment - A comment node which includes the variable name. - * @returns {number} The index of the variable name's location. - * @private - */ - function getColumnInComment(variable, comment) { - const namePattern = new RegExp(`[\\s,]${lodash.escapeRegExp(variable.name)}(?:$|[\\s,:])`, "gu"); - - // To ignore the first text "global". - namePattern.lastIndex = comment.value.indexOf("global") + 6; - - // Search a given variable name. - const match = namePattern.exec(comment.value); - - return match ? match.index + 1 : 0; - } - - /** - * Creates the correct location of a given variables. - * The location is at its name string in a `/*global` comment. - * - * @param {eslint-scope.Variable} variable - A variable to get its location. - * @returns {{line: number, column: number}} The location object for the variable. - * @private - */ - function getLocation(variable) { - const comment = variable.eslintExplicitGlobalComment; - - return sourceCode.getLocFromIndex(comment.range[0] + 2 + getColumnInComment(variable, comment)); - } - //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- @@ -632,14 +598,8 @@ module.exports = { for (let i = 0, l = unusedVars.length; i < l; ++i) { const unusedVar = unusedVars[i]; - if (unusedVar.eslintExplicitGlobal) { - context.report({ - node: programNode, - loc: getLocation(unusedVar), - message: getDefinedMessage(unusedVar), - data: unusedVar - }); - } else if (unusedVar.defs.length > 0) { + // Report the first declaration. + if (unusedVar.defs.length > 0) { context.report({ node: unusedVar.identifiers[0], message: unusedVar.references.some(ref => ref.isWrite()) @@ -647,6 +607,17 @@ module.exports = { : getDefinedMessage(unusedVar), data: unusedVar }); + + // If there are no regular declaration, report the first `/*globals*/` comment directive. + } else if (unusedVar.eslintExplicitGlobalComments) { + const directiveComment = unusedVar.eslintExplicitGlobalComments[0]; + + context.report({ + node: programNode, + loc: astUtils.getNameLocationInGlobalDirectiveComment(sourceCode, directiveComment, unusedVar.name), + message: getDefinedMessage(unusedVar), + data: unusedVar + }); } } } diff --git a/lib/util/ast-utils.js b/lib/util/ast-utils.js index 85205a75d24..0f9ba290a9d 100644 --- a/lib/util/ast-utils.js +++ b/lib/util/ast-utils.js @@ -11,6 +11,7 @@ const esutils = require("esutils"); const espree = require("espree"); +const lodash = require("lodash"); //------------------------------------------------------------------------------ // Helpers @@ -1342,5 +1343,29 @@ module.exports = { } return false; + }, + + /** + * Get the `loc` object of a given name in a `/*globals` directive comment. + * @param {SourceCode} sourceCode The source code to convert index to loc. + * @param {Comment} comment The `/*globals` directive comment which include the name. + * @param {string} name The name to find. + * @returns {SourceLocation} The `loc` object. + */ + getNameLocationInGlobalDirectiveComment(sourceCode, comment, name) { + const namePattern = new RegExp(`[\\s,]${lodash.escapeRegExp(name)}(?:$|[\\s,:])`, "gu"); + + // To ignore the first text "global". + namePattern.lastIndex = comment.value.indexOf("global") + 6; + + // Search a given variable name. + const match = namePattern.exec(comment.value); + + // Convert the index to loc. + return sourceCode.getLocFromIndex( + comment.range[0] + + "/*".length + + (match ? match.index + 1 : 0) + ); } }; diff --git a/package.json b/package.json index d2ea8a004cd..c3b47d47d9d 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@babel/core": "^7.4.3", "@babel/polyfill": "^7.4.3", "@babel/preset-env": "^7.4.3", + "acorn": "^6.1.1", "babel-loader": "^8.0.5", "beefy": "^2.1.8", "brfs": "^2.0.2", diff --git a/tests/lib/config/config-initializer.js b/tests/lib/config/config-initializer.js index d306db1b1c3..377633ec675 100644 --- a/tests/lib/config/config-initializer.js +++ b/tests/lib/config/config-initializer.js @@ -70,7 +70,7 @@ describe("configInitializer", () => { // copy into clean area so as not to get "infected" by this project's .eslintrc files before(() => { - fixtureDir = `${os.tmpdir()}/eslint/fixtures/config-initializer`; + fixtureDir = path.join(os.tmpdir(), "eslint/fixtures/config-initializer"); sh.mkdir("-p", fixtureDir); sh.cp("-r", "./tests/fixtures/config-initializer/.", fixtureDir); fixtureDir = fs.realpathSync(fixtureDir); diff --git a/tests/lib/config/config-ops.js b/tests/lib/config/config-ops.js index ffa65660d58..6a1f0b219d7 100644 --- a/tests/lib/config/config-ops.js +++ b/tests/lib/config/config-ops.js @@ -902,16 +902,16 @@ describe("ConfigOps", () => { describe("normalizeConfigGlobal", () => { [ ["off", "off"], - [true, "writeable"], - ["true", "writeable"], - [false, "readable"], - ["false", "readable"], - [null, "readable"], - ["writeable", "writeable"], - ["writable", "writeable"], - ["readable", "readable"], - ["readonly", "readable"], - ["writable", "writeable"] + [true, "writable"], + ["true", "writable"], + [false, "readonly"], + ["false", "readonly"], + [null, "readonly"], + ["writeable", "writable"], + ["writable", "writable"], + ["readable", "readonly"], + ["readonly", "readonly"], + ["writable", "writable"] ].forEach(([input, output]) => { it(util.inspect(input), () => { assert.strictEqual(ConfigOps.normalizeConfigGlobal(input), output); diff --git a/tests/lib/linter.js b/tests/lib/linter.js index bf8812c7bea..3df16711105 100644 --- a/tests/lib/linter.js +++ b/tests/lib/linter.js @@ -3469,18 +3469,18 @@ describe("Linter", () => { const foo = getVariable(scope, "foo"); - assert.strictEqual(true, foo.eslintExplicitGlobal); - assert.strictEqual(comments[0], foo.eslintExplicitGlobalComment); + assert.strictEqual(foo.eslintExplicitGlobal, true); + assert.strictEqual(foo.eslintExplicitGlobalComments[0], comments[0]); const bar = getVariable(scope, "bar"); - assert.strictEqual(true, bar.eslintExplicitGlobal); - assert.strictEqual(comments[1], bar.eslintExplicitGlobalComment); + assert.strictEqual(bar.eslintExplicitGlobal, true); + assert.strictEqual(bar.eslintExplicitGlobalComments[0], comments[1]); const baz = getVariable(scope, "baz"); - assert.strictEqual(true, baz.eslintExplicitGlobal); - assert.strictEqual(comments[1], baz.eslintExplicitGlobalComment); + assert.strictEqual(baz.eslintExplicitGlobal, true); + assert.strictEqual(baz.eslintExplicitGlobalComments[0], comments[1]); ok = true; } diff --git a/tests/lib/rules/no-redeclare.js b/tests/lib/rules/no-redeclare.js index 23a45f02185..15d8275d02b 100644 --- a/tests/lib/rules/no-redeclare.js +++ b/tests/lib/rules/no-redeclare.js @@ -9,13 +9,15 @@ // Requirements //------------------------------------------------------------------------------ -const rule = require("../../../lib/rules/no-redeclare"), - RuleTester = require("../../../lib/testers/rule-tester"); +const path = require("path"); +const rule = require("../../../lib/rules/no-redeclare"); +const RuleTester = require("../../../lib/testers/rule-tester"); //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ +const looseParserPath = path.resolve(__dirname, "../../tools/loose-parser.js"); const ruleTester = new RuleTester(); ruleTester.run("no-redeclare", rule, { @@ -28,11 +30,9 @@ ruleTester.run("no-redeclare", rule, { ecmaVersion: 6 } }, - "var Object = 0;", { code: "var Object = 0;", options: [{ builtinGlobals: false }] }, { code: "var Object = 0;", options: [{ builtinGlobals: true }], parserOptions: { sourceType: "module" } }, { code: "var Object = 0;", options: [{ builtinGlobals: true }], parserOptions: { ecmaFeatures: { globalReturn: true } } }, - { code: "var top = 0;", env: { browser: true } }, { code: "var top = 0;", options: [{ builtinGlobals: true }] }, { code: "var top = 0;", options: [{ builtinGlobals: true }], parserOptions: { ecmaFeatures: { globalReturn: true } }, env: { browser: true } }, { code: "var top = 0;", options: [{ builtinGlobals: true }], parserOptions: { sourceType: "module" }, env: { browser: true } }, @@ -40,6 +40,32 @@ ruleTester.run("no-redeclare", rule, { code: "var self = 1", options: [{ builtinGlobals: true }], env: { browser: false } + }, + + // Comments and built-ins. + { + code: "/*globals Array */", + options: [{ builtinGlobals: false }] + }, + { + code: "/*globals a */", + options: [{ builtinGlobals: false }], + globals: { a: "readonly" } + }, + { + code: "/*globals a */", + options: [{ builtinGlobals: false }], + globals: { a: "writable" } + }, + { + code: "/*globals a:off */", + options: [{ builtinGlobals: true }], + globals: { a: "readonly" } + }, + { + code: "/*globals a */", + options: [{ builtinGlobals: true }], + globals: { a: "off" } } ], invalid: [ @@ -57,13 +83,13 @@ ruleTester.run("no-redeclare", rule, { { code: "var Object = 0;", options: [{ builtinGlobals: true }], - errors: [{ message: "'Object' is already defined.", type: "Identifier" }] + errors: [{ message: "'Object' is already defined as a built-in global variable.", type: "Identifier" }] }, { code: "var top = 0;", options: [{ builtinGlobals: true }], env: { browser: true }, - errors: [{ message: "'top' is already defined.", type: "Identifier" }] + errors: [{ message: "'top' is already defined as a built-in global variable.", type: "Identifier" }] }, { code: "var a; var {a = 0, b: Object = 0} = {};", @@ -71,7 +97,7 @@ ruleTester.run("no-redeclare", rule, { parserOptions: { ecmaVersion: 6 }, errors: [ { message: "'a' is already defined.", type: "Identifier" }, - { message: "'Object' is already defined.", type: "Identifier" } + { message: "'Object' is already defined as a built-in global variable.", type: "Identifier" } ] }, { @@ -98,13 +124,203 @@ ruleTester.run("no-redeclare", rule, { { message: "'a' is already defined.", type: "Identifier" } ] }, - - // Notifications of readonly are moved from no-undef: https://github.com/eslint/eslint/issues/4504 { code: "/*global b:false*/ var b = 1;", options: [{ builtinGlobals: true }], errors: [ - { message: "'b' is already defined.", type: "Identifier" } + { message: "'b' is already defined by a variable declaration.", type: "Block" } + ] + }, + { + code: "/*global b:true*/ var b = 1;", + options: [{ builtinGlobals: true }], + errors: [ + { message: "'b' is already defined by a variable declaration.", type: "Block" } + ] + }, + { + code: "function f() { var a; var a; }", + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "function f(a) { var a; }", + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "function f() { var a; if (test) { var a; } }", + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "for (var a, a;;);", + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + + { + code: "var Object = 0;", + errors: [ + { message: "'Object' is already defined as a built-in global variable.", type: "Identifier" } + ] + }, + { + code: "var top = 0;", + env: { browser: true }, + errors: [ + { message: "'top' is already defined as a built-in global variable.", type: "Identifier" } + ] + }, + + // let/const + { + code: "let a; let a;", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "let a; let a;", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015, sourceType: "module" }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "let a; let a;", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015, ecmaFeatures: { globalReturn: true } }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "let a; const a = 0;", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "const a = 0; const a = 0;", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "if (test) { let a; let a; }", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "switch (test) { case 0: let a; let a; }", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "for (let a, a;;);", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "for (let [a, a] in xs);", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "for (let [a, a] of xs);", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "function f() { let a; let a; }", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "function f(a) { let a; }", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + { + code: "function f() { if (test) { let a; let a; } }", + parser: looseParserPath, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { message: "'a' is already defined.", type: "Identifier" } + ] + }, + + // Comments and built-ins. + { + code: "/*globals Array */", + options: [{ builtinGlobals: true }], + errors: [ + { message: "'Array' is already defined as a built-in global variable.", type: "Block" } + ] + }, + { + code: "/*globals a */", + options: [{ builtinGlobals: true }], + globals: { a: "readonly" }, + errors: [ + { message: "'a' is already defined as a built-in global variable.", type: "Block" } + ] + }, + { + code: "/*globals a */", + options: [{ builtinGlobals: true }], + globals: { a: "writable" }, + errors: [ + { message: "'a' is already defined as a built-in global variable.", type: "Block" } + ] + }, + { + code: "/*globals a */ /*globals a */", + errors: [ + { message: "'a' is already defined.", type: "Block", column: 26 } + ] + }, + { + code: "/*globals a */ /*globals a */ var a = 0", + options: [{ builtinGlobals: true }], + globals: { a: "writable" }, + errors: [ + { message: "'a' is already defined as a built-in global variable.", type: "Block", column: 11 }, + { message: "'a' is already defined as a built-in global variable.", type: "Block", column: 26 }, + { message: "'a' is already defined as a built-in global variable.", type: "Identifier", column: 35 } ] } ] diff --git a/tests/tools/loose-parser.js b/tests/tools/loose-parser.js new file mode 100644 index 00000000000..abb3aeb5bd0 --- /dev/null +++ b/tests/tools/loose-parser.js @@ -0,0 +1,31 @@ +/** + * @fileoverview Define a custom parser to ignore recoverable syntax errors. + * @author Toru Nagashima + * + * no-redeclare rule uses this parser to check redeclarations. + */ +"use strict"; + +const acorn = require("acorn"); +const espree = require("espree/lib/espree"); + +// eslint-disable-next-line valid-jsdoc +/** + * Define the parser which ignores recoverable errors. + * @returns {(parser:acorn.Parser) => acorn.Parser} The function that defines loose parser. + */ +function loose() { + return Parser => class LooseParser extends Parser { + raiseRecoverable() { // eslint-disable-line class-methods-use-this + // ignore + } + }; +} + +const LooseEspree = acorn.Parser.extend(espree(), loose()); + +module.exports = { + parse(code, options) { + return new LooseEspree(options, code).parse(); + } +};