123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817 |
- /**
- * @fileoverview Disallow unused properties, data and computed properties.
- * @author Learning Equality
- */
- 'use strict'
- // ------------------------------------------------------------------------------
- // Requirements
- // ------------------------------------------------------------------------------
- const utils = require('../utils')
- const eslintUtils = require('eslint-utils')
- /**
- * @typedef {import('../utils').ComponentPropertyData} ComponentPropertyData
- * @typedef {import('../utils').VueObjectData} VueObjectData
- */
- /**
- * @typedef {object} TemplatePropertiesContainer
- * @property {Set<string>} usedNames
- * @property {Set<string>} refNames
- * @typedef {object} VueComponentPropertiesContainer
- * @property {ComponentPropertyData[]} properties
- * @property {Set<string>} usedNames
- * @property {boolean} unknown
- * @property {Set<string>} usedPropsNames
- * @property {boolean} unknownProps
- * @typedef { { node: FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, index: number } } CallIdAndParamIndex
- * @typedef { { usedNames: UsedNames, unknown: boolean } } UsedProperties
- * @typedef { (context: RuleContext) => UsedProps } UsedPropsTracker
- */
- // ------------------------------------------------------------------------------
- // 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 PROPERTY_LABEL = {
- props: 'property',
- data: 'data',
- computed: 'computed property',
- methods: 'method',
- setup: 'property returned from `setup()`',
- watch: 'watch'
- }
- // ------------------------------------------------------------------------------
- // 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 getReferencesNames(references) {
- return references
- .filter((ref) => ref.variable == null)
- .map((ref) => ref.id.name)
- }
- class UsedNames {
- constructor() {
- /** @type {Map<string, UsedPropsTracker[]>} */
- this.map = new Map()
- }
- /**
- * @returns {IterableIterator<string>}
- */
- names() {
- return this.map.keys()
- }
- /**
- * @param {string} name
- * @returns {UsedPropsTracker[]}
- */
- get(name) {
- return this.map.get(name) || []
- }
- /**
- * @param {string} name
- * @param {UsedPropsTracker} tracker
- */
- add(name, tracker) {
- const list = this.map.get(name)
- if (list) {
- list.push(tracker)
- } else {
- this.map.set(name, [tracker])
- }
- }
- /**
- * @param {UsedNames} other
- */
- addAll(other) {
- other.map.forEach((trackers, name) => {
- const list = this.map.get(name)
- if (list) {
- list.push(...trackers)
- } else {
- this.map.set(name, trackers)
- }
- })
- }
- }
- /**
- * @param {ObjectPattern} node
- * @returns {UsedProperties}
- */
- function extractObjectPatternProperties(node) {
- const usedNames = new UsedNames()
- for (const prop of node.properties) {
- if (prop.type === 'Property') {
- const name = utils.getStaticPropertyName(prop)
- if (name) {
- usedNames.add(name, getObjectPatternPropertyPatternTracker(prop.value))
- } else {
- // If cannot trace name, everything is used!
- return {
- usedNames,
- unknown: true
- }
- }
- } else {
- // If use RestElement, everything is used!
- return {
- usedNames,
- unknown: true
- }
- }
- }
- return {
- usedNames,
- unknown: false
- }
- }
- /**
- * @param {Pattern} pattern
- * @returns {UsedPropsTracker}
- */
- function getObjectPatternPropertyPatternTracker(pattern) {
- if (pattern.type === 'ObjectPattern') {
- return () => {
- const result = new UsedProps()
- const { usedNames, unknown } = extractObjectPatternProperties(pattern)
- result.usedNames.addAll(usedNames)
- result.unknown = unknown
- return result
- }
- }
- if (pattern.type === 'Identifier') {
- return (context) => {
- const result = new UsedProps()
- const variable = findVariable(context, pattern)
- if (!variable) {
- return result
- }
- for (const reference of variable.references) {
- const id = reference.identifier
- const { usedNames, unknown, calls } = extractPatternOrThisProperties(
- id,
- context
- )
- result.usedNames.addAll(usedNames)
- result.unknown = result.unknown || unknown
- result.calls.push(...calls)
- }
- return result
- }
- } else if (pattern.type === 'AssignmentPattern') {
- return getObjectPatternPropertyPatternTracker(pattern.left)
- }
- return () => {
- const result = new UsedProps()
- result.unknown = true
- return result
- }
- }
- /**
- * @param {Identifier | MemberExpression | ChainExpression | ThisExpression} node
- * @param {RuleContext} context
- * @returns {UsedProps}
- */
- function extractPatternOrThisProperties(node, context) {
- const result = new UsedProps()
- const parent = node.parent
- if (parent.type === 'AssignmentExpression') {
- if (parent.right === node && parent.left.type === 'ObjectPattern') {
- // `({foo} = arg)`
- const { usedNames, unknown } = extractObjectPatternProperties(parent.left)
- result.usedNames.addAll(usedNames)
- result.unknown = result.unknown || unknown
- }
- return result
- } else if (parent.type === 'VariableDeclarator') {
- if (parent.init === node) {
- if (parent.id.type === 'ObjectPattern') {
- // `const {foo} = arg`
- const { usedNames, unknown } = extractObjectPatternProperties(parent.id)
- result.usedNames.addAll(usedNames)
- result.unknown = result.unknown || unknown
- } else if (parent.id.type === 'Identifier') {
- // `const foo = arg`
- const variable = findVariable(context, parent.id)
- if (!variable) {
- return result
- }
- for (const reference of variable.references) {
- const id = reference.identifier
- const { usedNames, unknown, calls } = extractPatternOrThisProperties(
- id,
- context
- )
- result.usedNames.addAll(usedNames)
- result.unknown = result.unknown || unknown
- result.calls.push(...calls)
- }
- }
- }
- return result
- } else if (parent.type === 'MemberExpression') {
- if (parent.object === node) {
- // `arg.foo`
- const name = utils.getStaticPropertyName(parent)
- if (name) {
- result.usedNames.add(name, () =>
- extractPatternOrThisProperties(parent, context)
- )
- } else {
- result.unknown = true
- }
- }
- return result
- } else if (parent.type === 'CallExpression') {
- const argIndex = parent.arguments.indexOf(node)
- if (argIndex > -1 && parent.callee.type === 'Identifier') {
- // `foo(arg)`
- const calleeVariable = findVariable(context, parent.callee)
- if (!calleeVariable) {
- return result
- }
- if (calleeVariable.defs.length === 1) {
- const def = calleeVariable.defs[0]
- if (
- def.type === 'Variable' &&
- def.parent.kind === 'const' &&
- def.node.init &&
- (def.node.init.type === 'FunctionExpression' ||
- def.node.init.type === 'ArrowFunctionExpression')
- ) {
- result.calls.push({
- node: def.node.init,
- index: argIndex
- })
- } else if (def.node.type === 'FunctionDeclaration') {
- result.calls.push({
- node: def.node,
- index: argIndex
- })
- }
- }
- }
- } else if (parent.type === 'ChainExpression') {
- const { usedNames, unknown, calls } = extractPatternOrThisProperties(
- parent,
- context
- )
- result.usedNames.addAll(usedNames)
- result.unknown = result.unknown || unknown
- result.calls.push(...calls)
- }
- return result
- }
- /**
- * Collects the property names used.
- */
- class UsedProps {
- constructor() {
- this.usedNames = new UsedNames()
- /** @type {CallIdAndParamIndex[]} */
- this.calls = []
- this.unknown = false
- }
- }
- /**
- * Collects the property names used for one parameter of the function.
- */
- class ParamUsedProps extends UsedProps {
- /**
- * @param {Pattern} paramNode
- * @param {RuleContext} context
- */
- constructor(paramNode, context) {
- super()
- while (paramNode.type === 'AssignmentPattern') {
- paramNode = paramNode.left
- }
- if (paramNode.type === 'RestElement' || paramNode.type === 'ArrayPattern') {
- // cannot check
- return
- }
- if (paramNode.type === 'ObjectPattern') {
- const { usedNames, unknown } = extractObjectPatternProperties(paramNode)
- this.usedNames.addAll(usedNames)
- this.unknown = this.unknown || unknown
- return
- }
- if (paramNode.type !== 'Identifier') {
- return
- }
- const variable = findVariable(context, paramNode)
- if (!variable) {
- return
- }
- for (const reference of variable.references) {
- const id = reference.identifier
- const { usedNames, unknown, calls } = extractPatternOrThisProperties(
- id,
- context
- )
- this.usedNames.addAll(usedNames)
- this.unknown = this.unknown || unknown
- this.calls.push(...calls)
- }
- }
- }
- /**
- * Collects the property names used for parameters of the function.
- */
- class ParamsUsedProps {
- /**
- * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
- * @param {RuleContext} context
- */
- constructor(node, context) {
- this.node = node
- this.context = context
- /** @type {ParamUsedProps[]} */
- this.params = []
- }
- /**
- * @param {number} index
- * @returns {ParamUsedProps | null}
- */
- getParam(index) {
- const param = this.params[index]
- if (param != null) {
- return param
- }
- if (this.node.params[index]) {
- return (this.params[index] = new ParamUsedProps(
- this.node.params[index],
- this.context
- ))
- }
- 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
- }
- },
- 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])
- /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, ParamsUsedProps>} */
- const paramsUsedPropsMap = new Map()
- /** @type {TemplatePropertiesContainer} */
- const templatePropertiesContainer = {
- usedNames: new Set(),
- refNames: new Set()
- }
- /** @type {Map<ASTNode, VueComponentPropertiesContainer>} */
- const vueComponentPropertiesContainerMap = new Map()
- /**
- * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
- * @returns {ParamsUsedProps}
- */
- function getParamsUsedProps(node) {
- let usedProps = paramsUsedPropsMap.get(node)
- if (!usedProps) {
- usedProps = new ParamsUsedProps(node, context)
- paramsUsedPropsMap.set(node, usedProps)
- }
- return usedProps
- }
- /**
- * @param {ASTNode} node
- * @returns {VueComponentPropertiesContainer}
- */
- function getVueComponentPropertiesContainer(node) {
- let container = vueComponentPropertiesContainerMap.get(node)
- if (!container) {
- container = {
- properties: [],
- usedNames: new Set(),
- usedPropsNames: new Set(),
- unknown: false,
- unknownProps: false
- }
- vueComponentPropertiesContainerMap.set(node, container)
- }
- return container
- }
- /**
- * Report all unused properties.
- */
- function reportUnusedProperties() {
- for (const container of vueComponentPropertiesContainerMap.values()) {
- if (container.unknown) {
- // unknown
- continue
- }
- for (const property of container.properties) {
- if (
- container.usedNames.has(property.name) ||
- templatePropertiesContainer.usedNames.has(property.name)
- ) {
- // used
- continue
- }
- if (
- property.groupName === 'props' &&
- (container.unknownProps ||
- container.usedPropsNames.has(property.name))
- ) {
- // used props
- continue
- }
- if (
- property.groupName === 'setup' &&
- templatePropertiesContainer.refNames.has(property.name)
- ) {
- // used template refs
- continue
- }
- context.report({
- node: property.node,
- messageId: 'unused',
- data: {
- group: PROPERTY_LABEL[property.groupName],
- name: property.name
- }
- })
- }
- }
- }
- /**
- * @param {UsedProps} usedProps
- * @param {Map<ASTNode,Set<number>>} already
- * @returns {IterableIterator<UsedProps>}
- */
- function* iterateUsedProps(usedProps, already = new Map()) {
- yield usedProps
- for (const call of usedProps.calls) {
- let alreadyIndexes = already.get(call.node)
- if (!alreadyIndexes) {
- alreadyIndexes = new Set()
- already.set(call.node, alreadyIndexes)
- }
- if (alreadyIndexes.has(call.index)) {
- continue
- }
- alreadyIndexes.add(call.index)
- const paramsUsedProps = getParamsUsedProps(call.node)
- const paramUsedProps = paramsUsedProps.getParam(call.index)
- if (!paramUsedProps) {
- continue
- }
- yield paramUsedProps
- yield* iterateUsedProps(paramUsedProps, already)
- }
- }
- /**
- * @param {VueComponentPropertiesContainer} container
- * @param {UsedProps} baseUseProps
- */
- function processParamPropsUsed(container, baseUseProps) {
- for (const { usedNames, unknown } of iterateUsedProps(baseUseProps)) {
- if (unknown) {
- container.unknownProps = true
- return
- }
- for (const name of usedNames.names()) {
- container.usedPropsNames.add(name)
- }
- }
- }
- /**
- * @param {VueComponentPropertiesContainer} container
- * @param {UsedProps} baseUseProps
- */
- function processUsed(container, baseUseProps) {
- for (const { usedNames, unknown } of iterateUsedProps(baseUseProps)) {
- if (unknown) {
- container.unknown = true
- return
- }
- for (const name of usedNames.names()) {
- container.usedNames.add(name)
- }
- }
- }
- /**
- * @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 (!property.parent || property.parent.type !== 'ObjectExpression') {
- return null
- }
- return /** @type {Property} */ (property)
- }
- const scriptVisitor = Object.assign(
- {},
- utils.defineVueVisitor(context, {
- onVueObjectEnter(node) {
- const container = getVueComponentPropertiesContainer(node)
- const watcherUsedProperties = new Set()
- for (const watcher of utils.iterateProperties(
- node,
- new Set([GROUP_WATCHER])
- )) {
- // Process `watch: { foo /* <- this */ () {} }`
- let path
- for (const seg of watcher.name.split('.')) {
- path = path ? `${path}.${seg}` : seg
- watcherUsedProperties.add(path)
- }
- // 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) {
- watcherUsedProperties.add(name)
- }
- }
- }
- }
- }
- }
- for (const prop of utils.iterateProperties(node, groups)) {
- if (watcherUsedProperties.has(prop.name)) {
- continue
- }
- container.properties.push(prop)
- }
- },
- /** @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.prop } } }
- } else {
- return
- }
- }
- }
- const paramsUsedProps = getParamsUsedProps(node)
- const usedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
- 0
- ))
- processUsed(
- getVueComponentPropertiesContainer(vueData.node),
- usedProps
- )
- },
- onSetupFunctionEnter(node, vueData) {
- const container = getVueComponentPropertiesContainer(vueData.node)
- if (node.params[0]) {
- const paramsUsedProps = getParamsUsedProps(node)
- const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
- 0
- ))
- processParamPropsUsed(container, paramUsedProps)
- }
- },
- onRenderFunctionEnter(node, vueData) {
- const container = getVueComponentPropertiesContainer(vueData.node)
- if (node.params[0]) {
- // for Vue 3.x render
- const paramsUsedProps = getParamsUsedProps(node)
- const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
- 0
- ))
- processParamPropsUsed(container, paramUsedProps)
- if (container.unknownProps) {
- return
- }
- }
- if (vueData.functional && node.params[1]) {
- // for Vue 2.x render & functional
- const paramsUsedProps = getParamsUsedProps(node)
- const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
- 1
- ))
- for (const { usedNames, unknown } of iterateUsedProps(
- paramUsedProps
- )) {
- if (unknown) {
- container.unknownProps = true
- return
- }
- for (const usedPropsTracker of usedNames.get('props')) {
- const propUsedProps = usedPropsTracker(context)
- processParamPropsUsed(container, propUsedProps)
- if (container.unknownProps) {
- return
- }
- }
- }
- }
- },
- /**
- * @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)
- processUsed(container, usedProps)
- }
- }),
- {
- /** @param {Program} node */
- 'Program:exit'(node) {
- if (!node.templateBody) {
- reportUnusedProperties()
- }
- }
- }
- )
- const templateVisitor = {
- /**
- * @param {VExpressionContainer} node
- */
- VExpressionContainer(node) {
- for (const name of getReferencesNames(node.references)) {
- templatePropertiesContainer.usedNames.add(name)
- }
- },
- /**
- * @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
- )
- }
- }
|