1107 lines
32 KiB
JavaScript
1107 lines
32 KiB
JavaScript
/**
|
|
* @fileoverview Disallow unused properties, data and computed properties.
|
|
* @author Learning Equality
|
|
*/
|
|
'use strict'
|
|
|
|
// ------------------------------------------------------------------------------
|
|
// Requirements
|
|
// ------------------------------------------------------------------------------
|
|
|
|
const utils = require('../utils')
|
|
const eslintUtils = require('eslint-utils')
|
|
const { getStyleVariablesContext } = require('../utils/style-variables')
|
|
|
|
/**
|
|
* @typedef {import('../utils').GroupName} GroupName
|
|
* @typedef {import('../utils').VueObjectData} VueObjectData
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} ComponentObjectPropertyData
|
|
* @property {string} name
|
|
* @property {GroupName} groupName
|
|
* @property {'object'} type
|
|
* @property {ASTNode} node
|
|
* @property {Property} property
|
|
*
|
|
* @typedef {object} ComponentNonObjectPropertyData
|
|
* @property {string} name
|
|
* @property {GroupName} groupName
|
|
* @property {'array' | 'type'} type
|
|
* @property {ASTNode} node
|
|
*
|
|
* @typedef { ComponentNonObjectPropertyData | ComponentObjectPropertyData } ComponentPropertyData
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} TemplatePropertiesContainer
|
|
* @property {UsedProperties} usedProperties
|
|
* @property {Set<string>} refNames
|
|
* @typedef {object} VueComponentPropertiesContainer
|
|
* @property {ComponentPropertyData[]} properties
|
|
* @property {UsedProperties} usedProperties
|
|
* @property {UsedProperties} usedPropertiesForProps
|
|
*/
|
|
|
|
// ------------------------------------------------------------------------------
|
|
// Constants
|
|
// ------------------------------------------------------------------------------
|
|
|
|
const GROUP_PROPERTY = 'props'
|
|
const GROUP_DATA = 'data'
|
|
const GROUP_COMPUTED_PROPERTY = 'computed'
|
|
const GROUP_METHODS = 'methods'
|
|
const GROUP_SETUP = 'setup'
|
|
const GROUP_WATCHER = 'watch'
|
|
const GROUP_EXPOSE = 'expose'
|
|
|
|
const PROPERTY_LABEL = {
|
|
props: 'property',
|
|
data: 'data',
|
|
computed: 'computed property',
|
|
methods: 'method',
|
|
setup: 'property returned from `setup()`',
|
|
|
|
// not use
|
|
watch: 'watch',
|
|
provide: 'provide',
|
|
inject: 'inject',
|
|
expose: 'expose'
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------
|
|
// 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]
|
|
}
|
|
|
|
/**
|
|
* Extract names from references objects.
|
|
* @param {VReference[]} references
|
|
*/
|
|
function getReferences(references) {
|
|
return references.filter((ref) => ref.variable == null).map((ref) => ref.id)
|
|
}
|
|
|
|
/**
|
|
* @param {RuleContext} context
|
|
* @param {Identifier} id
|
|
* @returns {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration | null}
|
|
*/
|
|
function findFunction(context, id) {
|
|
const calleeVariable = findVariable(context, id)
|
|
if (!calleeVariable) {
|
|
return null
|
|
}
|
|
if (calleeVariable.defs.length === 1) {
|
|
const def = calleeVariable.defs[0]
|
|
if (def.node.type === 'FunctionDeclaration') {
|
|
return def.node
|
|
}
|
|
if (
|
|
def.type === 'Variable' &&
|
|
def.parent.kind === 'const' &&
|
|
def.node.init
|
|
) {
|
|
if (
|
|
def.node.init.type === 'FunctionExpression' ||
|
|
def.node.init.type === 'ArrowFunctionExpression'
|
|
) {
|
|
return def.node.init
|
|
}
|
|
if (def.node.init.type === 'Identifier') {
|
|
return findFunction(context, def.node.init)
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* @param {RuleContext} context
|
|
* @param {Identifier} id
|
|
* @returns {Expression}
|
|
*/
|
|
function findExpression(context, id) {
|
|
const variable = findVariable(context, id)
|
|
if (!variable) {
|
|
return id
|
|
}
|
|
if (variable.defs.length === 1) {
|
|
const def = variable.defs[0]
|
|
if (
|
|
def.type === 'Variable' &&
|
|
def.parent.kind === 'const' &&
|
|
def.node.init
|
|
) {
|
|
if (def.node.init.type === 'Identifier') {
|
|
return findExpression(context, def.node.init)
|
|
}
|
|
return def.node.init
|
|
}
|
|
}
|
|
return id
|
|
}
|
|
|
|
/**
|
|
* @typedef { (context: RuleContext) => UsedProperties } UsedPropertiesTracker
|
|
* @typedef { { node: CallExpression, index: number } } CallAndParamIndex
|
|
*/
|
|
|
|
/**
|
|
* Collects the used property names.
|
|
*/
|
|
class UsedProperties {
|
|
/**
|
|
* @param {object} [option]
|
|
* @param {boolean} [option.unknown]
|
|
*/
|
|
constructor(option) {
|
|
/** @type {Record<string, UsedPropertiesTracker[]>} */
|
|
this.map = Object.create(null)
|
|
/** @type {CallAndParamIndex[]} */
|
|
this.calls = []
|
|
this.unknown = (option && option.unknown) || false
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
isUsed(name) {
|
|
if (this.unknown) {
|
|
// If it is unknown, it is considered used.
|
|
return true
|
|
}
|
|
return Boolean(this.map[name])
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {UsedPropertiesTracker | null} tracker
|
|
*/
|
|
addUsed(name, tracker) {
|
|
const trackers = this.map[name] || (this.map[name] = [])
|
|
if (tracker) trackers.push(tracker)
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @returns {UsedPropertiesTracker}
|
|
*/
|
|
getPropsTracker(name) {
|
|
if (this.unknown) {
|
|
return () => new UsedProperties({ unknown: true })
|
|
}
|
|
const trackers = this.map[name] || []
|
|
return (context) => {
|
|
const result = new UsedProperties()
|
|
for (const tracker of trackers) {
|
|
result.merge(tracker(context))
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {UsedProperties | null} other
|
|
*/
|
|
merge(other) {
|
|
if (!other) {
|
|
return
|
|
}
|
|
this.unknown = this.unknown || other.unknown
|
|
if (this.unknown) {
|
|
return
|
|
}
|
|
for (const [name, otherTrackers] of Object.entries(other.map)) {
|
|
const trackers = this.map[name] || (this.map[name] = [])
|
|
trackers.push(...otherTrackers)
|
|
}
|
|
this.calls.push(...other.calls)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collects the used property names for parameters of the function.
|
|
*/
|
|
class ParamsUsedProperties {
|
|
/**
|
|
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
|
|
* @param {RuleContext} context
|
|
*/
|
|
constructor(node, context) {
|
|
this.node = node
|
|
this.context = context
|
|
/** @type {UsedProperties[]} */
|
|
this.params = []
|
|
}
|
|
|
|
/**
|
|
* @param {number} index
|
|
* @returns {UsedProperties | null}
|
|
*/
|
|
getParam(index) {
|
|
const param = this.params[index]
|
|
if (param != null) {
|
|
return param
|
|
}
|
|
if (this.node.params[index]) {
|
|
return (this.params[index] = extractParamOrVerProperties(
|
|
this.node.params[index],
|
|
this.context
|
|
))
|
|
}
|
|
return null
|
|
}
|
|
}
|
|
/**
|
|
* Extract the used property name from one parameter of the function.
|
|
* @param {Pattern} node
|
|
* @param {RuleContext} context
|
|
* @returns {UsedProperties}
|
|
*/
|
|
function extractParamOrVerProperties(node, context) {
|
|
const result = new UsedProperties()
|
|
|
|
while (node.type === 'AssignmentPattern') {
|
|
node = node.left
|
|
}
|
|
if (node.type === 'RestElement' || node.type === 'ArrayPattern') {
|
|
// cannot check
|
|
return result
|
|
}
|
|
if (node.type === 'ObjectPattern') {
|
|
result.merge(extractObjectPatternProperties(node))
|
|
return result
|
|
}
|
|
if (node.type !== 'Identifier') {
|
|
return result
|
|
}
|
|
const variable = findVariable(context, node)
|
|
if (!variable) {
|
|
return result
|
|
}
|
|
for (const reference of variable.references) {
|
|
const id = reference.identifier
|
|
result.merge(extractPatternOrThisProperties(id, context, false))
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Extract the used property name from ObjectPattern.
|
|
* @param {ObjectPattern} node
|
|
* @returns {UsedProperties}
|
|
*/
|
|
function extractObjectPatternProperties(node) {
|
|
const result = new UsedProperties()
|
|
for (const prop of node.properties) {
|
|
if (prop.type === 'Property') {
|
|
const name = utils.getStaticPropertyName(prop)
|
|
if (name) {
|
|
result.addUsed(name, getObjectPatternPropertyPatternTracker(prop.value))
|
|
} else {
|
|
// If cannot trace name, everything is used!
|
|
result.unknown = true
|
|
return result
|
|
}
|
|
} else {
|
|
// If use RestElement, everything is used!
|
|
result.unknown = true
|
|
return result
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Extract the used property name from id.
|
|
* @param {Identifier} node
|
|
* @param {RuleContext} context
|
|
* @returns {UsedProperties}
|
|
*/
|
|
function extractIdentifierProperties(node, context) {
|
|
const result = new UsedProperties()
|
|
const variable = findVariable(context, node)
|
|
if (!variable) {
|
|
return result
|
|
}
|
|
for (const reference of variable.references) {
|
|
const id = reference.identifier
|
|
result.merge(extractPatternOrThisProperties(id, context, false))
|
|
}
|
|
return result
|
|
}
|
|
/**
|
|
* Extract the used property name from pattern or `this`.
|
|
* @param {Identifier | MemberExpression | ChainExpression | ThisExpression} node
|
|
* @param {RuleContext} context
|
|
* @param {boolean} withInTemplate
|
|
* @returns {UsedProperties}
|
|
*/
|
|
function extractPatternOrThisProperties(node, context, withInTemplate) {
|
|
const result = new UsedProperties()
|
|
const parent = node.parent
|
|
if (parent.type === 'AssignmentExpression') {
|
|
if (withInTemplate) {
|
|
return result
|
|
}
|
|
if (parent.right === node && parent.left.type === 'ObjectPattern') {
|
|
// `({foo} = arg)`
|
|
result.merge(extractObjectPatternProperties(parent.left))
|
|
}
|
|
return result
|
|
} else if (parent.type === 'VariableDeclarator') {
|
|
if (withInTemplate) {
|
|
return result
|
|
}
|
|
if (parent.init === node) {
|
|
if (parent.id.type === 'ObjectPattern') {
|
|
// `const {foo} = arg`
|
|
result.merge(extractObjectPatternProperties(parent.id))
|
|
} else if (parent.id.type === 'Identifier') {
|
|
// `const foo = arg`
|
|
result.merge(extractIdentifierProperties(parent.id, context))
|
|
}
|
|
}
|
|
return result
|
|
} else if (parent.type === 'MemberExpression') {
|
|
if (parent.object === node) {
|
|
// `arg.foo`
|
|
const name = utils.getStaticPropertyName(parent)
|
|
if (name) {
|
|
result.addUsed(name, () =>
|
|
extractPatternOrThisProperties(parent, context, withInTemplate)
|
|
)
|
|
} else {
|
|
result.unknown = true
|
|
}
|
|
}
|
|
return result
|
|
} else if (parent.type === 'CallExpression') {
|
|
if (withInTemplate) {
|
|
return result
|
|
}
|
|
const argIndex = parent.arguments.indexOf(node)
|
|
if (argIndex > -1) {
|
|
// `foo(arg)`
|
|
result.calls.push({
|
|
node: parent,
|
|
index: argIndex
|
|
})
|
|
}
|
|
} else if (parent.type === 'ChainExpression') {
|
|
result.merge(
|
|
extractPatternOrThisProperties(parent, context, withInTemplate)
|
|
)
|
|
} else if (
|
|
parent.type === 'ArrowFunctionExpression' ||
|
|
parent.type === 'ReturnStatement' ||
|
|
parent.type === 'VExpressionContainer' ||
|
|
parent.type === 'Property' ||
|
|
parent.type === 'ArrayExpression'
|
|
) {
|
|
// Maybe used externally.
|
|
if (maybeExternalUsed(parent)) {
|
|
result.unknown = true
|
|
}
|
|
}
|
|
return result
|
|
|
|
/**
|
|
* @param {ASTNode} parentTarget
|
|
* @returns {boolean}
|
|
*/
|
|
function maybeExternalUsed(parentTarget) {
|
|
if (
|
|
parentTarget.type === 'ReturnStatement' ||
|
|
parentTarget.type === 'VExpressionContainer'
|
|
) {
|
|
return true
|
|
}
|
|
if (parentTarget.type === 'ArrayExpression') {
|
|
return maybeExternalUsed(parentTarget.parent)
|
|
}
|
|
if (parentTarget.type === 'Property') {
|
|
return maybeExternalUsed(parentTarget.parent.parent)
|
|
}
|
|
if (parentTarget.type === 'ArrowFunctionExpression') {
|
|
return parentTarget.body === node
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Pattern} pattern
|
|
* @returns {UsedPropertiesTracker}
|
|
*/
|
|
function getObjectPatternPropertyPatternTracker(pattern) {
|
|
if (pattern.type === 'ObjectPattern') {
|
|
return () => extractObjectPatternProperties(pattern)
|
|
}
|
|
if (pattern.type === 'Identifier') {
|
|
return (context) => extractIdentifierProperties(pattern, context)
|
|
} else if (pattern.type === 'AssignmentPattern') {
|
|
return getObjectPatternPropertyPatternTracker(pattern.left)
|
|
}
|
|
return () => new UsedProperties({ unknown: true })
|
|
}
|
|
|
|
/**
|
|
* Check if the given component property is marked as `@public` in JSDoc comments.
|
|
* @param {ComponentPropertyData} property
|
|
* @param {SourceCode} sourceCode
|
|
*/
|
|
function isPublicMember(property, sourceCode) {
|
|
if (
|
|
property.type === 'object' &&
|
|
// Props do not support @public.
|
|
property.groupName !== 'props'
|
|
) {
|
|
return isPublicProperty(property.property, sourceCode)
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Check if the given property node is marked as `@public` in JSDoc comments.
|
|
* @param {Property} node
|
|
* @param {SourceCode} sourceCode
|
|
*/
|
|
function isPublicProperty(node, sourceCode) {
|
|
const jsdoc = getJSDocFromProperty(node, sourceCode)
|
|
if (jsdoc) {
|
|
return /(?:^|\s|\*)@public\b/u.test(jsdoc.value)
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Get the JSDoc comment for a given property node.
|
|
* @param {Property} node
|
|
* @param {SourceCode} sourceCode
|
|
*/
|
|
function getJSDocFromProperty(node, sourceCode) {
|
|
const jsdoc = findJSDocComment(node, sourceCode)
|
|
if (jsdoc) {
|
|
return jsdoc
|
|
}
|
|
if (
|
|
node.value.type === 'FunctionExpression' ||
|
|
node.value.type === 'ArrowFunctionExpression'
|
|
) {
|
|
return findJSDocComment(node.value, sourceCode)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Finds a JSDoc comment for the given node.
|
|
* @param {ASTNode} node
|
|
* @param {SourceCode} sourceCode
|
|
* @returns {Comment | null}
|
|
*/
|
|
function findJSDocComment(node, sourceCode) {
|
|
/** @type {ASTNode | Token} */
|
|
let currentNode = node
|
|
let tokenBefore = null
|
|
|
|
while (currentNode) {
|
|
tokenBefore = sourceCode.getTokenBefore(currentNode, {
|
|
includeComments: true
|
|
})
|
|
if (!tokenBefore || !eslintUtils.isCommentToken(tokenBefore)) {
|
|
return null
|
|
}
|
|
if (tokenBefore.type === 'Line') {
|
|
currentNode = tokenBefore
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
if (
|
|
tokenBefore &&
|
|
tokenBefore.type === 'Block' &&
|
|
tokenBefore.value.charAt(0) === '*'
|
|
) {
|
|
return tokenBefore
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------
|
|
// Rule Definition
|
|
// ------------------------------------------------------------------------------
|
|
|
|
module.exports = {
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description: 'disallow unused properties',
|
|
categories: undefined,
|
|
url: 'https://eslint.vuejs.org/rules/no-unused-properties.html'
|
|
},
|
|
fixable: null,
|
|
schema: [
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
groups: {
|
|
type: 'array',
|
|
items: {
|
|
enum: [
|
|
GROUP_PROPERTY,
|
|
GROUP_DATA,
|
|
GROUP_COMPUTED_PROPERTY,
|
|
GROUP_METHODS,
|
|
GROUP_SETUP
|
|
]
|
|
},
|
|
additionalItems: false,
|
|
uniqueItems: true
|
|
},
|
|
deepData: { type: 'boolean' },
|
|
ignorePublicMembers: { type: 'boolean' }
|
|
},
|
|
additionalProperties: false
|
|
}
|
|
],
|
|
messages: {
|
|
unused: "'{{name}}' of {{group}} found, but never used."
|
|
}
|
|
},
|
|
/** @param {RuleContext} context */
|
|
create(context) {
|
|
const options = context.options[0] || {}
|
|
const groups = new Set(options.groups || [GROUP_PROPERTY])
|
|
const deepData = Boolean(options.deepData)
|
|
const ignorePublicMembers = Boolean(options.ignorePublicMembers)
|
|
|
|
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, ParamsUsedProperties>} */
|
|
const paramsUsedPropertiesMap = new Map()
|
|
/** @type {TemplatePropertiesContainer} */
|
|
const templatePropertiesContainer = {
|
|
usedProperties: new UsedProperties(),
|
|
refNames: new Set()
|
|
}
|
|
/** @type {Map<ASTNode, VueComponentPropertiesContainer>} */
|
|
const vueComponentPropertiesContainerMap = new Map()
|
|
|
|
/**
|
|
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
|
|
* @returns {ParamsUsedProperties}
|
|
*/
|
|
function getParamsUsedProperties(node) {
|
|
let usedProps = paramsUsedPropertiesMap.get(node)
|
|
if (!usedProps) {
|
|
usedProps = new ParamsUsedProperties(node, context)
|
|
paramsUsedPropertiesMap.set(node, usedProps)
|
|
}
|
|
return usedProps
|
|
}
|
|
|
|
/**
|
|
* @param {ASTNode} node
|
|
* @returns {VueComponentPropertiesContainer}
|
|
*/
|
|
function getVueComponentPropertiesContainer(node) {
|
|
let container = vueComponentPropertiesContainerMap.get(node)
|
|
if (!container) {
|
|
container = {
|
|
properties: [],
|
|
usedProperties: new UsedProperties(),
|
|
usedPropertiesForProps: new UsedProperties()
|
|
}
|
|
vueComponentPropertiesContainerMap.set(node, container)
|
|
}
|
|
return container
|
|
}
|
|
/**
|
|
* @param {string[]} segments
|
|
* @param {Expression} propertyValue
|
|
* @param {UsedProperties} baseUsedProperties
|
|
*/
|
|
function verifyDataOptionDeepProperties(
|
|
segments,
|
|
propertyValue,
|
|
baseUsedProperties
|
|
) {
|
|
let targetExpr = propertyValue
|
|
if (targetExpr.type === 'Identifier') {
|
|
targetExpr = findExpression(context, targetExpr)
|
|
}
|
|
if (targetExpr.type === 'ObjectExpression') {
|
|
const usedProperties = resolvedUsedProperties(baseUsedProperties, {
|
|
allowUnknownCall: true
|
|
})
|
|
if (usedProperties.unknown) {
|
|
return
|
|
}
|
|
|
|
for (const prop of targetExpr.properties) {
|
|
if (prop.type !== 'Property') {
|
|
continue
|
|
}
|
|
const name = utils.getStaticPropertyName(prop)
|
|
if (name == null) {
|
|
continue
|
|
}
|
|
if (!usedProperties.isUsed(name)) {
|
|
// report
|
|
context.report({
|
|
node: prop.key,
|
|
messageId: 'unused',
|
|
data: {
|
|
group: PROPERTY_LABEL.data,
|
|
name: [...segments, name].join('.')
|
|
}
|
|
})
|
|
continue
|
|
}
|
|
// next
|
|
verifyDataOptionDeepProperties(
|
|
[...segments, name],
|
|
prop.value,
|
|
usedProperties.getPropsTracker(name)(context)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Report all unused properties.
|
|
*/
|
|
function reportUnusedProperties() {
|
|
for (const container of vueComponentPropertiesContainerMap.values()) {
|
|
const usedProperties = resolvedUsedProperties(container.usedProperties)
|
|
usedProperties.merge(templatePropertiesContainer.usedProperties)
|
|
if (usedProperties.unknown) {
|
|
continue
|
|
}
|
|
|
|
const usedPropertiesForProps = resolvedUsedProperties(
|
|
container.usedPropertiesForProps
|
|
)
|
|
|
|
for (const property of container.properties) {
|
|
if (
|
|
property.groupName === 'props' &&
|
|
usedPropertiesForProps.isUsed(property.name)
|
|
) {
|
|
// used props
|
|
continue
|
|
}
|
|
if (
|
|
property.groupName === 'setup' &&
|
|
templatePropertiesContainer.refNames.has(property.name)
|
|
) {
|
|
// used template refs
|
|
continue
|
|
}
|
|
if (
|
|
ignorePublicMembers &&
|
|
isPublicMember(property, context.getSourceCode())
|
|
) {
|
|
continue
|
|
}
|
|
if (usedProperties.isUsed(property.name)) {
|
|
// used
|
|
if (
|
|
deepData &&
|
|
property.groupName === 'data' &&
|
|
property.type === 'object'
|
|
) {
|
|
// Check the deep properties of the data option.
|
|
verifyDataOptionDeepProperties(
|
|
[property.name],
|
|
property.property.value,
|
|
usedProperties.getPropsTracker(property.name)(context)
|
|
)
|
|
}
|
|
continue
|
|
}
|
|
context.report({
|
|
node: property.node,
|
|
messageId: 'unused',
|
|
data: {
|
|
group: PROPERTY_LABEL[property.groupName],
|
|
name: property.name
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {UsedProperties | null} usedProps
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.allowUnknownCall]
|
|
* @returns {UsedProperties}
|
|
*/
|
|
function resolvedUsedProperties(usedProps, options) {
|
|
const allowUnknownCall = options && options.allowUnknownCall
|
|
const already = new Map()
|
|
|
|
const result = new UsedProperties()
|
|
for (const up of iterate(usedProps)) {
|
|
result.merge(up)
|
|
if (result.unknown) {
|
|
break
|
|
}
|
|
}
|
|
return result
|
|
|
|
/**
|
|
* @param {UsedProperties | null} usedProps
|
|
* @returns {IterableIterator<UsedProperties>}
|
|
*/
|
|
function* iterate(usedProps) {
|
|
if (!usedProps) {
|
|
return
|
|
}
|
|
yield usedProps
|
|
for (const call of usedProps.calls) {
|
|
if (call.node.callee.type !== 'Identifier') {
|
|
if (allowUnknownCall) {
|
|
yield new UsedProperties({ unknown: true })
|
|
}
|
|
continue
|
|
}
|
|
const fnNode = findFunction(context, call.node.callee)
|
|
if (!fnNode) {
|
|
if (allowUnknownCall) {
|
|
yield new UsedProperties({ unknown: true })
|
|
}
|
|
continue
|
|
}
|
|
|
|
let alreadyIndexes = already.get(fnNode)
|
|
if (!alreadyIndexes) {
|
|
alreadyIndexes = new Set()
|
|
already.set(fnNode, alreadyIndexes)
|
|
}
|
|
if (alreadyIndexes.has(call.index)) {
|
|
continue
|
|
}
|
|
alreadyIndexes.add(call.index)
|
|
const paramsUsedProps = getParamsUsedProperties(fnNode)
|
|
const paramUsedProps = paramsUsedProps.getParam(call.index)
|
|
yield* iterate(paramUsedProps)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Expression} node
|
|
* @returns {Property|null}
|
|
*/
|
|
function getParentProperty(node) {
|
|
if (
|
|
!node.parent ||
|
|
node.parent.type !== 'Property' ||
|
|
node.parent.value !== node
|
|
) {
|
|
return null
|
|
}
|
|
const property = node.parent
|
|
if (!utils.isProperty(property)) {
|
|
return null
|
|
}
|
|
return property
|
|
}
|
|
|
|
const scriptVisitor = utils.compositingVisitors(
|
|
utils.defineScriptSetupVisitor(context, {
|
|
onDefinePropsEnter(node, props) {
|
|
if (!groups.has('props')) {
|
|
return
|
|
}
|
|
const container = getVueComponentPropertiesContainer(node)
|
|
|
|
for (const prop of props) {
|
|
if (!prop.propName) {
|
|
continue
|
|
}
|
|
if (prop.type === 'object') {
|
|
container.properties.push({
|
|
type: prop.type,
|
|
name: prop.propName,
|
|
groupName: 'props',
|
|
node: prop.key,
|
|
property: prop.node
|
|
})
|
|
} else {
|
|
container.properties.push({
|
|
type: prop.type,
|
|
name: prop.propName,
|
|
groupName: 'props',
|
|
node: prop.key
|
|
})
|
|
}
|
|
}
|
|
let target = node
|
|
if (
|
|
target.parent &&
|
|
target.parent.type === 'CallExpression' &&
|
|
target.parent.arguments[0] === target &&
|
|
target.parent.callee.type === 'Identifier' &&
|
|
target.parent.callee.name === 'withDefaults'
|
|
) {
|
|
target = target.parent
|
|
}
|
|
|
|
if (
|
|
!target.parent ||
|
|
target.parent.type !== 'VariableDeclarator' ||
|
|
target.parent.init !== target
|
|
) {
|
|
return
|
|
}
|
|
|
|
const pattern = target.parent.id
|
|
const usedProps = extractParamOrVerProperties(pattern, context)
|
|
container.usedPropertiesForProps.merge(usedProps)
|
|
}
|
|
}),
|
|
utils.defineVueVisitor(context, {
|
|
onVueObjectEnter(node) {
|
|
const container = getVueComponentPropertiesContainer(node)
|
|
|
|
for (const watcherOrExpose of utils.iterateProperties(
|
|
node,
|
|
new Set([GROUP_WATCHER, GROUP_EXPOSE])
|
|
)) {
|
|
if (watcherOrExpose.groupName === GROUP_WATCHER) {
|
|
const watcher = watcherOrExpose
|
|
// Process `watch: { foo /* <- this */ () {} }`
|
|
const segments = watcher.name.split('.')
|
|
container.usedProperties.addUsed(segments[0], (context) => {
|
|
return buildChainTracker(segments)(context)
|
|
/**
|
|
* @param {string[]} baseSegments
|
|
* @returns {UsedPropertiesTracker}
|
|
*/
|
|
function buildChainTracker(baseSegments) {
|
|
return () => {
|
|
const subSegments = baseSegments.slice(1)
|
|
const usedProps = new UsedProperties()
|
|
if (subSegments.length) {
|
|
usedProps.addUsed(
|
|
subSegments[0],
|
|
buildChainTracker(subSegments)
|
|
)
|
|
}
|
|
return usedProps
|
|
}
|
|
}
|
|
})
|
|
|
|
// Process `watch: { x: 'foo' /* <- this */ }`
|
|
if (watcher.type === 'object') {
|
|
const property = watcher.property
|
|
if (property.kind === 'init') {
|
|
for (const handlerValueNode of utils.iterateWatchHandlerValues(
|
|
property
|
|
)) {
|
|
if (
|
|
handlerValueNode.type === 'Literal' ||
|
|
handlerValueNode.type === 'TemplateLiteral'
|
|
) {
|
|
const name = utils.getStringLiteralValue(handlerValueNode)
|
|
if (name != null) {
|
|
container.usedProperties.addUsed(name, null)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (watcherOrExpose.groupName === GROUP_EXPOSE) {
|
|
const expose = watcherOrExpose
|
|
container.usedProperties.addUsed(expose.name, null)
|
|
}
|
|
}
|
|
container.properties.push(...utils.iterateProperties(node, groups))
|
|
},
|
|
/** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property }} node */
|
|
'ObjectExpression > Property > :function[params.length>0]'(
|
|
node,
|
|
vueData
|
|
) {
|
|
const property = getParentProperty(node)
|
|
if (!property) {
|
|
return
|
|
}
|
|
if (property.parent === vueData.node) {
|
|
if (utils.getStaticPropertyName(property) !== 'data') {
|
|
return
|
|
}
|
|
// check { data: (vm) => vm.prop }
|
|
} else {
|
|
const parentProperty = getParentProperty(property.parent)
|
|
if (!parentProperty) {
|
|
return
|
|
}
|
|
if (parentProperty.parent === vueData.node) {
|
|
if (utils.getStaticPropertyName(parentProperty) !== 'computed') {
|
|
return
|
|
}
|
|
// check { computed: { foo: (vm) => vm.prop } }
|
|
} else {
|
|
const parentParentProperty = getParentProperty(
|
|
parentProperty.parent
|
|
)
|
|
if (!parentParentProperty) {
|
|
return
|
|
}
|
|
if (parentParentProperty.parent === vueData.node) {
|
|
if (
|
|
utils.getStaticPropertyName(parentParentProperty) !==
|
|
'computed' ||
|
|
utils.getStaticPropertyName(property) !== 'get'
|
|
) {
|
|
return
|
|
}
|
|
// check { computed: { foo: { get: (vm) => vm.prop } } }
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
const paramsUsedProps = getParamsUsedProperties(node)
|
|
const usedProps = paramsUsedProps.getParam(0)
|
|
const container = getVueComponentPropertiesContainer(vueData.node)
|
|
container.usedProperties.merge(usedProps)
|
|
},
|
|
onSetupFunctionEnter(node, vueData) {
|
|
const container = getVueComponentPropertiesContainer(vueData.node)
|
|
const paramsUsedProps = getParamsUsedProperties(node)
|
|
const paramUsedProps = paramsUsedProps.getParam(0)
|
|
container.usedPropertiesForProps.merge(paramUsedProps)
|
|
},
|
|
onRenderFunctionEnter(node, vueData) {
|
|
const container = getVueComponentPropertiesContainer(vueData.node)
|
|
|
|
// Check for Vue 3.x render
|
|
const paramsUsedProps = getParamsUsedProperties(node)
|
|
const ctxUsedProps = paramsUsedProps.getParam(0)
|
|
|
|
container.usedPropertiesForProps.merge(ctxUsedProps)
|
|
if (container.usedPropertiesForProps.unknown) {
|
|
return
|
|
}
|
|
|
|
if (vueData.functional) {
|
|
// Check for Vue 2.x render & functional
|
|
const contextUsedProps = resolvedUsedProperties(
|
|
paramsUsedProps.getParam(1)
|
|
)
|
|
const tracker = contextUsedProps.getPropsTracker('props')
|
|
const propUsedProps = tracker(context)
|
|
container.usedPropertiesForProps.merge(propUsedProps)
|
|
}
|
|
},
|
|
/**
|
|
* @param {ThisExpression | Identifier} node
|
|
* @param {VueObjectData} vueData
|
|
*/
|
|
'ThisExpression, Identifier'(node, vueData) {
|
|
if (!utils.isThis(node, context)) {
|
|
return
|
|
}
|
|
const container = getVueComponentPropertiesContainer(vueData.node)
|
|
const usedProps = extractPatternOrThisProperties(node, context, false)
|
|
container.usedProperties.merge(usedProps)
|
|
}
|
|
}),
|
|
{
|
|
/** @param {Program} node */
|
|
'Program:exit'(node) {
|
|
const styleVars = getStyleVariablesContext(context)
|
|
if (styleVars) {
|
|
for (const { id } of styleVars.references) {
|
|
templatePropertiesContainer.usedProperties.addUsed(
|
|
id.name,
|
|
(context) => extractPatternOrThisProperties(id, context, true)
|
|
)
|
|
}
|
|
}
|
|
if (!node.templateBody) {
|
|
reportUnusedProperties()
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
const templateVisitor = {
|
|
/**
|
|
* @param {VExpressionContainer} node
|
|
*/
|
|
VExpressionContainer(node) {
|
|
for (const id of getReferences(node.references)) {
|
|
templatePropertiesContainer.usedProperties.addUsed(
|
|
id.name,
|
|
(context) => extractPatternOrThisProperties(id, context, true)
|
|
)
|
|
}
|
|
},
|
|
/**
|
|
* @param {VAttribute} node
|
|
*/
|
|
'VAttribute[directive=false]'(node) {
|
|
if (node.key.name === 'ref' && node.value != null) {
|
|
templatePropertiesContainer.refNames.add(node.value.value)
|
|
}
|
|
},
|
|
"VElement[parent.type!='VElement']:exit"() {
|
|
reportUnusedProperties()
|
|
}
|
|
}
|
|
|
|
return utils.defineTemplateBodyVisitor(
|
|
context,
|
|
templateVisitor,
|
|
scriptVisitor
|
|
)
|
|
}
|
|
}
|