no-undef-properties.js 16 KB

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