no-unused-properties.js 18 KB

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