diff --git a/lib/rules/valid-typeof.js b/lib/rules/valid-typeof.js index 60463581233..cb85cd9cb90 100644 --- a/lib/rules/valid-typeof.js +++ b/lib/rules/valid-typeof.js @@ -19,6 +19,8 @@ module.exports = { url: "https://eslint.org/docs/rules/valid-typeof" }, + hasSuggestions: true, + schema: [ { type: "object", @@ -33,7 +35,8 @@ module.exports = { ], messages: { invalidValue: "Invalid typeof comparison value.", - notString: "Typeof comparisons should be to string literals." + notString: "Typeof comparisons should be to string literals.", + suggestString: 'Use `"{{type}}"` instead of `{{type}}`.' } }, @@ -44,6 +47,21 @@ module.exports = { const requireStringLiterals = context.options[0] && context.options[0].requireStringLiterals; + let globalScope; + + /** + * Checks whether the given node represents a reference to a global variable that is not declared in the source code. + * These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables. + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a reference to a global variable. + */ + function isReferenceToGlobalVariable(node) { + const variable = globalScope.set.get(node.name); + + return variable && variable.defs.length === 0 && + variable.references.some(ref => ref.identifier === node); + } + /** * Determines whether a node is a typeof expression. * @param {ASTNode} node The node @@ -59,6 +77,10 @@ module.exports = { return { + Program() { + globalScope = context.getScope(); + }, + UnaryExpression(node) { if (isTypeofExpression(node)) { const parent = context.getAncestors().pop(); @@ -72,6 +94,20 @@ module.exports = { if (VALID_TYPES.indexOf(value) === -1) { context.report({ node: sibling, messageId: "invalidValue" }); } + } else if (sibling.type === "Identifier" && sibling.name === "undefined" && isReferenceToGlobalVariable(sibling)) { + context.report({ + node: sibling, + messageId: requireStringLiterals ? "notString" : "invalidValue", + suggest: [ + { + messageId: "suggestString", + data: { type: "undefined" }, + fix(fixer) { + return fixer.replaceText(sibling, '"undefined"'); + } + } + ] + }); } else if (requireStringLiterals && !isTypeofExpression(sibling)) { context.report({ node: sibling, messageId: "notString" }); } diff --git a/tests/lib/rules/valid-typeof.js b/tests/lib/rules/valid-typeof.js index cd28088a0ed..35a52f0a6c0 100644 --- a/tests/lib/rules/valid-typeof.js +++ b/tests/lib/rules/valid-typeof.js @@ -45,6 +45,7 @@ ruleTester.run("valid-typeof", rule, { "typeof(foo) == 'string'", "typeof(foo) != 'string'", "var oddUse = typeof foo + 'thing'", + "function f(undefined) { typeof x === undefined }", { code: "typeof foo === 'number'", options: [{ requireStringLiterals: true }] @@ -136,6 +137,21 @@ ruleTester.run("valid-typeof", rule, { options: [{ requireStringLiterals: true }], errors: [{ messageId: "invalidValue", type: "Literal" }] }, + { + code: "if (typeof bar !== undefined) {}", + errors: [ + { + messageId: "invalidValue", + type: "Identifier", + suggestions: [ + { + messageId: "suggestString", + data: { type: "undefined" }, + output: 'if (typeof bar !== "undefined") {}' + } + ] + }] + }, { code: "typeof foo == Object", options: [{ requireStringLiterals: true }], @@ -144,17 +160,50 @@ ruleTester.run("valid-typeof", rule, { { code: "typeof foo === undefined", options: [{ requireStringLiterals: true }], - errors: [{ messageId: "notString", type: "Identifier" }] + errors: [ + { + messageId: "notString", + type: "Identifier", + suggestions: [ + { + messageId: "suggestString", + data: { type: "undefined" }, + output: 'typeof foo === "undefined"' + } + ] + }] }, { code: "undefined === typeof foo", options: [{ requireStringLiterals: true }], - errors: [{ messageId: "notString", type: "Identifier" }] + errors: [ + { + messageId: "notString", + type: "Identifier", + suggestions: [ + { + messageId: "suggestString", + data: { type: "undefined" }, + output: '"undefined" === typeof foo' + } + ] + }] }, { code: "undefined == typeof foo", options: [{ requireStringLiterals: true }], - errors: [{ messageId: "notString", type: "Identifier" }] + errors: [ + { + messageId: "notString", + type: "Identifier", + suggestions: [ + { + messageId: "suggestString", + data: { type: "undefined" }, + output: '"undefined" == typeof foo' + } + ] + }] }, { code: "typeof foo === `undefined${foo}`",