no-unused-properties.js 19 KB

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