diff --git a/docs/rules/README.md b/docs/rules/README.md index 2b7defe57..e160ba03d 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -295,6 +295,7 @@ For example: | [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | | | [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 | | | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: | | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | | [vue/prefer-template](./prefer-template.md) | require template literals instead of string concatenation | :wrench: | diff --git a/docs/rules/no-unused-properties.md b/docs/rules/no-unused-properties.md new file mode 100644 index 000000000..438187060 --- /dev/null +++ b/docs/rules/no-unused-properties.md @@ -0,0 +1,164 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-unused-properties +description: disallow unused properties +--- +# vue/no-unused-properties +> disallow unused properties + +## :book: Rule Details + +This rule is aimed at eliminating unused properties. + +::: warning Note +This rule cannot be checked for use in other components (e.g. `mixins`, Property access via `$refs`) and use in places where the scope cannot be determined. +::: + + + +```vue + + + +``` + + + + + +```vue + + + +``` + + + +## :wrench: Options + +```json +{ + "vue/no-unused-properties": ["error", { + "groups": ["props"] + }] +} +``` + +- `"groups"` (`string[]`) Array of groups to search for properties. Default is `["props"]`. The value of the array is some of the following strings: + - `"props"` + - `"data"` + - `"computed"` + - `"methods"` + - `"setup"` + +### `"groups": ["props", "data"]` + + + +```vue + + +``` + + + + + +```vue + + +``` + + + +### `"groups": ["props", "computed"]` + + + +```vue + + + +``` + + + + + +```vue + + + +``` + + + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unused-properties.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unused-properties.js) diff --git a/lib/index.js b/lib/index.js index 7b3b5d9d1..ba8da3e72 100644 --- a/lib/index.js +++ b/lib/index.js @@ -84,6 +84,7 @@ module.exports = { 'no-unregistered-components': require('./rules/no-unregistered-components'), 'no-unsupported-features': require('./rules/no-unsupported-features'), 'no-unused-components': require('./rules/no-unused-components'), + 'no-unused-properties': require('./rules/no-unused-properties'), 'no-unused-vars': require('./rules/no-unused-vars'), 'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'), 'no-v-html': require('./rules/no-v-html'), diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js new file mode 100644 index 000000000..2462446e4 --- /dev/null +++ b/lib/rules/no-unused-properties.js @@ -0,0 +1,498 @@ +/** + * @fileoverview Disallow unused properties, data and computed properties. + * @author Learning Equality + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') +const eslintUtils = require('eslint-utils') + +/** + * @typedef {import('vue-eslint-parser').AST.Node} Node + * @typedef {import('vue-eslint-parser').AST.ESLintNode} ASTNode + * @typedef {import('vue-eslint-parser').AST.ESLintObjectPattern} ObjectPattern + * @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier + * @typedef {import('vue-eslint-parser').AST.ESLintThisExpression} ThisExpression + * @typedef {import('vue-eslint-parser').AST.ESLintFunctionExpression} FunctionExpression + * @typedef {import('vue-eslint-parser').AST.ESLintArrowFunctionExpression} ArrowFunctionExpression + * @typedef {import('vue-eslint-parser').AST.ESLintFunctionDeclaration} FunctionDeclaration + * @typedef {import('eslint').Scope.Variable} Variable + * @typedef {import('eslint').Rule.RuleContext} RuleContext + */ +/** + * @typedef { { name: string, groupName: string, node: ASTNode } } PropertyData + * @typedef { { usedNames: Set } } TemplatePropertiesContainer + * @typedef { { properties: Array, usedNames: Set, unknown: boolean, usedPropsNames: Set, unknownProps: boolean } } VueComponentPropertiesContainer + * @typedef { { node: FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, index: number } } CallIdAndParamIndex + * @typedef { { usedNames: Set, unknown: boolean } } UsedProperties + */ + +// ------------------------------------------------------------------------------ +// Constants +// ------------------------------------------------------------------------------ + +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 PROPERTY_LABEL = { + [GROUP_PROPERTY]: 'property', + [GROUP_DATA]: 'data', + [GROUP_COMPUTED_PROPERTY]: 'computed property', + [GROUP_METHODS]: 'method', + [GROUP_SETUP]: 'property returned from `setup()`' +} + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Find the variable of a given name. + * @param {RuleContext} context The rule context + * @param {ASTNode} node The variable name to find. + * @returns {Variable|null} The found variable or null. + */ +function findVariable (context, node) { + // @ts-ignore + return eslintUtils.findVariable(getScope(context, node), node) +} +/** + * Gets the scope for the current node + * @param {RuleContext} context The rule context + * @param {ASTNode} 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 + + // @ts-ignore + for (let node = currentNode; node; node = node.parent) { + // @ts-ignore + 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. + */ +function getReferencesNames (references) { + return references + .filter(ref => ref.variable == null) + .map(ref => ref.id.name) +} + +/** + * @param {ObjectPattern} node + * @returns {UsedProperties} + */ +function extractObjectPatternProperties (node) { + const usedNames = new Set() + for (const prop of node.properties) { + if (prop.type === 'Property') { + usedNames.add(utils.getStaticPropertyName(prop)) + } else { + // If use RestElement, everything is used! + return { + usedNames, + unknown: true + } + } + } + return { + usedNames, + unknown: false + } +} + +/** + * @param {Identifier | ThisExpression} node + * @param {RuleContext} context + * @returns {UsedProps} + */ +function extractIdOrThisProperties (node, context) { + /** @type {UsedProps} */ + const result = new UsedProps() + const parent = node.parent + if (parent.type === 'AssignmentExpression') { + if (parent.right === node && parent.left.type === 'ObjectPattern') { + // `({foo} = arg)` + const { usedNames, unknown } = extractObjectPatternProperties(parent.left) + usedNames.forEach(name => result.usedNames.add(name)) + result.unknown = result.unknown || unknown + } + } else if (parent.type === 'VariableDeclarator') { + if (parent.init === node && parent.id.type === 'ObjectPattern') { + // `const {foo} = arg` + const { usedNames, unknown } = extractObjectPatternProperties(parent.id) + usedNames.forEach(name => result.usedNames.add(name)) + result.unknown = result.unknown || unknown + } + } else if (parent.type === 'MemberExpression') { + if (parent.object === node) { + // `arg.foo` + const name = utils.getStaticPropertyName(parent) + if (name) { + result.usedNames.add(name) + } else { + result.unknown = true + } + } + } else if (parent.type === 'CallExpression') { + const argIndex = parent.arguments.indexOf(node) + if (argIndex > -1 && parent.callee.type === 'Identifier') { + // `foo(arg)` + const calleeVariable = findVariable(context, parent.callee) + if (!calleeVariable) { + return result + } + if (calleeVariable.defs.length === 1) { + const def = calleeVariable.defs[0] + if ( + def.type === 'Variable' && + def.parent && + def.parent.kind === 'const' && + (def.node.init.type === 'FunctionExpression' || def.node.init.type === 'ArrowFunctionExpression') + ) { + result.calls.push({ + // @ts-ignore + node: def.node.init, + index: argIndex + }) + } else if (def.node.type === 'FunctionDeclaration') { + result.calls.push({ + node: def.node, + index: argIndex + }) + } + } + } + } + return result +} + +/** + * Collects the property names used. + */ +class UsedProps { + constructor () { + /** @type {Set} */ + this.usedNames = new Set() + /** @type {CallIdAndParamIndex[]} */ + this.calls = [] + this.unknown = false + } +} + +/** + * Collects the property names used for one parameter of the function. + */ +class ParamUsedProps extends UsedProps { + /** + * @param {ASTNode} paramNode + * @param {RuleContext} context + */ + constructor (paramNode, context) { + super() + + if (paramNode.type === 'RestElement' || paramNode.type === 'ArrayPattern') { + // cannot check + return + } + if (paramNode.type === 'ObjectPattern') { + const { usedNames, unknown } = extractObjectPatternProperties(paramNode) + usedNames.forEach(name => this.usedNames.add(name)) + this.unknown = this.unknown || unknown + return + } + const variable = findVariable(context, paramNode) + if (!variable) { + return + } + for (const reference of variable.references) { + /** @type {Identifier} */ + // @ts-ignore + const id = reference.identifier + const { usedNames, unknown, calls } = extractIdOrThisProperties(id, context) + usedNames.forEach(name => this.usedNames.add(name)) + this.unknown = this.unknown || unknown + this.calls.push(...calls) + } + } +} + +/** + * Collects the property names used for parameters of the function. + */ +class ParamsUsedProps { + /** + * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node + * @param {RuleContext} context + */ + constructor (node, context) { + this.node = node + this.context = context + /** @type {ParamUsedProps[]} */ + this.params = [] + } + + /** + * @param {number} index + * @returns {ParamUsedProps} + */ + getParam (index) { + const param = this.params[index] + if (param != null) { + return param + } + if (this.node.params[index]) { + return (this.params[index] = new ParamUsedProps(this.node.params[index], this.context)) + } + return null + } +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow unused properties', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/no-unused-properties.html' + }, + fixable: null, + schema: [ + { + type: 'object', + properties: { + groups: { + type: 'array', + items: { + enum: [ + GROUP_PROPERTY, + GROUP_DATA, + GROUP_COMPUTED_PROPERTY, + GROUP_METHODS, + GROUP_SETUP + ] + }, + additionalItems: false, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + messages: { + unused: "'{{name}}' of {{group}} found, but never used." + } + }, + + create (context) { + const options = context.options[0] || {} + const groups = new Set(options.groups || [GROUP_PROPERTY]) + + /** @type {Map} */ + const paramsUsedPropsMap = new Map() + /** @type {TemplatePropertiesContainer} */ + const templatePropertiesContainer = { + usedNames: new Set() + } + /** @type {Map} */ + const vueComponentPropertiesContainerMap = new Map() + + /** + * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node + * @returns {ParamsUsedProps} + */ + function getParamsUsedProps (node) { + let usedProps = paramsUsedPropsMap.get(node) + if (!usedProps) { + usedProps = new ParamsUsedProps(node, context) + paramsUsedPropsMap.set(node, usedProps) + } + return usedProps + } + + /** + * @param {ASTNode} node + * @returns {VueComponentPropertiesContainer} + */ + function getVueComponentPropertiesContainer (node) { + let container = vueComponentPropertiesContainerMap.get(node) + if (!container) { + container = { + properties: [], + usedNames: new Set(), + usedPropsNames: new Set(), + unknown: false, + unknownProps: false + } + vueComponentPropertiesContainerMap.set(node, container) + } + return container + } + + /** + * Report all unused properties. + */ + function reportUnusedProperties () { + for (const container of vueComponentPropertiesContainerMap.values()) { + if (container.unknown) { + continue + } + for (const property of container.properties) { + if (container.usedNames.has(property.name) || templatePropertiesContainer.usedNames.has(property.name)) { + continue + } + if (property.groupName === 'props' && (container.unknownProps || container.usedPropsNames.has(property.name))) { + continue + } + context.report({ + node: property.node, + messageId: 'unused', + data: { + group: PROPERTY_LABEL[property.groupName], + name: property.name + } + }) + } + } + } + + /** + * @param {UsedProps} usedProps + * @param {Map>} already + * @returns {Generator} + */ + function * iterateUsedProps (usedProps, already = new Map()) { + yield usedProps + for (const call of usedProps.calls) { + let alreadyIndexes = already.get(call.node) + if (!alreadyIndexes) { + alreadyIndexes = new Set() + already.set(call.node, alreadyIndexes) + } + if (alreadyIndexes.has(call.index)) { + continue + } + alreadyIndexes.add(call.index) + const paramsUsedProps = getParamsUsedProps(call.node) + const paramUsedProps = paramsUsedProps.getParam(call.index) + if (!paramUsedProps) { + continue + } + yield paramUsedProps + yield * iterateUsedProps(paramUsedProps, already) + } + } + + const scriptVisitor = Object.assign( + {}, + utils.defineVueVisitor(context, { + ObjectExpression (node, vueData) { + if (node !== vueData.node) { + return + } + + const container = getVueComponentPropertiesContainer(vueData.node) + const watcherNames = new Set() + for (const watcher of utils.iterateProperties(node, new Set([GROUP_WATCHER]))) { + watcherNames.add(watcher.name) + } + for (const prop of utils.iterateProperties(node, groups)) { + if (watcherNames.has(prop.name)) { + continue + } + container.properties.push(prop) + } + }, + 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, vueData) { + if (node.parent !== vueData.node) { + return + } + if (utils.getStaticPropertyName(node) !== 'setup') { + return + } + const container = getVueComponentPropertiesContainer(vueData.node) + const propsParam = node.value.params[0] + if (!propsParam) { + // no arguments + return + } + const paramsUsedProps = getParamsUsedProps(node.value) + const paramUsedProps = paramsUsedProps.getParam(0) + + for (const { usedNames, unknown } of iterateUsedProps(paramUsedProps)) { + if (unknown) { + container.unknownProps = true + return + } + for (const name of usedNames) { + container.usedPropsNames.add(name) + } + } + }, + 'ThisExpression, Identifier' (node, vueData) { + if (!utils.isThis(node, context)) { + return + } + const container = getVueComponentPropertiesContainer(vueData.node) + const usedProps = extractIdOrThisProperties(node, context) + + for (const { usedNames, unknown } of iterateUsedProps(usedProps)) { + if (unknown) { + container.unknown = true + return + } + for (const name of usedNames) { + container.usedNames.add(name) + } + } + } + }), + { + 'Program:exit' (node) { + if (!node.templateBody) { + reportUnusedProperties() + } + } + }, + ) + + const templateVisitor = { + 'VExpressionContainer' (node) { + for (const name of getReferencesNames(node.references)) { + templatePropertiesContainer.usedNames.add(name) + } + }, + "VElement[parent.type!='VElement']:exit" () { + reportUnusedProperties() + } + } + + return utils.defineTemplateBodyVisitor(context, templateVisitor, scriptVisitor) + } +} diff --git a/lib/utils/index.js b/lib/utils/index.js index 296acd87a..ad72908fe 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -966,6 +966,17 @@ module.exports = { if (node.type !== 'Identifier') { return false } + const parent = node.parent + if (parent.type === 'MemberExpression') { + if (parent.property === node) { + return false + } + } else if (parent.type === 'Property') { + if (parent.key === node && !parent.computed) { + return false + } + } + const variable = findVariable(context.getScope(), node) if (variable != null && variable.defs.length === 1) { diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js new file mode 100644 index 000000000..a17aff8b9 --- /dev/null +++ b/tests/lib/rules/no-unused-properties.js @@ -0,0 +1,1245 @@ +/** + * @fileoverview Disallow unused properties, data and computed properties. + * @author Learning Equality + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-unused-properties') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +const allOptions = [{ groups: ['props', 'computed', 'data', 'methods', 'setup'] }] + +tester.run('no-unused-properties', rule, { + valid: [ + // a property used in a script expression + { + filename: 'test.vue', + code: ` + + ` + }, + // default options + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + + // a property being watched + { + filename: 'test.vue', + code: ` + + ` + }, + + // a property used as a template identifier + { + filename: 'test.vue', + code: ` + + + ` + }, + + // properties used in a template expression + { + filename: 'test.vue', + code: ` + + + ` + }, + + // a property used in v-if + { + filename: 'test.vue', + code: ` + + + ` + }, + + // a property used in v-for + { + filename: 'test.vue', + code: ` + + + ` + }, + + // a property used in v-html + { + filename: 'test.vue', + code: ` + + + ` + }, + + // a property passed in a component + { + filename: 'test.vue', + code: ` + + + ` + }, + + // a property used in v-on + { + filename: 'test.vue', + code: ` + + + ` + }, + + // data used in a script expression + { + filename: 'test.vue', + code: ` + + `, + options: allOptions + }, + + // data being watched + { + filename: 'test.vue', + code: ` + + `, + options: allOptions + }, + + // data used as a template identifier + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // data used in a template expression + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // data used in v-if + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // data used in v-for + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // data used in v-html + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // data used in v-model + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // data passed in a component + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // data used in v-on + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property used in a script expression + { + filename: 'test.vue', + code: ` + + `, + options: allOptions + }, + + // computed property being watched + { + filename: 'test.vue', + code: ` + + `, + options: allOptions + }, + + // computed property used as a template identifier + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed properties used in a template expression + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property used in v-if + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property used in v-for + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property used in v-html + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property used in v-model + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property passed in a component + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // ignores unused data when marked with eslint-disable + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // trace this + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + // use rest + { + filename: 'test.vue', + code: ` + + + ` + }, + + // function trace + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + } + ], + + invalid: [ + // unused property + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: "'count' of property found, but never used.", + line: 7 + } + ] + }, + + // unused data + { + filename: 'test.vue', + code: ` + + + `, + options: [{ groups: ['props', 'computed', 'data'] }], + errors: [ + { + message: "'count' of data found, but never used.", + line: 9 + } + ] + }, + + // unused computed property + { + filename: 'test.vue', + code: ` + + + `, + options: [{ groups: ['props', 'computed', 'data'] }], + errors: [ + { + message: "'count' of computed property found, but never used.", + line: 8 + } + ] + }, + + // all options + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions, + errors: [ + { + message: "'a' of property found, but never used.", + line: 7 + }, + { + message: "'b' of data found, but never used.", + line: 9 + }, + { + message: "'c' of computed property found, but never used.", + line: 12 + }, + { + message: "'d' of method found, but never used.", + line: 17 + }, + { + message: "'e' of property returned from `setup()` found, but never used.", + line: 20 + } + ] + }, + + // trace this + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: "'count' of property found, but never used.", + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: "'count' of property found, but never used.", + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: "'count' of property found, but never used.", + line: 4 + } + ] + }, + + // setup + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used." + ] + }, + + // function trace + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'baz' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used.", + "'baz' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'baz' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used.", + "'baz' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used.", + "'baz' of property found, but never used." + ] + } + ] +})