diff --git a/docs/rules/README.md b/docs/rules/README.md index a93d5a640..96afcb5b6 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -315,6 +315,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..6aa637b30 --- /dev/null +++ b/docs/rules/no-undef-properties.md @@ -0,0 +1,90 @@ +--- +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. +::: + + + +```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 313cc6930..5795313c4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -115,6 +115,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..13f00e41c --- /dev/null +++ b/lib/rules/no-undef-properties.js @@ -0,0 +1,842 @@ +/** + * @fileoverview Disallow undefined properties. + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') +const eslintUtils = require('eslint-utils') +const reserved = require('../utils/vue-reserved.json') +const { toRegExp } = require('../utils/regexp') + +/** + * @typedef {import('../utils').ComponentPropertyData} ComponentPropertyData + * @typedef {import('../utils').VueObjectData} VueObjectData + */ +/** + * @typedef {object} PropertyData + * @property {boolean} hasNestProperty + * @property { (name: string) => PropertyData | null } get + * @property {boolean} [isProps] + */ + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Find the variable of a given name. + * @param {RuleContext} context The rule context + * @param {Identifier} node The variable name to find. + * @returns {Variable|null} The found variable or null. + */ +function findVariable(context, node) { + return eslintUtils.findVariable(getScope(context, node), node) +} +/** + * Gets the scope for the current node + * @param {RuleContext} context The rule context + * @param {ESNode} currentNode The node to get the scope of + * @returns { import('eslint').Scope.Scope } The scope information for this node + */ +function getScope(context, currentNode) { + // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. + const inner = currentNode.type !== 'Program' + const scopeManager = context.getSourceCode().scopeManager + + /** @type {ESNode | null} */ + let node = currentNode + for (; node; node = /** @type {ESNode | null} */ (node.parent)) { + const scope = scopeManager.acquire(node, inner) + + if (scope) { + if (scope.type === 'function-expression-name') { + return scope.childScopes[0] + } + return scope + } + } + + 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 + * @returns {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration | null} + */ +function findFunction(context, id) { + const calleeVariable = findVariable(context, id) + if (!calleeVariable) { + return null + } + if (calleeVariable.defs.length === 1) { + const def = calleeVariable.defs[0] + if (def.node.type === 'FunctionDeclaration') { + return def.node + } + if ( + def.type === 'Variable' && + def.parent.kind === 'const' && + def.node.init + ) { + if ( + def.node.init.type === 'FunctionExpression' || + def.node.init.type === 'ArrowFunctionExpression' + ) { + return def.node.init + } + if (def.node.init.type === 'Identifier') { + return findFunction(context, def.node.init) + } + } + } + return null +} + +/** + * @callback ReferencePropertiesTracker + * @param {RuleContext} context + * @returns {ReferenceProperties} + * + * @typedef {object} CallAndParamIndex + * @property {CallExpression} node + * @property {number} index + * + * @typedef {object} Ref + * @property {string} name + * @property {ASTNode} node + * @property {ReferencePropertiesTracker} tracker + * + * @typedef {object} ReferenceProperties + * @property { () => IterableIterator } iterateRefs + * @property { ReadonlyArray } list + * @property { ReadonlyArray } calls + */ + +/** + * Collects the property reference names. + */ +class ReferencePropertiesImpl { + constructor() { + /** @type {Ref[]} */ + this.list = [] + /** @type {CallAndParamIndex[]} */ + this.calls = [] + } + + /** + * @param {string} name + * @param {ASTNode} node + * @param {ReferencePropertiesTracker} tracker + */ + addReference(name, node, tracker) { + this.list.push({ + name, + node, + tracker + }) + } + + /** + * @returns {IterableIterator} + */ + *iterateRefs() { + yield* this.list + } + + /** + * @param {ReferenceProperties} other + */ + merge(other) { + this.list.push(...other.list) + this.calls.push(...other.calls) + } +} +/** @type { ReferenceProperties } */ +const EMPTY_REFS = new ReferencePropertiesImpl() + +/** + * Collects the property reference names for parameters of the function. + */ +class ParamsReferenceProperties { + /** + * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node + * @param {RuleContext} context + */ + constructor(node, context) { + this.node = node + this.context = context + /** @type {ReferenceProperties[]} */ + this.params = [] + } + + /** + * @param {number} index + * @returns {ReferenceProperties | null} + */ + getParam(index) { + const param = this.params[index] + if (param != null) { + return param + } + if (this.node.params[index]) { + return (this.params[index] = extractParamReferences( + this.node.params[index], + this.context + )) + } + return null + } +} +/** + * Extract the property reference name from one parameter of the function. + * @param {Pattern} node + * @param {RuleContext} context + * @returns {ReferenceProperties} + */ +function extractParamReferences(node, context) { + while (node.type === 'AssignmentPattern') { + node = node.left + } + if (node.type === 'RestElement' || node.type === 'ArrayPattern') { + // cannot check + return EMPTY_REFS + } + if (node.type === 'ObjectPattern') { + return extractObjectPatternReferences(node) + } + if (node.type !== 'Identifier') { + return EMPTY_REFS + } + const variable = findVariable(context, node) + if (!variable) { + return EMPTY_REFS + } + const result = new ReferencePropertiesImpl() + for (const reference of variable.references) { + const id = reference.identifier + result.merge(extractPatternOrThisReferences(id, context, false)) + } + + return result +} + +/** + * Extract the property reference name from ObjectPattern. + * @param {ObjectPattern} node + * @returns {ReferenceProperties} + */ +function extractObjectPatternReferences(node) { + const result = new ReferencePropertiesImpl() + for (const prop of node.properties) { + if (prop.type === 'Property') { + const name = utils.getStaticPropertyName(prop) + if (name) { + let pattern = prop.value + result.addReference(name, prop.key, (context) => { + while (pattern.type === 'AssignmentPattern') { + pattern = pattern.left + } + if (pattern.type === 'ObjectPattern') { + return extractObjectPatternReferences(pattern) + } + if (pattern.type === 'Identifier') { + return extractIdentifierReferences(pattern, context) + } + return EMPTY_REFS + }) + } + } + } + return result +} + +/** + * Extract the property reference name from id. + * @param {Identifier} node + * @param {RuleContext} context + * @returns {ReferenceProperties} + */ +function extractIdentifierReferences(node, context) { + const variable = findVariable(context, node) + if (!variable) { + return EMPTY_REFS + } + const result = new ReferencePropertiesImpl() + for (const reference of variable.references) { + const id = reference.identifier + result.merge(extractPatternOrThisReferences(id, context, false)) + } + return result +} +/** + * Extract the property reference name from pattern or `this`. + * @param {Identifier | MemberExpression | ChainExpression | ThisExpression} node + * @param {RuleContext} context + * @param {boolean} withInTemplate + * @returns {ReferenceProperties} + */ +function extractPatternOrThisReferences(node, context, withInTemplate) { + while (node.parent.type === 'ChainExpression') { + node = node.parent + } + const parent = node.parent + if (parent.type === 'AssignmentExpression') { + if (withInTemplate) { + return EMPTY_REFS + } + if (parent.right === node && parent.left.type === 'ObjectPattern') { + // `({foo} = arg)` + return extractObjectPatternReferences(parent.left) + } + } else if (parent.type === 'VariableDeclarator') { + if (withInTemplate) { + return EMPTY_REFS + } + if (parent.init === node) { + if (parent.id.type === 'ObjectPattern') { + // `const {foo} = arg` + return extractObjectPatternReferences(parent.id) + } else if (parent.id.type === 'Identifier') { + // `const foo = arg` + return extractIdentifierReferences(parent.id, context) + } + } + } else if (parent.type === 'MemberExpression') { + if (parent.object === node) { + // `arg.foo` + const name = utils.getStaticPropertyName(parent) + if (name) { + const result = new ReferencePropertiesImpl() + result.addReference(name, parent.property, () => + extractPatternOrThisReferences(parent, context, withInTemplate) + ) + return result + } + } + } else if (parent.type === 'CallExpression') { + if (withInTemplate) { + return EMPTY_REFS + } + const argIndex = parent.arguments.indexOf(node) + if (argIndex > -1) { + // `foo(arg)` + const result = new ReferencePropertiesImpl() + result.calls.push({ + node: parent, + index: argIndex + }) + return result + } + } + return EMPTY_REFS +} + +/** + * @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 + ) + + /** Vue component context */ + class VueComponentContext { + constructor() { + /** @type { Map } */ + this.properties = new Map() + + /** @type { Set } */ + this.reported = new Set() + } + + /** + * 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 + } + }) + } + + /** + * Verify reference properties + * @param {ReferenceProperties | null} refs + * @param {object} [options] + * @param {boolean} [options.props] + */ + verifyUndefProperties(refs, options) { + const that = this + verifyUndefProperties(this.properties, refs, null) + + /** + * @param { { get: (name: string) => PropertyData | null | undefined } } propData + * @param {ReferenceProperties|null} refs + * @param {string|null} pathName + */ + function verifyUndefProperties(propData, refs, pathName) { + for (const ref of iterateResolvedRefs(refs)) { + /** @type {'undef' | 'undefProps' | null} */ + let messageId = null + + const referencePathName = pathName + ? `${pathName}.${ref.name}` + : ref.name + + const prop = propData.get(ref.name) + if (prop) { + if (options && options.props) { + if (!prop.isProps) { + messageId = 'undefProps' + } + } + + if (!messageId) { + if (prop.hasNestProperty) { + verifyUndefProperties( + prop, + ref.tracker(context), + referencePathName + ) + } + continue + } + } else { + messageId = 'undef' + } + that.report(ref.node, referencePathName, messageId) + } + } + } + } + + /** @type {Map} */ + const paramsReferencePropertiesMap = new Map() + /** @type {Map} */ + const vueComponentContextMap = new Map() + + /** + * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node + * @returns {ParamsReferenceProperties} + */ + function getParamsReferenceProperties(node) { + let usedProps = paramsReferencePropertiesMap.get(node) + if (!usedProps) { + usedProps = new ParamsReferenceProperties(node, context) + paramsReferencePropertiesMap.set(node, usedProps) + } + return usedProps + } + + /** + * @param {ASTNode} node + * @returns {VueComponentContext} + */ + function getVueComponentContext(node) { + let ctx = vueComponentContextMap.get(node) + if (!ctx) { + ctx = new VueComponentContext() + vueComponentContextMap.set(node, ctx) + } + return ctx + } + + /** + * @param {ReferenceProperties|null} refs + * @returns { IterableIterator } + */ + function* iterateResolvedRefs(refs) { + const already = new Map() + + yield* iterate(refs) + + /** + * @param {ReferenceProperties|null} refs + * @returns {IterableIterator} + */ + function* iterate(refs) { + if (!refs) { + return + } + yield* refs.iterateRefs() + for (const call of refs.calls) { + if (call.node.callee.type !== 'Identifier') { + continue + } + const fnNode = findFunction(context, call.node.callee) + if (!fnNode) { + continue + } + + let alreadyIndexes = already.get(fnNode) + if (!alreadyIndexes) { + alreadyIndexes = new Set() + already.set(fnNode, alreadyIndexes) + } + if (alreadyIndexes.has(call.index)) { + continue + } + alreadyIndexes.add(call.index) + const paramsRefs = getParamsReferenceProperties(fnNode) + const paramRefs = paramsRefs.getParam(call.index) + yield* iterate(paramRefs) + } + } + } + + /** + * @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( + {}, + utils.defineVueVisitor(context, { + onVueObjectEnter(node) { + const ctx = getVueComponentContext(node) + + for (const prop of utils.iterateProperties( + node, + new Set(['props', 'data', 'computed', 'setup', 'methods', 'inject']) + )) { + const propertyMap = + prop.groupName === 'data' && + prop.type === 'object' && + prop.property.value.type === 'ObjectExpression' + ? getObjectPropertyMap(prop.property.value) + : null + ctx.properties.set(prop.name, { + hasNestProperty: Boolean(propertyMap), + isProps: prop.groupName === 'props', + get(name) { + if (!propertyMap) { + return null + } + return getPropertyDataFromObjectProperty(propertyMap.get(name)) + } + }) + } + + for (const watcher of utils.iterateProperties( + node, + new Set(['watch']) + )) { + // Process `watch: { foo /* <- this */ () {} }` + const segments = watcher.name.split('.') + + const propData = ctx.properties.get(segments[0]) + if (!propData) { + ctx.report(watcher.node, segments[0]) + } else { + let targetPropData = propData + let index = 1 + while ( + targetPropData.hasNestProperty && + index < segments.length + ) { + const nestPropData = targetPropData.get(segments[index]) + if (!nestPropData) { + ctx.report( + watcher.node, + segments.slice(0, index + 1).join('.') + ) + break + } else { + index++ + targetPropData = nestPropData + } + } + } + + // Process `watch: { x: 'foo' /* <- this */ }` + if (watcher.type === 'object') { + const property = watcher.property + if (property.kind === 'init') { + for (const handlerValueNode of utils.iterateWatchHandlerValues( + property + )) { + if ( + handlerValueNode.type === 'Literal' || + handlerValueNode.type === 'TemplateLiteral' + ) { + const name = utils.getStringLiteralValue(handlerValueNode) + if (name != null && !ctx.properties.get(name)) { + ctx.report(handlerValueNode, name) + } + } + } + } + } + } + }, + /** @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 paramsRefs = getParamsReferenceProperties(node) + const refs = paramsRefs.getParam(0) + const ctx = getVueComponentContext(vueData.node) + ctx.verifyUndefProperties(refs, { props }) + }, + onSetupFunctionEnter(node, vueData) { + const paramsRefs = getParamsReferenceProperties(node) + const paramRefs = paramsRefs.getParam(0) + const ctx = getVueComponentContext(vueData.node) + ctx.verifyUndefProperties(paramRefs, { + props: true + }) + }, + onRenderFunctionEnter(node, vueData) { + const ctx = getVueComponentContext(vueData.node) + + // Check for Vue 3.x render + const paramsRefs = getParamsReferenceProperties(node) + const ctxRefs = paramsRefs.getParam(0) + ctx.verifyUndefProperties(ctxRefs) + + if (vueData.functional) { + // Check for Vue 2.x render & functional + const propsRefs = new ReferencePropertiesImpl() + for (const ref of iterateResolvedRefs(paramsRefs.getParam(1))) { + if (ref.name === 'props') { + propsRefs.merge(ref.tracker(context)) + } + } + ctx.verifyUndefProperties(propsRefs, { + 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 usedProps = extractPatternOrThisReferences(node, context, false) + ctx.verifyUndefProperties(usedProps) + } + }) + ) + + const templateVisitor = { + /** + * @param {VExpressionContainer} node + */ + VExpressionContainer(node) { + const globalScope = + context.getSourceCode().scopeManager.globalScope || + context.getSourceCode().scopeManager.scopes[0] + + const refs = new ReferencePropertiesImpl() + for (const id of getReferences(node.references)) { + if (globalScope.set.has(id.name)) { + continue + } + refs.addReference(id.name, id, (context) => + extractPatternOrThisReferences(id, context, true) + ) + } + + const exported = [...vueComponentContextMap.keys()].find(isExportObject) + const ctx = exported && vueComponentContextMap.get(exported) + + if (ctx) { + ctx.verifyUndefProperties(refs) + } + + /** + * @param {ASTNode} node + */ + function isExportObject(node) { + let parent = node.parent + while (parent) { + if (parent.type === 'ExportDefaultDeclaration') { + return true + } + parent = parent.parent + } + return false + } + } + } + + return utils.defineTemplateBodyVisitor( + context, + templateVisitor, + scriptVisitor + ) + } +} diff --git a/lib/utils/index.js b/lib/utils/index.js index 06ef51420..e6c6a6683 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -81,7 +81,7 @@ * @typedef { {key: string | null, value: BlockStatement | null} } ComponentComputedProperty */ /** - * @typedef { 'props' | 'data' | 'computed' | 'setup' | 'watch' | 'methods' } GroupName + * @typedef { 'props' | 'data' | 'computed' | 'setup' | 'watch' | 'methods' | 'inject' } GroupName * @typedef { { type: 'array', name: string, groupName: GroupName, node: Literal | TemplateLiteral } } ComponentArrayPropertyData * @typedef { { type: 'object', name: string, groupName: GroupName, node: Identifier | Literal | TemplateLiteral, property: Property } } ComponentObjectPropertyData * @typedef { ComponentArrayPropertyData | ComponentObjectPropertyData } ComponentPropertyData diff --git a/tests/lib/rules/no-undef-properties.js b/tests/lib/rules/no-undef-properties.js new file mode 100644 index 000000000..1d8bb5894 --- /dev/null +++ b/tests/lib/rules/no-undef-properties.js @@ -0,0 +1,1039 @@ +/** + * @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 + } + ] + } + ] +})