no-undef-properties.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  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. }
  122. /**
  123. * Report
  124. * @param {IPropertyReferences} references
  125. * @param {object} [options]
  126. * @param {boolean} [options.props]
  127. */
  128. verifyReferences(references, options) {
  129. const report = this.report.bind(this)
  130. verifyUndefProperties(this.defineProperties, references, null)
  131. /**
  132. * @param { { get?: (name: string) => PropertyData | null | undefined } } defineProperties
  133. * @param {IPropertyReferences|null} references
  134. * @param {string|null} pathName
  135. */
  136. function verifyUndefProperties(defineProperties, references, pathName) {
  137. if (!references) {
  138. return
  139. }
  140. for (const [refName, { nodes }] of references.allProperties()) {
  141. const referencePathName = pathName
  142. ? `${pathName}.${refName}`
  143. : refName
  144. const prop = defineProperties.get && defineProperties.get(refName)
  145. if (prop) {
  146. if (options && options.props && !prop.isProps) {
  147. report(nodes[0], referencePathName, 'undefProps')
  148. continue
  149. }
  150. } else {
  151. report(nodes[0], referencePathName, 'undef')
  152. continue
  153. }
  154. if (prop.hasNestProperty) {
  155. verifyUndefProperties(
  156. prop,
  157. references.getNest(refName),
  158. referencePathName
  159. )
  160. }
  161. }
  162. }
  163. }
  164. /**
  165. * Report
  166. * @param {ASTNode} node
  167. * @param {string} name
  168. * @param {'undef' | 'undefProps'} messageId
  169. */
  170. report(node, name, messageId = 'undef') {
  171. if (
  172. reserved.includes(name) ||
  173. ignores.some((ignore) => ignore.test(name))
  174. ) {
  175. return
  176. }
  177. if (
  178. // Prevents reporting to the same node.
  179. this.reported.has(node) ||
  180. // Prevents reports with the same name.
  181. // This is so that intentional undefined properties can be resolved with
  182. // a single warning suppression comment (`// eslint-disable-line`).
  183. this.reported.has(name)
  184. ) {
  185. return
  186. }
  187. this.reported.add(node)
  188. this.reported.add(name)
  189. context.report({
  190. node,
  191. messageId,
  192. data: {
  193. name
  194. }
  195. })
  196. }
  197. }
  198. /** @type {Map<ASTNode, VueComponentContext>} */
  199. const vueComponentContextMap = new Map()
  200. /**
  201. * @param {ASTNode} node
  202. * @returns {VueComponentContext}
  203. */
  204. function getVueComponentContext(node) {
  205. let ctx = vueComponentContextMap.get(node)
  206. if (!ctx) {
  207. ctx = new VueComponentContext()
  208. vueComponentContextMap.set(node, ctx)
  209. }
  210. return ctx
  211. }
  212. /**
  213. * @returns {VueComponentContext|void}
  214. */
  215. function getVueComponentContextForTemplate() {
  216. const keys = [...vueComponentContextMap.keys()]
  217. const exported =
  218. keys.find(isScriptSetupProgram) || keys.find(utils.isInExportDefault)
  219. return exported && vueComponentContextMap.get(exported)
  220. }
  221. /**
  222. * @param {Expression} node
  223. * @returns {Property|null}
  224. */
  225. function getParentProperty(node) {
  226. if (
  227. !node.parent ||
  228. node.parent.type !== 'Property' ||
  229. node.parent.value !== node
  230. ) {
  231. return null
  232. }
  233. const property = node.parent
  234. if (!utils.isProperty(property)) {
  235. return null
  236. }
  237. return property
  238. }
  239. const scriptVisitor = utils.compositingVisitors(
  240. {
  241. Program() {
  242. if (!utils.isScriptSetup(context)) {
  243. return
  244. }
  245. const ctx = getVueComponentContext(programNode)
  246. const globalScope = context.getSourceCode().scopeManager.globalScope
  247. if (globalScope) {
  248. for (const variable of globalScope.variables) {
  249. ctx.defineProperties.set(variable.name, {})
  250. }
  251. const moduleScope = globalScope.childScopes.find(
  252. (scope) => scope.type === 'module'
  253. )
  254. for (const variable of (moduleScope && moduleScope.variables) ||
  255. []) {
  256. ctx.defineProperties.set(variable.name, {})
  257. }
  258. }
  259. }
  260. },
  261. utils.defineScriptSetupVisitor(context, {
  262. onDefinePropsEnter(node, props) {
  263. const ctx = getVueComponentContext(programNode)
  264. for (const prop of props) {
  265. if (!prop.propName) {
  266. continue
  267. }
  268. ctx.defineProperties.set(prop.propName, {
  269. isProps: true
  270. })
  271. }
  272. let target = node
  273. if (
  274. target.parent &&
  275. target.parent.type === 'CallExpression' &&
  276. target.parent.arguments[0] === target &&
  277. target.parent.callee.type === 'Identifier' &&
  278. target.parent.callee.name === 'withDefaults'
  279. ) {
  280. target = target.parent
  281. }
  282. if (
  283. !target.parent ||
  284. target.parent.type !== 'VariableDeclarator' ||
  285. target.parent.init !== target
  286. ) {
  287. return
  288. }
  289. const pattern = target.parent.id
  290. const propertyReferences =
  291. propertyReferenceExtractor.extractFromPattern(pattern)
  292. ctx.verifyReferences(propertyReferences)
  293. }
  294. }),
  295. utils.defineVueVisitor(context, {
  296. onVueObjectEnter(node) {
  297. const ctx = getVueComponentContext(node)
  298. for (const prop of utils.iterateProperties(
  299. node,
  300. new Set([
  301. GROUP_PROPERTY,
  302. GROUP_ASYNC_DATA,
  303. GROUP_DATA,
  304. GROUP_COMPUTED_PROPERTY,
  305. GROUP_SETUP,
  306. GROUP_METHODS,
  307. GROUP_INJECT
  308. ])
  309. )) {
  310. const propertyMap =
  311. (prop.groupName === GROUP_DATA ||
  312. prop.groupName === GROUP_ASYNC_DATA) &&
  313. prop.type === 'object' &&
  314. prop.property.value.type === 'ObjectExpression'
  315. ? getObjectPropertyMap(prop.property.value)
  316. : null
  317. ctx.defineProperties.set(prop.name, {
  318. hasNestProperty: Boolean(propertyMap),
  319. isProps: prop.groupName === GROUP_PROPERTY,
  320. get(name) {
  321. if (!propertyMap) {
  322. return null
  323. }
  324. return getPropertyDataFromObjectProperty(propertyMap.get(name))
  325. }
  326. })
  327. }
  328. for (const watcherOrExpose of utils.iterateProperties(
  329. node,
  330. new Set([GROUP_WATCHER, GROUP_EXPOSE])
  331. )) {
  332. if (watcherOrExpose.groupName === GROUP_WATCHER) {
  333. const watcher = watcherOrExpose
  334. // Process `watch: { foo /* <- this */ () {} }`
  335. ctx.verifyReferences(
  336. propertyReferenceExtractor.extractFromPath(
  337. watcher.name,
  338. watcher.node
  339. )
  340. )
  341. // Process `watch: { x: 'foo' /* <- this */ }`
  342. if (watcher.type === 'object') {
  343. const property = watcher.property
  344. if (property.kind === 'init') {
  345. for (const handlerValueNode of utils.iterateWatchHandlerValues(
  346. property
  347. )) {
  348. ctx.verifyReferences(
  349. propertyReferenceExtractor.extractFromNameLiteral(
  350. handlerValueNode
  351. )
  352. )
  353. }
  354. }
  355. }
  356. } else if (watcherOrExpose.groupName === GROUP_EXPOSE) {
  357. const expose = watcherOrExpose
  358. ctx.verifyReferences(
  359. propertyReferenceExtractor.extractFromName(
  360. expose.name,
  361. expose.node
  362. )
  363. )
  364. }
  365. }
  366. },
  367. /** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property }} node */
  368. 'ObjectExpression > Property > :function[params.length>0]'(
  369. node,
  370. vueData
  371. ) {
  372. let props = false
  373. const property = getParentProperty(node)
  374. if (!property) {
  375. return
  376. }
  377. if (property.parent === vueData.node) {
  378. if (utils.getStaticPropertyName(property) !== 'data') {
  379. return
  380. }
  381. // check { data: (vm) => vm.prop }
  382. props = true
  383. } else {
  384. const parentProperty = getParentProperty(property.parent)
  385. if (!parentProperty) {
  386. return
  387. }
  388. if (parentProperty.parent === vueData.node) {
  389. if (utils.getStaticPropertyName(parentProperty) !== 'computed') {
  390. return
  391. }
  392. // check { computed: { foo: (vm) => vm.prop } }
  393. } else {
  394. const parentParentProperty = getParentProperty(
  395. parentProperty.parent
  396. )
  397. if (!parentParentProperty) {
  398. return
  399. }
  400. if (parentParentProperty.parent === vueData.node) {
  401. if (
  402. utils.getStaticPropertyName(parentParentProperty) !==
  403. 'computed' ||
  404. utils.getStaticPropertyName(property) !== 'get'
  405. ) {
  406. return
  407. }
  408. // check { computed: { foo: { get: (vm) => vm.prop } } }
  409. } else {
  410. return
  411. }
  412. }
  413. }
  414. const propertyReferences =
  415. propertyReferenceExtractor.extractFromFunctionParam(node, 0)
  416. const ctx = getVueComponentContext(vueData.node)
  417. ctx.verifyReferences(propertyReferences, { props })
  418. },
  419. onSetupFunctionEnter(node, vueData) {
  420. const propertyReferences =
  421. propertyReferenceExtractor.extractFromFunctionParam(node, 0)
  422. const ctx = getVueComponentContext(vueData.node)
  423. ctx.verifyReferences(propertyReferences, {
  424. props: true
  425. })
  426. },
  427. onRenderFunctionEnter(node, vueData) {
  428. const ctx = getVueComponentContext(vueData.node)
  429. // Check for Vue 3.x render
  430. const propertyReferences =
  431. propertyReferenceExtractor.extractFromFunctionParam(node, 0)
  432. ctx.verifyReferences(propertyReferences)
  433. if (vueData.functional) {
  434. // Check for Vue 2.x render & functional
  435. const propertyReferencesForV2 =
  436. propertyReferenceExtractor.extractFromFunctionParam(node, 1)
  437. ctx.verifyReferences(propertyReferencesForV2.getNest('props'), {
  438. props: true
  439. })
  440. }
  441. },
  442. /**
  443. * @param {ThisExpression | Identifier} node
  444. * @param {VueObjectData} vueData
  445. */
  446. 'ThisExpression, Identifier'(node, vueData) {
  447. if (!utils.isThis(node, context)) {
  448. return
  449. }
  450. const ctx = getVueComponentContext(vueData.node)
  451. const propertyReferences =
  452. propertyReferenceExtractor.extractFromExpression(node, false)
  453. ctx.verifyReferences(propertyReferences)
  454. }
  455. }),
  456. {
  457. 'Program:exit'() {
  458. const ctx = getVueComponentContextForTemplate()
  459. if (!ctx) {
  460. return
  461. }
  462. const styleVars = getStyleVariablesContext(context)
  463. if (styleVars) {
  464. ctx.verifyReferences(
  465. propertyReferenceExtractor.extractFromStyleVariablesContext(
  466. styleVars
  467. )
  468. )
  469. }
  470. }
  471. }
  472. )
  473. const templateVisitor = {
  474. /**
  475. * @param {VExpressionContainer} node
  476. */
  477. VExpressionContainer(node) {
  478. const ctx = getVueComponentContextForTemplate()
  479. if (!ctx) {
  480. return
  481. }
  482. ctx.verifyReferences(
  483. propertyReferenceExtractor.extractFromVExpressionContainer(node, {
  484. ignoreGlobals: true
  485. })
  486. )
  487. }
  488. }
  489. return utils.defineTemplateBodyVisitor(
  490. context,
  491. templateVisitor,
  492. scriptVisitor
  493. )
  494. }
  495. }