diff --git a/docs/rules/no-deprecated-context-methods.md b/docs/rules/no-deprecated-context-methods.md index 4ac017f7..ee61dc85 100644 --- a/docs/rules/no-deprecated-context-methods.md +++ b/docs/rules/no-deprecated-context-methods.md @@ -39,8 +39,8 @@ Examples of **incorrect** code for this rule: module.exports = { create (context) { return { - Program (node) { - const firstToken = context.getFirstToken(node); + Program (ast) { + const firstToken = context.getFirstToken(ast); }, }; }, @@ -55,8 +55,8 @@ module.exports = { const sourceCode = context.getSourceCode(); return { - Program (node) { - const firstToken = sourceCode.getFirstToken(node); + Program (ast) { + const firstToken = sourceCode.getFirstToken(ast); }, }; }, diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.js index 54371889..0730c9ea 100644 --- a/lib/rules/fixer-return.js +++ b/lib/rules/fixer-return.js @@ -93,8 +93,9 @@ module.exports = { } return { - Program (node) { - contextIdentifiers = utils.getContextIdentifiers(context, node); + Program (ast) { + const sourceCode = context.getSourceCode(); + contextIdentifiers = utils.getContextIdentifiers(sourceCode.scopeManager, ast); }, // Stacks this function's information. diff --git a/lib/rules/no-deprecated-context-methods.js b/lib/rules/no-deprecated-context-methods.js index c3f96f02..e29cd351 100644 --- a/lib/rules/no-deprecated-context-methods.js +++ b/lib/rules/no-deprecated-context-methods.js @@ -58,8 +58,8 @@ module.exports = { // ---------------------------------------------------------------------- return { - 'Program:exit' () { - [...utils.getContextIdentifiers(context, sourceCode.ast)] + 'Program:exit' (ast) { + [...utils.getContextIdentifiers(sourceCode.scopeManager, ast)] .filter( contextId => contextId.parent.type === 'MemberExpression' && diff --git a/lib/rules/no-deprecated-report-api.js b/lib/rules/no-deprecated-report-api.js index f6532f12..1084f972 100644 --- a/lib/rules/no-deprecated-report-api.js +++ b/lib/rules/no-deprecated-report-api.js @@ -36,8 +36,8 @@ module.exports = { // ---------------------------------------------------------------------- return { - Program (node) { - contextIdentifiers = utils.getContextIdentifiers(context, node); + Program (ast) { + contextIdentifiers = utils.getContextIdentifiers(sourceCode.scopeManager, ast); }, CallExpression (node) { if ( diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.js index d0259591..ce0b606c 100644 --- a/lib/rules/no-missing-placeholders.js +++ b/lib/rules/no-missing-placeholders.js @@ -37,7 +37,8 @@ module.exports = { return { Program (ast) { - contextIdentifiers = utils.getContextIdentifiers(context, ast); + const sourceCode = context.getSourceCode(); + contextIdentifiers = utils.getContextIdentifiers(sourceCode.scopeManager, ast); }, CallExpression (node) { if ( diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.js index 61c6faba..3bc5f328 100644 --- a/lib/rules/no-unused-placeholders.js +++ b/lib/rules/no-unused-placeholders.js @@ -37,7 +37,8 @@ module.exports = { return { Program (ast) { - contextIdentifiers = utils.getContextIdentifiers(context, ast); + const sourceCode = context.getSourceCode(); + contextIdentifiers = utils.getContextIdentifiers(sourceCode.scopeManager, ast); }, CallExpression (node) { if ( diff --git a/lib/rules/no-useless-token-range.js b/lib/rules/no-useless-token-range.js index cbabf1a5..13e51f2e 100644 --- a/lib/rules/no-useless-token-range.js +++ b/lib/rules/no-useless-token-range.js @@ -107,7 +107,7 @@ module.exports = { return { 'Program:exit' (ast) { - [...utils.getSourceCodeIdentifiers(context, ast)] + [...utils.getSourceCodeIdentifiers(sourceCode.scopeManager, ast)] .filter(identifier => identifier.parent.type === 'MemberExpression' && identifier.parent.object === identifier && identifier.parent.property.type === 'Identifier' && diff --git a/lib/rules/prefer-message-ids.js b/lib/rules/prefer-message-ids.js index 832ed42a..104c89e1 100644 --- a/lib/rules/prefer-message-ids.js +++ b/lib/rules/prefer-message-ids.js @@ -36,9 +36,9 @@ module.exports = { return { Program (ast) { - contextIdentifiers = utils.getContextIdentifiers(context, ast); + contextIdentifiers = utils.getContextIdentifiers(sourceCode.scopeManager, ast); - if (info === null || info.meta === null) { + if (info === null) { return; } @@ -49,7 +49,7 @@ module.exports = { metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'messages'); if (!messagesNode) { - context.report({ node: metaNode, messageId: 'messagesMissing' }); + context.report({ node: metaNode || info.create, messageId: 'messagesMissing' }); return; } diff --git a/lib/rules/prefer-placeholders.js b/lib/rules/prefer-placeholders.js index de53adb3..1fce6b0f 100644 --- a/lib/rules/prefer-placeholders.js +++ b/lib/rules/prefer-placeholders.js @@ -39,8 +39,8 @@ module.exports = { // ---------------------------------------------------------------------- return { - Program (node) { - contextIdentifiers = utils.getContextIdentifiers(context, node); + Program (ast) { + contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast); }, CallExpression (node) { if ( diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.js index 685f9cf1..a99fb279 100644 --- a/lib/rules/prefer-replace-text.js +++ b/lib/rules/prefer-replace-text.js @@ -38,8 +38,8 @@ module.exports = { let contextIdentifiers; return { - Program (node) { - contextIdentifiers = utils.getContextIdentifiers(context, node); + Program (ast) { + contextIdentifiers = utils.getContextIdentifiers(sourceCode.scopeManager, ast); }, // Stacks this function's information. diff --git a/lib/rules/report-message-format.js b/lib/rules/report-message-format.js index cd620e54..1b67a035 100644 --- a/lib/rules/report-message-format.js +++ b/lib/rules/report-message-format.js @@ -59,9 +59,10 @@ module.exports = { // ---------------------------------------------------------------------- return { - Program (node) { - contextIdentifiers = utils.getContextIdentifiers(context, node); - const ruleInfo = utils.getRuleInfo(context.getSourceCode()); + Program (ast) { + const sourceCode = context.getSourceCode(); + contextIdentifiers = utils.getContextIdentifiers(sourceCode.scopeManager, ast); + const ruleInfo = utils.getRuleInfo(sourceCode); const messagesObject = ruleInfo && ruleInfo.meta && ruleInfo.meta.type === 'ObjectExpression' && diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.js index f1c029c7..f5aabb59 100644 --- a/lib/rules/require-meta-docs-description.js +++ b/lib/rules/require-meta-docs-description.js @@ -39,12 +39,12 @@ module.exports = { }, create (context) { - const sourceCode = context.getSourceCode(); - const info = utils.getRuleInfo(sourceCode); - return { Program () { - if (info === null || info.meta === null) { + const sourceCode = context.getSourceCode(); + const info = utils.getRuleInfo(sourceCode); + + if (info === null) { return; } @@ -62,7 +62,7 @@ module.exports = { docsNode.value.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'description'); if (!descriptionNode) { - context.report({ node: docsNode ? docsNode : metaNode, messageId: 'missing' }); + context.report({ node: docsNode || metaNode || info.create, messageId: 'missing' }); return; } diff --git a/lib/rules/require-meta-fixable.js b/lib/rules/require-meta-fixable.js index 0569fc53..cc2fa961 100644 --- a/lib/rules/require-meta-fixable.js +++ b/lib/rules/require-meta-fixable.js @@ -57,8 +57,8 @@ module.exports = { // ---------------------------------------------------------------------- return { - Program (node) { - contextIdentifiers = utils.getContextIdentifiers(context, node); + Program (ast) { + contextIdentifiers = utils.getContextIdentifiers(sourceCode.scopeManager, ast); }, CallExpression (node) { if ( diff --git a/lib/rules/require-meta-has-suggestions.js b/lib/rules/require-meta-has-suggestions.js index f62a9b0b..8977799a 100644 --- a/lib/rules/require-meta-has-suggestions.js +++ b/lib/rules/require-meta-has-suggestions.js @@ -31,8 +31,8 @@ module.exports = { let ruleReportsSuggestions; return { - Program (node) { - contextIdentifiers = utils.getContextIdentifiers(context, node); + Program (ast) { + contextIdentifiers = utils.getContextIdentifiers(sourceCode.scopeManager, ast); }, CallExpression (node) { if ( @@ -71,7 +71,7 @@ module.exports = { if (!hasSuggestionsProperty) { // Rule reports suggestions but is missing the `meta.hasSuggestions` property altogether. context.report({ - node: metaNode ? metaNode : ruleInfo.create, + node: metaNode || ruleInfo.create, messageId: 'shouldBeSuggestable', fix (fixer) { if (metaNode && metaNode.type === 'ObjectExpression') { diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.js index e838d85d..9ffed3e4 100644 --- a/lib/rules/require-meta-schema.js +++ b/lib/rules/require-meta-schema.js @@ -41,7 +41,7 @@ module.exports = { const sourceCode = context.getSourceCode(); const { scopeManager } = sourceCode; const info = utils.getRuleInfo(sourceCode); - if (info === null || info.meta === null) { + if (info === null) { return {}; } @@ -57,7 +57,7 @@ module.exports = { return { Program (ast) { - contextIdentifiers = utils.getContextIdentifiers(context, ast); + contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast); schemaNode = metaNode && @@ -106,16 +106,16 @@ module.exports = { 'Program:exit' () { if (!schemaNode && requireSchemaPropertyWhenOptionless) { context.report({ - node: metaNode, + node: metaNode || info.create, messageId: 'missing', - suggest: isUsingOptions ? [] : [ + suggest: !isUsingOptions && metaNode && metaNode.type === 'ObjectExpression' ? [ { messageId: 'addEmptySchema', fix (fixer) { return utils.insertProperty(fixer, metaNode, 'schema: []', sourceCode); }, }, - ], + ] : [], }); } }, @@ -130,7 +130,7 @@ module.exports = { node.property.name === 'options' ) { isUsingOptions = true; - context.report({ node: schemaNode || metaNode, messageId: 'foundOptionsUsage' }); + context.report({ node: schemaNode || metaNode || info.create, messageId: 'foundOptionsUsage' }); } }, }; diff --git a/lib/rules/require-meta-type.js b/lib/rules/require-meta-type.js index 88c22f98..1e8a1b03 100644 --- a/lib/rules/require-meta-type.js +++ b/lib/rules/require-meta-type.js @@ -31,20 +31,16 @@ module.exports = { }, create (context) { - const sourceCode = context.getSourceCode(); - const info = utils.getRuleInfo(sourceCode); - - // ---------------------------------------------------------------------- - // Helpers - // ---------------------------------------------------------------------- - // ---------------------------------------------------------------------- // Public // ---------------------------------------------------------------------- return { Program () { - if (info === null || info.meta === null) { + const sourceCode = context.getSourceCode(); + const info = utils.getRuleInfo(sourceCode); + + if (info === null) { return; } @@ -55,7 +51,7 @@ module.exports = { metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'type'); if (!typeNode) { - context.report({ node: metaNode, messageId: 'missing' }); + context.report({ node: metaNode || info.create, messageId: 'missing' }); return; } diff --git a/lib/utils.js b/lib/utils.js index 3b27af1d..d7a2b82d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,6 @@ 'use strict'; -const { getStaticValue } = require('eslint-utils'); +const { getStaticValue, findVariable } = require('eslint-utils'); const estraverse = require('estraverse'); /** @@ -17,59 +17,6 @@ function isNormalFunctionExpression (node) { return functionTypes.includes(node.type) && !node.generator && !node.async; } -/** -* Determines whether a node is a reference to function expression. -* @param {ASTNode} node The node in question -* @param {ScopeManager} scopeManager The scope manager to use for resolving references -* @returns {boolean} `true` if the node is a reference to a function expression -*/ -function isNormalFunctionExpressionReference (node, scopeManager) { - if (!node || !scopeManager) { - return false; - } - - if (node.type !== 'Identifier') { - return false; - } - - const scope = scopeManager.acquire(node) || scopeManager.globalScope; - const scopes = [scope]; - let createReference; - while (scopes.length > 0) { - const currentScope = scopes.shift(); - const found = currentScope.references.find(reference => { - return reference.resolved && reference.identifier === node; - }); - - if (found) { - createReference = found; - break; - } - - scopes.push(...currentScope.childScopes); - } - - if (!createReference) { - return false; - } - - const definitions = createReference.resolved.defs; - if (!definitions || definitions.length === 0) { - return false; - } - - // Assumes it is immediately initialized to a function - let definitionNode = definitions[0].node; - - // If we find something like `const create = () => {}` then send the - // righthand side into the type check. - if (definitionNode.type === 'VariableDeclarator') { - definitionNode = definitionNode.init; - } - - return isNormalFunctionExpression(definitionNode); -} - /** * Determines whether a node is constructing a RuleTester instance * @param {ASTNode} node The node in question @@ -234,6 +181,33 @@ function findObjectPropertyValueByKeyName (obj, keyName) { return property ? property.value : undefined; } +/** + * Get the first value (or function) that a variable is initialized to. + * @param {Node} node - the Identifier node for the variable. + * @param {ScopeManager} scopeManager + * @returns the first value (or function) that the given variable is initialized to. + */ +function findVariableValue (node, scopeManager) { + const variable = findVariable( + scopeManager.acquire(node) || scopeManager.globalScope, + node + ); + if ( + variable && + variable.defs && + variable.defs[0] && + variable.defs[0].node + ) { + if (variable.defs[0].node.type === 'VariableDeclarator' && variable.defs[0].node.init) { + // Given node `x`, get `123` from `const x = 123;`. + return variable.defs[0].node.init; + } else if (variable.defs[0].node.type === 'FunctionDeclaration') { + // Given node `foo`, get `function foo() {}` from `function foo() {}`. + return variable.defs[0].node; + } + } +} + module.exports = { /** * Performs static analysis on an AST to try to determine the final value of `module.exports`. @@ -251,10 +225,18 @@ module.exports = { return null; } - const createIsFunction = isNormalFunctionExpression(exportNodes.create); - const createIsFunctionReference = isNormalFunctionExpressionReference(exportNodes.create, scopeManager); + // If create/meta are defined in variables, get their values. + for (const key of Object.keys(exportNodes)) { + if (exportNodes[key] && exportNodes[key].type === 'Identifier') { + const value = findVariableValue(exportNodes[key], scopeManager); + if (value) { + exportNodes[key] = value; + } + } + } - if (!createIsFunction && !createIsFunctionReference) { + const createIsFunction = isNormalFunctionExpression(exportNodes.create); + if (!createIsFunction) { return null; } @@ -264,19 +246,19 @@ module.exports = { /** * Gets all the identifiers referring to the `context` variable in a rule source file. Note that this function will * only work correctly after traversing the AST has started (e.g. in the first `Program` node). - * @param {RuleContext} context The `context` variable for the source file itself + * @param {RuleContext} scopeManager * @param {ASTNode} ast The `Program` node for the file * @returns {Set} A Set of all `Identifier` nodes that are references to the `context` value for the file */ - getContextIdentifiers (context, ast) { - const ruleInfo = module.exports.getRuleInfo({ ast }); + getContextIdentifiers (scopeManager, ast) { + const ruleInfo = module.exports.getRuleInfo({ ast, scopeManager }); if (!ruleInfo || ruleInfo.create.params.length === 0 || ruleInfo.create.params[0].type !== 'Identifier') { return new Set(); } return new Set( - context.getDeclaredVariables(ruleInfo.create) + scopeManager.getDeclaredVariables(ruleInfo.create) .find(variable => variable.name === ruleInfo.create.params[0].name) .references .map(ref => ref.identifier) @@ -412,12 +394,12 @@ module.exports = { /** * Gets a set of all `sourceCode` identifiers. - * @param {RuleContext} context The context for the rule file + * @param {ScopeManager} scopeManager * @param {ASTNode} ast The AST of the file. This must have `parent` properties. * @returns {Set} A set of all identifiers referring to the `SourceCode` object. */ - getSourceCodeIdentifiers (context, ast) { - return new Set([...module.exports.getContextIdentifiers(context, ast)] + getSourceCodeIdentifiers (scopeManager, ast) { + return new Set([...module.exports.getContextIdentifiers(scopeManager, ast)] .filter(identifier => identifier.parent && identifier.parent.type === 'MemberExpression' && identifier === identifier.parent.object && @@ -429,7 +411,7 @@ module.exports = { identifier.parent.parent === identifier.parent.parent.parent.init && identifier.parent.parent.parent.id.type === 'Identifier' ) - .flatMap(identifier => context.getDeclaredVariables(identifier.parent.parent.parent)) + .flatMap(identifier => scopeManager.getDeclaredVariables(identifier.parent.parent.parent)) .flatMap(variable => variable.references) .map(ref => ref.identifier)); }, diff --git a/tests/lib/rules/fixer-return.js b/tests/lib/rules/fixer-return.js index 4e4afec8..00f04168 100644 --- a/tests/lib/rules/fixer-return.js +++ b/tests/lib/rules/fixer-return.js @@ -288,6 +288,20 @@ ruleTester.run('fixer-return', rule, { `, errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 24 }], }, + { + // Fix but missing return + code: ` + function create(context) { + context.report({ + fix(fixer) { + fixer.foo(); + } + }); + } + module.exports = { create }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 4, column: 20 }], + }, { // Fix but missing return (suggestion) code: ` diff --git a/tests/lib/rules/meta-property-ordering.js b/tests/lib/rules/meta-property-ordering.js index bc9386ca..7e851ddc 100644 --- a/tests/lib/rules/meta-property-ordering.js +++ b/tests/lib/rules/meta-property-ordering.js @@ -71,6 +71,7 @@ ruleTester.run('test-case-property-ordering', rule, { meta: {}, create() {}, };`, + 'module.exports = { create() {} };', // No `meta`. ], invalid: [ diff --git a/tests/lib/rules/no-deprecated-context-methods.js b/tests/lib/rules/no-deprecated-context-methods.js index b46c22fb..84e3ba9d 100644 --- a/tests/lib/rules/no-deprecated-context-methods.js +++ b/tests/lib/rules/no-deprecated-context-methods.js @@ -42,8 +42,8 @@ ruleTester.run('no-deprecated-context-methods', rule, { module.exports = { create(context) { return { - Program(node) { - context.getSource(node); + Program(ast) { + context.getSource(ast); } } } @@ -53,8 +53,8 @@ ruleTester.run('no-deprecated-context-methods', rule, { module.exports = { create(context) { return { - Program(node) { - context.getSourceCode().getText(node); + Program(ast) { + context.getSourceCode().getText(ast); } } } @@ -85,5 +85,17 @@ ruleTester.run('no-deprecated-context-methods', rule, { }, ], }, + { + // `create` in variable. + code: ` + const create = function(context) { return { Program(ast) { context.getSource(ast); } } }; + module.exports = { create }; + `, + output: ` + const create = function(context) { return { Program(ast) { context.getSourceCode().getText(ast); } } }; + module.exports = { create }; + `, + errors: [{ message: 'Use `context.getSourceCode().getText` instead of `context.getSource`.', type: 'MemberExpression' }], + }, ], }); diff --git a/tests/lib/rules/no-deprecated-report-api.js b/tests/lib/rules/no-deprecated-report-api.js index b8a101bb..0e65b591 100644 --- a/tests/lib/rules/no-deprecated-report-api.js +++ b/tests/lib/rules/no-deprecated-report-api.js @@ -313,5 +313,14 @@ ruleTester.run('no-deprecated-report-api', rule, { output: null, errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, + { + // `create` in variable. + code: ` + function create(context) { context.report(...error); } + module.exports = { create }; + `, + output: null, + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], + }, ], }); diff --git a/tests/lib/rules/no-missing-placeholders.js b/tests/lib/rules/no-missing-placeholders.js index 9e9129b1..f41e1051 100644 --- a/tests/lib/rules/no-missing-placeholders.js +++ b/tests/lib/rules/no-missing-placeholders.js @@ -270,5 +270,19 @@ ruleTester.run('no-missing-placeholders', rule, { `, errors: [error('bar')], }, + { + // `create` in variable. + code: ` + function create(context) { + context.report({ + node, + message: 'foo {{hasOwnProperty}}', + data: {} + }); + } + module.exports = { create }; + `, + errors: [error('hasOwnProperty')], + }, ], }); diff --git a/tests/lib/rules/no-unused-placeholders.js b/tests/lib/rules/no-unused-placeholders.js index 5900eef8..71ff539f 100644 --- a/tests/lib/rules/no-unused-placeholders.js +++ b/tests/lib/rules/no-unused-placeholders.js @@ -140,6 +140,20 @@ ruleTester.run('no-unused-placeholders', rule, { `, errors: [error('bar')], }, + { + // With `create` as variable. + code: ` + function create(context) { + context.report({ + node, + message: 'foo', + data: { bar } + }); + } + module.exports = { create }; + `, + errors: [error('bar')], + }, { // With message as variable. code: ` diff --git a/tests/lib/rules/no-useless-token-range.js b/tests/lib/rules/no-useless-token-range.js index 19671f10..8dc5cf39 100644 --- a/tests/lib/rules/no-useless-token-range.js +++ b/tests/lib/rules/no-useless-token-range.js @@ -32,6 +32,39 @@ function wrapRule (code) { // Tests // ------------------------------------------------------------------------------ +const INVALID_CASES = [ + { + code: 'sourceCode.getFirstToken(foo).range[0]', + output: 'foo.range[0]', + errors: [{ message: "Use 'foo.range[0]' instead.", type: 'CallExpression' }], + }, + { + code: 'sourceCode.getFirstToken(foo).start', + output: 'foo.start', + errors: [{ message: "Use 'foo.start' instead.", type: 'CallExpression' }], + }, + { + code: 'sourceCode.getLastToken(foo).range[1]', + output: 'foo.range[1]', + errors: [{ message: "Use 'foo.range[1]' instead.", type: 'CallExpression' }], + }, + { + code: 'sourceCode.getLastToken(foo).end', + output: 'foo.end', + errors: [{ message: "Use 'foo.end' instead.", type: 'CallExpression' }], + }, + { + code: 'sourceCode.getFirstToken(foo, { includeComments: true }).range[0]', + output: 'foo.range[0]', + errors: [{ message: "Use 'foo.range[0]' instead.", type: 'CallExpression' }], + }, + { + code: 'sourceCode.getLastToken(foo, { includeComments: true }).range[1]', + output: 'foo.range[1]', + errors: [{ message: "Use 'foo.range[1]' instead.", type: 'CallExpression' }], + }, +].map(invalidCase => Object.assign(invalidCase, { code: wrapRule(invalidCase.code), output: wrapRule(invalidCase.output) })); + const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); ruleTester.run('no-useless-token-range', rule, { @@ -49,35 +82,24 @@ ruleTester.run('no-useless-token-range', rule, { ].map(wrapRule), invalid: [ + ...INVALID_CASES, { - code: 'sourceCode.getFirstToken(foo).range[0]', - output: 'foo.range[0]', - errors: [{ message: "Use 'foo.range[0]' instead.", type: 'CallExpression' }], - }, - { - code: 'sourceCode.getFirstToken(foo).start', - output: 'foo.start', - errors: [{ message: "Use 'foo.start' instead.", type: 'CallExpression' }], - }, - { - code: 'sourceCode.getLastToken(foo).range[1]', - output: 'foo.range[1]', - errors: [{ message: "Use 'foo.range[1]' instead.", type: 'CallExpression' }], - }, - { - code: 'sourceCode.getLastToken(foo).end', - output: 'foo.end', - errors: [{ message: "Use 'foo.end' instead.", type: 'CallExpression' }], - }, - { - code: 'sourceCode.getFirstToken(foo, { includeComments: true }).range[0]', - output: 'foo.range[0]', + // `create` as variable. + code: ` + function create(context) { + const sourceCode = context.getSourceCode(); + sourceCode.getFirstToken(foo).range[0] + } + module.exports = { create }; + `, + output: ` + function create(context) { + const sourceCode = context.getSourceCode(); + foo.range[0] + } + module.exports = { create }; + `, errors: [{ message: "Use 'foo.range[0]' instead.", type: 'CallExpression' }], }, - { - code: 'sourceCode.getLastToken(foo, { includeComments: true }).range[1]', - output: 'foo.range[1]', - errors: [{ message: "Use 'foo.range[1]' instead.", type: 'CallExpression' }], - }, - ].map(invalidCase => Object.assign(invalidCase, { code: wrapRule(invalidCase.code), output: wrapRule(invalidCase.output) })), + ], }); diff --git a/tests/lib/rules/prefer-message-ids.js b/tests/lib/rules/prefer-message-ids.js index 952509f9..f97df121 100644 --- a/tests/lib/rules/prefer-message-ids.js +++ b/tests/lib/rules/prefer-message-ids.js @@ -17,6 +17,7 @@ ruleTester.run('prefer-message-ids', rule, { valid: [ ` module.exports = { + meta: { messages: { foo: 'hello world' } }, create(context) { context.report({ node }); } @@ -24,6 +25,7 @@ ruleTester.run('prefer-message-ids', rule, { `, ` module.exports = { + meta: { messages: { foo: 'hello world' } }, create(context) { context.report({ node, messageId: 'foo' }); } @@ -32,6 +34,7 @@ ruleTester.run('prefer-message-ids', rule, { // Suggestion ` module.exports = { + meta: { messages: { foo: 'hello world' } }, create(context) { context.report({ node, suggest: [{messageId:'foo'}] }); } @@ -41,6 +44,7 @@ ruleTester.run('prefer-message-ids', rule, { // ESM code: ` export default { + meta: { messages: { foo: 'hello world' } }, create(context) { context.report({ node, messageId: 'foo' }); } @@ -50,6 +54,7 @@ ruleTester.run('prefer-message-ids', rule, { }, ` module.exports = { + meta: { messages: { foo: 'hello world' } }, create(context) { foo.report({ node, message: 'foo' }); // unrelated function } @@ -57,6 +62,7 @@ ruleTester.run('prefer-message-ids', rule, { `, ` module.exports = { + meta: { messages: { foo: 'hello world' } }, create(context) { context.foo({ node, message: 'foo' }); // unrelated function } @@ -65,6 +71,7 @@ ruleTester.run('prefer-message-ids', rule, { ` context.report({ node, message: 'foo' }); // outside rule module.exports = { + meta: { messages: { foo: 'hello world' } }, create(context) { } }; @@ -103,6 +110,7 @@ ruleTester.run('prefer-message-ids', rule, { { code: ` module.exports = { + meta: { messages: { foo: 'hello world' } }, create(context) { context.report({ node, message: 'foo' }); } @@ -114,6 +122,7 @@ ruleTester.run('prefer-message-ids', rule, { // Suggestion code: ` module.exports = { + meta: { messages: { foo: 'hello world' } }, create(context) { context.report({ node, suggest: [{desc:'foo'}] }); } @@ -125,6 +134,7 @@ ruleTester.run('prefer-message-ids', rule, { // ESM code: ` export default { + meta: { messages: { foo: 'hello world' } }, create(context) { context.report({ node, message: 'foo' }); } @@ -138,6 +148,7 @@ ruleTester.run('prefer-message-ids', rule, { code: ` const MESSAGE = \`\${foo} is bad.\`; module.exports = { + meta: { messages: { foo: 'hello world' } }, create(context) { context.report({ node, @@ -152,6 +163,7 @@ ruleTester.run('prefer-message-ids', rule, { // With constructed message. code: ` module.exports = { + meta: { messages: { foo: 'hello world' } }, create(context) { context.report({ node, @@ -217,5 +229,28 @@ ruleTester.run('prefer-message-ids', rule, { { messageId: 'foundMessage', type: 'Property' }, ], }, + { + // `meta` missing. + code: ` + module.exports = { + create(context) { + context.report({ node }); + } + }; + `, + errors: [{ messageId: 'messagesMissing', type: 'FunctionExpression' }], + }, + { + // `meta` / `create` in variables, `messages` missing, using `message`. + code: ` + const meta = {}; + const create = function (context) { context.report({ node, message: 'foo' }); } + module.exports = { meta, create }; + `, + errors: [ + { messageId: 'messagesMissing', type: 'ObjectExpression' }, + { messageId: 'foundMessage', type: 'Property' }, + ], + }, ], }); diff --git a/tests/lib/rules/prefer-object-rule.js b/tests/lib/rules/prefer-object-rule.js index d7d7d78d..5189da81 100644 --- a/tests/lib/rules/prefer-object-rule.js +++ b/tests/lib/rules/prefer-object-rule.js @@ -61,7 +61,13 @@ ruleTester.run('prefer-object-rule', rule, { }; module.exports = rule; `, - + // `create` as variable. + ` + function create(context) { + return { Program() { context.report() } }; + }; + module.exports = { create }; + `, { // ESM code: ` diff --git a/tests/lib/rules/prefer-placeholders.js b/tests/lib/rules/prefer-placeholders.js index 1bf9f401..c9ef140b 100644 --- a/tests/lib/rules/prefer-placeholders.js +++ b/tests/lib/rules/prefer-placeholders.js @@ -184,5 +184,18 @@ ruleTester.run('prefer-placeholders', rule, { `, errors: [{ messageId: 'usePlaceholders', type: 'TemplateLiteral' }], }, + { + // `create` in variable. + code: ` + function create(context) { + context.report({ + node, + message: \`\${foo} is bad.\` + }); + } + module.exports = { create }; + `, + errors: [{ messageId: 'usePlaceholders', type: 'TemplateLiteral' }], + }, ], }); diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.js index 1341c71a..b38aead8 100644 --- a/tests/lib/rules/prefer-replace-text.js +++ b/tests/lib/rules/prefer-replace-text.js @@ -158,5 +158,19 @@ ruleTester.run('prefer-placeholders', rule, { `, errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }], }, + { + // `create` in variable. + code: ` + const create = function(context) { + context.report({ + fix(fixer) { + return fixer.replaceTextRange([node.range[0], node.range[1]], ''); + } + }); + }; + module.exports = { create }; + `, + errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }], + }, ], }); diff --git a/tests/lib/rules/report-message-format.js b/tests/lib/rules/report-message-format.js index 2dae34f1..4082a631 100644 --- a/tests/lib/rules/report-message-format.js +++ b/tests/lib/rules/report-message-format.js @@ -296,6 +296,16 @@ ruleTester.run('report-message-format', rule, { `, options: ['foo'], }, + { + // `create` in variable. + code: ` + function create(context) { + context.report(node, 'bar'); + } + module.exports = { create }; + `, + options: ['foo'], + }, ].map(invalidCase => { return Object.assign({ errors: [{ message: `Report message does not match the pattern '${invalidCase.options[0]}'.` }], diff --git a/tests/lib/rules/require-meta-docs-description.js b/tests/lib/rules/require-meta-docs-description.js index 815692b1..5627051c 100644 --- a/tests/lib/rules/require-meta-docs-description.js +++ b/tests/lib/rules/require-meta-docs-description.js @@ -101,6 +101,14 @@ ruleTester.run('require-meta-docs-description', rule, { `, options: [{ pattern: '.+' }], // any description allowed }, + // `meta` in variable, `description` present. + ` + const meta = { docs: { description: 'enforce foo' } }; + module.exports = { + meta, + create(context) {} + }; + `, ], invalid: [ @@ -114,6 +122,24 @@ ruleTester.run('require-meta-docs-description', rule, { output: null, errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, + { + // No `meta`. Violation on `create`. + code: 'module.exports = { create(context) {} };', + output: null, + errors: [{ messageId: 'missing', type: 'FunctionExpression' }], + }, + { + // `meta` in variable, `description` mismatch. + code: ` + const meta = { docs: { description: 'foo' } }; + module.exports = { + meta, + create(context) {} + }; + `, + output: null, + errors: [{ messageId: 'mismatch', type: 'Literal' }], + }, { // ESM code: ` diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.js index f7be9a8c..a1cd0805 100644 --- a/tests/lib/rules/require-meta-docs-url.js +++ b/tests/lib/rules/require-meta-docs-url.js @@ -130,6 +130,12 @@ tester.run('require-meta-docs-url', rule, { output: null, errors: [{ messageId: 'missing', type: 'FunctionExpression' }], }, + { + // No `meta`. Violation on `create`. + code: 'module.exports = { create() {} }', + output: null, + errors: [{ messageId: 'missing', type: 'FunctionExpression' }], + }, { code: ` module.exports = { diff --git a/tests/lib/rules/require-meta-fixable.js b/tests/lib/rules/require-meta-fixable.js index 2d2dd245..3fe38572 100644 --- a/tests/lib/rules/require-meta-fixable.js +++ b/tests/lib/rules/require-meta-fixable.js @@ -19,6 +19,12 @@ const RuleTester = require('eslint').RuleTester; const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); ruleTester.run('require-meta-fixable', rule, { valid: [ + // No `meta`. + ` + module.exports = { + create(context) {} + }; + `, ` module.exports = { meta: {}, @@ -125,9 +131,9 @@ ruleTester.run('require-meta-fixable', rule, { `, { code: ` - const meta = {}; + const extra = {}; module.exports = { - ...meta, + ...extra, meta: {}, create(context) { context.report(node, message, data, fix); } }; @@ -187,32 +193,41 @@ ruleTester.run('require-meta-fixable', rule, { invalid: [ { + // No `meta`. Violation on `create`. code: ` module.exports = { - meta: {}, create(context) { context.report({node, message, fix: foo}); } }; `, - errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + errors: [{ messageId: 'missing', type: 'FunctionExpression' }], }, { - // ESM + // `create` as variable. code: ` - export default { + const create = function(context) { context.report({node, message, fix: foo}); } + module.exports = { create }; + `, + errors: [{ messageId: 'missing', type: 'FunctionExpression' }], + }, + { + code: ` + module.exports = { meta: {}, create(context) { context.report({node, message, fix: foo}); } }; `, - parserOptions: { sourceType: 'module' }, errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { + // ESM code: ` - module.exports = { + export default { + meta: {}, create(context) { context.report({node, message, fix: foo}); } }; `, - errors: [{ messageId: 'missing', type: 'FunctionExpression' }], + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` diff --git a/tests/lib/rules/require-meta-has-suggestions.js b/tests/lib/rules/require-meta-has-suggestions.js index d49e5bdc..bf81e539 100644 --- a/tests/lib/rules/require-meta-has-suggestions.js +++ b/tests/lib/rules/require-meta-has-suggestions.js @@ -174,9 +174,9 @@ ruleTester.run('require-meta-has-suggestions', rule, { // Spread syntax. { code: ` - const meta = {}; + const extra = {}; module.exports = { - ...meta, + ...extra, meta: {}, create(context) { context.report(node, message, data, fix); } }; @@ -198,6 +198,15 @@ ruleTester.run('require-meta-has-suggestions', rule, { output: null, errors: [{ messageId: 'shouldBeSuggestable', type: 'FunctionExpression', line: 3, column: 17, endLine: 3, endColumn: 78 }], }, + { + // `create` as variable. + code: ` + function create(context) { context.report({node, message, suggest: [{}]}); } + module.exports = { create }; + `, + output: null, + errors: [{ messageId: 'shouldBeSuggestable', type: 'FunctionDeclaration', line: 2, column: 8, endLine: 2, endColumn: 84 }], + }, { // ESM: Reports suggestions, no meta object, violation should be on `create` function. code: ` diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.js index 07a0e5b0..8f7ee262 100644 --- a/tests/lib/rules/require-meta-schema.js +++ b/tests/lib/rules/require-meta-schema.js @@ -104,6 +104,11 @@ ruleTester.run('require-meta-schema', rule, { `, options: [{ requireSchemaPropertyWhenOptionless: false }], }, + { + // requireSchemaPropertyWhenOptionless = false, no `meta`. + code: 'module.exports = { create(context) {} };', + options: [{ requireSchemaPropertyWhenOptionless: false }], + }, ], invalid: [ @@ -133,6 +138,18 @@ schema: [] }], }], }, + { + // No `meta`. Violation on `create`. + code: 'module.exports = { create(context) {} };', + output: null, + errors: [ + { + messageId: 'missing', + type: 'FunctionExpression', + suggestions: [], + }, + ], + }, { // requireSchemaPropertyWhenOptionless = true. code: ` @@ -323,5 +340,18 @@ schema: [] }, { messageId: 'missing', type: 'ObjectExpression', suggestions: [] }, ], }, + { + // `create` as variable. + code: ` + const meta = {}; + const create = function create(context) { const options = context.options; } + module.exports = { meta, create }; + `, + output: null, + errors: [ + { messageId: 'foundOptionsUsage', type: 'ObjectExpression', suggestions: [] }, + { messageId: 'missing', type: 'ObjectExpression', suggestions: [] }, + ], + }, ], }); diff --git a/tests/lib/rules/require-meta-type.js b/tests/lib/rules/require-meta-type.js index 33b118c1..cb05f6d1 100644 --- a/tests/lib/rules/require-meta-type.js +++ b/tests/lib/rules/require-meta-type.js @@ -66,9 +66,6 @@ ruleTester.run('require-meta-type', rule, { create(context) {} }; `, - `module.exports = { - create(context) {} - }`, { code: ` const create = {}; @@ -91,6 +88,16 @@ ruleTester.run('require-meta-type', rule, { `, errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, + { + // No `meta`. Violation on `create`. + code: 'module.exports = { create(context) {} };', + errors: [{ messageId: 'missing', type: 'FunctionExpression' }], + }, + { + // `meta` in variable, missing `type`. + code: 'const meta = {}; module.exports = { meta, create(context) {} };', + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + }, { // ESM code: ` diff --git a/tests/lib/utils.js b/tests/lib/utils.js index bca41252..ac32bc65 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -26,6 +26,8 @@ describe('utils', () => { 'module.exports = { create: foo }', 'module.exports = { create: function* foo() {} }', 'module.exports = { create: async function foo() {} }', + 'module.exports = { create, meta }', + 'module.exports = { create: getCreate(), meta: getMeta() }', // Function-style rule but missing object return. 'module.exports = (context) => { }', @@ -48,7 +50,8 @@ describe('utils', () => { ].forEach(noRuleCase => { it(`returns null for ${noRuleCase}`, () => { const ast = espree.parse(noRuleCase, { ecmaVersion: 8, range: true }); - assert.isNull(utils.getRuleInfo({ ast }), 'Expected no rule to be found'); + const scopeManager = eslintScope.analyze(ast); + assert.isNull(utils.getRuleInfo({ ast, scopeManager }), 'Expected no rule to be found'); }); }); }); @@ -84,7 +87,8 @@ describe('utils', () => { ].forEach(noRuleCase => { it(`returns null for ${noRuleCase}`, () => { const ast = espree.parse(noRuleCase, { ecmaVersion: 8, range: true, sourceType: 'module' }); - assert.isNull(utils.getRuleInfo({ ast }), 'Expected no rule to be found'); + const scopeManager = eslintScope.analyze(ast); + assert.isNull(utils.getRuleInfo({ ast, scopeManager }), 'Expected no rule to be found'); }); }); }); @@ -101,7 +105,8 @@ describe('utils', () => { ].forEach(noRuleCase => { it(`returns null for ${noRuleCase}`, () => { const ast = typescriptEslintParser.parse(noRuleCase, { ecmaVersion: 8, range: true, sourceType: 'module' }); - assert.isNull(utils.getRuleInfo({ ast }), 'Expected no rule to be found'); + const scopeManager = eslintScope.analyze(ast); + assert.isNull(utils.getRuleInfo({ ast, scopeManager }), 'Expected no rule to be found'); }); }); }); @@ -115,7 +120,8 @@ describe('utils', () => { ].forEach(noRuleCase => { it(`returns null for ${noRuleCase}`, () => { const ast = typescriptEslintParser.parse(noRuleCase, { range: true, sourceType: 'script' }); - assert.isNull(utils.getRuleInfo({ ast }), 'Expected no rule to be found'); + const scopeManager = eslintScope.analyze(ast); + assert.isNull(utils.getRuleInfo({ ast, scopeManager }), 'Expected no rule to be found'); }); }); }); @@ -138,6 +144,11 @@ describe('utils', () => { meta: { type: 'ObjectExpression' }, isNewStyle: true, }, + 'const create = context => {}; const meta = {}; export default createESLintRule({ create, meta });': { + create: { type: 'ArrowFunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, // Util function with "{} as const". 'export default createESLintRule({ create() {}, meta: {} as const });': { @@ -174,7 +185,8 @@ describe('utils', () => { Object.keys(CASES).forEach(ruleSource => { it(ruleSource, () => { const ast = typescriptEslintParser.parse(ruleSource, { ecmaVersion: 6, range: true, sourceType: 'module' }); - const ruleInfo = utils.getRuleInfo({ ast }); + const scopeManager = eslintScope.analyze(ast); + const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert( lodash.isMatch(ruleInfo, CASES[ruleSource]), `Expected \n${inspect(ruleInfo)}\nto match\n${inspect(CASES[ruleSource])}` @@ -275,12 +287,18 @@ describe('utils', () => { meta: null, isNewStyle: false, }, + 'const create = function(context) { return {}; }; const meta = {}; module.exports = { create, meta };': { + create: { type: 'FunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, }; Object.keys(CASES).forEach(ruleSource => { it(ruleSource, () => { const ast = espree.parse(ruleSource, { ecmaVersion: 6, range: true, sourceType: 'script' }); - const ruleInfo = utils.getRuleInfo({ ast }); + const scopeManager = eslintScope.analyze(ast); + const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert( lodash.isMatch(ruleInfo, CASES[ruleSource]), `Expected \n${inspect(ruleInfo)}\nto match\n${inspect(CASES[ruleSource])}` @@ -302,6 +320,16 @@ describe('utils', () => { meta: { type: 'ObjectExpression' }, isNewStyle: true, }, + 'const create = function(context) { return {}; }; const meta = {}; export default { create, meta }': { + create: { type: 'FunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, + 'function create(context) { return {}; }; const meta = {}; export default { create, meta }': { + create: { type: 'FunctionDeclaration' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, // ESM (function style) 'export default function (context) { return {}; }': { @@ -324,7 +352,8 @@ describe('utils', () => { Object.keys(CASES).forEach(ruleSource => { it(ruleSource, () => { const ast = espree.parse(ruleSource, { ecmaVersion: 6, range: true, sourceType: 'module' }); - const ruleInfo = utils.getRuleInfo({ ast }); + const scopeManager = eslintScope.analyze(ast); + const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert( lodash.isMatch(ruleInfo, CASES[ruleSource]), `Expected \n${inspect(ruleInfo)}\nto match\n${inspect(CASES[ruleSource])}` @@ -343,10 +372,10 @@ describe('utils', () => { const create = (context) => {}; const meta = {}; module.exports = { create, meta }; - `, { ecmaVersion: 6 }); + `, { ecmaVersion: 6, range: true }); const expected = { - create: { type: 'Identifier' }, - meta: { type: 'Identifier' }, + create: { type: 'ArrowFunctionExpression' }, + meta: { type: 'ObjectExpression' }, isNewStyle: true, }; it(`ScopeOptions: ${JSON.stringify(scopeOptions)}`, () => { @@ -416,6 +445,11 @@ describe('utils', () => { ast.body[0].expression.right.properties[1].value.body.body[2].expression, ]; }, + 'const create = function(context) { context }; module.exports = { meta: {}, create };' (ast) { + return [ + ast.body[0].declarations[0].init.body.body[0].expression, + ]; + }, }; Object.keys(CASES).forEach(ruleSource => { @@ -425,6 +459,7 @@ describe('utils', () => { const identifiers = utils.getContextIdentifiers(scope, ast); assert(identifiers instanceof Set, 'getContextIdentifiers should return a Set'); + assert.strictEqual(identifiers.size, CASES[ruleSource](ast).length, 'has the correct number of results'); [...identifiers].forEach((identifier, index) => { assert.strictEqual(identifier, CASES[ruleSource](ast)[index]); });