From 852096e7bed2411d654026eeef88df6ccc3a9d64 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Wed, 6 Oct 2021 06:53:31 +0900 Subject: [PATCH] Add `vue/no-undef-properties` rule (#1472) * Add `vue/no-undef-properties` rule * support script setup * refactor --- docs/rules/README.md | 1 + docs/rules/no-undef-properties.md | 114 +++ lib/index.js | 1 + lib/rules/no-undef-properties.js | 548 ++++++++++++ lib/rules/no-unused-properties.js | 54 +- lib/utils/property-references.js | 119 ++- lib/utils/style-variables/index.js | 9 +- tests/lib/rules/no-undef-properties.js | 1074 ++++++++++++++++++++++++ 8 files changed, 1877 insertions(+), 43 deletions(-) create mode 100644 docs/rules/no-undef-properties.md create mode 100644 lib/rules/no-undef-properties.js create mode 100644 tests/lib/rules/no-undef-properties.js 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 + + +``` + + + + + +```vue + + +``` + + + +## :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 + + +``` + + + +## :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: ` + + + ` + }, + { + filename: 'test.vue', + code: ` + + + ` + }, + //default ignores + { + filename: 'test.vue', + code: ` + + + ` + }, + { + // global in template + filename: 'test.vue', + code: ` + + + ` + }, + + //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: ` + + + ` + }, + { + filename: 'test.vue', + code: ` + + + ` + }, + + // 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: ` + + + `, + 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: ` + + + `, + 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: ` + + + `, + 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: ` + + + `, + errors: ["'foo.baz' is not defined."] + }, + { + filename: 'test.vue', + code: ` + + + `, + 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: ` + + + `, + errors: [ + { + message: "'label' is not defined.", + line: 6 + }, + { + message: "'cnt' is not defined.", + line: 6 + }, + { + message: "'undef' is not defined.", + line: 16 + } + ] + } + ] +})