From c3497d0d8c91bbaa32ebfa49e763ec2b9dfb1601 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Tue, 10 Aug 2021 10:25:46 +0900 Subject: [PATCH] Improve no-use-computed-property-like-method rule - Change to track variables to check types. - Change to track the conditional expression to check types. - Change condition to track branch and return to check types. - Change to check the expressions in template as well. - Fix known bugs. --- lib/rules/no-undef-properties.js | 15 +- lib/rules/no-unused-properties.js | 38 +- .../no-use-computed-property-like-method.js | 671 +++++++++++++----- lib/utils/index.js | 62 ++ lib/utils/property-references.js | 40 +- .../no-use-computed-property-like-method.js | 341 ++++++++- 6 files changed, 907 insertions(+), 260 deletions(-) diff --git a/lib/rules/no-undef-properties.js b/lib/rules/no-undef-properties.js index 0acf49f15..fdd506cc5 100644 --- a/lib/rules/no-undef-properties.js +++ b/lib/rules/no-undef-properties.js @@ -235,7 +235,7 @@ module.exports = { function getVueComponentContextForTemplate() { const keys = [...vueComponentContextMap.keys()] const exported = - keys.find(isScriptSetupProgram) || keys.find(isExportObject) + keys.find(isScriptSetupProgram) || keys.find(utils.isInExportDefault) return exported && vueComponentContextMap.get(exported) /** @@ -244,19 +244,6 @@ module.exports = { function isScriptSetupProgram(node) { return node === programNode } - /** - * @param {ASTNode} node - */ - function isExportObject(node) { - let parent = node.parent - while (parent) { - if (parent.type === 'ExportDefaultDeclaration') { - return true - } - parent = parent.parent - } - return false - } } /** diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js index 00da49d63..0a36c6579 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -79,49 +79,13 @@ const PROPERTY_LABEL = { // Helpers // ------------------------------------------------------------------------------ -/** - * Find the variable of a given name. - * @param {RuleContext} context The rule context - * @param {Identifier} node The variable name to find. - * @returns {Variable|null} The found variable or null. - */ -function findVariable(context, node) { - return eslintUtils.findVariable(getScope(context, node), node) -} -/** - * Gets the scope for the current node - * @param {RuleContext} context The rule context - * @param {ESNode} currentNode The node to get the scope of - * @returns { import('eslint').Scope.Scope } The scope information for this node - */ -function getScope(context, currentNode) { - // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. - const inner = currentNode.type !== 'Program' - const scopeManager = context.getSourceCode().scopeManager - - /** @type {ESNode | null} */ - let node = currentNode - for (; node; node = /** @type {ESNode | null} */ (node.parent)) { - const scope = scopeManager.acquire(node, inner) - - if (scope) { - if (scope.type === 'function-expression-name') { - return scope.childScopes[0] - } - return scope - } - } - - return scopeManager.scopes[0] -} - /** * @param {RuleContext} context * @param {Identifier} id * @returns {Expression} */ function findExpression(context, id) { - const variable = findVariable(context, id) + const variable = utils.findVariableByIdentifier(context, id) if (!variable) { return id } diff --git a/lib/rules/no-use-computed-property-like-method.js b/lib/rules/no-use-computed-property-like-method.js index c1c8ca765..d5c4430d7 100644 --- a/lib/rules/no-use-computed-property-like-method.js +++ b/lib/rules/no-use-computed-property-like-method.js @@ -11,21 +11,99 @@ const eslintUtils = require('eslint-utils') const utils = require('../utils') /** - * @typedef {import('../utils').ComponentPropertyData} ComponentPropertyData + * @typedef {import('eslint').Scope.Scope} Scope * @typedef {import('../utils').ComponentObjectPropertyData} ComponentObjectPropertyData * @typedef {import('../utils').GroupName} GroupName - * - * @typedef {{[key: string]: ComponentPropertyData & { valueType: { type: string | null } }}} PropertyMap */ + +/** + * @typedef {object} CallMember + * @property {string} name + * @property {CallExpression} node + */ + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ +/** @type {Set} */ +const GROUPS = new Set(['data', 'props', 'computed', 'methods']) + +const NATIVE_NOT_FUNCTION_TYPES = new Set([ + 'String', + 'Number', + 'BigInt', + 'Boolean', + 'Object', + 'Array', + 'Symbol' +]) + +/** + * @param {RuleContext} context + * @param {Expression} node + * @returns {Set} + */ +function resolvedExpressions(context, node) { + /** @type {Map>} */ + const resolvedMap = new Map() + + return resolvedExpressionsInternal(node) + + /** + * @param {Expression} node + * @returns {Set} + */ + function resolvedExpressionsInternal(node) { + let resolvedSet = resolvedMap.get(node) + if (!resolvedSet) { + resolvedSet = new Set() + resolvedMap.set(node, resolvedSet) + for (const e of extractResolvedExpressions(node)) { + resolvedSet.add(e) + } + } + + if (!resolvedSet.size) { + resolvedSet.add(node) + } + + return resolvedSet + } + /** + * @param {Expression} node + * @returns {Iterable} + */ + function* extractResolvedExpressions(node) { + if (node.type === 'Identifier') { + const variable = utils.findVariableByIdentifier(context, node) + if (variable) { + for (const ref of variable.references) { + const id = ref.identifier + if (id.parent.type === 'VariableDeclarator') { + if (id.parent.id === id && id.parent.init) { + yield* resolvedExpressionsInternal(id.parent.init) + } + } else if (id.parent.type === 'AssignmentExpression') { + if (id.parent.left === id) { + yield* resolvedExpressionsInternal(id.parent.right) + } + } + } + } + } else if (node.type === 'ConditionalExpression') { + yield* resolvedExpressionsInternal(node.consequent) + yield* resolvedExpressionsInternal(node.alternate) + } + } +} + /** * Get type of props item. * Can't consider array props like: props: {propsA: [String, Number, Function]} - * @param {ComponentObjectPropertyData} property - * @return {string | null} + * @param {RuleContext} context + * @param {ComponentObjectPropertyData} prop + * @return {string[] | null} * * @example * props: { @@ -35,150 +113,367 @@ const utils = require('../utils') * }, * } */ -const getComponentPropsType = (property) => { +function getComponentPropsTypes(context, prop) { + const result = [] + for (const expr of resolvedExpressions(context, prop.property.value)) { + const types = getComponentPropsTypesFromExpression(expr) + if (types == null) { + return null + } + result.push(...types) + } + return result + /** - * Check basic props `props: { basicProps: ... }` + * @param {Expression} expr */ - if (property.property.value.type === 'Identifier') { - return property.property.value.name + function getComponentPropsTypesFromExpression(expr) { + let typeExprs + /** + * Check object props `props: { objectProps: {...} }` + */ + if (expr.type === 'ObjectExpression') { + const type = utils.findProperty(expr, 'type') + if (type == null) return null + + typeExprs = resolvedExpressions(context, type.value) + } else { + typeExprs = [expr] + } + + const result = [] + for (const typeExpr of typeExprs) { + const types = getComponentPropsTypesFromTypeExpression(typeExpr) + if (types == null) { + return null + } + result.push(...types) + } + return result } + /** - * Check object props `props: { objectProps: {...} }` + * @param {Expression} typeExpr */ - if (property.property.value.type === 'ObjectExpression') { - const typeProperty = utils.findProperty(property.property.value, 'type') - if (typeProperty == null) return null - - if (typeProperty.value.type === 'Identifier') return typeProperty.value.name + function getComponentPropsTypesFromTypeExpression(typeExpr) { + if (typeExpr.type === 'Identifier') { + return [typeExpr.name] + } + if (typeExpr.type === 'ArrayExpression') { + const types = [] + for (const element of typeExpr.elements) { + if (!element) { + continue + } + if (element.type === 'SpreadElement') { + return null + } + for (const elementExpr of resolvedExpressions(context, element)) { + if (elementExpr.type !== 'Identifier') { + return null + } + types.push(elementExpr.name) + } + } + return types + } + return null } - return null } /** - * - * @param {any} obj - */ -const getPrototypeType = (obj) => - Object.prototype.toString.call(obj).slice(8, -1) - -/** - * Get return type of property. - * @param {{ property: ComponentPropertyData, propertyMap?: PropertyMap }} args - * @returns {{type: string | null | 'ReturnStatementHasNotArgument'}} + * Check whether given expression may be a function. + * @param {RuleContext} context + * @param {Expression} node + * @returns {boolean} */ -const getValueType = ({ property, propertyMap }) => { - if (property.type === 'array') { - return { - type: null +function maybeFunction(context, node) { + for (const expr of resolvedExpressions(context, node)) { + if ( + expr.type === 'ObjectExpression' || + expr.type === 'ArrayExpression' || + expr.type === 'Literal' || + expr.type === 'TemplateLiteral' || + expr.type === 'BinaryExpression' || + expr.type === 'LogicalExpression' || + expr.type === 'UnaryExpression' || + expr.type === 'UpdateExpression' + ) { + continue } - } - - if (property.type === 'object') { - if (property.groupName === 'props') { - return { - type: getComponentPropsType(property) + if (expr.type === 'ConditionalExpression') { + if ( + !maybeFunction(context, expr.consequent) && + !maybeFunction(context, expr.alternate) + ) { + continue } } + const evaluated = eslintUtils.getStaticValue( + expr, + utils.getScope(context, expr) + ) + if (!evaluated) { + // It could be a function because we don't know what it is. + return true + } + if (typeof evaluated.value === 'function') { + return true + } + } + return false +} - if (property.groupName === 'computed' || property.groupName === 'methods') { - if ( - property.property.value.type === 'FunctionExpression' && - property.property.value.body.type === 'BlockStatement' - ) { - const blockStatement = property.property.value.body +class FunctionData { + /** + * @param {string} name + * @param {'methods' | 'computed'} kind + * @param {FunctionExpression | ArrowFunctionExpression} node + * @param {RuleContext} context + */ + constructor(name, kind, node, context) { + this.context = context + this.name = name + this.kind = kind + this.node = node + /** @type {(Expression | null)[]} */ + this.returnValues = [] + /** @type {boolean | null} */ + this.cacheMaybeReturnFunction = null + } - /** - * Only check return statement inside computed and methods - */ - const returnStatement = blockStatement.body.find( - (b) => b.type === 'ReturnStatement' - ) + /** + * @param {Expression | null} node + */ + addReturnValue(node) { + this.returnValues.push(node) + } - if (!returnStatement || returnStatement.type !== 'ReturnStatement') - return { - type: null - } + /** + * @param {ComponentStack} component + */ + maybeReturnFunction(component) { + if (this.cacheMaybeReturnFunction != null) { + return this.cacheMaybeReturnFunction + } + // Avoid infinite recursion. + this.cacheMaybeReturnFunction = true - if (returnStatement.argument === null) - return { - type: 'ReturnStatementHasNotArgument' - } + return (this.cacheMaybeReturnFunction = this.returnValues.some( + (returnValue) => + returnValue && component.maybeFunctionExpression(returnValue) + )) + } +} - if ( - property.groupName === 'computed' && - propertyMap && - propertyMap[property.name] && - returnStatement.argument - ) { - if ( - returnStatement.argument.type === 'MemberExpression' && - returnStatement.argument.object.type === 'ThisExpression' && - returnStatement.argument.property.type === 'Identifier' - ) - return { - type: propertyMap[returnStatement.argument.property.name] - .valueType.type - } +/** Component information class. */ +class ComponentStack { + /** + * @param {ObjectExpression} node + * @param {RuleContext} context + * @param {ComponentStack | null} upper + */ + constructor(node, context, upper) { + this.node = node + this.context = context + /** Upper scope component */ + this.upper = upper - if ( - returnStatement.argument.type === 'CallExpression' && - returnStatement.argument.callee.type === 'MemberExpression' && - returnStatement.argument.callee.object.type === 'ThisExpression' && - returnStatement.argument.callee.property.type === 'Identifier' - ) - return { - type: propertyMap[returnStatement.argument.callee.property.name] - .valueType.type - } - } + /** @type {Map} */ + const maybeFunctions = new Map() + /** @type {FunctionData[]} */ + const functions = [] - /** - * Use value as Object even if object includes method - */ - if ( - property.groupName === 'computed' && - returnStatement.argument.type === 'ObjectExpression' - ) { - return { - type: 'Object' + // Extract properties + for (const property of utils.iterateProperties(node, GROUPS)) { + if (property.type === 'array') { + continue + } + if (property.groupName === 'data') { + maybeFunctions.set( + property.name, + maybeFunction(context, property.property.value) + ) + } else if (property.groupName === 'props') { + const types = getComponentPropsTypes(context, property) + maybeFunctions.set( + property.name, + !types || types.some((type) => !NATIVE_NOT_FUNCTION_TYPES.has(type)) + ) + } else if (property.groupName === 'computed') { + let value = property.property.value + if (value.type === 'ObjectExpression') { + const getProp = utils.findProperty(value, 'get') + if (getProp) { + value = getProp.value } } + processFunction(property.name, value, 'computed') + } else if (property.groupName === 'methods') { + const value = property.property.value + processFunction(property.name, value, 'methods') + maybeFunctions.set(property.name, true) + } + } + this.maybeFunctions = maybeFunctions + this.functions = functions + /** @type {CallMember[]} */ + this.callMembers = [] + /** @type {Map} */ + this.cacheMaybeFunctionExpressions = new Map() - const evaluated = eslintUtils.getStaticValue(returnStatement.argument) - - if (evaluated) { - return { - type: getPrototypeType(evaluated.value) - } + /** + * @param {string} name + * @param {Expression} value + * @param {'methods' | 'computed'} kind + */ + function processFunction(name, value, kind) { + if (value.type === 'FunctionExpression') { + functions.push(new FunctionData(name, kind, value, context)) + } else if (value.type === 'ArrowFunctionExpression') { + const data = new FunctionData(name, kind, value, context) + if (value.expression) { + data.addReturnValue(value.body) } + functions.push(data) } } + } - const evaluated = eslintUtils.getStaticValue(property.property.value) - - if (evaluated) { - return { - type: getPrototypeType(evaluated.value) + /** + * Adds the given return statement to the return value of the function. + * @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} scopeFunction + * @param {ReturnStatement} returnNode + */ + addReturnStatement(scopeFunction, returnNode) { + for (const data of this.functions) { + if (data.node === scopeFunction) { + data.addReturnValue(returnNode.argument) + break } } } - return { - type: null + + verifyComponent() { + for (const call of this.callMembers) { + this.verifyCallMember(call) + } } -} -/** - * @param {Set} groups - * @param {ObjectExpression} vueNodeMap - * @param {PropertyMap} propertyMap - */ -const addPropertyMap = (groups, vueNodeMap, propertyMap) => { - const properties = utils.iterateProperties(vueNodeMap, groups) - for (const property of properties) { - propertyMap[property.name] = { - ...propertyMap[property.name], - ...property, - valueType: getValueType({ property, propertyMap }) + /** + * @param {CallMember} call + */ + verifyCallMember(call) { + const fnData = this.functions.find( + (data) => data.name === call.name && data.kind === 'computed' + ) + if (!fnData) { + // It is not computed, or unknown. + return + } + + if (!fnData.maybeReturnFunction(this)) { + const prefix = call.node.callee.type === 'MemberExpression' ? 'this.' : '' + this.context.report({ + node: call.node, + messageId: 'unexpected', + data: { + likeProperty: `${prefix}${call.name}`, + likeMethod: `${prefix}${call.name}()` + } + }) + } + } + + /** + * Check whether given expression may be a function. + * @param {Expression} node + * @returns {boolean} + */ + maybeFunctionExpression(node) { + const cache = this.cacheMaybeFunctionExpressions.get(node) + if (cache != null) { + return cache + } + // Avoid infinite recursion. + this.cacheMaybeFunctionExpressions.set(node, true) + + const result = maybeFunctionExpressionWithoutCache.call(this) + this.cacheMaybeFunctionExpressions.set(node, result) + return result + + /** + * @this {ComponentStack} + */ + function maybeFunctionExpressionWithoutCache() { + for (const expr of resolvedExpressions(this.context, node)) { + if (!maybeFunction(this.context, expr)) { + continue + } + if (expr.type === 'MemberExpression') { + if (utils.isThis(expr.object, this.context)) { + const name = utils.getStaticPropertyName(expr) + if (name && !this.maybeFunctionProperty(name)) { + continue + } + } + } else if (expr.type === 'CallExpression') { + if ( + expr.callee.type === 'MemberExpression' && + utils.isThis(expr.callee.object, this.context) + ) { + const name = utils.getStaticPropertyName(expr.callee) + const fnData = this.functions.find((data) => data.name === name) + if ( + fnData && + fnData.kind === 'methods' && + !fnData.maybeReturnFunction(this) + ) { + continue + } + } + } else if (expr.type === 'ConditionalExpression') { + if ( + !this.maybeFunctionExpression(expr.consequent) && + !this.maybeFunctionExpression(expr.alternate) + ) { + continue + } + } + // It could be a function because we don't know what it is. + return true + } + return false + } + } + + /** + * Check whether given property name may be a function. + * @param {string} name + * @returns {boolean} + */ + maybeFunctionProperty(name) { + const cache = this.maybeFunctions.get(name) + if (cache != null) { + return cache + } + // Avoid infinite recursion. + this.maybeFunctions.set(name, true) + + const result = maybeFunctionPropertyWithoutCache.call(this) + this.maybeFunctions.set(name, result) + return result + + /** + * @this {ComponentStack} + */ + function maybeFunctionPropertyWithoutCache() { + const fnData = this.functions.find((data) => data.name === name) + if (fnData && fnData.kind === 'computed') { + return fnData.maybeReturnFunction(this) + } + // It could be a function because we don't know what it is. + return true } } } @@ -199,63 +494,99 @@ module.exports = { }, /** @param {RuleContext} context */ create(context) { - /** @type {GroupName[]} */ - const GROUP_NAMES = ['data', 'props', 'computed', 'methods'] - const groups = new Set(GROUP_NAMES) - - /** @type {PropertyMap} */ - const propertyMap = Object.create(null) - - return utils.defineVueVisitor(context, { - onVueObjectEnter(node) { - const properties = utils.iterateProperties(node, groups) - - for (const property of properties) { - propertyMap[property.name] = { - ...propertyMap[property.name], - ...property, - valueType: getValueType({ property }) - } - } - }, + /** + * @typedef {object} ScopeStack + * @property {ScopeStack | null} upper + * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} scopeNode + */ + /** @type {ScopeStack | null} */ + let scopeStack = null - /** @param {ThisExpression} node */ - 'CallExpression > MemberExpression > ThisExpression'( - node, - { node: vueNode } - ) { - addPropertyMap(groups, vueNode, propertyMap) + /** @type {ComponentStack | null} */ + let componentStack = null + /** @type {ComponentStack | null} */ + let templateComponent = null - if (node.parent.type !== 'MemberExpression') return - if (node.parent.property.type !== 'Identifier') return - if (node.parent.parent.type !== 'CallExpression') return - if (node.parent.parent.callee.type !== 'MemberExpression') return - if (!Object.is(node.parent.parent.callee, node.parent)) return - - const thisMember = node.parent.property.name + return utils.compositingVisitors( + {}, + utils.defineVueVisitor(context, { + onVueObjectEnter(node) { + componentStack = new ComponentStack(node, context, componentStack) + if (!templateComponent && utils.isInExportDefault(node)) { + templateComponent = componentStack + } + }, + onVueObjectExit() { + if (componentStack) { + componentStack.verifyComponent() + componentStack = componentStack.upper + } + }, + /** + * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node + */ + ':function'(node) { + scopeStack = { + upper: scopeStack, + scopeNode: node + } + }, + ReturnStatement(node) { + if (scopeStack && componentStack) { + componentStack.addReturnStatement(scopeStack.scopeNode, node) + } + }, + ':function:exit'() { + scopeStack = scopeStack && scopeStack.upper + }, - if ( - !propertyMap[thisMember] || - !propertyMap[thisMember].valueType || - !propertyMap[thisMember].valueType.type - ) - return - - if ( - propertyMap[thisMember].groupName === 'computed' && - propertyMap[thisMember].valueType.type !== 'Function' - ) { - context.report({ - node: node.parent.parent, - loc: node.parent.parent.loc, - messageId: 'unexpected', - data: { - likeProperty: `this.${thisMember}`, - likeMethod: `this.${thisMember}()` + /** + * @param {ThisExpression | Identifier} node + */ + 'ThisExpression, Identifier'(node) { + if ( + !componentStack || + node.parent.type !== 'MemberExpression' || + node.parent.object !== node || + node.parent.parent.type !== 'CallExpression' || + node.parent.parent.callee !== node.parent || + !utils.isThis(node, context) + ) { + return + } + const name = utils.getStaticPropertyName(node.parent) + if (name) { + componentStack.callMembers.push({ + name, + node: node.parent.parent + }) + } + } + }), + utils.defineTemplateBodyVisitor(context, { + /** + * @param {VExpressionContainer} node + */ + VExpressionContainer(node) { + if (!templateComponent) { + return + } + for (const id of node.references + .filter((ref) => ref.variable == null) + .map((ref) => ref.id)) { + if ( + id.parent.type !== 'CallExpression' || + id.parent.callee !== id + ) { + continue } - }) + templateComponent.verifyCallMember({ + name: id.name, + node: id.parent + }) + } } - } - }) + }) + ) } } diff --git a/lib/utils/index.js b/lib/utils/index.js index b17de0821..36614cd14 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1717,6 +1717,14 @@ module.exports = { * Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it. */ skipChainExpression, + findVariableByIdentifier, + getScope, + /** + * Checks whether the given node is in export default. + * @param {ASTNode} node + * @returns {boolean} + */ + isInExportDefault, /** * Check whether the given node is `this` or variable that stores `this`. @@ -2004,6 +2012,43 @@ function compositingVisitors(visitor, ...visitors) { // AST Helpers // ------------------------------------------------------------------------------ +/** + * Find the variable of a given identifier. + * @param {RuleContext} context The rule context + * @param {Identifier} node The variable name to find. + * @returns {Variable|null} The found variable or null. + */ +function findVariableByIdentifier(context, node) { + return findVariable(getScope(context, node), node) +} + +/** + * Gets the scope for the current node + * @param {RuleContext} context The rule context + * @param {ESNode} currentNode The node to get the scope of + * @returns { import('eslint').Scope.Scope } The scope information for this node + */ +function getScope(context, currentNode) { + // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. + const inner = currentNode.type !== 'Program' + const scopeManager = context.getSourceCode().scopeManager + + /** @type {ESNode | null} */ + let node = currentNode + for (; node; node = /** @type {ESNode | null} */ (node.parent)) { + const scope = scopeManager.acquire(node, inner) + + if (scope) { + if (scope.type === 'function-expression-name') { + return scope.childScopes[0] + } + return scope + } + } + + return scopeManager.scopes[0] +} + /** * Finds the property with the given name from the given ObjectExpression node. * @param {ObjectExpression} node @@ -2081,6 +2126,23 @@ function isVElement(node) { return node.type === 'VElement' } +/** + * Checks whether the given node is in export default. + * @param {ASTNode} node + * @returns {boolean} + */ +function isInExportDefault(node) { + /** @type {ASTNode | null} */ + let parent = node.parent + while (parent) { + if (parent.type === 'ExportDefaultDeclaration') { + return true + } + parent = parent.parent + } + return false +} + /** * Retrieve `TSAsExpression#expression` value if the given node a `TSAsExpression` node. Otherwise, pass through it. * @template T Node type diff --git a/lib/utils/property-references.js b/lib/utils/property-references.js index f375170c3..37709ee0b 100644 --- a/lib/utils/property-references.js +++ b/lib/utils/property-references.js @@ -40,49 +40,13 @@ const NEVER = { getNest: () => NEVER } -/** - * Find the variable of a given name. - * @param {RuleContext} context The rule context - * @param {Identifier} node The variable name to find. - * @returns {Variable|null} The found variable or null. - */ -function findVariable(context, node) { - return eslintUtils.findVariable(getScope(context, node), node) -} -/** - * Gets the scope for the current node - * @param {RuleContext} context The rule context - * @param {ESNode} currentNode The node to get the scope of - * @returns { import('eslint').Scope.Scope } The scope information for this node - */ -function getScope(context, currentNode) { - // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. - const inner = currentNode.type !== 'Program' - const scopeManager = context.getSourceCode().scopeManager - - /** @type {ESNode | null} */ - let node = currentNode - for (; node; node = /** @type {ESNode | null} */ (node.parent)) { - const scope = scopeManager.acquire(node, inner) - - if (scope) { - if (scope.type === 'function-expression-name') { - return scope.childScopes[0] - } - return scope - } - } - - return scopeManager.scopes[0] -} - /** * @param {RuleContext} context * @param {Identifier} id * @returns {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration | null} */ function findFunction(context, id) { - const calleeVariable = findVariable(context, id) + const calleeVariable = utils.findVariableByIdentifier(context, id) if (!calleeVariable) { return null } @@ -420,7 +384,7 @@ function definePropertyReferenceExtractor(context) { * @returns {IPropertyReferences} */ function extractFromIdentifier(node) { - const variable = findVariable(context, node) + const variable = utils.findVariableByIdentifier(context, node) if (!variable) { return NEVER } diff --git a/tests/lib/rules/no-use-computed-property-like-method.js b/tests/lib/rules/no-use-computed-property-like-method.js index 902f29b19..539e6275a 100644 --- a/tests/lib/rules/no-use-computed-property-like-method.js +++ b/tests/lib/rules/no-use-computed-property-like-method.js @@ -16,7 +16,7 @@ const rule = require('../../../lib/rules/no-use-computed-property-like-method') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2015, sourceType: 'module' } + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }) tester.run('no-use-computed-property-like-method', rule, { @@ -438,6 +438,137 @@ tester.run('no-use-computed-property-like-method', rule, { } ` + }, + { + //https://github.com/vuejs/eslint-plugin-vue/issues/1649 + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` } ], invalid: [ @@ -762,6 +893,214 @@ tester.run('no-use-computed-property-like-method', rule, { 'Use this.computedReturnArray instead of this.computedReturnArray().', 'Use this.computedReturnArray2 instead of this.computedReturnArray2().' ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: ['Use this.x instead of this.x().'] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: ['Use this.x instead of this.x().'] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: ['Use this.x instead of this.x().'] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: ['Use this.x instead of this.x().'] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: ['Use this.x instead of this.x().'] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: ['Use this.x instead of this.x().'] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: ['Use this.x instead of this.x().'] + }, + { + filename: 'test.vue', + code: ` + + + `, + errors: ['Use x instead of x().'] } ] })