no-undef-properties.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. /**
  2. * @fileoverview Disallow undefined properties.
  3. * @author Yosuke Ota
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. const reserved = require('../utils/vue-reserved.json')
  8. const { toRegExp } = require('../utils/regexp')
  9. const { getStyleVariablesContext } = require('../utils/style-variables')
  10. const {
  11. definePropertyReferenceExtractor
  12. } = require('../utils/property-references')
  13. /**
  14. * @typedef {import('../utils').VueObjectData} VueObjectData
  15. * @typedef {import('../utils/property-references').IPropertyReferences} IPropertyReferences
  16. */
  17. /**
  18. * @typedef {object} PropertyData
  19. * @property {boolean} [hasNestProperty]
  20. * @property { (name: string) => PropertyData | null } [get]
  21. * @property {boolean} [isProps]
  22. */
  23. const GROUP_PROPERTY = 'props'
  24. const GROUP_ASYNC_DATA = 'asyncData' // Nuxt.js
  25. const GROUP_DATA = 'data'
  26. const GROUP_COMPUTED_PROPERTY = 'computed'
  27. const GROUP_METHODS = 'methods'
  28. const GROUP_SETUP = 'setup'
  29. const GROUP_WATCHER = 'watch'
  30. const GROUP_EXPOSE = 'expose'
  31. const GROUP_INJECT = 'inject'
  32. /**
  33. * @param {ObjectExpression} object
  34. * @returns {Map<string, Property> | null}
  35. */
  36. function getObjectPropertyMap(object) {
  37. /** @type {Map<string, Property>} */
  38. const props = new Map()
  39. for (const p of object.properties) {
  40. if (p.type !== 'Property') {
  41. return null
  42. }
  43. const name = utils.getStaticPropertyName(p)
  44. if (name == null) {
  45. return null
  46. }
  47. props.set(name, p)
  48. }
  49. return props
  50. }
  51. /**
  52. * @param {Property | undefined} property
  53. * @returns {PropertyData | null}
  54. */
  55. function getPropertyDataFromObjectProperty(property) {
  56. if (property == null) {
  57. return null
  58. }
  59. const propertyMap =
  60. property.value.type === 'ObjectExpression'
  61. ? getObjectPropertyMap(property.value)
  62. : null
  63. return {
  64. hasNestProperty: Boolean(propertyMap),
  65. get(name) {
  66. if (!propertyMap) {
  67. return null
  68. }
  69. return getPropertyDataFromObjectProperty(propertyMap.get(name))
  70. }
  71. }
  72. }
  73. module.exports = {
  74. meta: {
  75. type: 'suggestion',
  76. docs: {
  77. description: 'disallow undefined properties',
  78. categories: undefined,
  79. url: 'https://eslint.vuejs.org/rules/no-undef-properties.html'
  80. },
  81. fixable: null,
  82. schema: [
  83. {
  84. type: 'object',
  85. properties: {
  86. ignores: {
  87. type: 'array',
  88. items: { type: 'string' },
  89. uniqueItems: true
  90. }
  91. },
  92. additionalProperties: false
  93. }
  94. ],
  95. messages: {
  96. undef: "'{{name}}' is not defined.",
  97. undefProps: "'{{name}}' is not defined in props."
  98. }
  99. },
  100. /** @param {RuleContext} context */
  101. create(context) {
  102. const options = context.options[0] || {}
  103. const ignores = /** @type {string[]} */ (
  104. options.ignores || [String.raw`/^\$/`]
  105. ).map(toRegExp)
  106. const propertyReferenceExtractor = definePropertyReferenceExtractor(context)
  107. const programNode = context.getSourceCode().ast
  108. /**
  109. * Property names identified as defined via a Vuex or Pinia helpers
  110. * @type {Set<string>}
  111. */
  112. const propertiesDefinedByStoreHelpers = new Set()
  113. /**
  114. * @param {ASTNode} node
  115. */
  116. function isScriptSetupProgram(node) {
  117. return node === programNode
  118. }
  119. /** Vue component context */
  120. class VueComponentContext {
  121. constructor() {
  122. /** @type { Map<string, PropertyData> } */
  123. this.defineProperties = new Map()
  124. /** @type { Set<string | ASTNode> } */
  125. this.reported = new Set()
  126. this.hasUnknownProperty = false
  127. }
  128. /**
  129. * Report
  130. * @param {IPropertyReferences} references
  131. * @param {object} [options]
  132. * @param {boolean} [options.props]
  133. */
  134. verifyReferences(references, options) {
  135. if (this.hasUnknownProperty) return
  136. const report = this.report.bind(this)
  137. verifyUndefProperties(this.defineProperties, references, null)
  138. /**
  139. * @param { { get?: (name: string) => PropertyData | null | undefined } } defineProperties
  140. * @param {IPropertyReferences|null} references
  141. * @param {string|null} pathName
  142. */
  143. function verifyUndefProperties(defineProperties, references, pathName) {
  144. if (!references) {
  145. return
  146. }
  147. for (const [refName, { nodes }] of references.allProperties()) {
  148. const referencePathName = pathName
  149. ? `${pathName}.${refName}`
  150. : refName
  151. const prop = defineProperties.get && defineProperties.get(refName)
  152. if (prop) {
  153. if (options && options.props && !prop.isProps) {
  154. report(nodes[0], referencePathName, 'undefProps')
  155. continue
  156. }
  157. } else {
  158. report(nodes[0], referencePathName, 'undef')
  159. continue
  160. }
  161. if (prop.hasNestProperty) {
  162. verifyUndefProperties(
  163. prop,
  164. references.getNest(refName),
  165. referencePathName
  166. )
  167. }
  168. }
  169. }
  170. }
  171. /**
  172. * Report
  173. * @param {ASTNode} node
  174. * @param {string} name
  175. * @param {'undef' | 'undefProps'} messageId
  176. */
  177. report(node, name, messageId = 'undef') {
  178. if (
  179. reserved.includes(name) ||
  180. ignores.some((ignore) => ignore.test(name)) ||
  181. propertiesDefinedByStoreHelpers.has(name)
  182. ) {
  183. return
  184. }
  185. if (
  186. // Prevents reporting to the same node.
  187. this.reported.has(node) ||
  188. // Prevents reports with the same name.
  189. // This is so that intentional undefined properties can be resolved with
  190. // a single warning suppression comment (`// eslint-disable-line`).
  191. this.reported.has(name)
  192. ) {
  193. return
  194. }
  195. this.reported.add(node)
  196. this.reported.add(name)
  197. context.report({
  198. node,
  199. messageId,
  200. data: {
  201. name
  202. }
  203. })
  204. }
  205. markAsHasUnknownProperty() {
  206. this.hasUnknownProperty = true
  207. }
  208. }
  209. /** @type {Map<ASTNode, VueComponentContext>} */
  210. const vueComponentContextMap = new Map()
  211. /**
  212. * @param {ASTNode} node
  213. * @returns {VueComponentContext}
  214. */
  215. function getVueComponentContext(node) {
  216. let ctx = vueComponentContextMap.get(node)
  217. if (!ctx) {
  218. ctx = new VueComponentContext()
  219. vueComponentContextMap.set(node, ctx)
  220. }
  221. return ctx
  222. }
  223. /**
  224. * @returns {VueComponentContext|void}
  225. */
  226. function getVueComponentContextForTemplate() {
  227. const keys = [...vueComponentContextMap.keys()]
  228. const exported =
  229. keys.find(isScriptSetupProgram) || keys.find(utils.isInExportDefault)
  230. return exported && vueComponentContextMap.get(exported)
  231. }
  232. /**
  233. * @param {Expression} node
  234. * @returns {Property|null}
  235. */
  236. function getParentProperty(node) {
  237. if (
  238. !node.parent ||
  239. node.parent.type !== 'Property' ||
  240. node.parent.value !== node
  241. ) {
  242. return null
  243. }
  244. const property = node.parent
  245. if (!utils.isProperty(property)) {
  246. return null
  247. }
  248. return property
  249. }
  250. const scriptVisitor = utils.compositingVisitors(
  251. {
  252. Program() {
  253. if (!utils.isScriptSetup(context)) {
  254. return
  255. }
  256. const ctx = getVueComponentContext(programNode)
  257. const globalScope = context.getSourceCode().scopeManager.globalScope
  258. if (globalScope) {
  259. for (const variable of globalScope.variables) {
  260. ctx.defineProperties.set(variable.name, {})
  261. }
  262. const moduleScope = globalScope.childScopes.find(
  263. (scope) => scope.type === 'module'
  264. )
  265. for (const variable of (moduleScope && moduleScope.variables) ||
  266. []) {
  267. ctx.defineProperties.set(variable.name, {})
  268. }
  269. }
  270. }
  271. },
  272. utils.defineScriptSetupVisitor(context, {
  273. onDefinePropsEnter(node, props) {
  274. const ctx = getVueComponentContext(programNode)
  275. for (const prop of props) {
  276. if (prop.type === 'unknown') {
  277. ctx.markAsHasUnknownProperty()
  278. return
  279. }
  280. if (!prop.propName) {
  281. continue
  282. }
  283. ctx.defineProperties.set(prop.propName, {
  284. isProps: true
  285. })
  286. }
  287. let target = node
  288. if (
  289. target.parent &&
  290. target.parent.type === 'CallExpression' &&
  291. target.parent.arguments[0] === target &&
  292. target.parent.callee.type === 'Identifier' &&
  293. target.parent.callee.name === 'withDefaults'
  294. ) {
  295. target = target.parent
  296. }
  297. if (
  298. !target.parent ||
  299. target.parent.type !== 'VariableDeclarator' ||
  300. target.parent.init !== target
  301. ) {
  302. return
  303. }
  304. const pattern = target.parent.id
  305. const propertyReferences =
  306. propertyReferenceExtractor.extractFromPattern(pattern)
  307. ctx.verifyReferences(propertyReferences)
  308. },
  309. onDefineModelEnter(_node, model) {
  310. const ctx = getVueComponentContext(programNode)
  311. ctx.defineProperties.set(model.name.modelName, {
  312. isProps: true
  313. })
  314. }
  315. }),
  316. utils.defineVueVisitor(context, {
  317. /**
  318. * @param {CallExpression} node
  319. */
  320. CallExpression(node) {
  321. if (node.callee.type !== 'Identifier') return
  322. /** @type {'methods'|'computed'|null} */
  323. let groupName = null
  324. if (/^mapMutations|mapActions$/u.test(node.callee.name)) {
  325. groupName = GROUP_METHODS
  326. } else if (
  327. /^mapState|mapGetters|mapWritableState$/u.test(node.callee.name)
  328. ) {
  329. groupName = GROUP_COMPUTED_PROPERTY
  330. }
  331. if (!groupName || node.arguments.length === 0) return
  332. // On Pinia the store is always the first argument
  333. const arg =
  334. node.arguments.length === 2 ? node.arguments[1] : node.arguments[0]
  335. if (arg.type === 'ObjectExpression') {
  336. // e.g.
  337. // `mapMutations({ add: 'increment' })`
  338. // `mapState({ count: state => state.todosCount })`
  339. for (const prop of arg.properties) {
  340. const name =
  341. prop.type === 'SpreadElement'
  342. ? null
  343. : utils.getStaticPropertyName(prop)
  344. if (name) {
  345. propertiesDefinedByStoreHelpers.add(name)
  346. }
  347. }
  348. } else if (arg.type === 'ArrayExpression') {
  349. // e.g. `mapMutations(['add'])`
  350. for (const element of arg.elements) {
  351. if (!element || !utils.isStringLiteral(element)) {
  352. continue
  353. }
  354. const name = utils.getStringLiteralValue(element)
  355. if (name) {
  356. propertiesDefinedByStoreHelpers.add(name)
  357. }
  358. }
  359. }
  360. },
  361. onVueObjectEnter(node) {
  362. const ctx = getVueComponentContext(node)
  363. for (const prop of utils.iterateProperties(
  364. node,
  365. new Set([
  366. GROUP_PROPERTY,
  367. GROUP_ASYNC_DATA,
  368. GROUP_DATA,
  369. GROUP_COMPUTED_PROPERTY,
  370. GROUP_SETUP,
  371. GROUP_METHODS,
  372. GROUP_INJECT
  373. ])
  374. )) {
  375. const propertyMap =
  376. (prop.groupName === GROUP_DATA ||
  377. prop.groupName === GROUP_ASYNC_DATA) &&
  378. prop.type === 'object' &&
  379. prop.property.value.type === 'ObjectExpression'
  380. ? getObjectPropertyMap(prop.property.value)
  381. : null
  382. ctx.defineProperties.set(prop.name, {
  383. hasNestProperty: Boolean(propertyMap),
  384. isProps: prop.groupName === GROUP_PROPERTY,
  385. get(name) {
  386. if (!propertyMap) {
  387. return null
  388. }
  389. return getPropertyDataFromObjectProperty(propertyMap.get(name))
  390. }
  391. })
  392. }
  393. for (const watcherOrExpose of utils.iterateProperties(
  394. node,
  395. new Set([GROUP_WATCHER, GROUP_EXPOSE])
  396. )) {
  397. if (watcherOrExpose.groupName === GROUP_WATCHER) {
  398. const watcher = watcherOrExpose
  399. // Process `watch: { foo /* <- this */ () {} }`
  400. ctx.verifyReferences(
  401. propertyReferenceExtractor.extractFromPath(
  402. watcher.name,
  403. watcher.node
  404. )
  405. )
  406. // Process `watch: { x: 'foo' /* <- this */ }`
  407. if (watcher.type === 'object') {
  408. const property = watcher.property
  409. if (property.kind === 'init') {
  410. for (const handlerValueNode of utils.iterateWatchHandlerValues(
  411. property
  412. )) {
  413. ctx.verifyReferences(
  414. propertyReferenceExtractor.extractFromNameLiteral(
  415. handlerValueNode
  416. )
  417. )
  418. }
  419. }
  420. }
  421. } else if (watcherOrExpose.groupName === GROUP_EXPOSE) {
  422. const expose = watcherOrExpose
  423. ctx.verifyReferences(
  424. propertyReferenceExtractor.extractFromName(
  425. expose.name,
  426. expose.node
  427. )
  428. )
  429. }
  430. }
  431. },
  432. /** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property }} node */
  433. 'ObjectExpression > Property > :function[params.length>0]'(
  434. node,
  435. vueData
  436. ) {
  437. let props = false
  438. const property = getParentProperty(node)
  439. if (!property) {
  440. return
  441. }
  442. if (property.parent === vueData.node) {
  443. if (utils.getStaticPropertyName(property) !== 'data') {
  444. return
  445. }
  446. // check { data: (vm) => vm.prop }
  447. props = true
  448. } else {
  449. const parentProperty = getParentProperty(property.parent)
  450. if (!parentProperty) {
  451. return
  452. }
  453. if (parentProperty.parent === vueData.node) {
  454. if (utils.getStaticPropertyName(parentProperty) !== 'computed') {
  455. return
  456. }
  457. // check { computed: { foo: (vm) => vm.prop } }
  458. } else {
  459. const parentParentProperty = getParentProperty(
  460. parentProperty.parent
  461. )
  462. if (!parentParentProperty) {
  463. return
  464. }
  465. if (parentParentProperty.parent === vueData.node) {
  466. if (
  467. utils.getStaticPropertyName(parentParentProperty) !==
  468. 'computed' ||
  469. utils.getStaticPropertyName(property) !== 'get'
  470. ) {
  471. return
  472. }
  473. // check { computed: { foo: { get: (vm) => vm.prop } } }
  474. } else {
  475. return
  476. }
  477. }
  478. }
  479. const propertyReferences =
  480. propertyReferenceExtractor.extractFromFunctionParam(node, 0)
  481. const ctx = getVueComponentContext(vueData.node)
  482. ctx.verifyReferences(propertyReferences, { props })
  483. },
  484. onSetupFunctionEnter(node, vueData) {
  485. const propertyReferences =
  486. propertyReferenceExtractor.extractFromFunctionParam(node, 0)
  487. const ctx = getVueComponentContext(vueData.node)
  488. ctx.verifyReferences(propertyReferences, {
  489. props: true
  490. })
  491. },
  492. onRenderFunctionEnter(node, vueData) {
  493. const ctx = getVueComponentContext(vueData.node)
  494. // Check for Vue 3.x render
  495. const propertyReferences =
  496. propertyReferenceExtractor.extractFromFunctionParam(node, 0)
  497. ctx.verifyReferences(propertyReferences)
  498. if (vueData.functional) {
  499. // Check for Vue 2.x render & functional
  500. const propertyReferencesForV2 =
  501. propertyReferenceExtractor.extractFromFunctionParam(node, 1)
  502. ctx.verifyReferences(propertyReferencesForV2.getNest('props'), {
  503. props: true
  504. })
  505. }
  506. },
  507. /**
  508. * @param {ThisExpression | Identifier} node
  509. * @param {VueObjectData} vueData
  510. */
  511. 'ThisExpression, Identifier'(node, vueData) {
  512. if (!utils.isThis(node, context)) {
  513. return
  514. }
  515. const ctx = getVueComponentContext(vueData.node)
  516. const propertyReferences =
  517. propertyReferenceExtractor.extractFromExpression(node, false)
  518. ctx.verifyReferences(propertyReferences)
  519. }
  520. }),
  521. {
  522. 'Program:exit'() {
  523. const ctx = getVueComponentContextForTemplate()
  524. if (!ctx) {
  525. return
  526. }
  527. const styleVars = getStyleVariablesContext(context)
  528. if (styleVars) {
  529. ctx.verifyReferences(
  530. propertyReferenceExtractor.extractFromStyleVariablesContext(
  531. styleVars
  532. )
  533. )
  534. }
  535. }
  536. }
  537. )
  538. const templateVisitor = {
  539. /**
  540. * @param {VExpressionContainer} node
  541. */
  542. VExpressionContainer(node) {
  543. const ctx = getVueComponentContextForTemplate()
  544. if (!ctx) {
  545. return
  546. }
  547. ctx.verifyReferences(
  548. propertyReferenceExtractor.extractFromVExpressionContainer(node, {
  549. ignoreGlobals: true
  550. })
  551. )
  552. }
  553. }
  554. return utils.defineTemplateBodyVisitor(
  555. context,
  556. templateVisitor,
  557. scriptVisitor
  558. )
  559. }
  560. }