no-unused-properties.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. /**
  2. * @fileoverview Disallow unused properties, data and computed properties.
  3. * @author Learning Equality
  4. */
  5. 'use strict'
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. const utils = require('../utils')
  10. const eslintUtils = require('eslint-utils')
  11. const { getStyleVariablesContext } = require('../utils/style-variables')
  12. const {
  13. definePropertyReferenceExtractor,
  14. mergePropertyReferences
  15. } = require('../utils/property-references')
  16. /**
  17. * @typedef {import('../utils').GroupName} GroupName
  18. * @typedef {import('../utils').VueObjectData} VueObjectData
  19. * @typedef {import('../utils/property-references').IPropertyReferences} IPropertyReferences
  20. */
  21. /**
  22. * @typedef {object} ComponentObjectPropertyData
  23. * @property {string} name
  24. * @property {GroupName} groupName
  25. * @property {'object'} type
  26. * @property {ASTNode} node
  27. * @property {Property} property
  28. *
  29. * @typedef {object} ComponentNonObjectPropertyData
  30. * @property {string} name
  31. * @property {GroupName} groupName
  32. * @property {'array' | 'type'} type
  33. * @property {ASTNode} node
  34. *
  35. * @typedef { ComponentNonObjectPropertyData | ComponentObjectPropertyData } ComponentPropertyData
  36. */
  37. /**
  38. * @typedef {object} TemplatePropertiesContainer
  39. * @property {IPropertyReferences[]} propertyReferences
  40. * @property {Set<string>} refNames
  41. * @typedef {object} VueComponentPropertiesContainer
  42. * @property {ComponentPropertyData[]} properties
  43. * @property {IPropertyReferences[]} propertyReferences
  44. * @property {IPropertyReferences[]} propertyReferencesForProps
  45. */
  46. // ------------------------------------------------------------------------------
  47. // Constants
  48. // ------------------------------------------------------------------------------
  49. const GROUP_PROPERTY = 'props'
  50. const GROUP_DATA = 'data'
  51. const GROUP_ASYNC_DATA = 'asyncData'
  52. const GROUP_COMPUTED_PROPERTY = 'computed'
  53. const GROUP_METHODS = 'methods'
  54. const GROUP_SETUP = 'setup'
  55. const GROUP_WATCHER = 'watch'
  56. const GROUP_EXPOSE = 'expose'
  57. const PROPERTY_LABEL = {
  58. props: 'property',
  59. data: 'data',
  60. asyncData: 'async data',
  61. computed: 'computed property',
  62. methods: 'method',
  63. setup: 'property returned from `setup()`',
  64. // not use
  65. watch: 'watch',
  66. provide: 'provide',
  67. inject: 'inject',
  68. expose: 'expose'
  69. }
  70. // ------------------------------------------------------------------------------
  71. // Helpers
  72. // ------------------------------------------------------------------------------
  73. /**
  74. * @param {RuleContext} context
  75. * @param {Identifier} id
  76. * @returns {Expression}
  77. */
  78. function findExpression(context, id) {
  79. const variable = utils.findVariableByIdentifier(context, id)
  80. if (!variable) {
  81. return id
  82. }
  83. if (variable.defs.length === 1) {
  84. const def = variable.defs[0]
  85. if (
  86. def.type === 'Variable' &&
  87. def.parent.kind === 'const' &&
  88. def.node.init
  89. ) {
  90. if (def.node.init.type === 'Identifier') {
  91. return findExpression(context, def.node.init)
  92. }
  93. return def.node.init
  94. }
  95. }
  96. return id
  97. }
  98. /**
  99. * Check if the given component property is marked as `@public` in JSDoc comments.
  100. * @param {ComponentPropertyData} property
  101. * @param {SourceCode} sourceCode
  102. */
  103. function isPublicMember(property, sourceCode) {
  104. if (
  105. property.type === 'object' &&
  106. // Props do not support @public.
  107. property.groupName !== 'props'
  108. ) {
  109. return isPublicProperty(property.property, sourceCode)
  110. }
  111. return false
  112. }
  113. /**
  114. * Check if the given property node is marked as `@public` in JSDoc comments.
  115. * @param {Property} node
  116. * @param {SourceCode} sourceCode
  117. */
  118. function isPublicProperty(node, sourceCode) {
  119. const jsdoc = getJSDocFromProperty(node, sourceCode)
  120. if (jsdoc) {
  121. return /(?:^|\s|\*)@public\b/u.test(jsdoc.value)
  122. }
  123. return false
  124. }
  125. /**
  126. * Get the JSDoc comment for a given property node.
  127. * @param {Property} node
  128. * @param {SourceCode} sourceCode
  129. */
  130. function getJSDocFromProperty(node, sourceCode) {
  131. const jsdoc = findJSDocComment(node, sourceCode)
  132. if (jsdoc) {
  133. return jsdoc
  134. }
  135. if (
  136. node.value.type === 'FunctionExpression' ||
  137. node.value.type === 'ArrowFunctionExpression'
  138. ) {
  139. return findJSDocComment(node.value, sourceCode)
  140. }
  141. return null
  142. }
  143. /**
  144. * Finds a JSDoc comment for the given node.
  145. * @param {ASTNode} node
  146. * @param {SourceCode} sourceCode
  147. * @returns {Comment | null}
  148. */
  149. function findJSDocComment(node, sourceCode) {
  150. /** @type {ASTNode | Token} */
  151. let currentNode = node
  152. let tokenBefore = null
  153. while (currentNode) {
  154. tokenBefore = sourceCode.getTokenBefore(currentNode, {
  155. includeComments: true
  156. })
  157. if (!tokenBefore || !eslintUtils.isCommentToken(tokenBefore)) {
  158. return null
  159. }
  160. if (tokenBefore.type === 'Line') {
  161. currentNode = tokenBefore
  162. continue
  163. }
  164. break
  165. }
  166. if (
  167. tokenBefore &&
  168. tokenBefore.type === 'Block' &&
  169. tokenBefore.value.charAt(0) === '*'
  170. ) {
  171. return tokenBefore
  172. }
  173. return null
  174. }
  175. // ------------------------------------------------------------------------------
  176. // Rule Definition
  177. // ------------------------------------------------------------------------------
  178. module.exports = {
  179. meta: {
  180. type: 'suggestion',
  181. docs: {
  182. description: 'disallow unused properties',
  183. categories: undefined,
  184. url: 'https://eslint.vuejs.org/rules/no-unused-properties.html'
  185. },
  186. fixable: null,
  187. schema: [
  188. {
  189. type: 'object',
  190. properties: {
  191. groups: {
  192. type: 'array',
  193. items: {
  194. enum: [
  195. GROUP_PROPERTY,
  196. GROUP_DATA,
  197. GROUP_ASYNC_DATA,
  198. GROUP_COMPUTED_PROPERTY,
  199. GROUP_METHODS,
  200. GROUP_SETUP
  201. ]
  202. },
  203. additionalItems: false,
  204. uniqueItems: true
  205. },
  206. deepData: { type: 'boolean' },
  207. ignorePublicMembers: { type: 'boolean' }
  208. },
  209. additionalProperties: false
  210. }
  211. ],
  212. messages: {
  213. unused: "'{{name}}' of {{group}} found, but never used."
  214. }
  215. },
  216. /** @param {RuleContext} context */
  217. create(context) {
  218. const options = context.options[0] || {}
  219. const groups = new Set(options.groups || [GROUP_PROPERTY])
  220. const deepData = Boolean(options.deepData)
  221. const ignorePublicMembers = Boolean(options.ignorePublicMembers)
  222. const propertyReferenceExtractor = definePropertyReferenceExtractor(context)
  223. /** @type {TemplatePropertiesContainer} */
  224. const templatePropertiesContainer = {
  225. propertyReferences: [],
  226. refNames: new Set()
  227. }
  228. /** @type {Map<ASTNode, VueComponentPropertiesContainer>} */
  229. const vueComponentPropertiesContainerMap = new Map()
  230. /**
  231. * @param {ASTNode} node
  232. * @returns {VueComponentPropertiesContainer}
  233. */
  234. function getVueComponentPropertiesContainer(node) {
  235. let container = vueComponentPropertiesContainerMap.get(node)
  236. if (!container) {
  237. container = {
  238. properties: [],
  239. propertyReferences: [],
  240. propertyReferencesForProps: []
  241. }
  242. vueComponentPropertiesContainerMap.set(node, container)
  243. }
  244. return container
  245. }
  246. /**
  247. * @param {string[]} segments
  248. * @param {Expression} propertyValue
  249. * @param {IPropertyReferences} propertyReferences
  250. */
  251. function verifyDataOptionDeepProperties(
  252. segments,
  253. propertyValue,
  254. propertyReferences
  255. ) {
  256. let targetExpr = propertyValue
  257. if (targetExpr.type === 'Identifier') {
  258. targetExpr = findExpression(context, targetExpr)
  259. }
  260. if (targetExpr.type === 'ObjectExpression') {
  261. for (const prop of targetExpr.properties) {
  262. if (prop.type !== 'Property') {
  263. continue
  264. }
  265. const name = utils.getStaticPropertyName(prop)
  266. if (name == null) {
  267. continue
  268. }
  269. if (
  270. !propertyReferences.hasProperty(name, { unknownCallAsAny: true })
  271. ) {
  272. // report
  273. context.report({
  274. node: prop.key,
  275. messageId: 'unused',
  276. data: {
  277. group: PROPERTY_LABEL.data,
  278. name: [...segments, name].join('.')
  279. }
  280. })
  281. continue
  282. }
  283. // next
  284. verifyDataOptionDeepProperties(
  285. [...segments, name],
  286. prop.value,
  287. propertyReferences.getNest(name)
  288. )
  289. }
  290. }
  291. }
  292. /**
  293. * Report all unused properties.
  294. */
  295. function reportUnusedProperties() {
  296. for (const container of vueComponentPropertiesContainerMap.values()) {
  297. const propertyReferences = mergePropertyReferences([
  298. ...container.propertyReferences,
  299. ...templatePropertiesContainer.propertyReferences
  300. ])
  301. const propertyReferencesForProps = mergePropertyReferences(
  302. container.propertyReferencesForProps
  303. )
  304. for (const property of container.properties) {
  305. if (
  306. property.groupName === 'props' &&
  307. propertyReferencesForProps.hasProperty(property.name)
  308. ) {
  309. // used props
  310. continue
  311. }
  312. if (
  313. property.groupName === 'setup' &&
  314. templatePropertiesContainer.refNames.has(property.name)
  315. ) {
  316. // used template refs
  317. continue
  318. }
  319. if (
  320. ignorePublicMembers &&
  321. isPublicMember(property, context.getSourceCode())
  322. ) {
  323. continue
  324. }
  325. if (propertyReferences.hasProperty(property.name)) {
  326. // used
  327. if (
  328. deepData &&
  329. (property.groupName === 'data' ||
  330. property.groupName === 'asyncData') &&
  331. property.type === 'object'
  332. ) {
  333. // Check the deep properties of the data option.
  334. verifyDataOptionDeepProperties(
  335. [property.name],
  336. property.property.value,
  337. propertyReferences.getNest(property.name)
  338. )
  339. }
  340. continue
  341. }
  342. context.report({
  343. node: property.node,
  344. messageId: 'unused',
  345. data: {
  346. group: PROPERTY_LABEL[property.groupName],
  347. name: property.name
  348. }
  349. })
  350. }
  351. }
  352. }
  353. /**
  354. * @param {Expression} node
  355. * @returns {Property|null}
  356. */
  357. function getParentProperty(node) {
  358. if (
  359. !node.parent ||
  360. node.parent.type !== 'Property' ||
  361. node.parent.value !== node
  362. ) {
  363. return null
  364. }
  365. const property = node.parent
  366. if (!utils.isProperty(property)) {
  367. return null
  368. }
  369. return property
  370. }
  371. const scriptVisitor = utils.compositingVisitors(
  372. utils.defineScriptSetupVisitor(context, {
  373. onDefinePropsEnter(node, props) {
  374. if (!groups.has('props')) {
  375. return
  376. }
  377. const container = getVueComponentPropertiesContainer(node)
  378. for (const prop of props) {
  379. if (!prop.propName) {
  380. continue
  381. }
  382. if (prop.type === 'object') {
  383. container.properties.push({
  384. type: prop.type,
  385. name: prop.propName,
  386. groupName: 'props',
  387. node: prop.key,
  388. property: prop.node
  389. })
  390. } else {
  391. container.properties.push({
  392. type: prop.type,
  393. name: prop.propName,
  394. groupName: 'props',
  395. node: prop.key
  396. })
  397. }
  398. }
  399. let target = node
  400. if (
  401. target.parent &&
  402. target.parent.type === 'CallExpression' &&
  403. target.parent.arguments[0] === target &&
  404. target.parent.callee.type === 'Identifier' &&
  405. target.parent.callee.name === 'withDefaults'
  406. ) {
  407. target = target.parent
  408. }
  409. if (
  410. !target.parent ||
  411. target.parent.type !== 'VariableDeclarator' ||
  412. target.parent.init !== target
  413. ) {
  414. return
  415. }
  416. const pattern = target.parent.id
  417. const propertyReferences =
  418. propertyReferenceExtractor.extractFromPattern(pattern)
  419. container.propertyReferencesForProps.push(propertyReferences)
  420. }
  421. }),
  422. utils.defineVueVisitor(context, {
  423. onVueObjectEnter(node) {
  424. const container = getVueComponentPropertiesContainer(node)
  425. for (const watcherOrExpose of utils.iterateProperties(
  426. node,
  427. new Set([GROUP_WATCHER, GROUP_EXPOSE])
  428. )) {
  429. if (watcherOrExpose.groupName === GROUP_WATCHER) {
  430. const watcher = watcherOrExpose
  431. // Process `watch: { foo /* <- this */ () {} }`
  432. container.propertyReferences.push(
  433. propertyReferenceExtractor.extractFromPath(
  434. watcher.name,
  435. watcher.node
  436. )
  437. )
  438. // Process `watch: { x: 'foo' /* <- this */ }`
  439. if (watcher.type === 'object') {
  440. const property = watcher.property
  441. if (property.kind === 'init') {
  442. for (const handlerValueNode of utils.iterateWatchHandlerValues(
  443. property
  444. )) {
  445. container.propertyReferences.push(
  446. propertyReferenceExtractor.extractFromNameLiteral(
  447. handlerValueNode
  448. )
  449. )
  450. }
  451. }
  452. }
  453. } else if (watcherOrExpose.groupName === GROUP_EXPOSE) {
  454. const expose = watcherOrExpose
  455. container.propertyReferences.push(
  456. propertyReferenceExtractor.extractFromName(
  457. expose.name,
  458. expose.node
  459. )
  460. )
  461. }
  462. }
  463. container.properties.push(...utils.iterateProperties(node, groups))
  464. },
  465. /** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property }} node */
  466. 'ObjectExpression > Property > :function[params.length>0]'(
  467. node,
  468. vueData
  469. ) {
  470. const property = getParentProperty(node)
  471. if (!property) {
  472. return
  473. }
  474. if (property.parent === vueData.node) {
  475. if (utils.getStaticPropertyName(property) !== 'data') {
  476. return
  477. }
  478. // check { data: (vm) => vm.prop }
  479. } else {
  480. const parentProperty = getParentProperty(property.parent)
  481. if (!parentProperty) {
  482. return
  483. }
  484. if (parentProperty.parent === vueData.node) {
  485. if (utils.getStaticPropertyName(parentProperty) !== 'computed') {
  486. return
  487. }
  488. // check { computed: { foo: (vm) => vm.prop } }
  489. } else {
  490. const parentParentProperty = getParentProperty(
  491. parentProperty.parent
  492. )
  493. if (!parentParentProperty) {
  494. return
  495. }
  496. if (parentParentProperty.parent === vueData.node) {
  497. if (
  498. utils.getStaticPropertyName(parentParentProperty) !==
  499. 'computed' ||
  500. utils.getStaticPropertyName(property) !== 'get'
  501. ) {
  502. return
  503. }
  504. // check { computed: { foo: { get: (vm) => vm.prop } } }
  505. } else {
  506. return
  507. }
  508. }
  509. }
  510. const propertyReferences =
  511. propertyReferenceExtractor.extractFromFunctionParam(node, 0)
  512. const container = getVueComponentPropertiesContainer(vueData.node)
  513. container.propertyReferences.push(propertyReferences)
  514. },
  515. onSetupFunctionEnter(node, vueData) {
  516. const container = getVueComponentPropertiesContainer(vueData.node)
  517. const propertyReferences =
  518. propertyReferenceExtractor.extractFromFunctionParam(node, 0)
  519. container.propertyReferencesForProps.push(propertyReferences)
  520. },
  521. onRenderFunctionEnter(node, vueData) {
  522. const container = getVueComponentPropertiesContainer(vueData.node)
  523. // Check for Vue 3.x render
  524. const propertyReferences =
  525. propertyReferenceExtractor.extractFromFunctionParam(node, 0)
  526. container.propertyReferencesForProps.push(propertyReferences)
  527. if (vueData.functional) {
  528. // Check for Vue 2.x render & functional
  529. const propertyReferencesForV2 =
  530. propertyReferenceExtractor.extractFromFunctionParam(node, 1)
  531. container.propertyReferencesForProps.push(
  532. propertyReferencesForV2.getNest('props')
  533. )
  534. }
  535. },
  536. /**
  537. * @param {ThisExpression | Identifier} node
  538. * @param {VueObjectData} vueData
  539. */
  540. 'ThisExpression, Identifier'(node, vueData) {
  541. if (!utils.isThis(node, context)) {
  542. return
  543. }
  544. const container = getVueComponentPropertiesContainer(vueData.node)
  545. const propertyReferences =
  546. propertyReferenceExtractor.extractFromExpression(node, false)
  547. container.propertyReferences.push(propertyReferences)
  548. }
  549. }),
  550. {
  551. Program() {
  552. const styleVars = getStyleVariablesContext(context)
  553. if (styleVars) {
  554. templatePropertiesContainer.propertyReferences.push(
  555. propertyReferenceExtractor.extractFromStyleVariablesContext(
  556. styleVars
  557. )
  558. )
  559. }
  560. },
  561. /** @param {Program} node */
  562. 'Program:exit'(node) {
  563. if (!node.templateBody) {
  564. reportUnusedProperties()
  565. }
  566. }
  567. }
  568. )
  569. const templateVisitor = {
  570. /**
  571. * @param {VExpressionContainer} node
  572. */
  573. VExpressionContainer(node) {
  574. templatePropertiesContainer.propertyReferences.push(
  575. propertyReferenceExtractor.extractFromVExpressionContainer(node)
  576. )
  577. },
  578. /**
  579. * @param {VAttribute} node
  580. */
  581. 'VAttribute[directive=false]'(node) {
  582. if (node.key.name === 'ref' && node.value != null) {
  583. templatePropertiesContainer.refNames.add(node.value.value)
  584. }
  585. },
  586. "VElement[parent.type!='VElement']:exit"() {
  587. reportUnusedProperties()
  588. }
  589. }
  590. return utils.defineTemplateBodyVisitor(
  591. context,
  592. templateVisitor,
  593. scriptVisitor
  594. )
  595. }
  596. }