no-undef-properties.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  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[]} */ (options.ignores || ['/^\\$/']).map(
  104. toRegExp
  105. )
  106. const propertyReferenceExtractor = definePropertyReferenceExtractor(context)
  107. const programNode = context.getSourceCode().ast
  108. /**
  109. * @param {ASTNode} node
  110. */
  111. function isScriptSetupProgram(node) {
  112. return node === programNode
  113. }
  114. /** Vue component context */
  115. class VueComponentContext {
  116. constructor() {
  117. /** @type { Map<string, PropertyData> } */
  118. this.defineProperties = new Map()
  119. /** @type { Set<string | ASTNode> } */
  120. this.reported = new Set()
  121. this.hasUnknownProperty = false
  122. }
  123. /**
  124. * Report
  125. * @param {IPropertyReferences} references
  126. * @param {object} [options]
  127. * @param {boolean} [options.props]
  128. */
  129. verifyReferences(references, options) {
  130. if (this.hasUnknownProperty) return
  131. const report = this.report.bind(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 && !prop.isProps) {
  149. report(nodes[0], referencePathName, 'undefProps')
  150. continue
  151. }
  152. } else {
  153. report(nodes[0], referencePathName, 'undef')
  154. continue
  155. }
  156. if (prop.hasNestProperty) {
  157. verifyUndefProperties(
  158. prop,
  159. references.getNest(refName),
  160. referencePathName
  161. )
  162. }
  163. }
  164. }
  165. }
  166. /**
  167. * Report
  168. * @param {ASTNode} node
  169. * @param {string} name
  170. * @param {'undef' | 'undefProps'} messageId
  171. */
  172. report(node, name, messageId = 'undef') {
  173. if (
  174. reserved.includes(name) ||
  175. ignores.some((ignore) => ignore.test(name))
  176. ) {
  177. return
  178. }
  179. if (
  180. // Prevents reporting to the same node.
  181. this.reported.has(node) ||
  182. // Prevents reports with the same name.
  183. // This is so that intentional undefined properties can be resolved with
  184. // a single warning suppression comment (`// eslint-disable-line`).
  185. this.reported.has(name)
  186. ) {
  187. return
  188. }
  189. this.reported.add(node)
  190. this.reported.add(name)
  191. context.report({
  192. node,
  193. messageId,
  194. data: {
  195. name
  196. }
  197. })
  198. }
  199. markAsHasUnknownProperty() {
  200. this.hasUnknownProperty = true
  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. /**
  227. * @param {Expression} node
  228. * @returns {Property|null}
  229. */
  230. function getParentProperty(node) {
  231. if (
  232. !node.parent ||
  233. node.parent.type !== 'Property' ||
  234. node.parent.value !== node
  235. ) {
  236. return null
  237. }
  238. const property = node.parent
  239. if (!utils.isProperty(property)) {
  240. return null
  241. }
  242. return property
  243. }
  244. const scriptVisitor = utils.compositingVisitors(
  245. {
  246. Program() {
  247. if (!utils.isScriptSetup(context)) {
  248. return
  249. }
  250. const ctx = getVueComponentContext(programNode)
  251. const globalScope = context.getSourceCode().scopeManager.globalScope
  252. if (globalScope) {
  253. for (const variable of globalScope.variables) {
  254. ctx.defineProperties.set(variable.name, {})
  255. }
  256. const moduleScope = globalScope.childScopes.find(
  257. (scope) => scope.type === 'module'
  258. )
  259. for (const variable of (moduleScope && moduleScope.variables) ||
  260. []) {
  261. ctx.defineProperties.set(variable.name, {})
  262. }
  263. }
  264. }
  265. },
  266. utils.defineScriptSetupVisitor(context, {
  267. onDefinePropsEnter(node, props) {
  268. const ctx = getVueComponentContext(programNode)
  269. for (const prop of props) {
  270. if (prop.type === 'unknown') {
  271. ctx.markAsHasUnknownProperty()
  272. return
  273. }
  274. if (!prop.propName) {
  275. continue
  276. }
  277. ctx.defineProperties.set(prop.propName, {
  278. isProps: true
  279. })
  280. }
  281. let target = node
  282. if (
  283. target.parent &&
  284. target.parent.type === 'CallExpression' &&
  285. target.parent.arguments[0] === target &&
  286. target.parent.callee.type === 'Identifier' &&
  287. target.parent.callee.name === 'withDefaults'
  288. ) {
  289. target = target.parent
  290. }
  291. if (
  292. !target.parent ||
  293. target.parent.type !== 'VariableDeclarator' ||
  294. target.parent.init !== target
  295. ) {
  296. return
  297. }
  298. const pattern = target.parent.id
  299. const propertyReferences =
  300. propertyReferenceExtractor.extractFromPattern(pattern)
  301. ctx.verifyReferences(propertyReferences)
  302. }
  303. }),
  304. utils.defineVueVisitor(context, {
  305. onVueObjectEnter(node) {
  306. const ctx = getVueComponentContext(node)
  307. for (const prop of utils.iterateProperties(
  308. node,
  309. new Set([
  310. GROUP_PROPERTY,
  311. GROUP_ASYNC_DATA,
  312. GROUP_DATA,
  313. GROUP_COMPUTED_PROPERTY,
  314. GROUP_SETUP,
  315. GROUP_METHODS,
  316. GROUP_INJECT
  317. ])
  318. )) {
  319. const propertyMap =
  320. (prop.groupName === GROUP_DATA ||
  321. prop.groupName === GROUP_ASYNC_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. }