no-undef-properties.js 16 KB

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