first
This commit is contained in:
592
node_modules/eslint-plugin-vue/lib/rules/no-use-computed-property-like-method.js
generated
vendored
Normal file
592
node_modules/eslint-plugin-vue/lib/rules/no-use-computed-property-like-method.js
generated
vendored
Normal file
@ -0,0 +1,592 @@
|
||||
/**
|
||||
* @author tyankatsu <https://github.com/tyankatsu0105>
|
||||
* See LICENSE file in root directory for full license.
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
// ------------------------------------------------------------------------------
|
||||
const eslintUtils = require('eslint-utils')
|
||||
const utils = require('../utils')
|
||||
|
||||
/**
|
||||
* @typedef {import('eslint').Scope.Scope} Scope
|
||||
* @typedef {import('../utils').ComponentObjectPropertyData} ComponentObjectPropertyData
|
||||
* @typedef {import('../utils').GroupName} GroupName
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} CallMember
|
||||
* @property {string} name
|
||||
* @property {CallExpression} node
|
||||
*/
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/** @type {Set<GroupName>} */
|
||||
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<Expression>}
|
||||
*/
|
||||
function resolvedExpressions(context, node) {
|
||||
/** @type {Map<Expression, Set<Expression>>} */
|
||||
const resolvedMap = new Map()
|
||||
|
||||
return resolvedExpressionsInternal(node)
|
||||
|
||||
/**
|
||||
* @param {Expression} node
|
||||
* @returns {Set<Expression>}
|
||||
*/
|
||||
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<Expression>}
|
||||
*/
|
||||
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 {RuleContext} context
|
||||
* @param {ComponentObjectPropertyData} prop
|
||||
* @return {string[] | null}
|
||||
*
|
||||
* @example
|
||||
* props: {
|
||||
* propA: String, // => String
|
||||
* propB: {
|
||||
* type: Number // => String
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
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
|
||||
|
||||
/**
|
||||
* @param {Expression} expr
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Expression} typeExpr
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given expression may be a function.
|
||||
* @param {RuleContext} context
|
||||
* @param {Expression} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
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 (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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Expression | null} node
|
||||
*/
|
||||
addReturnValue(node) {
|
||||
this.returnValues.push(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ComponentStack} component
|
||||
*/
|
||||
maybeReturnFunction(component) {
|
||||
if (this.cacheMaybeReturnFunction != null) {
|
||||
return this.cacheMaybeReturnFunction
|
||||
}
|
||||
// Avoid infinite recursion.
|
||||
this.cacheMaybeReturnFunction = true
|
||||
|
||||
return (this.cacheMaybeReturnFunction = this.returnValues.some(
|
||||
(returnValue) =>
|
||||
returnValue && component.maybeFunctionExpression(returnValue)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
||||
/** @type {Map<string, boolean>} */
|
||||
const maybeFunctions = new Map()
|
||||
/** @type {FunctionData[]} */
|
||||
const functions = []
|
||||
|
||||
// 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<Expression, boolean>} */
|
||||
this.cacheMaybeFunctionExpressions = new Map()
|
||||
|
||||
/**
|
||||
* @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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verifyComponent() {
|
||||
for (const call of this.callMembers) {
|
||||
this.verifyCallMember(call)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'disallow use computed property like method',
|
||||
categories: undefined,
|
||||
url: 'https://eslint.vuejs.org/rules/no-use-computed-property-like-method.html'
|
||||
},
|
||||
fixable: null,
|
||||
schema: [],
|
||||
messages: {
|
||||
unexpected: 'Use {{ likeProperty }} instead of {{ likeMethod }}.'
|
||||
}
|
||||
},
|
||||
/** @param {RuleContext} context */
|
||||
create(context) {
|
||||
/**
|
||||
* @typedef {object} ScopeStack
|
||||
* @property {ScopeStack | null} upper
|
||||
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} scopeNode
|
||||
*/
|
||||
/** @type {ScopeStack | null} */
|
||||
let scopeStack = null
|
||||
|
||||
/** @type {ComponentStack | null} */
|
||||
let componentStack = null
|
||||
/** @type {ComponentStack | null} */
|
||||
let templateComponent = null
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
/**
|
||||
* @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
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user