no-unused-properties.js 18 KB

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