diff --git a/docs/rules/README.md b/docs/rules/README.md
index 544d24db7..c15454cba 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -319,6 +319,7 @@ For example:
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
| [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | |
| [vue/no-this-in-before-route-enter](./no-this-in-before-route-enter.md) | disallow `this` usage in a `beforeRouteEnter` method | |
+| [vue/no-undef-properties](./no-undef-properties.md) | disallow undefined properties | |
| [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | |
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
| [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties | |
diff --git a/docs/rules/no-undef-properties.md b/docs/rules/no-undef-properties.md
new file mode 100644
index 000000000..e7979756a
--- /dev/null
+++ b/docs/rules/no-undef-properties.md
@@ -0,0 +1,114 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-undef-properties
+description: disallow undefined properties
+---
+# vue/no-undef-properties
+
+> disallow undefined properties
+
+- :exclamation: ***This rule has not been released yet.***
+
+## :book: Rule Details
+
+This rule warns of using undefined properties.
+This rule can help you locate potential errors resulting from misspellings property names, and implicitly added properties.
+
+::: warning Note
+This rule cannot detect properties defined in other files or components.
+Note that there are many false positives if you are using mixins.
+:::
+
+
+
+```vue
+
+
+ {{ name }}: {{ count }}
+
+ {{ label }}: {{ cnt }}
+
+
+```
+
+
+
+
+
+```vue
+
+
+ {{ name }}: {{ count }}
+
+ {{ label }}: {{ cnt }}
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/no-undef-properties": ["error", {
+ "ignores": ["/^\\$/"]
+ }]
+}
+```
+
+- `ignores` (`string[]`) ... An array of property names or patterns that have already been defined property, or property to ignore from the check. Default is `["/^\\$/"]`.
+
+### `"ignores": ["/^\\$/"]` (default)
+
+
+
+```vue
+
+
+ {{ $t('foo') }}
+
+
+```
+
+
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-undef-properties.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-undef-properties.js)
diff --git a/lib/index.js b/lib/index.js
index 9ddfe4cd7..c599048d0 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -119,6 +119,7 @@ module.exports = {
'no-template-target-blank': require('./rules/no-template-target-blank'),
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
'no-this-in-before-route-enter': require('./rules/no-this-in-before-route-enter'),
+ 'no-undef-properties': require('./rules/no-undef-properties'),
'no-unregistered-components': require('./rules/no-unregistered-components'),
'no-unsupported-features': require('./rules/no-unsupported-features'),
'no-unused-components': require('./rules/no-unused-components'),
diff --git a/lib/rules/no-undef-properties.js b/lib/rules/no-undef-properties.js
new file mode 100644
index 000000000..0acf49f15
--- /dev/null
+++ b/lib/rules/no-undef-properties.js
@@ -0,0 +1,548 @@
+/**
+ * @fileoverview Disallow undefined properties.
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const reserved = require('../utils/vue-reserved.json')
+const { toRegExp } = require('../utils/regexp')
+const { getStyleVariablesContext } = require('../utils/style-variables')
+const {
+ definePropertyReferenceExtractor
+} = require('../utils/property-references')
+
+/**
+ * @typedef {import('../utils').VueObjectData} VueObjectData
+ * @typedef {import('../utils/property-references').IPropertyReferences} IPropertyReferences
+ */
+/**
+ * @typedef {object} PropertyData
+ * @property {boolean} [hasNestProperty]
+ * @property { (name: string) => PropertyData | null } [get]
+ * @property {boolean} [isProps]
+ */
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+const GROUP_PROPERTY = 'props'
+const GROUP_DATA = 'data'
+const GROUP_COMPUTED_PROPERTY = 'computed'
+const GROUP_METHODS = 'methods'
+const GROUP_SETUP = 'setup'
+const GROUP_WATCHER = 'watch'
+const GROUP_EXPOSE = 'expose'
+const GROUP_INJECT = 'inject'
+
+/**
+ * @param {ObjectExpression} object
+ * @returns {Map | null}
+ */
+function getObjectPropertyMap(object) {
+ /** @type {Map} */
+ const props = new Map()
+ for (const p of object.properties) {
+ if (p.type !== 'Property') {
+ return null
+ }
+ const name = utils.getStaticPropertyName(p)
+ if (name == null) {
+ return null
+ }
+ props.set(name, p)
+ }
+ return props
+}
+
+/**
+ * @param {Property | undefined} property
+ * @returns {PropertyData | null}
+ */
+function getPropertyDataFromObjectProperty(property) {
+ if (property == null) {
+ return null
+ }
+ const propertyMap =
+ property.value.type === 'ObjectExpression'
+ ? getObjectPropertyMap(property.value)
+ : null
+ return {
+ hasNestProperty: Boolean(propertyMap),
+ get(name) {
+ if (!propertyMap) {
+ return null
+ }
+ return getPropertyDataFromObjectProperty(propertyMap.get(name))
+ }
+ }
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'disallow undefined properties',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/no-undef-properties.html'
+ },
+ fixable: null,
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ ignores: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ undef: "'{{name}}' is not defined.",
+ undefProps: "'{{name}}' is not defined in props."
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const options = context.options[0] || {}
+ const ignores = /** @type {string[]} */ (options.ignores || ['/^\\$/']).map(
+ toRegExp
+ )
+ const propertyReferenceExtractor = definePropertyReferenceExtractor(context)
+ const programNode = context.getSourceCode().ast
+
+ /** Vue component context */
+ class VueComponentContext {
+ constructor() {
+ /** @type { Map } */
+ this.defineProperties = new Map()
+
+ /** @type { Set } */
+ this.reported = new Set()
+ }
+ /**
+ * Report
+ * @param {IPropertyReferences} references
+ * @param {object} [options]
+ * @param {boolean} [options.props]
+ */
+ verifyReferences(references, options) {
+ const that = this
+ verifyUndefProperties(this.defineProperties, references, null)
+
+ /**
+ * @param { { get?: (name: string) => PropertyData | null | undefined } } defineProperties
+ * @param {IPropertyReferences|null} references
+ * @param {string|null} pathName
+ */
+ function verifyUndefProperties(defineProperties, references, pathName) {
+ if (!references) {
+ return
+ }
+ for (const [refName, { nodes }] of references.allProperties()) {
+ const referencePathName = pathName
+ ? `${pathName}.${refName}`
+ : refName
+
+ const prop = defineProperties.get && defineProperties.get(refName)
+ if (prop) {
+ if (options && options.props) {
+ if (!prop.isProps) {
+ that.report(nodes[0], referencePathName, 'undefProps')
+ continue
+ }
+ }
+ } else {
+ that.report(nodes[0], referencePathName, 'undef')
+ continue
+ }
+
+ if (prop.hasNestProperty) {
+ verifyUndefProperties(
+ prop,
+ references.getNest(refName),
+ referencePathName
+ )
+ }
+ }
+ }
+ }
+ /**
+ * Report
+ * @param {ASTNode} node
+ * @param {string} name
+ * @param {'undef' | 'undefProps'} messageId
+ */
+ report(node, name, messageId = 'undef') {
+ if (
+ reserved.includes(name) ||
+ ignores.some((ignore) => ignore.test(name))
+ ) {
+ return
+ }
+ if (
+ // Prevents reporting to the same node.
+ this.reported.has(node) ||
+ // Prevents reports with the same name.
+ // This is so that intentional undefined properties can be resolved with
+ // a single warning suppression comment (`// eslint-disable-line`).
+ this.reported.has(name)
+ ) {
+ return
+ }
+ this.reported.add(node)
+ this.reported.add(name)
+ context.report({
+ node,
+ messageId,
+ data: {
+ name
+ }
+ })
+ }
+ }
+
+ /** @type {Map} */
+ const vueComponentContextMap = new Map()
+
+ /**
+ * @param {ASTNode} node
+ * @returns {VueComponentContext}
+ */
+ function getVueComponentContext(node) {
+ let ctx = vueComponentContextMap.get(node)
+ if (!ctx) {
+ ctx = new VueComponentContext()
+ vueComponentContextMap.set(node, ctx)
+ }
+ return ctx
+ }
+ /**
+ * @returns {VueComponentContext|void}
+ */
+ function getVueComponentContextForTemplate() {
+ const keys = [...vueComponentContextMap.keys()]
+ const exported =
+ keys.find(isScriptSetupProgram) || keys.find(isExportObject)
+ return exported && vueComponentContextMap.get(exported)
+
+ /**
+ * @param {ASTNode} node
+ */
+ function isScriptSetupProgram(node) {
+ return node === programNode
+ }
+ /**
+ * @param {ASTNode} node
+ */
+ function isExportObject(node) {
+ let parent = node.parent
+ while (parent) {
+ if (parent.type === 'ExportDefaultDeclaration') {
+ return true
+ }
+ parent = parent.parent
+ }
+ return false
+ }
+ }
+
+ /**
+ * @param {Expression} node
+ * @returns {Property|null}
+ */
+ function getParentProperty(node) {
+ if (
+ !node.parent ||
+ node.parent.type !== 'Property' ||
+ node.parent.value !== node
+ ) {
+ return null
+ }
+ const property = node.parent
+ if (!utils.isProperty(property)) {
+ return null
+ }
+ return property
+ }
+
+ const scriptVisitor = utils.compositingVisitors(
+ {
+ /** @param {Program} node */
+ Program() {
+ if (!utils.isScriptSetup(context)) {
+ return
+ }
+
+ const ctx = getVueComponentContext(programNode)
+ const globalScope = context.getSourceCode().scopeManager.globalScope
+ if (globalScope) {
+ for (const variable of globalScope.variables) {
+ ctx.defineProperties.set(variable.name, {})
+ }
+ const moduleScope = globalScope.childScopes.find(
+ (scope) => scope.type === 'module'
+ )
+ for (const variable of (moduleScope && moduleScope.variables) ||
+ []) {
+ ctx.defineProperties.set(variable.name, {})
+ }
+ }
+ }
+ },
+ utils.defineScriptSetupVisitor(context, {
+ onDefinePropsEnter(node, props) {
+ const ctx = getVueComponentContext(programNode)
+
+ for (const prop of props) {
+ if (!prop.propName) {
+ continue
+ }
+ ctx.defineProperties.set(prop.propName, {
+ isProps: true
+ })
+ }
+ let target = node
+ if (
+ target.parent &&
+ target.parent.type === 'CallExpression' &&
+ target.parent.arguments[0] === target &&
+ target.parent.callee.type === 'Identifier' &&
+ target.parent.callee.name === 'withDefaults'
+ ) {
+ target = target.parent
+ }
+
+ if (
+ !target.parent ||
+ target.parent.type !== 'VariableDeclarator' ||
+ target.parent.init !== target
+ ) {
+ return
+ }
+
+ const pattern = target.parent.id
+ const propertyReferences =
+ propertyReferenceExtractor.extractFromPattern(pattern)
+ ctx.verifyReferences(propertyReferences)
+ }
+ }),
+ utils.defineVueVisitor(context, {
+ onVueObjectEnter(node) {
+ const ctx = getVueComponentContext(node)
+
+ for (const prop of utils.iterateProperties(
+ node,
+ new Set([
+ GROUP_PROPERTY,
+ GROUP_DATA,
+ GROUP_COMPUTED_PROPERTY,
+ GROUP_SETUP,
+ GROUP_METHODS,
+ GROUP_INJECT
+ ])
+ )) {
+ const propertyMap =
+ prop.groupName === GROUP_DATA &&
+ prop.type === 'object' &&
+ prop.property.value.type === 'ObjectExpression'
+ ? getObjectPropertyMap(prop.property.value)
+ : null
+ ctx.defineProperties.set(prop.name, {
+ hasNestProperty: Boolean(propertyMap),
+ isProps: prop.groupName === GROUP_PROPERTY,
+ get(name) {
+ if (!propertyMap) {
+ return null
+ }
+ return getPropertyDataFromObjectProperty(propertyMap.get(name))
+ }
+ })
+ }
+
+ for (const watcherOrExpose of utils.iterateProperties(
+ node,
+ new Set([GROUP_WATCHER, GROUP_EXPOSE])
+ )) {
+ if (watcherOrExpose.groupName === GROUP_WATCHER) {
+ const watcher = watcherOrExpose
+ // Process `watch: { foo /* <- this */ () {} }`
+ ctx.verifyReferences(
+ propertyReferenceExtractor.extractFromPath(
+ watcher.name,
+ watcher.node
+ )
+ )
+ // Process `watch: { x: 'foo' /* <- this */ }`
+ if (watcher.type === 'object') {
+ const property = watcher.property
+ if (property.kind === 'init') {
+ for (const handlerValueNode of utils.iterateWatchHandlerValues(
+ property
+ )) {
+ ctx.verifyReferences(
+ propertyReferenceExtractor.extractFromNameLiteral(
+ handlerValueNode
+ )
+ )
+ }
+ }
+ }
+ } else if (watcherOrExpose.groupName === GROUP_EXPOSE) {
+ const expose = watcherOrExpose
+ ctx.verifyReferences(
+ propertyReferenceExtractor.extractFromName(
+ expose.name,
+ expose.node
+ )
+ )
+ }
+ }
+ },
+ /** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property }} node */
+ 'ObjectExpression > Property > :function[params.length>0]'(
+ node,
+ vueData
+ ) {
+ let props = false
+ const property = getParentProperty(node)
+ if (!property) {
+ return
+ }
+ if (property.parent === vueData.node) {
+ if (utils.getStaticPropertyName(property) !== 'data') {
+ return
+ }
+ // check { data: (vm) => vm.prop }
+ props = true
+ } else {
+ const parentProperty = getParentProperty(property.parent)
+ if (!parentProperty) {
+ return
+ }
+ if (parentProperty.parent === vueData.node) {
+ if (utils.getStaticPropertyName(parentProperty) !== 'computed') {
+ return
+ }
+ // check { computed: { foo: (vm) => vm.prop } }
+ } else {
+ const parentParentProperty = getParentProperty(
+ parentProperty.parent
+ )
+ if (!parentParentProperty) {
+ return
+ }
+ if (parentParentProperty.parent === vueData.node) {
+ if (
+ utils.getStaticPropertyName(parentParentProperty) !==
+ 'computed' ||
+ utils.getStaticPropertyName(property) !== 'get'
+ ) {
+ return
+ }
+ // check { computed: { foo: { get: (vm) => vm.prop } } }
+ } else {
+ return
+ }
+ }
+ }
+
+ const propertyReferences =
+ propertyReferenceExtractor.extractFromFunctionParam(node, 0)
+ const ctx = getVueComponentContext(vueData.node)
+ ctx.verifyReferences(propertyReferences, { props })
+ },
+ onSetupFunctionEnter(node, vueData) {
+ const propertyReferences =
+ propertyReferenceExtractor.extractFromFunctionParam(node, 0)
+ const ctx = getVueComponentContext(vueData.node)
+ ctx.verifyReferences(propertyReferences, {
+ props: true
+ })
+ },
+ onRenderFunctionEnter(node, vueData) {
+ const ctx = getVueComponentContext(vueData.node)
+
+ // Check for Vue 3.x render
+ const propertyReferences =
+ propertyReferenceExtractor.extractFromFunctionParam(node, 0)
+ ctx.verifyReferences(propertyReferences)
+
+ if (vueData.functional) {
+ // Check for Vue 2.x render & functional
+ const propertyReferencesForV2 =
+ propertyReferenceExtractor.extractFromFunctionParam(node, 1)
+
+ ctx.verifyReferences(propertyReferencesForV2.getNest('props'), {
+ props: true
+ })
+ }
+ },
+ /**
+ * @param {ThisExpression | Identifier} node
+ * @param {VueObjectData} vueData
+ */
+ 'ThisExpression, Identifier'(node, vueData) {
+ if (!utils.isThis(node, context)) {
+ return
+ }
+ const ctx = getVueComponentContext(vueData.node)
+ const propertyReferences =
+ propertyReferenceExtractor.extractFromExpression(node, false)
+ ctx.verifyReferences(propertyReferences)
+ }
+ }),
+ {
+ 'Program:exit'() {
+ const ctx = getVueComponentContextForTemplate()
+ if (!ctx) {
+ return
+ }
+ const styleVars = getStyleVariablesContext(context)
+ if (styleVars) {
+ ctx.verifyReferences(
+ propertyReferenceExtractor.extractFromStyleVariablesContext(
+ styleVars
+ )
+ )
+ }
+ }
+ }
+ )
+
+ const templateVisitor = {
+ /**
+ * @param {VExpressionContainer} node
+ */
+ VExpressionContainer(node) {
+ const ctx = getVueComponentContextForTemplate()
+ if (!ctx) {
+ return
+ }
+ ctx.verifyReferences(
+ propertyReferenceExtractor.extractFromVExpressionContainer(node, {
+ ignoreGlobals: true
+ })
+ )
+ }
+ }
+
+ return utils.defineTemplateBodyVisitor(
+ context,
+ templateVisitor,
+ scriptVisitor
+ )
+ }
+}
diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js
index 722e95c31..00da49d63 100644
--- a/lib/rules/no-unused-properties.js
+++ b/lib/rules/no-unused-properties.js
@@ -115,14 +115,6 @@ function getScope(context, currentNode) {
return scopeManager.scopes[0]
}
-/**
- * Extract names from references objects.
- * @param {VReference[]} references
- */
-function getReferences(references) {
- return references.filter((ref) => ref.variable == null).map((ref) => ref.id)
-}
-
/**
* @param {RuleContext} context
* @param {Identifier} id
@@ -503,7 +495,10 @@ module.exports = {
const watcher = watcherOrExpose
// Process `watch: { foo /* <- this */ () {} }`
container.propertyReferences.push(
- propertyReferenceExtractor.extractFromPath(watcher.name)
+ propertyReferenceExtractor.extractFromPath(
+ watcher.name,
+ watcher.node
+ )
)
// Process `watch: { x: 'foo' /* <- this */ }`
@@ -513,24 +508,21 @@ module.exports = {
for (const handlerValueNode of utils.iterateWatchHandlerValues(
property
)) {
- if (
- handlerValueNode.type === 'Literal' ||
- handlerValueNode.type === 'TemplateLiteral'
- ) {
- const name = utils.getStringLiteralValue(handlerValueNode)
- if (name != null) {
- container.propertyReferences.push(
- propertyReferenceExtractor.extractFromName(name)
- )
- }
- }
+ container.propertyReferences.push(
+ propertyReferenceExtractor.extractFromNameLiteral(
+ handlerValueNode
+ )
+ )
}
}
}
} else if (watcherOrExpose.groupName === GROUP_EXPOSE) {
const expose = watcherOrExpose
container.propertyReferences.push(
- propertyReferenceExtractor.extractFromName(expose.name)
+ propertyReferenceExtractor.extractFromName(
+ expose.name,
+ expose.node
+ )
)
}
}
@@ -630,13 +622,11 @@ module.exports = {
'Program:exit'(node) {
const styleVars = getStyleVariablesContext(context)
if (styleVars) {
- for (const { id } of styleVars.references) {
- templatePropertiesContainer.propertyReferences.push(
- propertyReferenceExtractor.extractFromName(id.name, () =>
- propertyReferenceExtractor.extractFromExpression(id, true)
- )
+ templatePropertiesContainer.propertyReferences.push(
+ propertyReferenceExtractor.extractFromStyleVariablesContext(
+ styleVars
)
- }
+ )
}
if (!node.templateBody) {
reportUnusedProperties()
@@ -650,13 +640,9 @@ module.exports = {
* @param {VExpressionContainer} node
*/
VExpressionContainer(node) {
- for (const id of getReferences(node.references)) {
- templatePropertiesContainer.propertyReferences.push(
- propertyReferenceExtractor.extractFromName(id.name, () =>
- propertyReferenceExtractor.extractFromExpression(id, true)
- )
- )
- }
+ templatePropertiesContainer.propertyReferences.push(
+ propertyReferenceExtractor.extractFromVExpressionContainer(node)
+ )
},
/**
* @param {VAttribute} node
diff --git a/lib/utils/property-references.js b/lib/utils/property-references.js
index 2291ea717..f375170c3 100644
--- a/lib/utils/property-references.js
+++ b/lib/utils/property-references.js
@@ -9,7 +9,7 @@ const utils = require('./index')
const eslintUtils = require('eslint-utils')
/**
- * @typedef {'props' | 'data' | 'computed' | 'methods' | 'setup'} Group
+ * @typedef {import('./style-variables').StyleVariablesContext} StyleVariablesContext
*/
/**
* @typedef {object} IHasPropertyOption
@@ -18,6 +18,7 @@ const eslintUtils = require('eslint-utils')
/**
* @typedef {object} IPropertyReferences
* @property { (name: string, option?: IHasPropertyOption) => boolean } hasProperty
+ * @property { () => Map } allProperties
* @property { (name: string) => IPropertyReferences } getNest
*/
@@ -28,12 +29,14 @@ const eslintUtils = require('eslint-utils')
/** @type {IPropertyReferences} */
const ANY = {
hasProperty: () => true,
+ allProperties: () => new Map(),
getNest: () => ANY
}
/** @type {IPropertyReferences} */
const NEVER = {
hasProperty: () => false,
+ allProperties: () => new Map(),
getNest: () => NEVER
}
@@ -186,6 +189,10 @@ function definePropertyReferenceExtractor(context) {
return name === this.name
}
+ allProperties() {
+ return new Map([[this.name, { nodes: [this.node.property] }]])
+ }
+
/**
* @param {string} name
* @returns {IPropertyReferences}
@@ -213,6 +220,14 @@ function definePropertyReferenceExtractor(context) {
return Boolean(this.properties[name])
}
+ allProperties() {
+ const result = new Map()
+ for (const [name, nodes] of Object.entries(this.properties)) {
+ result.set(name, { nodes: nodes.map((node) => node.key) })
+ }
+ return result
+ }
+
/**
* @param {string} name
* @returns {IPropertyReferences}
@@ -429,6 +444,7 @@ function definePropertyReferenceExtractor(context) {
hasProperty(_name, options) {
return Boolean(options && options.unknownCallAsAny)
},
+ allProperties: () => new Map(),
getNest: () => ANY
}
}
@@ -446,6 +462,7 @@ function definePropertyReferenceExtractor(context) {
hasProperty(_name, options) {
return Boolean(options && options.unknownCallAsAny)
},
+ allProperties: () => new Map(),
getNest: () => ANY
}
}
@@ -482,9 +499,10 @@ function definePropertyReferenceExtractor(context) {
/**
* Extract the property references from path.
* @param {string} pathString
+ * @param {Identifier | Literal | TemplateLiteral} node
* @returns {IPropertyReferences}
*/
- function extractFromPath(pathString) {
+ function extractFromPath(pathString, node) {
return extractFromSegments(pathString.split('.'))
/**
@@ -498,21 +516,45 @@ function definePropertyReferenceExtractor(context) {
const segmentName = segments[0]
return {
hasProperty: (name) => name === segmentName,
+ allProperties: () => new Map([[segmentName, { nodes: [node] }]]),
getNest: (name) =>
name === segmentName ? extractFromSegments(segments.slice(1)) : NEVER
}
}
}
+ /**
+ * Extract the property references from name literal.
+ * @param {Expression} node
+ * @returns {IPropertyReferences}
+ */
+ function extractFromNameLiteral(node) {
+ const referenceName =
+ node.type === 'Literal' || node.type === 'TemplateLiteral'
+ ? utils.getStringLiteralValue(node)
+ : null
+ if (referenceName) {
+ return {
+ hasProperty: (name) => name === referenceName,
+ allProperties: () => new Map([[referenceName, { nodes: [node] }]]),
+ getNest: (name) => (name === referenceName ? ANY : NEVER)
+ }
+ } else {
+ return NEVER
+ }
+ }
+
/**
* Extract the property references from name.
* @param {string} referenceName
+ * @param {Expression|SpreadElement} nameNode
* @param { () => IPropertyReferences } [getNest]
* @returns {IPropertyReferences}
*/
- function extractFromName(referenceName, getNest) {
+ function extractFromName(referenceName, nameNode, getNest) {
return {
hasProperty: (name) => name === referenceName,
+ allProperties: () => new Map([[referenceName, { nodes: [nameNode] }]]),
getNest: (name) =>
name === referenceName ? (getNest ? getNest() : ANY) : NEVER
}
@@ -534,7 +576,7 @@ function definePropertyReferenceExtractor(context) {
// unknown name
return ANY
}
- return extractFromName(refName, () => {
+ return extractFromName(refName, nameNode, () => {
return extractFromExpression(node, false).getNest('value')
})
}
@@ -548,15 +590,67 @@ function definePropertyReferenceExtractor(context) {
const base = extractFromExpression(node, false)
return {
hasProperty: (name, option) => base.hasProperty(name, option),
+ allProperties: () => base.allProperties(),
getNest: (name) => base.getNest(name).getNest('value')
}
}
+
+ /**
+ * Extract the property references from VExpressionContainer.
+ * @param {VExpressionContainer} node
+ * @param {object} [options]
+ * @param {boolean} [options.ignoreGlobals]
+ * @returns {IPropertyReferences}
+ */
+ function extractFromVExpressionContainer(node, options) {
+ const ignoreGlobals = options && options.ignoreGlobals
+
+ /** @type { (name:string)=>boolean } */
+ let ignoreRef = () => false
+ if (ignoreGlobals) {
+ const globalScope =
+ context.getSourceCode().scopeManager.globalScope ||
+ context.getSourceCode().scopeManager.scopes[0]
+
+ ignoreRef = (name) => globalScope.set.has(name)
+ }
+ const references = []
+ for (const id of node.references
+ .filter((ref) => ref.variable == null)
+ .map((ref) => ref.id)) {
+ if (ignoreRef(id.name)) {
+ continue
+ }
+ references.push(
+ extractFromName(id.name, id, () => extractFromExpression(id, true))
+ )
+ }
+ return mergePropertyReferences(references)
+ }
+ /**
+ * Extract the property references from StyleVariablesContext.
+ * @param {StyleVariablesContext} ctx
+ * @returns {IPropertyReferences}
+ */
+ function extractFromStyleVariablesContext(ctx) {
+ const references = []
+ for (const { id } of ctx.references) {
+ references.push(
+ extractFromName(id.name, id, () => extractFromExpression(id, true))
+ )
+ }
+ return mergePropertyReferences(references)
+ }
+
return {
extractFromExpression,
extractFromPattern,
extractFromFunctionParam,
extractFromPath,
- extractFromName
+ extractFromName,
+ extractFromNameLiteral,
+ extractFromVExpressionContainer,
+ extractFromStyleVariablesContext
}
}
@@ -594,6 +688,21 @@ class PropertyReferencesForMerge {
return this.references.some((ref) => ref.hasProperty(name, option))
}
+ allProperties() {
+ const result = new Map()
+ for (const reference of this.references) {
+ for (const [name, { nodes }] of reference.allProperties()) {
+ const r = result.get(name)
+ if (r) {
+ r.nodes = [...new Set([...r.nodes, ...nodes])]
+ } else {
+ result.set(name, { nodes: [...nodes] })
+ }
+ }
+ }
+ return result
+ }
+
/**
* @param {string} name
* @returns {IPropertyReferences}
diff --git a/lib/utils/style-variables/index.js b/lib/utils/style-variables/index.js
index 63caf208a..3127166a4 100644
--- a/lib/utils/style-variables/index.js
+++ b/lib/utils/style-variables/index.js
@@ -1,9 +1,5 @@
const { isVElement } = require('..')
-module.exports = {
- getStyleVariablesContext
-}
-
class StyleVariablesContext {
/**
* @param {RuleContext} context
@@ -31,6 +27,11 @@ class StyleVariablesContext {
}
}
+module.exports = {
+ getStyleVariablesContext,
+ StyleVariablesContext
+}
+
/** @type {Map} */
const cache = new Map()
/**
diff --git a/tests/lib/rules/no-undef-properties.js b/tests/lib/rules/no-undef-properties.js
new file mode 100644
index 000000000..09e16bda9
--- /dev/null
+++ b/tests/lib/rules/no-undef-properties.js
@@ -0,0 +1,1074 @@
+/**
+ * @fileoverview Disallow undefined properties.
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-undef-properties')
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+})
+
+tester.run('no-undef-properties', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ bar }}
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ bar }}
+
+
+ `
+ },
+ //default ignores
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ $t('foo') }}
+
+
+ `
+ },
+ {
+ // global in template
+ filename: 'test.vue',
+ code: `
+
+ {{ undefined }}
+
+
+ `
+ },
+
+ //watch
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+
+ // props
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+
+ // arg vm
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+
+ // deep
+ {
+ filename: 'test.vue',
+ code: `
+ {{ foo.bar }}
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ {{ foo.bar.baz }}
+
+ `
+ },
+
+ // track
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ }
+ ],
+
+ invalid: [
+ // undef property
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ bar2 }}
+
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 3
+ },
+ {
+ message: "'bar2' is not defined.",
+ line: 3
+ },
+ {
+ message: "'baz2' is not defined.",
+ line: 14
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ bar2 }}
+
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 3
+ },
+ {
+ message: "'bar2' is not defined.",
+ line: 3
+ },
+ {
+ message: "'baz2' is not defined.",
+ line: 19
+ }
+ ]
+ },
+
+ // same names
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ foo2 }}
+
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 9
+ }
+ ]
+ },
+
+ //watch
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 6
+ },
+ {
+ message: "'bar2' is not defined.",
+ line: 6
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 12
+ },
+ {
+ message: "'foo.bar2' is not defined.",
+ line: 13
+ }
+ ]
+ },
+
+ // props
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: ["'foo' is not defined in props."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: ["'foo' is not defined in props."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: ["'foo' is not defined in props."]
+ },
+
+ // arg vm
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: ["'bar' is not defined."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: ["'bar' is not defined."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: ["'bar' is not defined."]
+ },
+
+ // deep
+ {
+ filename: 'test.vue',
+ code: `
+ {{ foo.baz }}
+
+ `,
+ errors: ["'foo.baz' is not defined."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ {{ foo.bar.baz2 }}
+
+ `,
+ errors: ["'foo.bar.baz2' is not defined."]
+ },
+
+ // track
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 15
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 10
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo.bar2' is not defined.",
+ line: 12
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo.bar2' is not defined.",
+ line: 12
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo2' is not defined.",
+ line: 11
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: "'foo.bar2' is not defined.",
+ line: 13
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ {{ name }}: {{ count }}
+
+ {{ label }}: {{ cnt }}
+
+
+ `,
+ errors: [
+ {
+ message: "'label' is not defined.",
+ line: 6
+ },
+ {
+ message: "'cnt' is not defined.",
+ line: 6
+ },
+ {
+ message: "'undef' is not defined.",
+ line: 16
+ }
+ ]
+ }
+ ]
+})