From 7608deafd74ba6032dce169c1bdcd4e6069d2731 Mon Sep 17 00:00:00 2001 From: Loren Date: Sun, 16 Feb 2020 04:42:32 -0500 Subject: [PATCH 0001/1010] New Rule vue/sort-keys (#997) * sort keys * fix for eslint 6.7 * ignore children/grandchildren * more finegrained parent analysis * expand the test * Update docs/rules/sort-keys.md Co-Authored-By: Yosuke Ota Co-authored-by: Yosuke Ota --- .eslintignore | 2 +- docs/rules/README.md | 1 + docs/rules/sort-keys.md | 109 ++++ lib/index.js | 1 + lib/rules/sort-keys.js | 265 ++++++++ package.json | 1 + tests/lib/rules/sort-keys.js | 1195 ++++++++++++++++++++++++++++++++++ 7 files changed, 1573 insertions(+), 1 deletion(-) create mode 100644 docs/rules/sort-keys.md create mode 100644 lib/rules/sort-keys.js create mode 100644 tests/lib/rules/sort-keys.js diff --git a/.eslintignore b/.eslintignore index 20f3203c8..1905aa4a8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,7 +2,7 @@ /coverage /node_modules /tests/fixtures -/tests/integrations/*/node_modules +/tests/integrations/eslint-plugin-import !.vuepress /docs/.vuepress/dist diff --git a/docs/rules/README.md b/docs/rules/README.md index 30c0f93d1..ef6387ffc 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -168,6 +168,7 @@ For example: | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | | | [vue/script-indent](./script-indent.md) | enforce consistent indentation in ` +``` + + + + + +```vue + +``` + + + +## :books: Further reading + +- [sorts-keys] + +[sort-keys]: https://eslint.org/docs/rules/sort-keys + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/sort-keys.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/sort-keys.js) diff --git a/lib/index.js b/lib/index.js index d1b9bd9ef..efd41aaee 100644 --- a/lib/index.js +++ b/lib/index.js @@ -78,6 +78,7 @@ module.exports = { 'return-in-computed-property': require('./rules/return-in-computed-property'), 'script-indent': require('./rules/script-indent'), 'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'), + 'sort-keys': require('./rules/sort-keys'), 'space-infix-ops': require('./rules/space-infix-ops'), 'space-unary-ops': require('./rules/space-unary-ops'), 'static-class-names-order': require('./rules/static-class-names-order'), diff --git a/lib/rules/sort-keys.js b/lib/rules/sort-keys.js new file mode 100644 index 000000000..8f900ef0e --- /dev/null +++ b/lib/rules/sort-keys.js @@ -0,0 +1,265 @@ +/** + * @fileoverview enforce sort-keys in a manner that is compatible with order-in-components + * @author Loren Klingman + * Original ESLint sort-keys by Toru Nagashima + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const naturalCompare = require('natural-compare') +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Gets the property name of the given `Property` node. + * + * - If the property's key is an `Identifier` node, this returns the key's name + * whether it's a computed property or not. + * - If the property has a static name, this returns the static name. + * - Otherwise, this returns null. + * @param {ASTNode} node The `Property` node to get. + * @returns {string|null} The property name or null. + * @private + */ +function getPropertyName (node) { + const staticName = utils.getStaticPropertyName(node) + + if (staticName !== null) { + return staticName + } + + return node.key.name || null +} + +/** + * Functions which check that the given 2 names are in specific order. + * + * Postfix `I` is meant insensitive. + * Postfix `N` is meant natual. + * @private + */ +const isValidOrders = { + asc (a, b) { + return a <= b + }, + ascI (a, b) { + return a.toLowerCase() <= b.toLowerCase() + }, + ascN (a, b) { + return naturalCompare(a, b) <= 0 + }, + ascIN (a, b) { + return naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0 + }, + desc (a, b) { + return isValidOrders.asc(b, a) + }, + descI (a, b) { + return isValidOrders.ascI(b, a) + }, + descN (a, b) { + return isValidOrders.ascN(b, a) + }, + descIN (a, b) { + return isValidOrders.ascIN(b, a) + } +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'enforce sort-keys in a manner that is compatible with order-in-components', + category: null, + recommended: false, + url: 'https://eslint.vuejs.org/rules/sort-keys.html' + }, + fixable: null, + schema: [ + { + enum: ['asc', 'desc'] + }, + { + type: 'object', + properties: { + caseSensitive: { + type: 'boolean', + default: true + }, + ignoreChildrenOf: { + type: 'array' + }, + ignoreGrandchildrenOf: { + type: 'array' + }, + minKeys: { + type: 'integer', + minimum: 2, + default: 2 + }, + natural: { + type: 'boolean', + default: false + }, + runOutsideVue: { + type: 'boolean', + default: true + } + }, + additionalProperties: false + } + ] + }, + + create (context) { + // Parse options. + const options = context.options[1] + const order = context.options[0] || 'asc' + + const ignoreGrandchildrenOf = (options && options.ignoreGrandchildrenOf) || ['computed', 'directives', 'inject', 'props', 'watch'] + const ignoreChildrenOf = (options && options.ignoreChildrenOf) || ['model'] + const insensitive = options && options.caseSensitive === false + const minKeys = options && options.minKeys + const natual = options && options.natural + const isValidOrder = isValidOrders[ + order + (insensitive ? 'I' : '') + (natual ? 'N' : '') + ] + + // The stack to save the previous property's name for each object literals. + let stack = null + + let errors = [] + const names = {} + + const reportErrors = (isVue) => { + if (isVue) { + errors = errors.filter((error) => { + let parentIsRoot = !error.hasUpper + let grandParentIsRoot = !error.grandparent + let greatGrandparentIsRoot = !error.greatGrandparent + + const stackPrevChar = stack && stack.prevChar + if (stackPrevChar) { + parentIsRoot = stackPrevChar === error.parent + grandParentIsRoot = stackPrevChar === error.grandparent + greatGrandparentIsRoot = stackPrevChar === error.greatGrandparent + } + + if (parentIsRoot) { + return false + } else if (grandParentIsRoot) { + return !error.parentIsProperty || !ignoreChildrenOf.includes(names[error.parent]) + } else if (greatGrandparentIsRoot) { + return !error.parentIsProperty || !ignoreGrandchildrenOf.includes(names[error.grandparent]) + } + return true + }) + } + errors.forEach((error) => error.errors.forEach((e) => context.report(e))) + errors = [] + } + + const sortTests = { + ObjectExpression (node) { + if (!stack) { + reportErrors(false) + } + stack = { + upper: stack, + prevChar: null, + prevName: null, + numKeys: node.properties.length, + parentIsProperty: node.parent.type === 'Property', + errors: [] + } + }, + 'ObjectExpression:exit' (node) { + errors.push({ + errors: stack.errors, + hasUpper: !!stack.upper, + parentIsProperty: node.parent.type === 'Property', + parent: stack.upper && stack.upper.prevChar, + grandparent: stack.upper && stack.upper.upper && stack.upper.upper.prevChar, + greatGrandparent: stack.upper && stack.upper.upper && stack.upper.upper.upper && stack.upper.upper.upper.prevChar + }) + stack = stack.upper + }, + SpreadElement (node) { + if (node.parent.type === 'ObjectExpression') { + stack.prevName = null + stack.prevChar = null + } + }, + 'Program:exit' () { + reportErrors(false) + }, + Property (node) { + if (node.parent.type === 'ObjectPattern') { + return + } + + const prevName = stack.prevName + const numKeys = stack.numKeys + const thisName = getPropertyName(node) + + if (thisName !== null) { + stack.prevName = thisName + stack.prevChar = node.range[0] + if (Object.prototype.hasOwnProperty.call(names, node.range[0])) { + throw new Error('Name clash') + } + names[node.range[0]] = thisName + } + + if (prevName === null || thisName === null || numKeys < minKeys) { + return + } + + if (!isValidOrder(prevName, thisName)) { + stack.errors.push({ + node, + loc: node.key.loc, + message: "Expected object keys to be in {{natual}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'.", + data: { + thisName, + prevName, + order, + insensitive: insensitive ? 'insensitive ' : '', + natual: natual ? 'natural ' : '' + } + }) + } + } + } + + const execOnVue = utils.executeOnVue(context, (obj) => { + reportErrors(true) + }) + + const result = { ...sortTests } + + Object.keys(execOnVue).forEach((key) => { + // Ensure we call both the callback from sortTests and execOnVue if they both use the same key + if (Object.prototype.hasOwnProperty.call(sortTests, key)) { + result[key] = (node) => { + sortTests[key](node) + execOnVue[key](node) + } + } else { + result[key] = execOnVue[key] + } + }) + + return result + } +} diff --git a/package.json b/package.json index a48871f95..472689b0f 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "eslint": "^5.0.0 || ^6.0.0" }, "dependencies": { + "natural-compare": "^1.4.0", "vue-eslint-parser": "^7.0.0", "semver": "^5.6.0" }, diff --git a/tests/lib/rules/sort-keys.js b/tests/lib/rules/sort-keys.js new file mode 100644 index 000000000..c8094b5b3 --- /dev/null +++ b/tests/lib/rules/sort-keys.js @@ -0,0 +1,1195 @@ +/** + * @fileoverview Enforces sort-keys within components after the top level details + * @author Loren Klingman + */ +'use strict' + +const rule = require('../../../lib/rules/sort-keys') +const RuleTester = require('eslint').RuleTester + +const ruleTester = new RuleTester() + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module' +} + +ruleTester.run('sort-keys', rule, { + + valid: [ + { + filename: 'test.vue', + code: ` + const obj = { + foo() { + Vue.component('my-component', { + name: 'app', + data() {} + }) + } + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + name: 'app', + props: { + propA: Number, + }, + ...a, + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + }, + } + `, + parserOptions + }, + { + filename: 'propsOrder.vue', + code: ` + export default { + name: 'app', + model: { + prop: 'checked', + event: 'change' + }, + props: { + propA: { + type: String, + default: 'abc', + }, + propB: { + type: String, + default: 'abc', + }, + }, + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default {} + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default 'example-text' + `, + parserOptions + }, + { + filename: 'test.jsx', + code: ` + export default { + name: 'app', + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + }, + } + `, + parserOptions + }, + { + filename: 'test.js', + code: ` + Vue.component('example') + `, + parserOptions: { ecmaVersion: 6 } + }, + { + filename: 'test.js', + code: ` + const { component } = Vue; + component('smart-list', { + name: 'app', + components: {}, + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + } + }) + `, + parserOptions: { ecmaVersion: 6 } + }, + { + filename: 'test.js', + code: ` + new Vue({ + el: '#app', + name: 'app', + components: {}, + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + } + }) + `, + parserOptions: { ecmaVersion: 6 } + }, + { + filename: 'test.js', + code: ` + new Vue() + `, + parserOptions: { ecmaVersion: 6 } + }, + // default (asc) + { code: "var obj = {'':1, [``]:2}", options: [], parserOptions: { ecmaVersion: 6 }}, + { code: "var obj = {[``]:1, '':2}", options: [], parserOptions: { ecmaVersion: 6 }}, + { code: "var obj = {'':1, a:2}", options: [] }, + { code: 'var obj = {[``]:1, a:2}', options: [], parserOptions: { ecmaVersion: 6 }}, + { code: 'var obj = {_:2, a:1, b:3} // default', options: [] }, + { code: 'var obj = {a:1, b:3, c:2}', options: [] }, + { code: 'var obj = {a:2, b:3, b_:1}', options: [] }, + { code: 'var obj = {C:3, b_:1, c:2}', options: [] }, + { code: 'var obj = {$:1, A:3, _:2, a:4}', options: [] }, + { code: "var obj = {1:1, '11':2, 2:4, A:3}", options: [] }, + { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: [] }, + + // ignore non-simple computed properties. + { code: 'var obj = {a:1, b:3, [a + b]: -1, c:2}', options: [], parserOptions: { ecmaVersion: 6 }}, + { code: "var obj = {'':1, [f()]:2, a:3}", options: [], parserOptions: { ecmaVersion: 6 }}, + { code: "var obj = {a:1, [b++]:2, '':3}", options: ['desc'], parserOptions: { ecmaVersion: 6 }}, + + // ignore properties separated by spread properties + { code: 'var obj = {a:1, ...z, b:1}', options: [], parserOptions: { ecmaVersion: 2018 }}, + { code: 'var obj = {b:1, ...z, a:1}', options: [], parserOptions: { ecmaVersion: 2018 }}, + { code: 'var obj = {...a, b:1, ...c, d:1}', options: [], parserOptions: { ecmaVersion: 2018 }}, + { code: 'var obj = {...a, b:1, ...d, ...c, e:2, z:5}', options: [], parserOptions: { ecmaVersion: 2018 }}, + { code: 'var obj = {b:1, ...c, ...d, e:2}', options: [], parserOptions: { ecmaVersion: 2018 }}, + { code: "var obj = {a:1, ...z, '':2}", options: [], parserOptions: { ecmaVersion: 2018 }}, + { code: "var obj = {'':1, ...z, 'a':2}", options: ['desc'], parserOptions: { ecmaVersion: 2018 }}, + + // not ignore properties not separated by spread properties + { code: 'var obj = {...z, a:1, b:1}', options: [], parserOptions: { ecmaVersion: 2018 }}, + { code: 'var obj = {...z, ...c, a:1, b:1}', options: [], parserOptions: { ecmaVersion: 2018 }}, + { code: 'var obj = {a:1, b:1, ...z}', options: [], parserOptions: { ecmaVersion: 2018 }}, + { code: 'var obj = {...z, ...x, a:1, ...c, ...d, f:5, e:4}', options: ['desc'], parserOptions: { ecmaVersion: 2018 }}, + + // works when spread occurs somewhere other than an object literal + { code: 'function fn(...args) { return [...args].length; }', options: [], parserOptions: { ecmaVersion: 2018 }}, + { code: 'function g() {}; function f(...args) { return g(...args); }', options: [], parserOptions: { ecmaVersion: 2018 }}, + + // ignore destructuring patterns. + { code: 'let {a, b} = {}', options: [], parserOptions: { ecmaVersion: 6 }}, + + // nested + { code: 'var obj = {a:1, b:{x:1, y:1}, c:1}', options: [] }, + + // asc + { code: 'var obj = {_:2, a:1, b:3} // asc', options: ['asc'] }, + { code: 'var obj = {a:1, b:3, c:2}', options: ['asc'] }, + { code: 'var obj = {a:2, b:3, b_:1}', options: ['asc'] }, + { code: 'var obj = {C:3, b_:1, c:2}', options: ['asc'] }, + { code: 'var obj = {$:1, A:3, _:2, a:4}', options: ['asc'] }, + { code: "var obj = {1:1, '11':2, 2:4, A:3}", options: ['asc'] }, + { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ['asc'] }, + + // asc, minKeys should ignore unsorted keys when number of keys is less than minKeys + { code: 'var obj = {a:1, c:2, b:3}', options: ['asc', { minKeys: 4 }] }, + + // asc, insensitive + { code: 'var obj = {_:2, a:1, b:3} // asc, insensitive', options: ['asc', { caseSensitive: false }] }, + { code: 'var obj = {a:1, b:3, c:2}', options: ['asc', { caseSensitive: false }] }, + { code: 'var obj = {a:2, b:3, b_:1}', options: ['asc', { caseSensitive: false }] }, + { code: 'var obj = {b_:1, C:3, c:2}', options: ['asc', { caseSensitive: false }] }, + { code: 'var obj = {b_:1, c:3, C:2}', options: ['asc', { caseSensitive: false }] }, + { code: 'var obj = {$:1, _:2, A:3, a:4}', options: ['asc', { caseSensitive: false }] }, + { code: "var obj = {1:1, '11':2, 2:4, A:3}", options: ['asc', { caseSensitive: false }] }, + { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ['asc', { caseSensitive: false }] }, + + // asc, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys + { code: 'var obj = {$:1, A:3, _:2, a:4}', options: ['asc', { caseSensitive: false, minKeys: 5 }] }, + + // asc, natural + { code: 'var obj = {_:2, a:1, b:3} // asc, natural', options: ['asc', { natural: true }] }, + { code: 'var obj = {a:1, b:3, c:2}', options: ['asc', { natural: true }] }, + { code: 'var obj = {a:2, b:3, b_:1}', options: ['asc', { natural: true }] }, + { code: 'var obj = {C:3, b_:1, c:2}', options: ['asc', { natural: true }] }, + { code: 'var obj = {$:1, _:2, A:3, a:4}', options: ['asc', { natural: true }] }, + { code: "var obj = {1:1, 2:4, '11':2, A:3}", options: ['asc', { natural: true }] }, + { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ['asc', { natural: true }] }, + + // asc, natural, minKeys should ignore unsorted keys when number of keys is less than minKeys + { code: 'var obj = {b_:1, a:2, b:3}', options: ['asc', { natural: true, minKeys: 4 }] }, + + // asc, natural, insensitive + { code: 'var obj = {_:2, a:1, b:3} // asc, natural, insensitive', options: ['asc', { natural: true, caseSensitive: false }] }, + { code: 'var obj = {a:1, b:3, c:2}', options: ['asc', { natural: true, caseSensitive: false }] }, + { code: 'var obj = {a:2, b:3, b_:1}', options: ['asc', { natural: true, caseSensitive: false }] }, + { code: 'var obj = {b_:1, C:3, c:2}', options: ['asc', { natural: true, caseSensitive: false }] }, + { code: 'var obj = {b_:1, c:3, C:2}', options: ['asc', { natural: true, caseSensitive: false }] }, + { code: 'var obj = {$:1, _:2, A:3, a:4}', options: ['asc', { natural: true, caseSensitive: false }] }, + { code: "var obj = {1:1, 2:4, '11':2, A:3}", options: ['asc', { natural: true, caseSensitive: false }] }, + { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ['asc', { natural: true, caseSensitive: false }] }, + + // asc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys + { code: 'var obj = {a:1, _:2, b:3}', options: ['asc', { natural: true, caseSensitive: false, minKeys: 4 }] }, + + // desc + { code: 'var obj = {b:3, a:1, _:2} // desc', options: ['desc'] }, + { code: 'var obj = {c:2, b:3, a:1}', options: ['desc'] }, + { code: 'var obj = {b_:1, b:3, a:2}', options: ['desc'] }, + { code: 'var obj = {c:2, b_:1, C:3}', options: ['desc'] }, + { code: 'var obj = {a:4, _:2, A:3, $:1}', options: ['desc'] }, + { code: "var obj = {A:3, 2:4, '11':2, 1:1}", options: ['desc'] }, + { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ['desc'] }, + + // desc, minKeys should ignore unsorted keys when number of keys is less than minKeys + { code: 'var obj = {a:1, c:2, b:3}', options: ['desc', { minKeys: 4 }] }, + + // desc, insensitive + { code: 'var obj = {b:3, a:1, _:2} // desc, insensitive', options: ['desc', { caseSensitive: false }] }, + { code: 'var obj = {c:2, b:3, a:1}', options: ['desc', { caseSensitive: false }] }, + { code: 'var obj = {b_:1, b:3, a:2}', options: ['desc', { caseSensitive: false }] }, + { code: 'var obj = {c:2, C:3, b_:1}', options: ['desc', { caseSensitive: false }] }, + { code: 'var obj = {C:2, c:3, b_:1}', options: ['desc', { caseSensitive: false }] }, + { code: 'var obj = {a:4, A:3, _:2, $:1}', options: ['desc', { caseSensitive: false }] }, + { code: "var obj = {A:3, 2:4, '11':2, 1:1}", options: ['desc', { caseSensitive: false }] }, + { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ['desc', { caseSensitive: false }] }, + + // desc, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys + { code: 'var obj = {$:1, _:2, A:3, a:4}', options: ['desc', { caseSensitive: false, minKeys: 5 }] }, + + // desc, natural + { code: 'var obj = {b:3, a:1, _:2} // desc, natural', options: ['desc', { natural: true }] }, + { code: 'var obj = {c:2, b:3, a:1}', options: ['desc', { natural: true }] }, + { code: 'var obj = {b_:1, b:3, a:2}', options: ['desc', { natural: true }] }, + { code: 'var obj = {c:2, b_:1, C:3}', options: ['desc', { natural: true }] }, + { code: 'var obj = {a:4, A:3, _:2, $:1}', options: ['desc', { natural: true }] }, + { code: "var obj = {A:3, '11':2, 2:4, 1:1}", options: ['desc', { natural: true }] }, + { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ['desc', { natural: true }] }, + + // desc, natural, minKeys should ignore unsorted keys when number of keys is less than minKeys + { code: 'var obj = {b_:1, a:2, b:3}', options: ['desc', { natural: true, minKeys: 4 }] }, + + // desc, natural, insensitive + { code: 'var obj = {b:3, a:1, _:2} // desc, natural, insensitive', options: ['desc', { natural: true, caseSensitive: false }] }, + { code: 'var obj = {c:2, b:3, a:1}', options: ['desc', { natural: true, caseSensitive: false }] }, + { code: 'var obj = {b_:1, b:3, a:2}', options: ['desc', { natural: true, caseSensitive: false }] }, + { code: 'var obj = {c:2, C:3, b_:1}', options: ['desc', { natural: true, caseSensitive: false }] }, + { code: 'var obj = {C:2, c:3, b_:1}', options: ['desc', { natural: true, caseSensitive: false }] }, + { code: 'var obj = {a:4, A:3, _:2, $:1}', options: ['desc', { natural: true, caseSensitive: false }] }, + { code: "var obj = {A:3, '11':2, 2:4, 1:1}", options: ['desc', { natural: true, caseSensitive: false }] }, + { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ['desc', { natural: true, caseSensitive: false }] }, + + // desc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys + { code: 'var obj = {a:1, _:2, b:3}', options: ['desc', { natural: true, caseSensitive: false, minKeys: 4 }] } + ], + + invalid: [ + // default (asc) + { + code: "var obj = {a:1, '':2} // default", + errors: ["Expected object keys to be in ascending order. '' should be before 'a'."] + }, + { + code: 'var obj = {a:1, [``]:2} // default', + parserOptions: { ecmaVersion: 6 }, + errors: ["Expected object keys to be in ascending order. '' should be before 'a'."] + }, + { + code: 'var obj = {a:1, _:2, b:3} // default', + errors: ["Expected object keys to be in ascending order. '_' should be before 'a'."] + }, + { + code: 'var obj = {a:1, c:2, b:3}', + errors: ["Expected object keys to be in ascending order. 'b' should be before 'c'."] + }, + { + code: 'var obj = {b_:1, a:2, b:3}', + errors: ["Expected object keys to be in ascending order. 'a' should be before 'b_'."] + }, + { + code: 'var obj = {b_:1, c:2, C:3}', + errors: ["Expected object keys to be in ascending order. 'C' should be before 'c'."] + }, + { + code: 'var obj = {$:1, _:2, A:3, a:4}', + errors: ["Expected object keys to be in ascending order. 'A' should be before '_'."] + }, + { + code: "var obj = {1:1, 2:4, A:3, '11':2}", + errors: ["Expected object keys to be in ascending order. '11' should be before 'A'."] + }, + { + code: "var obj = {'#':1, À:3, 'Z':2, è:4}", + errors: ["Expected object keys to be in ascending order. 'Z' should be before 'À'."] + }, + + // not ignore properties not separated by spread properties + { + code: 'var obj = {...z, c:1, b:1}', + options: [], + parserOptions: { ecmaVersion: 2018 }, + errors: ["Expected object keys to be in ascending order. 'b' should be before 'c'."] + }, + { + code: 'var obj = {...z, ...c, d:4, b:1, ...y, ...f, e:2, a:1}', + options: [], + parserOptions: { ecmaVersion: 2018 }, + errors: [ + "Expected object keys to be in ascending order. 'b' should be before 'd'.", + "Expected object keys to be in ascending order. 'a' should be before 'e'." + ] + }, + { + code: 'var obj = {c:1, b:1, ...a}', + options: [], + parserOptions: { ecmaVersion: 2018 }, + errors: ["Expected object keys to be in ascending order. 'b' should be before 'c'."] + }, + { + code: 'var obj = {...z, ...a, c:1, b:1}', + options: [], + parserOptions: { ecmaVersion: 2018 }, + errors: ["Expected object keys to be in ascending order. 'b' should be before 'c'."] + }, + { + code: 'var obj = {...z, b:1, a:1, ...d, ...c}', + options: [], + parserOptions: { ecmaVersion: 2018 }, + errors: ["Expected object keys to be in ascending order. 'a' should be before 'b'."] + }, + { + code: 'var obj = {...z, a:2, b:0, ...x, ...c}', + options: ['desc'], + parserOptions: { ecmaVersion: 2018 }, + errors: ["Expected object keys to be in descending order. 'b' should be before 'a'."] + }, + { + code: 'var obj = {...z, a:2, b:0, ...x}', + options: ['desc'], + parserOptions: { ecmaVersion: 2018 }, + errors: ["Expected object keys to be in descending order. 'b' should be before 'a'."] + }, + { + code: "var obj = {...z, '':1, a:2}", + options: ['desc'], + parserOptions: { ecmaVersion: 2018 }, + errors: ["Expected object keys to be in descending order. 'a' should be before ''."] + }, + + // ignore non-simple computed properties, but their position shouldn't affect other comparisons. + { + code: "var obj = {a:1, [b+c]:2, '':3}", + parserOptions: { ecmaVersion: 6 }, + errors: ["Expected object keys to be in ascending order. '' should be before 'a'."] + }, + { + code: "var obj = {'':1, [b+c]:2, a:3}", + options: ['desc'], + parserOptions: { ecmaVersion: 6 }, + errors: ["Expected object keys to be in descending order. 'a' should be before ''."] + }, + { + code: "var obj = {b:1, [f()]:2, '':3, a:4}", + options: ['desc'], + parserOptions: { ecmaVersion: 6 }, + errors: ["Expected object keys to be in descending order. 'a' should be before ''."] + }, + + // not ignore simple computed properties. + { + code: 'var obj = {a:1, b:3, [a]: -1, c:2}', + parserOptions: { ecmaVersion: 6 }, + errors: ["Expected object keys to be in ascending order. 'a' should be before 'b'."] + }, + + // nested + { + code: 'var obj = {a:1, c:{y:1, x:1}, b:1}', + errors: [ + "Expected object keys to be in ascending order. 'x' should be before 'y'.", + "Expected object keys to be in ascending order. 'b' should be before 'c'." + ] + }, + + // asc + { + code: 'var obj = {a:1, _:2, b:3} // asc', + options: ['asc'], + errors: [ + "Expected object keys to be in ascending order. '_' should be before 'a'." + ] + }, + { + code: 'var obj = {a:1, c:2, b:3}', + options: ['asc'], + errors: [ + "Expected object keys to be in ascending order. 'b' should be before 'c'." + ] + }, + { + code: 'var obj = {b_:1, a:2, b:3}', + options: ['asc'], + errors: [ + "Expected object keys to be in ascending order. 'a' should be before 'b_'." + ] + }, + { + code: 'var obj = {b_:1, c:2, C:3}', + options: ['asc'], + errors: [ + "Expected object keys to be in ascending order. 'C' should be before 'c'." + ] + }, + { + code: 'var obj = {$:1, _:2, A:3, a:4}', + options: ['asc'], + errors: [ + "Expected object keys to be in ascending order. 'A' should be before '_'." + ] + }, + { + code: "var obj = {1:1, 2:4, A:3, '11':2}", + options: ['asc'], + errors: [ + "Expected object keys to be in ascending order. '11' should be before 'A'." + ] + }, + { + code: "var obj = {'#':1, À:3, 'Z':2, è:4}", + options: ['asc'], + errors: [ + "Expected object keys to be in ascending order. 'Z' should be before 'À'." + ] + }, + + // asc, minKeys should error when number of keys is greater than or equal to minKeys + { + code: 'var obj = {a:1, _:2, b:3}', + options: ['asc', { minKeys: 3 }], + errors: [ + "Expected object keys to be in ascending order. '_' should be before 'a'." + ] + }, + + // asc, insensitive + { + code: 'var obj = {a:1, _:2, b:3} // asc, insensitive', + options: ['asc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive ascending order. '_' should be before 'a'." + ] + }, + { + code: 'var obj = {a:1, c:2, b:3}', + options: ['asc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive ascending order. 'b' should be before 'c'." + ] + }, + { + code: 'var obj = {b_:1, a:2, b:3}', + options: ['asc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive ascending order. 'a' should be before 'b_'." + ] + }, + { + code: 'var obj = {$:1, A:3, _:2, a:4}', + options: ['asc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive ascending order. '_' should be before 'A'." + ] + }, + { + code: "var obj = {1:1, 2:4, A:3, '11':2}", + options: ['asc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive ascending order. '11' should be before 'A'." + ] + }, + { + code: "var obj = {'#':1, À:3, 'Z':2, è:4}", + options: ['asc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive ascending order. 'Z' should be before 'À'." + ] + }, + + // asc, insensitive, minKeys should error when number of keys is greater than or equal to minKeys + { + code: 'var obj = {a:1, _:2, b:3}', + options: ['asc', { caseSensitive: false, minKeys: 3 }], + errors: [ + "Expected object keys to be in insensitive ascending order. '_' should be before 'a'." + ] + }, + + // asc, natural + { + code: 'var obj = {a:1, _:2, b:3} // asc, natural', + options: ['asc', { natural: true }], + errors: [ + "Expected object keys to be in natural ascending order. '_' should be before 'a'." + ] + }, + { + code: 'var obj = {a:1, c:2, b:3}', + options: ['asc', { natural: true }], + errors: [ + "Expected object keys to be in natural ascending order. 'b' should be before 'c'." + ] + }, + { + code: 'var obj = {b_:1, a:2, b:3}', + options: ['asc', { natural: true }], + errors: [ + "Expected object keys to be in natural ascending order. 'a' should be before 'b_'." + ] + }, + { + code: 'var obj = {b_:1, c:2, C:3}', + options: ['asc', { natural: true }], + errors: [ + "Expected object keys to be in natural ascending order. 'C' should be before 'c'." + ] + }, + { + code: 'var obj = {$:1, A:3, _:2, a:4}', + options: ['asc', { natural: true }], + errors: [ + "Expected object keys to be in natural ascending order. '_' should be before 'A'." + ] + }, + { + code: "var obj = {1:1, 2:4, A:3, '11':2}", + options: ['asc', { natural: true }], + errors: [ + "Expected object keys to be in natural ascending order. '11' should be before 'A'." + ] + }, + { + code: "var obj = {'#':1, À:3, 'Z':2, è:4}", + options: ['asc', { natural: true }], + errors: [ + "Expected object keys to be in natural ascending order. 'Z' should be before 'À'." + ] + }, + + // asc, natural, minKeys should error when number of keys is greater than or equal to minKeys + { + code: 'var obj = {a:1, _:2, b:3}', + options: ['asc', { natural: true, minKeys: 2 }], + errors: [ + "Expected object keys to be in natural ascending order. '_' should be before 'a'." + ] + }, + + // asc, natural, insensitive + { + code: 'var obj = {a:1, _:2, b:3} // asc, natural, insensitive', + options: ['asc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive ascending order. '_' should be before 'a'." + ] + }, + { + code: 'var obj = {a:1, c:2, b:3}', + options: ['asc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive ascending order. 'b' should be before 'c'." + ] + }, + { + code: 'var obj = {b_:1, a:2, b:3}', + options: ['asc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive ascending order. 'a' should be before 'b_'." + ] + }, + { + code: 'var obj = {$:1, A:3, _:2, a:4}', + options: ['asc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive ascending order. '_' should be before 'A'." + ] + }, + { + code: "var obj = {1:1, '11':2, 2:4, A:3}", + options: ['asc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive ascending order. '2' should be before '11'." + ] + }, + { + code: "var obj = {'#':1, À:3, 'Z':2, è:4}", + options: ['asc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive ascending order. 'Z' should be before 'À'." + ] + }, + + // asc, natural, insensitive, minKeys should error when number of keys is greater than or equal to minKeys + { + code: 'var obj = {a:1, _:2, b:3}', + options: ['asc', { natural: true, caseSensitive: false, minKeys: 3 }], + errors: [ + "Expected object keys to be in natural insensitive ascending order. '_' should be before 'a'." + ] + }, + + // desc + { + code: "var obj = {'':1, a:'2'} // desc", + options: ['desc'], + errors: [ + "Expected object keys to be in descending order. 'a' should be before ''." + ] + }, + { + code: "var obj = {[``]:1, a:'2'} // desc", + options: ['desc'], + parserOptions: { ecmaVersion: 6 }, + errors: [ + "Expected object keys to be in descending order. 'a' should be before ''." + ] + }, + { + code: 'var obj = {a:1, _:2, b:3} // desc', + options: ['desc'], + errors: [ + "Expected object keys to be in descending order. 'b' should be before '_'." + ] + }, + { + code: 'var obj = {a:1, c:2, b:3}', + options: ['desc'], + errors: [ + "Expected object keys to be in descending order. 'c' should be before 'a'." + ] + }, + { + code: 'var obj = {b_:1, a:2, b:3}', + options: ['desc'], + errors: [ + "Expected object keys to be in descending order. 'b' should be before 'a'." + ] + }, + { + code: 'var obj = {b_:1, c:2, C:3}', + options: ['desc'], + errors: [ + "Expected object keys to be in descending order. 'c' should be before 'b_'." + ] + }, + { + code: 'var obj = {$:1, _:2, A:3, a:4}', + options: ['desc'], + errors: [ + "Expected object keys to be in descending order. '_' should be before '$'.", + "Expected object keys to be in descending order. 'a' should be before 'A'." + ] + }, + { + code: "var obj = {1:1, 2:4, A:3, '11':2}", + options: ['desc'], + errors: [ + "Expected object keys to be in descending order. '2' should be before '1'.", + "Expected object keys to be in descending order. 'A' should be before '2'." + ] + }, + { + code: "var obj = {'#':1, À:3, 'Z':2, è:4}", + options: ['desc'], + errors: [ + "Expected object keys to be in descending order. 'À' should be before '#'.", + "Expected object keys to be in descending order. 'è' should be before 'Z'." + ] + }, + + // desc, minKeys should error when number of keys is greater than or equal to minKeys + { + code: 'var obj = {a:1, _:2, b:3}', + options: ['desc', { minKeys: 3 }], + errors: [ + "Expected object keys to be in descending order. 'b' should be before '_'." + ] + }, + + // desc, insensitive + { + code: 'var obj = {a:1, _:2, b:3} // desc, insensitive', + options: ['desc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive descending order. 'b' should be before '_'." + ] + }, + { + code: 'var obj = {a:1, c:2, b:3}', + options: ['desc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive descending order. 'c' should be before 'a'." + ] + }, + { + code: 'var obj = {b_:1, a:2, b:3}', + options: ['desc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive descending order. 'b' should be before 'a'." + ] + }, + { + code: 'var obj = {b_:1, c:2, C:3}', + options: ['desc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive descending order. 'c' should be before 'b_'." + ] + }, + { + code: 'var obj = {$:1, _:2, A:3, a:4}', + options: ['desc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive descending order. '_' should be before '$'.", + "Expected object keys to be in insensitive descending order. 'A' should be before '_'." + ] + }, + { + code: "var obj = {1:1, 2:4, A:3, '11':2}", + options: ['desc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive descending order. '2' should be before '1'.", + "Expected object keys to be in insensitive descending order. 'A' should be before '2'." + ] + }, + { + code: "var obj = {'#':1, À:3, 'Z':2, è:4}", + options: ['desc', { caseSensitive: false }], + errors: [ + "Expected object keys to be in insensitive descending order. 'À' should be before '#'.", + "Expected object keys to be in insensitive descending order. 'è' should be before 'Z'." + ] + }, + + // desc, insensitive should error when number of keys is greater than or equal to minKeys + { + code: 'var obj = {a:1, _:2, b:3}', + options: ['desc', { caseSensitive: false, minKeys: 2 }], + errors: [ + "Expected object keys to be in insensitive descending order. 'b' should be before '_'." + ] + }, + + // desc, natural + { + code: 'var obj = {a:1, _:2, b:3} // desc, natural', + options: ['desc', { natural: true }], + errors: [ + "Expected object keys to be in natural descending order. 'b' should be before '_'." + ] + }, + { + code: 'var obj = {a:1, c:2, b:3}', + options: ['desc', { natural: true }], + errors: [ + "Expected object keys to be in natural descending order. 'c' should be before 'a'." + ] + }, + { + code: 'var obj = {b_:1, a:2, b:3}', + options: ['desc', { natural: true }], + errors: [ + "Expected object keys to be in natural descending order. 'b' should be before 'a'." + ] + }, + { + code: 'var obj = {b_:1, c:2, C:3}', + options: ['desc', { natural: true }], + errors: [ + "Expected object keys to be in natural descending order. 'c' should be before 'b_'." + ] + }, + { + code: 'var obj = {$:1, _:2, A:3, a:4}', + options: ['desc', { natural: true }], + errors: [ + "Expected object keys to be in natural descending order. '_' should be before '$'.", + "Expected object keys to be in natural descending order. 'A' should be before '_'.", + "Expected object keys to be in natural descending order. 'a' should be before 'A'." + ] + }, + { + code: "var obj = {1:1, 2:4, A:3, '11':2}", + options: ['desc', { natural: true }], + errors: [ + "Expected object keys to be in natural descending order. '2' should be before '1'.", + "Expected object keys to be in natural descending order. 'A' should be before '2'." + ] + }, + { + code: "var obj = {'#':1, À:3, 'Z':2, è:4}", + options: ['desc', { natural: true }], + errors: [ + "Expected object keys to be in natural descending order. 'À' should be before '#'.", + "Expected object keys to be in natural descending order. 'è' should be before 'Z'." + ] + }, + + // desc, natural should error when number of keys is greater than or equal to minKeys + { + code: 'var obj = {a:1, _:2, b:3}', + options: ['desc', { natural: true, minKeys: 3 }], + errors: [ + "Expected object keys to be in natural descending order. 'b' should be before '_'." + ] + }, + + // desc, natural, insensitive + { + code: 'var obj = {a:1, _:2, b:3} // desc, natural, insensitive', + options: ['desc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive descending order. 'b' should be before '_'." + ] + }, + { + code: 'var obj = {a:1, c:2, b:3}', + options: ['desc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive descending order. 'c' should be before 'a'." + ] + }, + { + code: 'var obj = {b_:1, a:2, b:3}', + options: ['desc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive descending order. 'b' should be before 'a'." + ] + }, + { + code: 'var obj = {b_:1, c:2, C:3}', + options: ['desc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive descending order. 'c' should be before 'b_'." + ] + }, + { + code: 'var obj = {$:1, _:2, A:3, a:4}', + options: ['desc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive descending order. '_' should be before '$'.", + "Expected object keys to be in natural insensitive descending order. 'A' should be before '_'." + ] + }, + { + code: "var obj = {1:1, 2:4, '11':2, A:3}", + options: ['desc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive descending order. '2' should be before '1'.", + "Expected object keys to be in natural insensitive descending order. '11' should be before '2'.", + "Expected object keys to be in natural insensitive descending order. 'A' should be before '11'." + ] + }, + { + code: "var obj = {'#':1, À:3, 'Z':2, è:4}", + options: ['desc', { natural: true, caseSensitive: false }], + errors: [ + "Expected object keys to be in natural insensitive descending order. 'À' should be before '#'.", + "Expected object keys to be in natural insensitive descending order. 'è' should be before 'Z'." + ] + }, + + // desc, natural, insensitive should error when number of keys is greater than or equal to minKeys + { + code: 'var obj = {a:1, _:2, b:3}', + options: ['desc', { natural: true, caseSensitive: false, minKeys: 2 }], + errors: [ + "Expected object keys to be in natural insensitive descending order. 'b' should be before '_'." + ] + }, + { + filename: 'test.vue', + code: ` + export default { + name: 'app', + props: { + z: Number, + propA: Number, + }, + ...a, + data () { + return { + zd: 1, + msg: 'Welcome to Your Vue.js App' + } + }, + } + `, + parserOptions, + + errors: [{ + message: 'Expected object keys to be in ascending order. \'propA\' should be before \'z\'.', + line: 6 + }, { + message: 'Expected object keys to be in ascending order. \'msg\' should be before \'zd\'.', + line: 12 + }] + }, + { + filename: 'example.vue', + code: ` + export default { + data() { + return { + isActive: false, + }; + }, + methods: { + toggleMenu() { + this.isActive = !this.isActive; + }, + closeMenu() { + this.isActive = false; + } + }, + name: 'burger', + }; + `, + parserOptions, + errors: [{ + message: 'Expected object keys to be in ascending order. \'closeMenu\' should be before \'toggleMenu\'.', + line: 12 + }] + }, + { + filename: 'example.vue', + code: ` + export default { + methods: { + toggleMenu() { + return { + // These should have errors since they are not part of the vue component + model: { + prop: 'checked', + event: 'change' + }, + props: { + propA: { + z: 1, + a: 2 + }, + }, + }; + } + }, + name: 'burger', + }; + `, + parserOptions, + errors: [{ + message: 'Expected object keys to be in ascending order. \'event\' should be before \'prop\'.', + line: 9 + }, { + message: 'Expected object keys to be in ascending order. \'a\' should be before \'z\'.', + line: 14 + }] + }, + { + filename: 'example.vue', + code: ` + const dict = { zd: 1, a: 2 }; + + export default { + data() { + return { + z: 2, + isActive: false, + }; + }, + name: 'burger', + }; + `, + parserOptions, + errors: [{ + message: 'Expected object keys to be in ascending order. \'a\' should be before \'zd\'.', + line: 2 + }, { + message: 'Expected object keys to be in ascending order. \'isActive\' should be before \'z\'.', + line: 8 + }] + }, + { + filename: 'example.vue', + code: ` + export default { + data() { + return { + z: 2, + isActive: false, + }; + }, + name: 'burger', + }; + + const dict = { zd: 1, a: 2 }; + `, + parserOptions, + errors: [{ + message: 'Expected object keys to be in ascending order. \'isActive\' should be before \'z\'.', + line: 6 + }, { + message: 'Expected object keys to be in ascending order. \'a\' should be before \'zd\'.', + line: 12 + }] + }, + { + filename: 'test.js', + code: ` + const dict = { zd: 1, a: 2 }; + new Vue({ + name: 'app', + el: '#app', + data () { + return { + z: 2, + msg: 'Welcome to Your Vue.js App' + } + }, + components: {}, + template: '
' + }) + `, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + message: 'Expected object keys to be in ascending order. \'a\' should be before \'zd\'.', + line: 2 + }, + { + message: 'Expected object keys to be in ascending order. \'msg\' should be before \'z\'.', + line: 9 + }] + }, + { + filename: 'test.js', + code: ` + const dict = { zd: 1, a: 2 }; + + Vue.component('smart-list', { + name: 'app', + data () { + return { + z: 2, + msg: 'Welcome to Your Vue.js App' + } + }, + components: {}, + template: '
' + }) + `, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + message: 'Expected object keys to be in ascending order. \'a\' should be before \'zd\'.', + line: 2 + }, + { + message: 'Expected object keys to be in ascending order. \'msg\' should be before \'z\'.', + line: 9 + }] + }, + { + filename: 'test.js', + code: ` + const dict = { zd: 1, a: 2 }; + const { component } = Vue; + component('smart-list', { + name: 'app', + data () { + return { + z: 2, + msg: 'Welcome to Your Vue.js App' + } + }, + components: {}, + template: '
' + }) + `, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + message: 'Expected object keys to be in ascending order. \'a\' should be before \'zd\'.', + line: 2 + }, + { + message: 'Expected object keys to be in ascending order. \'msg\' should be before \'z\'.', + line: 9 + }] + }, + { + filename: 'propsOrder.vue', + code: ` + export default { + name: 'app', + props: { + propA: { + type: String, + default: 'abc', + }, + }, + } + `, + options: ['asc', { ignoreGrandchildrenOf: [] }], + parserOptions, + errors: [{ + message: 'Expected object keys to be in ascending order. \'default\' should be before \'type\'.', + line: 7 + }] + }, + { + filename: 'propsOrder.vue', + code: ` + const obj = { + z: 1, + foo() { + Vue.component('my-component', { + name: 'app', + data: {} + }) + }, + a: 2 + } + `, + parserOptions, + errors: [{ + message: 'Expected object keys to be in ascending order. \'foo\' should be before \'z\'.', + line: 4 + }, { + message: 'Expected object keys to be in ascending order. \'a\' should be before \'foo\'.', + line: 10 + }] + }, + { + filename: 'propsOrder.vue', + code: ` + export default { + computed: { + foo () { + return { + b, + a + } + } + } + } + `, + parserOptions, + errors: [{ + message: 'Expected object keys to be in ascending order. \'a\' should be before \'b\'.', + line: 7 + }] + } + ] +}) From b394ca6457250956071231b7af43525762bd26f5 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sun, 16 Feb 2020 18:45:43 +0900 Subject: [PATCH 0002/1010] New: Add `vue/padding-line-between-blocks` rule (#1021) --- docs/rules/README.md | 1 + docs/rules/padding-line-between-blocks.md | 139 ++++++++ lib/configs/no-layout-rules.js | 1 + lib/index.js | 1 + lib/rules/component-tags-order.js | 2 +- lib/rules/padding-line-between-blocks.js | 194 +++++++++++ .../lib/rules/padding-line-between-blocks.js | 314 ++++++++++++++++++ 7 files changed, 651 insertions(+), 1 deletion(-) create mode 100644 docs/rules/padding-line-between-blocks.md create mode 100644 lib/rules/padding-line-between-blocks.js create mode 100644 tests/lib/rules/padding-line-between-blocks.js diff --git a/docs/rules/README.md b/docs/rules/README.md index ef6387ffc..1e6d30bdc 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -165,6 +165,7 @@ For example: | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | | | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: | | [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/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | | | [vue/script-indent](./script-indent.md) | enforce consistent indentation in ` + + +``` + + + + + +```vue + + + + +``` + + + +## :wrench: Options + +```json +{ + "vue/padding-line-between-blocks": ["error", "always" | "never"] +} +``` + +- `"always"` (default) ... Requires one or more blank lines. Note it does not count lines that comments exist as blank lines. +- `"never"` ... Disallows blank lines. + +### `"always"` (default) + + + +```vue + + + + + + +``` + + + + + +```vue + + + + +``` + + + +### `"never"` + + + +```vue + + + + +``` + + + + + +```vue + + + + + + +``` + + + +## :books: Further reading + +- [padding-line-between-statements] +- [lines-between-class-members] + +[padding-line-between-statements]: https://eslint.org/docs/rules/padding-line-between-statements +[lines-between-class-members]: https://eslint.org/docs/rules/lines-between-class-members + + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/padding-line-between-blocks.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/padding-line-between-blocks.js) diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js index 3bf2fec4e..e45c080bc 100644 --- a/lib/configs/no-layout-rules.js +++ b/lib/configs/no-layout-rules.js @@ -25,6 +25,7 @@ module.exports = { 'vue/no-multi-spaces': 'off', 'vue/no-spaces-around-equal-signs-in-attribute': 'off', 'vue/object-curly-spacing': 'off', + 'vue/padding-line-between-blocks': 'off', 'vue/script-indent': 'off', 'vue/singleline-html-element-content-newline': 'off', 'vue/space-infix-ops': 'off', diff --git a/lib/index.js b/lib/index.js index efd41aaee..02e65ba96 100644 --- a/lib/index.js +++ b/lib/index.js @@ -65,6 +65,7 @@ module.exports = { 'no-v-html': require('./rules/no-v-html'), 'object-curly-spacing': require('./rules/object-curly-spacing'), 'order-in-components': require('./rules/order-in-components'), + 'padding-line-between-blocks': require('./rules/padding-line-between-blocks'), 'prop-name-casing': require('./rules/prop-name-casing'), 'require-component-is': require('./rules/require-component-is'), 'require-default-prop': require('./rules/require-default-prop'), diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js index d6400dec0..7129aa36e 100644 --- a/lib/rules/component-tags-order.js +++ b/lib/rules/component-tags-order.js @@ -45,7 +45,7 @@ module.exports = { function getTopLevelHTMLElements () { if (documentFragment) { - return documentFragment.children + return documentFragment.children.filter(e => e.type === 'VElement') } return [] } diff --git a/lib/rules/padding-line-between-blocks.js b/lib/rules/padding-line-between-blocks.js new file mode 100644 index 000000000..6f08a5d7a --- /dev/null +++ b/lib/rules/padding-line-between-blocks.js @@ -0,0 +1,194 @@ +/** + * @fileoverview Require or disallow padding lines between blocks + * @author Yosuke Ota + */ +'use strict' +const utils = require('../utils') + +/** + * Split the source code into multiple lines based on the line delimiters. + * @param {string} text Source code as a string. + * @returns {string[]} Array of source code lines. + */ +function splitLines (text) { + return text.split(/\r\n|[\r\n\u2028\u2029]/gu) +} + +/** + * Check and report blocks for `never` configuration. + * This autofix removes blank lines between the given 2 blocks. + * @param {RuleContext} context The rule context to report. + * @param {VElement} prevBlock The previous block to check. + * @param {VElement} nextBlock The next block to check. + * @param {Token[]} betweenTokens The array of tokens between blocks. + * @returns {void} + * @private + */ +function verifyForNever (context, prevBlock, nextBlock, betweenTokens) { + if (prevBlock.loc.end.line === nextBlock.loc.start.line) { + // same line + return + } + const tokenOrNodes = [...betweenTokens, nextBlock] + let prev = prevBlock + const paddingLines = [] + for (const tokenOrNode of tokenOrNodes) { + const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line + if (numOfLineBreaks > 1) { + paddingLines.push([prev, tokenOrNode]) + } + prev = tokenOrNode + } + if (!paddingLines.length) { + return + } + + context.report({ + node: nextBlock, + messageId: 'never', + fix (fixer) { + return paddingLines.map(([prevToken, nextToken]) => { + const start = prevToken.range[1] + const end = nextToken.range[0] + const paddingText = context.getSourceCode().text + .slice(start, end) + const lastSpaces = splitLines(paddingText).pop() + return fixer.replaceTextRange([start, end], '\n' + lastSpaces) + }) + } + }) +} + +/** + * Check and report blocks for `always` configuration. + * This autofix inserts a blank line between the given 2 blocks. + * @param {RuleContext} context The rule context to report. + * @param {VElement} prevBlock The previous block to check. + * @param {VElement} nextBlock The next block to check. + * @param {Token[]} betweenTokens The array of tokens between blocks. + * @returns {void} + * @private + */ +function verifyForAlways (context, prevBlock, nextBlock, betweenTokens) { + const tokenOrNodes = [...betweenTokens, nextBlock] + let prev = prevBlock + let linebreak + for (const tokenOrNode of tokenOrNodes) { + const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line + if (numOfLineBreaks > 1) { + // Already padded. + return + } + if (!linebreak && numOfLineBreaks > 0) { + linebreak = prev + } + prev = tokenOrNode + } + + context.report({ + node: nextBlock, + messageId: 'always', + fix (fixer) { + if (linebreak) { + return fixer.insertTextAfter(linebreak, '\n') + } + return fixer.insertTextAfter(prevBlock, '\n\n') + } + }) +} + +/** + * Types of blank lines. + * `never` and `always` are defined. + * Those have `verify` method to check and report statements. + * @private + */ +const PaddingTypes = { + never: { verify: verifyForNever }, + always: { verify: verifyForAlways } +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'layout', + docs: { + description: 'require or disallow padding lines between blocks', + category: undefined, + url: 'https://eslint.vuejs.org/rules/padding-line-between-blocks.html' + }, + fixable: 'whitespace', + schema: [ + { + enum: Object.keys(PaddingTypes) + } + ], + messages: { + never: 'Unexpected blank line before this block.', + always: 'Expected blank line before this block.' + } + }, + create (context) { + const paddingType = PaddingTypes[context.options[0] || 'always'] + const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment() + + let tokens + function getTopLevelHTMLElements () { + if (documentFragment) { + return documentFragment.children.filter(e => e.type === 'VElement') + } + return [] + } + + function getTokenAndCommentsBetween (prev, next) { + // When there is no ` + }, + { + filename: 'test.vue', + code: + ``, + options: [{ alphabetical: true }] } ], @@ -318,23 +337,23 @@ tester.run('attributes-order', rule, { { filename: 'test.vue', code: - ``, + ``, output: - ``, + ``, errors: [{ message: 'Attribute "v-model" should go before "model".', type: 'VDirectiveKey' @@ -347,25 +366,25 @@ tester.run('attributes-order', rule, { { filename: 'test.vue', code: - ``, + ``, output: - ``, + ``, errors: [{ message: 'Attribute "v-model" should go before "v-on".', type: 'VDirectiveKey' @@ -389,17 +408,17 @@ tester.run('attributes-order', rule, { code: '', options: [ { order: - ['LIST_RENDERING', - 'CONDITIONALS', - 'RENDER_MODIFIERS', - 'GLOBAL', - 'UNIQUE', - 'TWO_WAY_BINDING', - 'DEFINITION', - 'OTHER_DIRECTIVES', - 'OTHER_ATTR', - 'EVENTS', - 'CONTENT'] + ['LIST_RENDERING', + 'CONDITIONALS', + 'RENDER_MODIFIERS', + 'GLOBAL', + 'UNIQUE', + 'TWO_WAY_BINDING', + 'DEFINITION', + 'OTHER_DIRECTIVES', + 'OTHER_ATTR', + 'EVENTS', + 'CONTENT'] }], output: '', errors: [{ @@ -410,17 +429,17 @@ tester.run('attributes-order', rule, { { filename: 'test.vue', code: - ``, + ``, output: - ``, + ``, errors: [{ message: 'Attribute "is" should go before "v-cloak".', type: 'VIdentifier' @@ -429,37 +448,37 @@ tester.run('attributes-order', rule, { { filename: 'test.vue', code: - ``, + ``, output: - ``, + ``, errors: [ { message: 'Attribute "v-for" should go before "v-if".', @@ -490,53 +509,53 @@ tester.run('attributes-order', rule, { { filename: 'test.vue', code: - ``, + ``, options: [ { order: - [ - 'EVENTS', - 'TWO_WAY_BINDING', - 'UNIQUE', - 'DEFINITION', - 'CONDITIONALS', - 'LIST_RENDERING', - 'RENDER_MODIFIERS', - 'GLOBAL', - 'OTHER_ATTR', - 'OTHER_DIRECTIVES', - 'CONTENT' - ] + [ + 'EVENTS', + 'TWO_WAY_BINDING', + 'UNIQUE', + 'DEFINITION', + 'CONDITIONALS', + 'LIST_RENDERING', + 'RENDER_MODIFIERS', + 'GLOBAL', + 'OTHER_ATTR', + 'OTHER_DIRECTIVES', + 'CONTENT' + ] }], output: - ``, + ``, errors: [ { message: 'Attribute "is" should go before "v-once".', @@ -562,39 +581,39 @@ tester.run('attributes-order', rule, { }, { code: - ``, + ``, options: [ { order: - [ - 'CONDITIONALS', - 'LIST_RENDERING', - 'RENDER_MODIFIERS', - 'DEFINITION', - 'EVENTS', - 'UNIQUE', - ['BINDING', 'OTHER_ATTR'], - 'CONTENT', - 'GLOBAL' - ] + [ + 'CONDITIONALS', + 'LIST_RENDERING', + 'RENDER_MODIFIERS', + 'DEFINITION', + 'EVENTS', + 'UNIQUE', + ['BINDING', 'OTHER_ATTR'], + 'CONTENT', + 'GLOBAL' + ] }], output: - ``, + ``, errors: [ { message: 'Attribute "v-if" should go before "class".', @@ -604,29 +623,161 @@ tester.run('attributes-order', rule, { }, { code: - ``, + ``, output: - ``, + ``, errors: [ { message: 'Attribute "v-slot" should go before "v-model".', nodeType: 'VIdentifier' } ] + }, + { + filename: 'test.vue', + code: + ``, + options: [{ alphabetical: true }], + output: + ``, + errors: [{ + message: 'Attribute "a-prop" should go before "z-prop".', + type: 'VIdentifier' + }] + }, + { + filename: 'test.vue', + code: + ``, + options: [{ alphabetical: true }], + output: + ``, + errors: [{ + message: 'Attribute ":a-prop" should go before ":z-prop".', + type: 'VDirectiveKey' + }] + }, + { + filename: 'test.vue', + code: + ``, + options: [{ alphabetical: true }], + output: + ``, + errors: [{ + message: 'Attribute "@change" should go before "@input".', + type: 'VDirectiveKey' + }] + }, + { + filename: 'test.vue', + code: + ``, + options: [{ alphabetical: true }], + output: + ``, + errors: [{ + message: 'Attribute "boolean-prop" should go before "z-prop".', + type: 'VIdentifier' + }] + }, + { + filename: 'test.vue', + code: + ``, + options: [{ alphabetical: true }], + output: + ``, + errors: [{ + message: 'Attribute "v-on:[c]" should go before "v-on:click".', + type: 'VDirectiveKey' + }] + }, + { + filename: 'test.vue', + code: + ``, + options: [{ alphabetical: true }], + output: + ``, + errors: [{ + message: 'Attribute "v-on:click" should go before "v-text".', + type: 'VDirectiveKey' + }] } ] }) From a4e3f0fd45cf2c1e5562cdeeb35653d310bf10db Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sun, 16 Feb 2020 18:50:02 +0900 Subject: [PATCH 0004/1010] Fixed false positives in `no-side-effects-in-computed-properties` (#1027) --- .../no-side-effects-in-computed-properties.js | 32 ++++++++++++++++--- lib/utils/index.js | 2 +- .../no-side-effects-in-computed-properties.js | 30 +++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js index 3abe009bf..9bda00681 100644 --- a/lib/rules/no-side-effects-in-computed-properties.js +++ b/lib/rules/no-side-effects-in-computed-properties.js @@ -24,20 +24,38 @@ module.exports = { create (context) { const forbiddenNodes = [] + let scopeStack = { upper: null, body: null } + + function onFunctionEnter (node) { + scopeStack = { upper: scopeStack, body: node.body } + } + + function onFunctionExit () { + scopeStack = scopeStack.upper + } return Object.assign({}, { + ':function': onFunctionEnter, + ':function:exit': onFunctionExit, + // this.xxx <=|+=|-=> 'AssignmentExpression' (node) { if (node.left.type !== 'MemberExpression') return if (utils.parseMemberExpression(node.left)[0] === 'this') { - forbiddenNodes.push(node) + forbiddenNodes.push({ + node, + targetBody: scopeStack.body + }) } }, // this.xxx <++|--> 'UpdateExpression > MemberExpression' (node) { if (utils.parseMemberExpression(node)[0] === 'this') { - forbiddenNodes.push(node) + forbiddenNodes.push({ + node, + targetBody: scopeStack.body + }) } }, // this.xxx.func() @@ -46,7 +64,10 @@ module.exports = { const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g if (MUTATION_REGEX.test(code)) { - forbiddenNodes.push(node) + forbiddenNodes.push({ + node, + targetBody: scopeStack.body + }) } } }, @@ -54,11 +75,12 @@ module.exports = { const computedProperties = utils.getComputedProperties(obj) computedProperties.forEach(cp => { - forbiddenNodes.forEach(node => { + forbiddenNodes.forEach(({ node, targetBody }) => { if ( cp.value && node.loc.start.line >= cp.value.loc.start.line && - node.loc.end.line <= cp.value.loc.end.line + node.loc.end.line <= cp.value.loc.end.line && + targetBody === cp.value ) { context.report({ node: node, diff --git a/lib/utils/index.js b/lib/utils/index.js index 184ebbe3f..b24420a3a 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -822,7 +822,7 @@ module.exports = { isFunc = true } else { if (n.computed) { - parsedCallee.push('[]') + parsedCallee.push('[]' + (isFunc ? '()' : '')) } else if (n.property.type === 'Identifier') { parsedCallee.push(n.property.name + (isFunc ? '()' : '')) } diff --git a/tests/lib/rules/no-side-effects-in-computed-properties.js b/tests/lib/rules/no-side-effects-in-computed-properties.js index 3dc0f3fc8..df186b3a9 100644 --- a/tests/lib/rules/no-side-effects-in-computed-properties.js +++ b/tests/lib/rules/no-side-effects-in-computed-properties.js @@ -143,6 +143,36 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, { } })`, parserOptions + }, + { + code: `Vue.component('test', { + computed: { + test () { + return { + action1() { + this.something++ + }, + action2() { + this.something = 1 + }, + action3() { + this.something.reverse() + } + } + }, + } + })`, + parserOptions + }, + { + code: `Vue.component('test', { + computed: { + test () { + return this.something['a']().reverse() + }, + } + })`, + parserOptions } ], invalid: [ From 5980cdc2885df37c460619f5ba4b617e12255049 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sun, 16 Feb 2020 18:50:25 +0900 Subject: [PATCH 0005/1010] Fixed an error when using spread elements in `vue/require-default-prop`. (#1046) * Fixed an error when using spread elements in `vue/require-default-prop`. * Revert vscode/settings --- lib/rules/require-default-prop.js | 43 ++++++++++++-------- lib/utils/index.js | 26 ++++++++++-- tests/lib/rules/require-default-prop.js | 54 +++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 20 deletions(-) diff --git a/lib/rules/require-default-prop.js b/lib/rules/require-default-prop.js index 91dd90840..3f6f18613 100644 --- a/lib/rules/require-default-prop.js +++ b/lib/rules/require-default-prop.js @@ -4,6 +4,16 @@ */ 'use strict' +/** + * @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression + * @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression + * @typedef {import('vue-eslint-parser').AST.ESLintPattern} Pattern + */ +/** + * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp + * @typedef {ComponentObjectProp & { value: ObjectExpression} } ComponentObjectPropObject + */ + const utils = require('../utils') const NATIVE_TYPES = new Set([ @@ -39,14 +49,14 @@ module.exports = { /** * Checks if the passed prop is required - * @param {Property} prop - Property AST node for a single prop + * @param {ComponentObjectPropObject} prop - Property AST node for a single prop * @return {boolean} */ function propIsRequired (prop) { const propRequiredNode = prop.value.properties .find(p => p.type === 'Property' && - p.key.name === 'required' && + utils.getStaticPropertyName(p) === 'required' && p.value.type === 'Literal' && p.value.value === true ) @@ -56,14 +66,13 @@ module.exports = { /** * Checks if the passed prop has a default value - * @param {Property} prop - Property AST node for a single prop + * @param {ComponentObjectPropObject} prop - Property AST node for a single prop * @return {boolean} */ function propHasDefault (prop) { const propDefaultNode = prop.value.properties .find(p => - p.key && - (p.key.name === 'default' || p.key.value === 'default') + p.type === 'Property' && utils.getStaticPropertyName(p) === 'default' ) return Boolean(propDefaultNode) @@ -71,23 +80,24 @@ module.exports = { /** * Finds all props that don't have a default value set - * @param {Array} props - Vue component's "props" node - * @return {Array} Array of props without "default" value + * @param {ComponentObjectProp[]} props - Vue component's "props" node + * @return {ComponentObjectProp[]} Array of props without "default" value */ function findPropsWithoutDefaultValue (props) { return props .filter(prop => { if (prop.value.type !== 'ObjectExpression') { - return (prop.value.type !== 'CallExpression' && prop.value.type !== 'Identifier') || NATIVE_TYPES.has(prop.value.name) + return (prop.value.type !== 'CallExpression' && prop.value.type !== 'Identifier') || + (prop.value.type === 'Identifier' && NATIVE_TYPES.has(prop.value.name)) } - return !propIsRequired(prop) && !propHasDefault(prop) + return !propIsRequired(/** @type {ComponentObjectPropObject} */(prop)) && !propHasDefault(/** @type {ComponentObjectPropObject} */(prop)) }) } /** * Detects whether given value node is a Boolean type - * @param {Node} value + * @param {Expression | Pattern} value * @return {Boolean} */ function isValueNodeOfBooleanType (value) { @@ -104,7 +114,7 @@ module.exports = { /** * Detects whether given prop node is a Boolean - * @param {Node} prop + * @param {ComponentObjectProp} prop * @return {Boolean} */ function isBooleanProp (prop) { @@ -112,7 +122,8 @@ module.exports = { return isValueNodeOfBooleanType(value) || ( value.type === 'ObjectExpression' && - value.properties.find(p => + value.properties.some(p => + p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'type' && isValueNodeOfBooleanType(p.value) @@ -122,8 +133,8 @@ module.exports = { /** * Excludes purely Boolean props from the Array - * @param {Array} props - Array with props - * @return {Array} + * @param {ComponentObjectProp[]} props - Array with props + * @return {ComponentObjectProp[]} */ function excludeBooleanProps (props) { return props.filter(prop => !isBooleanProp(prop)) @@ -135,9 +146,9 @@ module.exports = { return utils.executeOnVue(context, (obj) => { const props = utils.getComponentProps(obj) - .filter(prop => prop.key && prop.value && !prop.node.shorthand) + .filter(prop => prop.key && prop.value && !(prop.node.type === 'Property' && prop.node.shorthand)) - const propsWithoutDefault = findPropsWithoutDefaultValue(props) + const propsWithoutDefault = findPropsWithoutDefaultValue(/** @type {ComponentObjectProp[]} */(props)) const propsToReport = excludeBooleanProps(propsWithoutDefault) for (const prop of propsToReport) { diff --git a/lib/utils/index.js b/lib/utils/index.js index b24420a3a..011ec6571 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -5,6 +5,23 @@ */ 'use strict' +/** + * @typedef {import('vue-eslint-parser').AST.ESLintArrayExpression} ArrayExpression + * @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression + * @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier + * @typedef {import('vue-eslint-parser').AST.ESLintLiteral} Literal + * @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression + * @typedef {import('vue-eslint-parser').AST.ESLintMethodDefinition} MethodDefinition + * @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression + * @typedef {import('vue-eslint-parser').AST.ESLintProperty} Property + * @typedef {import('vue-eslint-parser').AST.ESLintTemplateLiteral} TemplateLiteral + */ + +/** + * @typedef { {key: Literal | null, value: null, node: ArrayExpression['elements'][0], propName: string} } ComponentArrayProp + * @typedef { {key: Property['key'], value: Property['value'], node: Property, propName: string} } ComponentObjectProp + */ + // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ @@ -382,7 +399,7 @@ module.exports = { /** * Gets the property name of a given node. - * @param {ASTNode} node - The node to get. + * @param {Property|MethodDefinition|MemberExpression|Literal|TemplateLiteral|Identifier} node - The node to get. * @return {string|null} The property name if static. Otherwise, null. */ getStaticPropertyName (node) { @@ -425,7 +442,7 @@ module.exports = { /** * Get all props by looking at all component's properties * @param {ObjectExpression} componentObject Object with component definition - * @return {Array} Array of component props in format: [{key?: String, value?: ASTNode, node: ASTNod}] + * @return {(ComponentArrayProp | ComponentObjectProp)[]} Array of component props in format: [{key?: String, value?: ASTNode, node: ASTNod}] */ getComponentProps (componentObject) { const propsNode = componentObject.properties @@ -844,8 +861,9 @@ module.exports = { /** * Unwrap typescript types like "X as F" - * @param {ASTNode} node - * @return {ASTNode} + * @template T + * @param {T} node + * @return {T} */ unwrapTypes (node) { return node.type === 'TSAsExpression' ? node.expression : node diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js index 62ee53342..b544921bd 100644 --- a/tests/lib/rules/require-default-prop.js +++ b/tests/lib/rules/require-default-prop.js @@ -153,6 +153,33 @@ ruleTester.run('require-default-prop', rule, { } } ` + }, + { + // https://github.com/vuejs/eslint-plugin-vue/issues/1040 + filename: 'destructuring-test.vue', + code: ` + export default { + props: { + foo: { + ...foo, + default: 0 + }, + } + } + ` + }, + { + filename: 'unknown-prop-details-test.vue', + code: ` + export default { + props: { + foo: { + [bar]: true, + default: 0 + }, + } + } + ` } ], @@ -282,6 +309,33 @@ ruleTester.run('require-default-prop', rule, { message: `Prop '[baz.baz]' requires default value to be set.`, line: 6 }] + }, + { + // https://github.com/vuejs/eslint-plugin-vue/issues/1040 + filename: 'destructuring-test.vue', + code: ` + export default { + props: { + foo: { + ...foo + }, + } + } + `, + errors: ['Prop \'foo\' requires default value to be set.'] + }, + { + filename: 'unknown-prop-details-test.vue', + code: ` + export default { + props: { + foo: { + [bar]: true + }, + } + } + `, + errors: ['Prop \'foo\' requires default value to be set.'] } ] }) From ca2c962d85b3039e0db1062bcc4ec0290cb476f4 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sun, 16 Feb 2020 18:51:44 +0900 Subject: [PATCH 0006/1010] Add `avoidEscape` option to `vue/html-quotes` rule (#1031) --- docs/rules/html-quotes.md | 25 +++++++++++++++- lib/rules/html-quotes.js | 39 ++++++++++++++++++++---- tests/lib/rules/html-quotes.js | 54 ++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 6 deletions(-) diff --git a/docs/rules/html-quotes.md b/docs/rules/html-quotes.md index aa0180de9..2d95009fe 100644 --- a/docs/rules/html-quotes.md +++ b/docs/rules/html-quotes.md @@ -43,13 +43,19 @@ Default is set to `double`. ```json { - "vue/html-quotes": ["error", "double" | "single"] + "vue/html-quotes": [ "error", "double" | "single", { "avoidEscape": false } ] } ``` +String option: + - `"double"` (default) ... requires double quotes. - `"single"` ... requires single quotes. +Object option: + +- `avoidEscape` ... If `true`, allows strings to use single-quotes or double-quotes so long as the string contains a quote that would have to be escaped otherwise. + ### `"single"` @@ -67,6 +73,23 @@ Default is set to `double`. +### `"double", { "avoidEscape": true }` + + + +```vue + +``` + + + ## :books: Further reading - [Style guide - Quoted attribute values](https://vuejs.org/v2/style-guide/#Quoted-attribute-values-strongly-recommended) diff --git a/lib/rules/html-quotes.js b/lib/rules/html-quotes.js index c8392abef..b27b9d98d 100644 --- a/lib/rules/html-quotes.js +++ b/lib/rules/html-quotes.js @@ -25,17 +25,25 @@ module.exports = { }, fixable: 'code', schema: [ - { enum: ['double', 'single'] } + { enum: ['double', 'single'] }, + { + type: 'object', + properties: { + avoidEscape: { + type: 'boolean' + } + }, + additionalProperties: false + } ] }, create (context) { const sourceCode = context.getSourceCode() const double = context.options[0] !== 'single' + const avoidEscape = context.options[1] && context.options[1].avoidEscape === true const quoteChar = double ? '"' : "'" const quoteName = double ? 'double quotes' : 'single quotes' - const quotePattern = double ? /"/g : /'/g - const quoteEscaped = double ? '"' : ''' let hasInvalidEOF return utils.defineTemplateBodyVisitor(context, { @@ -48,14 +56,35 @@ module.exports = { const firstChar = text[0] if (firstChar !== quoteChar) { + const quoted = (firstChar === "'" || firstChar === '"') + if (avoidEscape && quoted) { + const contentText = text.slice(1, -1) + if (contentText.includes(quoteChar)) { + return + } + } + context.report({ node: node.value, loc: node.value.loc, message: 'Expected to be enclosed by {{kind}}.', data: { kind: quoteName }, fix (fixer) { - const contentText = (firstChar === "'" || firstChar === '"') ? text.slice(1, -1) : text - const replacement = quoteChar + contentText.replace(quotePattern, quoteEscaped) + quoteChar + const contentText = quoted ? text.slice(1, -1) : text + + const fixToDouble = avoidEscape && !quoted && contentText.includes(quoteChar) + ? ( + double + ? contentText.includes("'") + : !contentText.includes('"') + ) + : double + + const quotePattern = fixToDouble ? /"/g : /'/g + const quoteEscaped = fixToDouble ? '"' : ''' + const fixQuoteChar = fixToDouble ? '"' : "'" + + const replacement = fixQuoteChar + contentText.replace(quotePattern, quoteEscaped) + fixQuoteChar return fixer.replaceText(node.value, replacement) } }) diff --git a/tests/lib/rules/html-quotes.js b/tests/lib/rules/html-quotes.js index f54cc1d50..95338825a 100644 --- a/tests/lib/rules/html-quotes.js +++ b/tests/lib/rules/html-quotes.js @@ -55,6 +55,17 @@ tester.run('html-quotes', rule, { code: "", options: ['single'] }, + // avoidEscape + { + filename: 'test.vue', + code: "", + options: ['double', { avoidEscape: true }] + }, + { + filename: 'test.vue', + code: "", + options: ['single', { avoidEscape: true }] + }, // Invalid EOF { @@ -166,6 +177,49 @@ tester.run('html-quotes', rule, { output: "", options: ['single'], errors: ['Expected to be enclosed by single quotes.'] + }, + // avoidEscape + { + filename: 'test.vue', + code: "", + output: '', + options: ['double', { avoidEscape: true }], + errors: ['Expected to be enclosed by double quotes.'] + }, + { + filename: 'test.vue', + code: '', + output: "", + options: ['single', { avoidEscape: true }], + errors: ['Expected to be enclosed by single quotes.'] + }, + { + filename: 'test.vue', + code: '', + output: '', + options: ['double', { avoidEscape: true }], + errors: ['Expected to be enclosed by double quotes.'] + }, + { + filename: 'test.vue', + code: '', + output: "", + options: ['single', { avoidEscape: true }], + errors: ['Expected to be enclosed by single quotes.'] + }, + { + filename: 'test.vue', + code: '', + output: '', + options: ['double', { avoidEscape: true }], + errors: ['Expected to be enclosed by double quotes.'] + }, + { + filename: 'test.vue', + code: '', + output: '', + options: ['single', { avoidEscape: true }], + errors: ['Expected to be enclosed by single quotes.'] } ] }) From 667bb2e3cfa30042895aae987f4f127bc8829edc Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sun, 16 Feb 2020 19:04:32 +0900 Subject: [PATCH 0007/1010] version 6.2.0 --- docs/rules/README.md | 2 +- docs/rules/sort-keys.md | 12 ++++++------ package.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index 1e6d30bdc..ab1f5b220 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -169,7 +169,7 @@ For example: | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | | | [vue/script-indent](./script-indent.md) | enforce consistent indentation in ` +``` + + + +## :wrench: Options + +Nothing. + +## :books: Further reading + +- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-ref-as-operand.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-ref-as-operand.js) diff --git a/lib/index.js b/lib/index.js index a8ebcd318..4d3357cbb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -52,6 +52,7 @@ module.exports = { 'no-multi-spaces': require('./rules/no-multi-spaces'), 'no-multiple-template-root': require('./rules/no-multiple-template-root'), 'no-parsing-error': require('./rules/no-parsing-error'), + 'no-ref-as-operand': require('./rules/no-ref-as-operand'), 'no-reserved-component-names': require('./rules/no-reserved-component-names'), 'no-reserved-keys': require('./rules/no-reserved-keys'), 'no-restricted-syntax': require('./rules/no-restricted-syntax'), diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js new file mode 100644 index 000000000..ea57d9da4 --- /dev/null +++ b/lib/rules/no-ref-as-operand.js @@ -0,0 +1,124 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' +const { ReferenceTracker, findVariable } = require('eslint-utils') + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow use of value wrapped by `ref()` (Composition API) as an operand', + category: undefined, + url: 'https://eslint.vuejs.org/rules/no-ref-as-operand.html' + }, + fixable: null, + schema: [], + messages: { + requireDotValue: 'Must use `.value` to read or write the value wrapped by `ref()`.' + } + }, + create (context) { + const refReferenceIds = new Map() + + function reportIfRefWrapped (node) { + if (!refReferenceIds.has(node)) { + return + } + context.report({ + node, + messageId: 'requireDotValue' + }) + } + return { + 'Program' () { + const tracker = new ReferenceTracker(context.getScope()) + const traceMap = { + vue: { + [ReferenceTracker.ESM]: true, + ref: { + [ReferenceTracker.CALL]: true + } + } + } + + for (const { node } of tracker.iterateEsmReferences(traceMap)) { + const variableDeclarator = node.parent + if ( + !variableDeclarator || + variableDeclarator.type !== 'VariableDeclarator' || + variableDeclarator.id.type !== 'Identifier' + ) { + continue + } + const variable = findVariable(context.getScope(), variableDeclarator.id) + if (!variable) { + continue + } + const variableDeclaration = ( + variableDeclarator.parent && + variableDeclarator.parent.type === 'VariableDeclaration' && + variableDeclarator.parent + ) || null + for (const reference of variable.references) { + if (!reference.isRead()) { + continue + } + + refReferenceIds.set(reference.identifier, { + variableDeclarator, + variableDeclaration + }) + } + } + }, + // if (refValue) + 'IfStatement>Identifier' (node) { + reportIfRefWrapped(node) + }, + // switch (refValue) + 'SwitchStatement>Identifier' (node) { + reportIfRefWrapped(node) + }, + // -refValue, +refValue, !refValue, ~refValue, typeof refValue + 'UnaryExpression>Identifier' (node) { + reportIfRefWrapped(node) + }, + // refValue++, refValue-- + 'UpdateExpression>Identifier' (node) { + reportIfRefWrapped(node) + }, + // refValue+1, refValue-1 + 'BinaryExpression>Identifier' (node) { + reportIfRefWrapped(node) + }, + // refValue+=1, refValue-=1, foo+=refValue, foo-=refValue + 'AssignmentExpression>Identifier' (node) { + reportIfRefWrapped(node) + }, + // refValue || other, refValue && other. ignore: other || refValue + 'LogicalExpression>Identifier' (node) { + if (node.parent.left !== node) { + return + } + // Report only constants. + const info = refReferenceIds.get(node) + if (!info) { + return + } + if (!info.variableDeclaration || info.variableDeclaration.kind !== 'const') { + return + } + reportIfRefWrapped(node) + }, + // refValue ? x : y + 'ConditionalExpression>Identifier' (node) { + if (node.parent.test !== node) { + return + } + reportIfRefWrapped(node) + } + } + } +} diff --git a/package.json b/package.json index e6d02ba4c..619e27af5 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,10 @@ "eslint": "^5.0.0 || ^6.0.0" }, "dependencies": { + "eslint-utils": "^2.0.0", "natural-compare": "^1.4.0", - "vue-eslint-parser": "^7.0.0", - "semver": "^5.6.0" + "semver": "^5.6.0", + "vue-eslint-parser": "^7.0.0" }, "devDependencies": { "@types/node": "^4.2.16", diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js new file mode 100644 index 000000000..b8632c88b --- /dev/null +++ b/tests/lib/rules/no-ref-as-operand.js @@ -0,0 +1,334 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-ref-as-operand') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2019, sourceType: 'module' } +}) + +tester.run('no-ref-as-operand', rule, { + valid: [ + ` + import { ref } from 'vue' + const count = ref(0) + console.log(count.value) // 0 + + count.value++ + console.log(count.value) // 1 + `, + ` + + `, + ` + import { ref } from 'vue' + const count = ref(0) + if (count.value) {} + switch (count.value) {} + var foo = -count.value + var foo = +count.value + count.value++ + count.value-- + count.value + 1 + 1 - count.value + count.value || other + count.value && other + var foo = count.value ? x : y + `, + ` + import { ref } from 'vue' + const foo = ref(true) + if (bar) foo + `, + ` + import { ref } from 'vue' + const foo = ref(true) + var a = other || foo // ignore + var b = other && foo // ignore + + let bar = ref(true) + var a = bar || other + var b = bar || other + `, + ` + import { ref } from 'vue' + let count = not_ref(0) + + count++ + `, + ` + import { ref } from 'vue' + const foo = ref(0) + const bar = ref(0) + var baz = x ? foo : bar + `, + ` + import { ref } from 'vue' + // Probably wrong, but not checked by this rule. + const {value} = ref(0) + value++ + `, + ` + import { ref } from 'vue' + const count = ref(0) + function foo() { + let count = 0 + count++ + } + `, + ` + import { ref } from 'unknown' + const count = ref(0) + count++ + `, + ` + import { ref } from 'vue' + const count = ref + count++ + ` + ], + invalid: [ + { + code: ` + import { ref } from 'vue' + let count = ref(0) + + count++ // error + console.log(count + 1) // error + console.log(1 + count) // error + `, + errors: [ + { + message: 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 5, + column: 7, + endLine: 5, + endColumn: 12 + }, + { + message: 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 6, + column: 19, + endLine: 6, + endColumn: 24 + }, + { + message: 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 7, + column: 23, + endLine: 7, + endColumn: 28 + } + ] + }, + { + code: ` + + `, + errors: [ + { + messageId: 'requireDotValue', + line: 8, + column: 13, + endLine: 8, + endColumn: 18 + }, + { + messageId: 'requireDotValue', + line: 9, + column: 25, + endLine: 9, + endColumn: 30 + }, + { + messageId: 'requireDotValue', + line: 10, + column: 29, + endLine: 10, + endColumn: 34 + } + ] + }, + { + code: ` + import { ref } from 'vue' + const foo = ref(true) + if (foo) { + // + } + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + } + ] + }, + { + code: ` + import { ref } from 'vue' + const foo = ref(true) + switch (foo) { + // + } + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + } + ] + }, + { + code: ` + import { ref } from 'vue' + const foo = ref(0) + var a = -foo + var b = +foo + var c = !foo + var d = ~foo + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + }, + { + messageId: 'requireDotValue', + line: 5 + }, + { + messageId: 'requireDotValue', + line: 6 + }, + { + messageId: 'requireDotValue', + line: 7 + } + ] + }, + { + code: ` + import { ref } from 'vue' + let foo = ref(0) + foo += 1 + foo -= 1 + baz += foo + baz -= foo + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + }, + { + messageId: 'requireDotValue', + line: 5 + }, + { + messageId: 'requireDotValue', + line: 6 + }, + { + messageId: 'requireDotValue', + line: 7 + } + ] + }, + { + code: ` + import { ref } from 'vue' + const foo = ref(true) + var a = foo || other + var b = foo && other + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + }, + { + messageId: 'requireDotValue', + line: 5 + } + ] + }, + { + code: ` + import { ref } from 'vue' + let foo = ref(true) + var a = foo ? x : y + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + } + ] + }, + { + code: ` + + `, + errors: [ + { + messageId: 'requireDotValue', + line: 7 + }, + { + messageId: 'requireDotValue', + line: 8 + }, + { + messageId: 'requireDotValue', + line: 9 + } + ] + } + ] +}) From 2c92d3d58f1b1563d23b3a364e22d3b266a6b69c Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sat, 14 Mar 2020 17:04:32 +0900 Subject: [PATCH 0019/1010] New: Add `vue/no-setup-props-destructure` rule (#1066) * Add no-setup-props-destructure rule * update * Add testcases * update * update * update doc --- docs/rules/README.md | 1 + docs/rules/no-setup-props-destructure.md | 98 +++++ lib/index.js | 1 + lib/rules/no-setup-props-destructure.js | 136 +++++++ package.json | 1 + tests/lib/rules/no-setup-props-destructure.js | 335 ++++++++++++++++++ 6 files changed, 572 insertions(+) create mode 100644 docs/rules/no-setup-props-destructure.md create mode 100644 lib/rules/no-setup-props-destructure.js create mode 100644 tests/lib/rules/no-setup-props-destructure.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 8bf02045c..86b51d484 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -170,6 +170,7 @@ For example: | [vue/no-ref-as-operand](./no-ref-as-operand.md) | disallow use of value wrapped by `ref()` (Composition API) as an operand | | | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | | | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | | +| [vue/no-setup-props-destructure](./no-setup-props-destructure.md) | disallow destructuring of `props` passed to `setup` | | | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | | | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: | | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: | diff --git a/docs/rules/no-setup-props-destructure.md b/docs/rules/no-setup-props-destructure.md new file mode 100644 index 000000000..2c56d9aa2 --- /dev/null +++ b/docs/rules/no-setup-props-destructure.md @@ -0,0 +1,98 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-setup-props-destructure +description: disallow destructuring of `props` passed to `setup` +--- +# vue/no-setup-props-destructure +> disallow destructuring of `props` passed to `setup` + +## :book: Rule Details + +This rule reports the destructuring of `props` passed to `setup` causing the value to lose reactivity. + + + +```vue + +``` + + + +Destructuring the `props` passed to `setup` will cause the value to lose reactivity. + + + +```vue + +``` + + + +Also, destructuring in root scope of `setup()` should error, but ok inside nested callbacks or returned render functions: + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :books: Further reading + +- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-setup-props-destructure.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-setup-props-destructure.js) diff --git a/lib/index.js b/lib/index.js index 4d3357cbb..e772dc590 100644 --- a/lib/index.js +++ b/lib/index.js @@ -56,6 +56,7 @@ module.exports = { 'no-reserved-component-names': require('./rules/no-reserved-component-names'), 'no-reserved-keys': require('./rules/no-reserved-keys'), 'no-restricted-syntax': require('./rules/no-restricted-syntax'), + 'no-setup-props-destructure': require('./rules/no-setup-props-destructure'), 'no-shared-component-data': require('./rules/no-shared-component-data'), 'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'), 'no-spaces-around-equal-signs-in-attribute': require('./rules/no-spaces-around-equal-signs-in-attribute'), diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js new file mode 100644 index 000000000..af7e3754a --- /dev/null +++ b/lib/rules/no-setup-props-destructure.js @@ -0,0 +1,136 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' +const { findVariable } = require('eslint-utils') +const utils = require('../utils') + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow destructuring of `props` passed to `setup`', + category: undefined, + url: 'https://eslint.vuejs.org/rules/no-setup-props-destructure.html' + }, + fixable: null, + schema: [], + messages: { + destructuring: 'Destructuring the `props` will cause the value to lose reactivity.', + getProperty: 'Getting a value from the `props` in root scope of `setup()` will cause the value to lose reactivity.' + } + }, + create (context) { + const setupFunctions = new Map() + const forbiddenNodes = new Map() + + function addForbiddenNode (property, node, messageId) { + let list = forbiddenNodes.get(property) + if (!list) { + list = [] + forbiddenNodes.set(property, list) + } + list.push({ + node, + messageId + }) + } + + function verify (left, right, { propsReferenceIds, setupProperty }) { + if (!right) { + return + } + + if (left.type === 'ArrayPattern' || left.type === 'ObjectPattern') { + if (propsReferenceIds.has(right)) { + addForbiddenNode(setupProperty, left, 'getProperty') + } + } else if (left.type === 'Identifier' && right.type === 'MemberExpression') { + if (propsReferenceIds.has(right.object)) { + addForbiddenNode(setupProperty, right, 'getProperty') + } + } + } + + let scopeStack = { upper: null, functionNode: null } + + return Object.assign( + { + 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) { + if (utils.getStaticPropertyName(node) !== 'setup') { + return + } + const param = node.value.params[0] + if (!param) { + // no arguments + return + } + if (param.type === 'RestElement') { + // cannot check + return + } + if (param.type === 'ArrayPattern' || param.type === 'ObjectPattern') { + addForbiddenNode(node, param, 'destructuring') + return + } + setupFunctions.set(node.value, { + setupProperty: node, + propsParam: param, + propsReferenceIds: new Set() + }) + }, + ':function' (node) { + scopeStack = { upper: scopeStack, functionNode: node } + }, + ':function>*' (node) { + const setupFunctionData = setupFunctions.get(node.parent) + if (!setupFunctionData || setupFunctionData.propsParam !== node) { + return + } + const variable = findVariable(context.getScope(), node) + if (!variable) { + return + } + const { propsReferenceIds } = setupFunctionData + for (const reference of variable.references) { + if (!reference.isRead()) { + continue + } + + propsReferenceIds.add(reference.identifier) + } + }, + 'VariableDeclarator' (node) { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData) { + return + } + verify(node.id, node.init, setupFunctionData) + }, + 'AssignmentExpression' (node) { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData) { + return + } + verify(node.left, node.right, setupFunctionData) + }, + ':function:exit' (node) { + scopeStack = scopeStack.upper + + setupFunctions.delete(node) + } + }, + utils.executeOnVue(context, obj => { + const reportsList = obj.properties + .map(item => forbiddenNodes.get(item)) + .filter(reports => !!reports) + for (const reports of reportsList) { + for (const report of reports) { + context.report(report) + } + } + }) + ) + } +} diff --git a/package.json b/package.json index 619e27af5..57a44ce2f 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test:base": "mocha \"tests/lib/**/*.js\" --reporter dot", "test": "nyc npm run test:base -- \"tests/integrations/*.js\" --timeout 60000", "debug": "mocha --inspect-brk \"tests/lib/**/*.js\" --reporter dot --timeout 60000", + "cover:report": "nyc report --reporter=html", "lint": "eslint . --rulesdir eslint-internal-rules", "pretest": "npm run lint", "preversion": "npm test && npm run update && git add .", diff --git a/tests/lib/rules/no-setup-props-destructure.js b/tests/lib/rules/no-setup-props-destructure.js new file mode 100644 index 000000000..2f3bccc51 --- /dev/null +++ b/tests/lib/rules/no-setup-props-destructure.js @@ -0,0 +1,335 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-setup-props-destructure') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2019, sourceType: 'module' } +}) + +tester.run('no-setup-props-destructure', rule, { + valid: [ + { + 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: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'destructuring', + line: 4, + column: 15, + endLine: 4, + endColumn: 24 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5, + column: 17, + endLine: 5, + endColumn: 26 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + }, + { + messageId: 'getProperty', + line: 11 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + }, + { + messageId: 'getProperty', + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 6 + } + ] + } + ] +}) From a634e3cf1e31c96400d19233cdab4732ee099701 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sat, 14 Mar 2020 17:09:10 +0900 Subject: [PATCH 0020/1010] Updated to detect Vue3 components. (#1073) --- docs/user-guide/README.md | 3 + lib/utils/index.js | 162 +++++++++++------- .../rules/component-definition-name-casing.js | 34 ++++ tests/lib/rules/match-component-file-name.js | 23 +++ .../lib/rules/no-reserved-component-names.js | 29 ++++ tests/lib/rules/no-shared-component-data.js | 24 +++ .../no-side-effects-in-computed-properties.js | 19 ++ tests/lib/rules/order-in-components.js | 32 ++++ tests/lib/rules/require-render-return.js | 19 +- tests/lib/utils/vue-component.js | 31 ++++ 10 files changed, 312 insertions(+), 64 deletions(-) diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md index 01710c07a..1c4d0e38d 100644 --- a/docs/user-guide/README.md +++ b/docs/user-guide/README.md @@ -85,6 +85,9 @@ All component-related rules are applied to code that passes any of the following * `Vue.component()` expression * `Vue.extend()` expression * `Vue.mixin()` expression +* `app.component()` expression +* `app.mixin()` expression +* `createApp()` expression * `export default {}` in `.vue` or `.jsx` file However, if you want to take advantage of the rules in any of your custom objects that are Vue components, you might need to use the special comment `// @vue/component` that marks an object in the next line as a Vue component in any file, e.g.: diff --git a/lib/utils/index.js b/lib/utils/index.js index 011ec6571..d0c918299 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -264,7 +264,7 @@ module.exports = { return componentsNode.value.properties .filter(p => p.type === 'Property') .map(node => { - const name = this.getStaticPropertyName(node) + const name = getStaticPropertyName(node) return name ? { node, name } : null }) .filter(comp => comp != null) @@ -402,42 +402,7 @@ module.exports = { * @param {Property|MethodDefinition|MemberExpression|Literal|TemplateLiteral|Identifier} node - The node to get. * @return {string|null} The property name if static. Otherwise, null. */ - getStaticPropertyName (node) { - let prop - switch (node && node.type) { - case 'Property': - case 'MethodDefinition': - prop = node.key - break - case 'MemberExpression': - prop = node.property - break - case 'Literal': - case 'TemplateLiteral': - case 'Identifier': - prop = node - break - // no default - } - - switch (prop && prop.type) { - case 'Literal': - return String(prop.value) - case 'TemplateLiteral': - if (prop.expressions.length === 0 && prop.quasis.length === 1) { - return prop.quasis[0].value.cooked - } - break - case 'Identifier': - if (!node.computed) { - return prop.name - } - break - // no default - } - - return null - }, + getStaticPropertyName, /** * Get all props by looking at all component's properties @@ -464,8 +429,8 @@ module.exports = { .filter(prop => prop.type === 'Property') .map(prop => { return { - key: prop.key, value: this.unwrapTypes(prop.value), node: prop, - propName: this.getStaticPropertyName(prop) + key: prop.key, value: unwrapTypes(prop.value), node: prop, + propName: getStaticPropertyName(prop) } }) } else { @@ -548,28 +513,52 @@ module.exports = { const callee = node.callee if (callee.type === 'MemberExpression') { - const calleeObject = this.unwrapTypes(callee.object) + const calleeObject = unwrapTypes(callee.object) + + if (calleeObject.type === 'Identifier') { + const propName = getStaticPropertyName(callee.property) + if (calleeObject.name === 'Vue') { + // for Vue.js 2.x + // Vue.component('xxx', {}) || Vue.mixin({}) || Vue.extend('xxx', {}) + const isFullVueComponentForVue2 = + ['component', 'mixin', 'extend'].includes(propName) && + isObjectArgument(node) + + return isFullVueComponentForVue2 + } - const isFullVueComponent = calleeObject.type === 'Identifier' && - calleeObject.name === 'Vue' && - callee.property.type === 'Identifier' && - ['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 && - node.arguments.length >= 1 && - node.arguments.slice(-1)[0].type === 'ObjectExpression' + // for Vue.js 3.x + // app.component('xxx', {}) || app.mixin({}) + const isFullVueComponent = + ['component', 'mixin'].includes(propName) && + isObjectArgument(node) - return isFullVueComponent + return isFullVueComponent + } } if (callee.type === 'Identifier') { - const isDestructedVueComponent = callee.name === 'component' && - node.arguments.length >= 1 && - node.arguments.slice(-1)[0].type === 'ObjectExpression' - - return isDestructedVueComponent + if (callee.name === 'component') { + // for Vue.js 2.x + // component('xxx', {}) + const isDestructedVueComponent = isObjectArgument(node) + return isDestructedVueComponent + } + if (callee.name === 'createApp') { + // for Vue.js 3.x + // createApp({}) + const isAppVueComponent = isObjectArgument(node) + return isAppVueComponent + } } } return false + + function isObjectArgument (node) { + return node.arguments.length > 0 && + unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression' + } }, /** @@ -584,7 +573,7 @@ module.exports = { callee.type === 'Identifier' && callee.name === 'Vue' && node.arguments.length && - node.arguments[0].type === 'ObjectExpression' + unwrapTypes(node.arguments[0]).type === 'ObjectExpression' }, /** @@ -647,7 +636,7 @@ module.exports = { 'CallExpression:exit' (node) { // Vue.component('xxx', {}) || component('xxx', {}) if (!_this.isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) return - cb(node.arguments.slice(-1)[0]) + cb(unwrapTypes(node.arguments.slice(-1)[0])) } } }, @@ -664,10 +653,10 @@ module.exports = { const callee = callExpr.callee if (callee.type === 'MemberExpression') { - const calleeObject = this.unwrapTypes(callee.object) + const calleeObject = unwrapTypes(callee.object) if (calleeObject.type === 'Identifier' && - calleeObject.name === 'Vue' && + // calleeObject.name === 'Vue' && // Any names can be used in Vue.js 3.x. e.g. app.component() callee.property === node && callExpr.arguments.length >= 1) { cb(callExpr) @@ -682,9 +671,9 @@ module.exports = { * @param {Set} groups Name of parent group */ * iterateProperties (node, groups) { - const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(this.getStaticPropertyName(p.key))) + const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(getStaticPropertyName(p.key))) for (const item of nodes) { - const name = this.getStaticPropertyName(item.key) + const name = getStaticPropertyName(item.key) if (!name) continue if (item.value.type === 'ArrayExpression') { @@ -705,7 +694,7 @@ module.exports = { * iterateArrayExpression (node, groupName) { assert(node.type === 'ArrayExpression') for (const item of node.elements) { - const name = this.getStaticPropertyName(item) + const name = getStaticPropertyName(item) if (name) { const obj = { name, groupName, node: item } yield obj @@ -721,7 +710,7 @@ module.exports = { * iterateObjectExpression (node, groupName) { assert(node.type === 'ObjectExpression') for (const item of node.properties) { - const name = this.getStaticPropertyName(item) + const name = getStaticPropertyName(item) if (name) { const obj = { name, groupName, node: item.key } yield obj @@ -865,7 +854,56 @@ module.exports = { * @param {T} node * @return {T} */ - unwrapTypes (node) { - return node.type === 'TSAsExpression' ? node.expression : node + unwrapTypes +} +/** +* Unwrap typescript types like "X as F" +* @template T +* @param {T} node +* @return {T} +*/ +function unwrapTypes (node) { + return node.type === 'TSAsExpression' ? node.expression : node +} + +/** + * Gets the property name of a given node. + * @param {Property|MethodDefinition|MemberExpression|Literal|TemplateLiteral|Identifier} node - The node to get. + * @return {string|null} The property name if static. Otherwise, null. + */ +function getStaticPropertyName (node) { + let prop + switch (node && node.type) { + case 'Property': + case 'MethodDefinition': + prop = node.key + break + case 'MemberExpression': + prop = node.property + break + case 'Literal': + case 'TemplateLiteral': + case 'Identifier': + prop = node + break + // no default } + + switch (prop && prop.type) { + case 'Literal': + return String(prop.value) + case 'TemplateLiteral': + if (prop.expressions.length === 0 && prop.quasis.length === 1) { + return prop.quasis[0].value.cooked + } + break + case 'Identifier': + if (!node.computed) { + return prop.name + } + break + // no default + } + + return null } diff --git a/tests/lib/rules/component-definition-name-casing.js b/tests/lib/rules/component-definition-name-casing.js index 98cb43679..d14eedb7a 100644 --- a/tests/lib/rules/component-definition-name-casing.js +++ b/tests/lib/rules/component-definition-name-casing.js @@ -116,6 +116,12 @@ ruleTester.run('component-definition-name-casing', rule, { options: ['kebab-case'], parserOptions }, + { + filename: 'test.vue', + code: `app.component('FooBar', component)`, + options: ['PascalCase'], + parserOptions + }, { filename: 'test.vue', code: `Vue.mixin({})`, @@ -137,6 +143,12 @@ ruleTester.run('component-definition-name-casing', rule, { options: ['kebab-case'], parserOptions }, + { + filename: 'test.vue', + code: `app.component(\`fooBar\${foo}\`, component)`, + options: ['kebab-case'], + parserOptions + }, // https://github.com/vuejs/eslint-plugin-vue/issues/1018 { filename: 'test.js', @@ -292,6 +304,17 @@ ruleTester.run('component-definition-name-casing', rule, { line: 1 }] }, + { + filename: 'test.vue', + code: `app.component('foo-bar', component)`, + output: `app.component('FooBar', component)`, + parserOptions, + errors: [{ + message: 'Property name "foo-bar" is not PascalCase.', + type: 'Literal', + line: 1 + }] + }, { filename: 'test.vue', code: `(Vue as VueConstructor).component('foo-bar', component)`, @@ -315,6 +338,17 @@ ruleTester.run('component-definition-name-casing', rule, { line: 1 }] }, + { + filename: 'test.vue', + code: `app.component('foo-bar', {})`, + output: `app.component('FooBar', {})`, + parserOptions, + errors: [{ + message: 'Property name "foo-bar" is not PascalCase.', + type: 'Literal', + line: 1 + }] + }, { filename: 'test.js', code: `Vue.component('foo_bar', {})`, diff --git a/tests/lib/rules/match-component-file-name.js b/tests/lib/rules/match-component-file-name.js index d8a1b141d..a97c5941e 100644 --- a/tests/lib/rules/match-component-file-name.js +++ b/tests/lib/rules/match-component-file-name.js @@ -429,6 +429,16 @@ ruleTester.run('match-component-file-name', rule, { options: [{ extensions: ['js'] }], parserOptions }, + { + filename: 'MyComponent.js', + code: ` + app.component('MyComponent', { + template: '
' + }) + `, + options: [{ extensions: ['js'] }], + parserOptions + }, { filename: 'MyComponent.js', code: ` @@ -701,6 +711,19 @@ ruleTester.run('match-component-file-name', rule, { message: 'Component name `MComponent` should match file name `MyComponent`.' }] }, + { + filename: 'MyComponent.js', + code: ` + app.component(\`MComponent\`, { + template: '
' + }) + `, + options: [{ extensions: ['js'] }], + parserOptions, + errors: [{ + message: 'Component name `MComponent` should match file name `MyComponent`.' + }] + }, // casing { diff --git a/tests/lib/rules/no-reserved-component-names.js b/tests/lib/rules/no-reserved-component-names.js index 43a34f153..0585b5c92 100644 --- a/tests/lib/rules/no-reserved-component-names.js +++ b/tests/lib/rules/no-reserved-component-names.js @@ -274,6 +274,11 @@ ruleTester.run('no-reserved-component-names', rule, { code: `Vue.component('FooBar', {})`, parserOptions }, + { + filename: 'test.vue', + code: `app.component('FooBar', {})`, + parserOptions + }, { filename: 'test.js', code: ` @@ -349,6 +354,18 @@ ruleTester.run('no-reserved-component-names', rule, { }] } }), + ...invalidElements.map(name => { + return { + filename: 'test.vue', + code: `app.component('${name}', component)`, + parserOptions, + errors: [{ + message: `Name "${name}" is reserved.`, + type: 'Literal', + line: 1 + }] + } + }), ...invalidElements.map(name => { return { filename: 'test.vue', @@ -361,6 +378,18 @@ ruleTester.run('no-reserved-component-names', rule, { }] } }), + ...invalidElements.map(name => { + return { + filename: 'test.vue', + code: `app.component(\`${name}\`, {})`, + parserOptions, + errors: [{ + message: `Name "${name}" is reserved.`, + type: 'TemplateLiteral', + line: 1 + }] + } + }), ...invalidElements.map(name => { return { filename: 'test.vue', diff --git a/tests/lib/rules/no-shared-component-data.js b/tests/lib/rules/no-shared-component-data.js index ce0e0062b..ade9e9efe 100644 --- a/tests/lib/rules/no-shared-component-data.js +++ b/tests/lib/rules/no-shared-component-data.js @@ -135,6 +135,30 @@ ruleTester.run('no-shared-component-data', rule, { return { foo: 'bar' }; +} + }) + `, + parserOptions, + errors: [{ + message: '`data` property in component must be a function.', + line: 3 + }] + }, + { + filename: 'test.js', + code: ` + app.component('some-comp', { + data: { + foo: 'bar' + } + }) + `, + output: ` + app.component('some-comp', { + data: function() { +return { + foo: 'bar' + }; } }) `, diff --git a/tests/lib/rules/no-side-effects-in-computed-properties.js b/tests/lib/rules/no-side-effects-in-computed-properties.js index df186b3a9..fa14ef248 100644 --- a/tests/lib/rules/no-side-effects-in-computed-properties.js +++ b/tests/lib/rules/no-side-effects-in-computed-properties.js @@ -296,6 +296,25 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, { message: 'Unexpected side effect in "test1" computed property.' }], parser: require.resolve('@typescript-eslint/parser') + }, + + { + code: `app.component('test', { + computed: { + test1() { + this.firstName = 'lorem' + asd.qwe.zxc = 'lorem' + return this.firstName + ' ' + this.lastName + }, + } + })`, + parserOptions, + errors: [ + { + line: 4, + message: 'Unexpected side effect in "test1" computed property.' + } + ] } ] }) diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js index b022508fa..f94b4ec67 100644 --- a/tests/lib/rules/order-in-components.js +++ b/tests/lib/rules/order-in-components.js @@ -243,6 +243,38 @@ ruleTester.run('order-in-components', rule, { line: 9 }] }, + { + filename: 'test.js', + code: ` + app.component('smart-list', { + name: 'app', + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + }, + components: {}, + template: '
' + }) + `, + parserOptions: { ecmaVersion: 6 }, + output: ` + app.component('smart-list', { + name: 'app', + components: {}, + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + }, + template: '
' + }) + `, + errors: [{ + message: 'The "components" property should be above the "data" property on line 4.', + line: 9 + }] + }, { filename: 'test.js', code: ` diff --git a/tests/lib/rules/require-render-return.js b/tests/lib/rules/require-render-return.js index e7fde4e60..580a6b89e 100644 --- a/tests/lib/rules/require-render-return.js +++ b/tests/lib/rules/require-render-return.js @@ -86,7 +86,7 @@ ruleTester.run('require-render-return', rule, { render() { if (a) { if (b) { - + } if (c) { return true @@ -180,6 +180,21 @@ ruleTester.run('require-render-return', rule, { line: 2 }] }, + { + code: `app.component('test', { + render: function () { + if (a) { + return + } + } + })`, + parserOptions, + errors: [{ + message: 'Expected to return a value in render function.', + type: 'Identifier', + line: 2 + }] + }, { code: `Vue.component('test2', { render: function () { @@ -199,7 +214,7 @@ ruleTester.run('require-render-return', rule, { code: `Vue.component('test2', { render: function () { if (a) { - + } else { return h('div', 'hello') } diff --git a/tests/lib/utils/vue-component.js b/tests/lib/utils/vue-component.js index db5d97337..69d663e92 100644 --- a/tests/lib/utils/vue-component.js +++ b/tests/lib/utils/vue-component.js @@ -108,6 +108,12 @@ function validTests (ext) { code: `export default Foo.extend({})`, parser: require.resolve('@typescript-eslint/parser'), parserOptions + }, + { + filename: `test.${ext}`, + code: `export default Foo.extend({} as ComponentOptions)`, + parser: require.resolve('@typescript-eslint/parser'), + parserOptions } ] } @@ -144,6 +150,18 @@ function invalidTests (ext) { parserOptions, errors: [makeError(1)] }, + { + filename: `test.${ext}`, + code: `app.component('name', {})`, + parserOptions, + errors: [makeError(1)] + }, + { + filename: `test.${ext}`, + code: `app.mixin({})`, + parserOptions, + errors: [makeError(1)] + }, { filename: `test.${ext}`, code: `export default (Vue as VueConstructor).extend({})`, @@ -158,6 +176,19 @@ function invalidTests (ext) { parserOptions, errors: [makeError(1)] }, + { + filename: `test.${ext}`, + code: `export default Vue.extend({} as ComponentOptions)`, + parser: require.resolve('@typescript-eslint/parser'), + parserOptions, + errors: [makeError(1)] + }, + { + filename: `test.${ext}`, + code: `createApp({})`, + parserOptions, + errors: [makeError(1)] + }, { filename: `test.${ext}`, code: ` From 3cc5ac00cb0db328c30b9d689502319d35f2e7c5 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sat, 14 Mar 2020 17:10:58 +0900 Subject: [PATCH 0021/1010] New: Add `vue/no-lifecycle-after-await` rule (#1067) --- docs/rules/README.md | 1 + docs/rules/no-lifecycle-after-await.md | 47 +++++ lib/index.js | 1 + lib/rules/no-lifecycle-after-await.js | 111 ++++++++++++ tests/lib/rules/no-lifecycle-after-await.js | 180 ++++++++++++++++++++ 5 files changed, 340 insertions(+) create mode 100644 docs/rules/no-lifecycle-after-await.md create mode 100644 lib/rules/no-lifecycle-after-await.js create mode 100644 tests/lib/rules/no-lifecycle-after-await.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 86b51d484..5b21a0840 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -167,6 +167,7 @@ For example: | [vue/no-deprecated-v-bind-sync](./no-deprecated-v-bind-sync.md) | disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+) | :wrench: | | [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | | | [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | | +| [vue/no-lifecycle-after-await](./no-lifecycle-after-await.md) | disallow asynchronously registered lifecycle hooks | | | [vue/no-ref-as-operand](./no-ref-as-operand.md) | disallow use of value wrapped by `ref()` (Composition API) as an operand | | | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | | | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | | diff --git a/docs/rules/no-lifecycle-after-await.md b/docs/rules/no-lifecycle-after-await.md new file mode 100644 index 000000000..aaa521f7c --- /dev/null +++ b/docs/rules/no-lifecycle-after-await.md @@ -0,0 +1,47 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-lifecycle-after-await +description: disallow asynchronously registered lifecycle hooks +--- +# vue/no-lifecycle-after-await +> disallow asynchronously registered lifecycle hooks + +## :book: Rule Details + +This rule reports the lifecycle hooks after `await` expression. +In `setup()` function, `onXXX` lifecycle hooks should be registered synchronously. + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :books: Further reading + +- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-lifecycle-after-await.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-lifecycle-after-await.js) diff --git a/lib/index.js b/lib/index.js index e772dc590..55d753058 100644 --- a/lib/index.js +++ b/lib/index.js @@ -49,6 +49,7 @@ module.exports = { 'no-duplicate-attributes': require('./rules/no-duplicate-attributes'), 'no-empty-pattern': require('./rules/no-empty-pattern'), 'no-irregular-whitespace': require('./rules/no-irregular-whitespace'), + 'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'), 'no-multi-spaces': require('./rules/no-multi-spaces'), 'no-multiple-template-root': require('./rules/no-multiple-template-root'), 'no-parsing-error': require('./rules/no-parsing-error'), diff --git a/lib/rules/no-lifecycle-after-await.js b/lib/rules/no-lifecycle-after-await.js new file mode 100644 index 000000000..1f78165e5 --- /dev/null +++ b/lib/rules/no-lifecycle-after-await.js @@ -0,0 +1,111 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' +const { ReferenceTracker } = require('eslint-utils') +const utils = require('../utils') + +const LIFECYCLE_HOOKS = ['onBeforeMount', 'onBeforeUnmount', 'onBeforeUpdate', 'onErrorCaptured', 'onMounted', 'onRenderTracked', 'onRenderTriggered', 'onUnmounted', 'onUpdated', 'onActivated', 'onDeactivated'] + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow asynchronously registered lifecycle hooks', + category: undefined, + url: 'https://eslint.vuejs.org/rules/no-lifecycle-after-await.html' + }, + fixable: null, + schema: [], + messages: { + forbidden: 'The lifecycle hooks after `await` expression are forbidden.' + } + }, + create (context) { + const lifecycleHookCallNodes = new Set() + const setupFunctions = new Map() + const forbiddenNodes = new Map() + + function addForbiddenNode (property, node) { + let list = forbiddenNodes.get(property) + if (!list) { + list = [] + forbiddenNodes.set(property, list) + } + list.push(node) + } + + let scopeStack = { upper: null, functionNode: null } + + return Object.assign( + { + 'Program' () { + const tracker = new ReferenceTracker(context.getScope()) + const traceMap = { + vue: { + [ReferenceTracker.ESM]: true + } + } + for (const lifecycleHook of LIFECYCLE_HOOKS) { + traceMap.vue[lifecycleHook] = { + [ReferenceTracker.CALL]: true + } + } + + for (const { node } of tracker.iterateEsmReferences(traceMap)) { + lifecycleHookCallNodes.add(node) + } + }, + 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) { + if (utils.getStaticPropertyName(node) !== 'setup') { + return + } + + setupFunctions.set(node.value, { + setupProperty: node, + afterAwait: false + }) + }, + ':function' (node) { + scopeStack = { upper: scopeStack, functionNode: node } + }, + 'AwaitExpression' () { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData) { + return + } + setupFunctionData.afterAwait = true + }, + 'CallExpression' (node) { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData || !setupFunctionData.afterAwait) { + return + } + + if (lifecycleHookCallNodes.has(node)) { + addForbiddenNode(setupFunctionData.setupProperty, node) + } + }, + ':function:exit' (node) { + scopeStack = scopeStack.upper + + setupFunctions.delete(node) + } + }, + utils.executeOnVue(context, obj => { + const reportsList = obj.properties + .map(item => forbiddenNodes.get(item)) + .filter(reports => !!reports) + for (const reports of reportsList) { + for (const node of reports) { + context.report({ + node, + messageId: 'forbidden' + }) + } + } + }) + ) + } +} diff --git a/tests/lib/rules/no-lifecycle-after-await.js b/tests/lib/rules/no-lifecycle-after-await.js new file mode 100644 index 000000000..eaa99f843 --- /dev/null +++ b/tests/lib/rules/no-lifecycle-after-await.js @@ -0,0 +1,180 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-lifecycle-after-await') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2019, sourceType: 'module' } +}) + +tester.run('no-lifecycle-after-await', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, { + filename: 'test.vue', + code: ` + + ` + }, { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'The lifecycle hooks after `await` expression are forbidden.', + line: 8, + column: 11, + endLine: 8, + endColumn: 41 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 8 + }, + { + messageId: 'forbidden', + line: 9 + }, + { + messageId: 'forbidden', + line: 10 + }, + { + messageId: 'forbidden', + line: 11 + }, + { + messageId: 'forbidden', + line: 12 + }, + { + messageId: 'forbidden', + line: 13 + }, + { + messageId: 'forbidden', + line: 14 + }, + { + messageId: 'forbidden', + line: 15 + }, + { + messageId: 'forbidden', + line: 16 + }, + { + messageId: 'forbidden', + line: 17 + }, + { + messageId: 'forbidden', + line: 18 + } + ] + } + ] +}) From 7a790bc157a08f42b07358a950ad179b0b417f48 Mon Sep 17 00:00:00 2001 From: frenchrabbit Date: Sat, 14 Mar 2020 11:14:34 +0300 Subject: [PATCH 0022/1010] Fix iterateProperties to support arrow functions (#1064) * Fix iterateProperties to support arrow functions * indent mistake * add no-dupe-keys test cases using arrow function * add no-reserved-keys no-template-shadow test cases using arrow function Co-authored-by: maddocnc --- lib/utils/index.js | 20 +++ tests/lib/rules/no-dupe-keys.js | 186 ++++++++++++++++++++++++++ tests/lib/rules/no-reserved-keys.js | 76 +++++++++++ tests/lib/rules/no-template-shadow.js | 116 ++++++++++++++++ 4 files changed, 398 insertions(+) diff --git a/lib/utils/index.js b/lib/utils/index.js index d0c918299..e784f2ab3 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -682,6 +682,8 @@ module.exports = { yield * this.iterateObjectExpression(item.value, name) } else if (item.value.type === 'FunctionExpression') { yield * this.iterateFunctionExpression(item.value, name) + } else if (item.value.type === 'ArrowFunctionExpression') { + yield * this.iterateArrowFunctionExpression(item.value, name) } } }, @@ -734,6 +736,24 @@ module.exports = { } }, + /** + * Return generator with all elements inside ArrowFunctionExpression + * @param {ASTNode} node Node to check + * @param {string} groupName Name of parent group + */ + * iterateArrowFunctionExpression (node, groupName) { + assert(node.type === 'ArrowFunctionExpression') + if (node.body.type === 'BlockStatement') { + for (const item of node.body.body) { + if (item.type === 'ReturnStatement' && item.argument && item.argument.type === 'ObjectExpression') { + yield * this.iterateObjectExpression(item.argument, groupName) + } + } + } else if (node.body.type === 'ObjectExpression') { + yield * this.iterateObjectExpression(node.body, groupName) + } + }, + /** * Find all functions which do not always return values * @param {boolean} treatUndefinedAsUnspecified diff --git a/tests/lib/rules/no-dupe-keys.js b/tests/lib/rules/no-dupe-keys.js index 1abb67642..f1a35568a 100644 --- a/tests/lib/rules/no-dupe-keys.js +++ b/tests/lib/rules/no-dupe-keys.js @@ -45,6 +45,58 @@ ruleTester.run('no-dupe-keys', rule, { parserOptions: { ecmaVersion: 6, sourceType: 'module' } }, + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + computed: { + bar () { + } + }, + data: () => { + return { + dat: null + } + }, + data: () => { + return + }, + methods: { + _foo () {}, + test () { + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + computed: { + bar () { + } + }, + data: () => ({ + dat: null + }), + data: () => { + return + }, + methods: { + _foo () {}, + test () { + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { filename: 'test.vue', code: ` @@ -82,6 +134,78 @@ ruleTester.run('no-dupe-keys', rule, { parserOptions: { ecmaVersion: 2018, sourceType: 'module' } }, + { + filename: 'test.vue', + code: ` + export default { + ...foo(), + props: { + ...foo(), + foo: String + }, + computed: { + ...mapGetters({ + test: 'getTest' + }), + bar: { + get () { + } + } + }, + data: { + ...foo(), + dat: null + }, + methods: { + ...foo(), + test () { + } + }, + data: () => { + return { + ...dat + } + }, + } + `, + parserOptions: { ecmaVersion: 2018, sourceType: 'module' } + }, + + { + filename: 'test.vue', + code: ` + export default { + ...foo(), + props: { + ...foo(), + foo: String + }, + computed: { + ...mapGetters({ + test: 'getTest' + }), + bar: { + get () { + } + } + }, + data: { + ...foo(), + dat: null + }, + methods: { + ...foo(), + test () { + } + }, + data: () => ({ + ...dat + }), + } + `, + parserOptions: { ecmaVersion: 2018, sourceType: 'module' } + }, + { filename: 'test.js', code: ` @@ -136,6 +260,68 @@ ruleTester.run('no-dupe-keys', rule, { line: 14 }] }, + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + computed: { + foo () { + } + }, + data: () => { + return { + foo: null + } + }, + methods: { + foo () { + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [{ + message: 'Duplicated key \'foo\'.', + line: 5 + }, { + message: 'Duplicated key \'foo\'.', + line: 10 + }, { + message: 'Duplicated key \'foo\'.', + line: 14 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + computed: { + foo () { + } + }, + data: () => ({ + foo: null + }), + methods: { + foo () { + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [{ + message: 'Duplicated key \'foo\'.', + line: 5 + }, { + message: 'Duplicated key \'foo\'.', + line: 9 + }, { + message: 'Duplicated key \'foo\'.', + line: 12 + }] + }, { filename: 'test.vue', code: ` diff --git a/tests/lib/rules/no-reserved-keys.js b/tests/lib/rules/no-reserved-keys.js index 08c7f244c..05836bc47 100644 --- a/tests/lib/rules/no-reserved-keys.js +++ b/tests/lib/rules/no-reserved-keys.js @@ -45,6 +45,50 @@ ruleTester.run('no-reserved-keys', rule, { } `, parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + computed: { + bar () { + } + }, + data: () => { + return { + dat: null + } + }, + methods: { + _foo () {}, + test () { + } + } + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + computed: { + bar () { + } + }, + data: () => ({ + dat: null + }), + methods: { + _foo () {}, + test () { + } + } + } + `, + parserOptions } ], @@ -79,6 +123,38 @@ ruleTester.run('no-reserved-keys', rule, { line: 4 }] }, + { + filename: 'test.js', + code: ` + new Vue({ + data: () => { + return { + _foo: String + } + } + }) + `, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + message: "Keys starting with with '_' are reserved in '_foo' group.", + line: 5 + }] + }, + { + filename: 'test.js', + code: ` + new Vue({ + data: () => ({ + _foo: String + }) + }) + `, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + message: "Keys starting with with '_' are reserved in '_foo' group.", + line: 4 + }] + }, { filename: 'test.js', code: ` diff --git a/tests/lib/rules/no-template-shadow.js b/tests/lib/rules/no-template-shadow.js index 68fefce0e..08e0e03b6 100644 --- a/tests/lib/rules/no-template-shadow.js +++ b/tests/lib/rules/no-template-shadow.js @@ -70,6 +70,52 @@ ruleTester.run('no-template-shadow', rule, { } } ` + }, + { + filename: 'test.vue', + code: ` + ` + }, + { + filename: 'test.vue', + code: ` + ` } ], @@ -208,6 +254,76 @@ ruleTester.run('no-template-shadow', rule, { type: 'Identifier', line: 7 }] + }, + { + filename: 'test.vue', + code: ` + `, + errors: [{ + message: "Variable 'e' is already declared in the upper scope.", + type: 'Identifier', + line: 6 + }, { + message: "Variable 'f' is already declared in the upper scope.", + type: 'Identifier', + line: 7 + }] + }, + { + filename: 'test.vue', + code: ` + `, + errors: [{ + message: "Variable 'e' is already declared in the upper scope.", + type: 'Identifier', + line: 6 + }, { + message: "Variable 'f' is already declared in the upper scope.", + type: 'Identifier', + line: 7 + }] } ] }) From 60a5f6ce4c99cc5797390139900414d43cb42311 Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY <974153916@qq.com> Date: Sat, 14 Mar 2020 16:18:52 +0800 Subject: [PATCH 0023/1010] feat: add fixable, no more error when unused variable with prefix _ (#1070) * feat: add fixable, no more error when unused variable with prefix _ implement proposal https://github.com/vuejs/eslint-plugin-vue/issues/1058 feat https://github.com/vuejs/eslint-plugin-vue/issues/1058 * fix: make fix optional,add varIgnorePattern * feat: change varIgnorepattern to ignorePattern, add test, update readme * docs: update doc --- docs/rules/no-unused-vars.md | 12 ++++++++-- lib/rules/no-unused-vars.js | 31 +++++++++++++++++++++--- tests/lib/rules/no-unused-vars.js | 39 +++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/docs/rules/no-unused-vars.md b/docs/rules/no-unused-vars.md index cde104f4f..3ce57fa94 100644 --- a/docs/rules/no-unused-vars.md +++ b/docs/rules/no-unused-vars.md @@ -33,8 +33,16 @@ This rule report variable definitions of v-for directives or scope attributes if ## :wrench: Options -Nothing. - +```js +{ + "vue/no-unsed-vars": [{ + "ignorePattern": '^_', + }] +} +``` +- `ignorePattern` ... disables reporting when your definitions of v-for directives or scope attributes match your ignorePattern Regular expression. default `null`, will ignore nothing +## :rocket: Suggestion +- When your ignorePattern set to `^_`, we could provide a suggestion which add a prefix`_` to your variable and no more eslint error ## :mag: Implementation - [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unused-vars.js) diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js index 205531f31..6e3e39e4c 100644 --- a/lib/rules/no-unused-vars.js +++ b/lib/rules/no-unused-vars.js @@ -19,17 +19,34 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-unused-vars.html' }, fixable: null, - schema: [] + schema: [ + { + 'type': 'object', + 'properties': { + 'ignorePattern': { + 'type': 'string' + } + }, + 'additionalProperties': false + } + ] }, create (context) { + const option = context.options[0] || { } + const pattern = option['ignorePattern'] + let regExp = null + if (pattern) { + regExp = new RegExp(pattern, 'u') + } return utils.defineTemplateBodyVisitor(context, { VElement (node) { const variables = node.variables for ( let i = variables.length - 1; - i >= 0 && !variables[i].references.length; + // eslint-disable-next-line no-unmodified-loop-condition + i >= 0 && !variables[i].references.length && (regExp === null || !regExp.test(variables[i].id.name)); i-- ) { const variable = variables[i] @@ -37,7 +54,15 @@ module.exports = { node: variable.id, loc: variable.id.loc, message: `'{{name}}' is defined but never used.`, - data: variable.id + data: variable.id, + suggest: pattern === '^_' ? [ + { + desc: `Replace the ${variable.id.name} with _${variable.id.name}`, + fix: function (fixer) { + return fixer.replaceText(variable.id, `_${variable.id.name}`) + } + } + ] : [] }) } } diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js index 09cfde9fc..cd2eb645d 100644 --- a/tests/lib/rules/no-unused-vars.js +++ b/tests/lib/rules/no-unused-vars.js @@ -52,6 +52,18 @@ tester.run('no-unused-vars', rule, { }, { code: '' + }, + { + code: '', + options: [{ ignorePattern: '^_' }] + }, + { + code: '', + options: [{ ignorePattern: '^ignore' }] + }, + { + code: '', + options: [{ ignorePattern: 'ignore$' }] } ], invalid: [ @@ -82,6 +94,7 @@ tester.run('no-unused-vars', rule, { { code: '', errors: ["'a' is defined but never used.", "'b' is defined but never used.", "'c' is defined but never used."] + }, { code: '', @@ -97,7 +110,33 @@ tester.run('no-unused-vars', rule, { }, { code: '', + errors: [{ + message: "'x' is defined but never used.", + suggestions: [{ + desc: 'Replace the x with _x', + output: '' + }] + }], + options: [{ ignorePattern: '^_' }] + }, + { + code: '', + options: [{ ignorePattern: 'ignore$' }], errors: ["'x' is defined but never used."] + }, + { + code: '', + errors: ["'props' is defined but never used."], + options: [{ ignorePattern: '^ignore' }] + }, + { + code: '', + errors: ["'props' is defined but never used."], + options: [{ ignorePattern: '^ignore' }] + }, + { + code: '', + errors: ["'_i' is defined but never used."] } ] }) From 566357264a5a4f54c109f9cdfe82bf34f06fdde2 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sat, 14 Mar 2020 18:09:30 +0900 Subject: [PATCH 0024/1010] Add the ruleset for Vue.js 3 (#1061) - Add `plugin:vue/vue3-essential` config - Add `plugin:vue/vue3-strongly-recommended` config - Add `plugin:vue/vue3-recommended` config --- .eslintrc.js | 1 + docs/.vuepress/config.js | 46 +++++- docs/rules/README.md | 121 ++++++++++++-- docs/rules/attribute-hyphenation.md | 2 +- docs/rules/attributes-order.md | 2 +- .../rules/component-definition-name-casing.md | 2 +- docs/rules/component-tags-order.md | 2 +- docs/rules/html-closing-bracket-newline.md | 2 +- docs/rules/html-closing-bracket-spacing.md | 2 +- docs/rules/html-end-tags.md | 2 +- docs/rules/html-indent.md | 2 +- docs/rules/html-quotes.md | 2 +- docs/rules/html-self-closing.md | 2 +- docs/rules/max-attributes-per-line.md | 2 +- .../multiline-html-element-content-newline.md | 2 +- docs/rules/mustache-interpolation-spacing.md | 2 +- docs/rules/no-async-in-computed-properties.md | 2 +- docs/rules/no-deprecated-filter.md | 1 + docs/rules/no-deprecated-scope-attribute.md | 1 + docs/rules/no-deprecated-slot-attribute.md | 1 + .../no-deprecated-slot-scope-attribute.md | 1 + docs/rules/no-deprecated-v-bind-sync.md | 1 + docs/rules/no-dupe-keys.md | 2 +- docs/rules/no-duplicate-attributes.md | 2 +- docs/rules/no-lifecycle-after-await.md | 2 + docs/rules/no-multi-spaces.md | 2 +- docs/rules/no-parsing-error.md | 2 +- docs/rules/no-ref-as-operand.md | 2 + docs/rules/no-reserved-keys.md | 2 +- docs/rules/no-setup-props-destructure.md | 2 + docs/rules/no-shared-component-data.md | 2 +- .../no-side-effects-in-computed-properties.md | 2 +- ...-spaces-around-equal-signs-in-attribute.md | 2 +- docs/rules/no-template-key.md | 2 +- docs/rules/no-template-shadow.md | 2 +- docs/rules/no-textarea-mustache.md | 2 +- docs/rules/no-unused-components.md | 2 +- docs/rules/no-unused-vars.md | 2 +- docs/rules/no-use-v-if-with-v-for.md | 2 +- docs/rules/no-v-html.md | 2 +- docs/rules/order-in-components.md | 2 +- docs/rules/prop-name-casing.md | 2 +- docs/rules/require-component-is.md | 2 +- docs/rules/require-default-prop.md | 2 +- docs/rules/require-prop-type-constructor.md | 2 +- docs/rules/require-prop-types.md | 2 +- docs/rules/require-render-return.md | 2 +- docs/rules/require-v-for-key.md | 2 +- docs/rules/require-valid-default-prop.md | 2 +- docs/rules/return-in-computed-property.md | 2 +- ...singleline-html-element-content-newline.md | 2 +- docs/rules/this-in-template.md | 2 +- docs/rules/use-v-on-exact.md | 2 +- docs/rules/v-bind-style.md | 2 +- docs/rules/v-on-style.md | 2 +- docs/rules/v-slot-style.md | 2 +- docs/rules/valid-template-root.md | 2 +- docs/rules/valid-v-bind.md | 2 +- docs/rules/valid-v-cloak.md | 2 +- docs/rules/valid-v-else-if.md | 2 +- docs/rules/valid-v-else.md | 2 +- docs/rules/valid-v-for.md | 2 +- docs/rules/valid-v-html.md | 2 +- docs/rules/valid-v-if.md | 2 +- docs/rules/valid-v-model.md | 2 +- docs/rules/valid-v-on.md | 2 +- docs/rules/valid-v-once.md | 2 +- docs/rules/valid-v-pre.md | 2 +- docs/rules/valid-v-show.md | 2 +- docs/rules/valid-v-slot.md | 2 +- docs/rules/valid-v-text.md | 2 +- docs/user-guide/README.md | 4 +- .../consistent-docs-description.js | 2 +- .../no-invalid-meta-docs-categories.js | 147 ++++++++++++++++++ eslint-internal-rules/no-invalid-meta.js | 10 +- .../require-meta-docs-url.js | 2 +- lib/configs/vue3-essential.js | 52 +++++++ lib/configs/vue3-recommended.js | 15 ++ lib/configs/vue3-strongly-recommended.js | 31 ++++ lib/index.js | 7 +- lib/rules/array-bracket-spacing.js | 2 +- lib/rules/attribute-hyphenation.js | 2 +- lib/rules/attributes-order.js | 2 +- lib/rules/block-spacing.js | 2 +- lib/rules/brace-style.js | 2 +- lib/rules/comment-directive.js | 2 +- lib/rules/component-definition-name-casing.js | 2 +- .../component-name-in-template-casing.js | 2 +- lib/rules/component-tags-order.js | 2 +- lib/rules/html-closing-bracket-newline.js | 2 +- lib/rules/html-closing-bracket-spacing.js | 2 +- lib/rules/html-end-tags.js | 2 +- lib/rules/html-indent.js | 2 +- lib/rules/html-quotes.js | 2 +- lib/rules/html-self-closing.js | 2 +- lib/rules/jsx-uses-vars.js | 2 +- lib/rules/key-spacing.js | 2 +- lib/rules/keyword-spacing.js | 2 +- lib/rules/match-component-file-name.js | 2 +- lib/rules/max-attributes-per-line.js | 2 +- lib/rules/max-len.js | 2 +- .../multiline-html-element-content-newline.js | 2 +- lib/rules/mustache-interpolation-spacing.js | 2 +- lib/rules/name-property-casing.js | 2 +- lib/rules/no-async-in-computed-properties.js | 2 +- lib/rules/no-boolean-default.js | 2 +- lib/rules/no-confusing-v-for-v-if.js | 2 +- lib/rules/no-custom-modifiers-on-v-model.js | 2 +- lib/rules/no-deprecated-filter.js | 2 +- lib/rules/no-deprecated-scope-attribute.js | 2 +- lib/rules/no-deprecated-slot-attribute.js | 2 +- .../no-deprecated-slot-scope-attribute.js | 2 +- lib/rules/no-deprecated-v-bind-sync.js | 2 +- lib/rules/no-dupe-keys.js | 2 +- lib/rules/no-duplicate-attributes.js | 2 +- lib/rules/no-irregular-whitespace.js | 2 +- lib/rules/no-lifecycle-after-await.js | 2 +- lib/rules/no-multi-spaces.js | 2 +- lib/rules/no-multiple-template-root.js | 2 +- lib/rules/no-parsing-error.js | 2 +- lib/rules/no-ref-as-operand.js | 2 +- lib/rules/no-reserved-component-names.js | 2 +- lib/rules/no-reserved-keys.js | 2 +- lib/rules/no-setup-props-destructure.js | 2 +- lib/rules/no-shared-component-data.js | 2 +- .../no-side-effects-in-computed-properties.js | 2 +- ...-spaces-around-equal-signs-in-attribute.js | 2 +- lib/rules/no-static-inline-styles.js | 2 +- lib/rules/no-template-key.js | 2 +- lib/rules/no-template-shadow.js | 2 +- lib/rules/no-textarea-mustache.js | 2 +- lib/rules/no-unsupported-features.js | 2 +- lib/rules/no-unused-components.js | 2 +- lib/rules/no-unused-vars.js | 2 +- lib/rules/no-use-v-if-with-v-for.js | 2 +- lib/rules/no-v-html.js | 2 +- lib/rules/no-v-model-argument.js | 2 +- lib/rules/object-curly-spacing.js | 2 +- lib/rules/order-in-components.js | 2 +- lib/rules/padding-line-between-blocks.js | 2 +- lib/rules/prop-name-casing.js | 2 +- lib/rules/require-component-is.js | 2 +- lib/rules/require-default-prop.js | 2 +- lib/rules/require-direct-export.js | 2 +- lib/rules/require-name-property.js | 2 +- lib/rules/require-prop-type-constructor.js | 2 +- lib/rules/require-prop-types.js | 2 +- lib/rules/require-render-return.js | 2 +- lib/rules/require-v-for-key.js | 2 +- lib/rules/require-valid-default-prop.js | 2 +- lib/rules/return-in-computed-property.js | 2 +- lib/rules/script-indent.js | 2 +- ...singleline-html-element-content-newline.js | 2 +- lib/rules/sort-keys.js | 2 +- lib/rules/space-infix-ops.js | 2 +- lib/rules/space-unary-ops.js | 2 +- lib/rules/static-class-names-order.js | 2 +- lib/rules/this-in-template.js | 2 +- lib/rules/use-v-on-exact.js | 2 +- lib/rules/v-bind-style.js | 2 +- lib/rules/v-on-function-call.js | 2 +- lib/rules/v-on-style.js | 2 +- lib/rules/v-slot-style.js | 2 +- lib/rules/valid-template-root.js | 2 +- lib/rules/valid-v-bind-sync.js | 2 +- lib/rules/valid-v-bind.js | 2 +- lib/rules/valid-v-cloak.js | 2 +- lib/rules/valid-v-else-if.js | 2 +- lib/rules/valid-v-else.js | 2 +- lib/rules/valid-v-for.js | 2 +- lib/rules/valid-v-html.js | 2 +- lib/rules/valid-v-if.js | 2 +- lib/rules/valid-v-model.js | 2 +- lib/rules/valid-v-on.js | 2 +- lib/rules/valid-v-once.js | 2 +- lib/rules/valid-v-pre.js | 2 +- lib/rules/valid-v-show.js | 2 +- lib/rules/valid-v-slot.js | 2 +- lib/rules/valid-v-text.js | 2 +- package.json | 1 + tools/lib/categories.js | 57 +++++-- tools/lib/rules.js | 18 ++- tools/update-docs-rules-index.js | 4 +- tools/update-docs.js | 36 ++++- tools/update-lib-configs.js | 33 ++-- 185 files changed, 696 insertions(+), 220 deletions(-) create mode 100644 eslint-internal-rules/no-invalid-meta-docs-categories.js create mode 100644 lib/configs/vue3-essential.js create mode 100644 lib/configs/vue3-recommended.js create mode 100644 lib/configs/vue3-strongly-recommended.js diff --git a/.eslintrc.js b/.eslintrc.js index eade1e93d..2693499d0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,6 +29,7 @@ module.exports = { rules: { "consistent-docs-description": "error", "no-invalid-meta": "error", + "no-invalid-meta-docs-categories": "error", 'eslint-plugin/require-meta-type': 'error', "require-meta-docs-url": ["error", { "pattern": `https://eslint.vuejs.org/rules/{{name}}.html` diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 3f8282598..0b78e7eba 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -5,11 +5,47 @@ 'use strict' const rules = require('../../tools/lib/rules') -const categories = require('../../tools/lib/categories') -const uncategorizedRules = rules.filter(rule => !rule.meta.docs.category && !rule.meta.deprecated) +const uncategorizedRules = rules.filter(rule => !rule.meta.docs.categories && !rule.meta.deprecated) const deprecatedRules = rules.filter(rule => rule.meta.deprecated) +const sidebarCategories = [ + { title: 'Base Rules', categoryIds: ['base'] }, + { title: 'Priority A: Essential', categoryIds: ['vue3-essential', 'essential'] }, + { title: 'Priority A: Essential for Vue.js 3.x', categoryIds: ['vue3-essential'] }, + { title: 'Priority A: Essential for Vue.js 2.x', categoryIds: ['essential'] }, + { title: 'Priority B: Strongly Recommended', categoryIds: ['vue3-strongly-recommended', 'strongly-recommended'] }, + { title: 'Priority B: Strongly Recommended for Vue.js 3.x', categoryIds: ['vue3-strongly-recommended'] }, + { title: 'Priority B: Strongly Recommended for Vue.js 2.x', categoryIds: ['strongly-recommended'] }, + { title: 'Priority C: Recommended', categoryIds: ['vue3-recommended', 'recommended'] }, + { title: 'Priority C: Recommended for Vue.js 3.x', categoryIds: ['vue3-recommended'] }, + { title: 'Priority C: Recommended for Vue.js 2.x', categoryIds: ['recommended'] } +] + +const categorizedRules = [] +for (const { title, categoryIds } of sidebarCategories) { + const categoryRules = rules + .filter(rule => rule.meta.docs.categories && !rule.meta.deprecated) + .filter(rule => categoryIds + .every(categoryId => rule.meta.docs.categories.includes(categoryId)) + ) + const children = categoryRules + .filter(({ ruleId }) => { + const exists = categorizedRules.some(({ children }) => children.some(([, alreadyRuleId]) => alreadyRuleId === ruleId)) + return !exists + }) + .map(({ ruleId, name }) => [`/rules/${name}`, ruleId]) + + if (children.length === 0) { + continue + } + categorizedRules.push({ + title, + collapsable: false, + children + }) +} + const extraCategories = [] if (uncategorizedRules.length > 0) { extraCategories.push({ @@ -59,11 +95,7 @@ module.exports = { '/rules/', // Rules in each category. - ...categories.map(({ title, rules }) => ({ - title: title.replace(/ \(.+?\)/, ''), - collapsable: false, - children: rules.map(({ ruleId, name }) => [`/rules/${name}`, ruleId]) - })), + ...categorizedRules, // Rules in no category. ...extraCategories diff --git a/docs/rules/README.md b/docs/rules/README.md index 5b21a0840..e184873e5 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -26,7 +26,114 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | [vue/comment-directive](./comment-directive.md) | support comment-directives in ` `, - errors: [{ - message: 'The "CustomComponent" component has been used but not registered.', - line: 3 - }] + errors: [ + { + message: + 'The "CustomComponent" component has been used but not registered.', + line: 3 + } + ] }, { filename: 'test.vue', @@ -453,15 +444,16 @@ tester.run('no-unregistered-components', rule, { `, options: [ { - ignorePatterns: [ - 'custom(\\-\\w+)+' - ] + ignorePatterns: ['custom(\\-\\w+)+'] } ], - errors: [{ - message: 'The "WarmButton" component has been used but not registered.', - line: 3 - }] + errors: [ + { + message: + 'The "WarmButton" component has been used but not registered.', + line: 3 + } + ] }, { filename: 'test.vue', @@ -477,10 +469,13 @@ tester.run('no-unregistered-components', rule, { } `, - errors: [{ - message: 'The "CustomComponent" component has been used but not registered.', - line: 3 - }] + errors: [ + { + message: + 'The "CustomComponent" component has been used but not registered.', + line: 3 + } + ] }, { filename: 'test.vue', @@ -498,10 +493,13 @@ tester.run('no-unregistered-components', rule, { } `, - errors: [{ - message: 'The "CustomComponent" component has been used but not registered.', - line: 3 - }] + errors: [ + { + message: + 'The "CustomComponent" component has been used but not registered.', + line: 3 + } + ] }, { filename: 'test.vue', @@ -517,10 +515,13 @@ tester.run('no-unregistered-components', rule, { } `, - errors: [{ - message: 'The "CustomComponent" component has been used but not registered.', - line: 3 - }] + errors: [ + { + message: + 'The "CustomComponent" component has been used but not registered.', + line: 3 + } + ] }, { filename: 'test.vue', @@ -538,10 +539,13 @@ tester.run('no-unregistered-components', rule, { } `, - errors: [{ - message: 'The "CustomComponent" component has been used but not registered.', - line: 3 - }] + errors: [ + { + message: + 'The "CustomComponent" component has been used but not registered.', + line: 3 + } + ] }, { filename: 'test.vue', @@ -557,10 +561,13 @@ tester.run('no-unregistered-components', rule, { } `, - errors: [{ - message: 'The "CustomComponent" component has been used but not registered.', - line: 3 - }] + errors: [ + { + message: + 'The "CustomComponent" component has been used but not registered.', + line: 3 + } + ] } ] }) diff --git a/tests/lib/rules/no-unsupported-features.js b/tests/lib/rules/no-unsupported-features.js index 6bebaae1a..d12109311 100644 --- a/tests/lib/rules/no-unsupported-features.js +++ b/tests/lib/rules/no-unsupported-features.js @@ -69,4 +69,3 @@ tester.run('no-unsupported-features', rule, { } ] }) - diff --git a/tests/lib/rules/no-unsupported-features/dynamic-directive-arguments.js b/tests/lib/rules/no-unsupported-features/dynamic-directive-arguments.js index de403b7ec..5dd9bba77 100644 --- a/tests/lib/rules/no-unsupported-features/dynamic-directive-arguments.js +++ b/tests/lib/rules/no-unsupported-features/dynamic-directive-arguments.js @@ -8,7 +8,10 @@ const RuleTester = require('eslint').RuleTester const rule = require('../../../../lib/rules/no-unsupported-features') const utils = require('./utils') -const buildOptions = utils.optionsBuilder('dynamic-directive-arguments', '^2.5.0') +const buildOptions = utils.optionsBuilder( + 'dynamic-directive-arguments', + '^2.5.0' +) const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), parserOptions: { @@ -83,4 +86,3 @@ tester.run('no-unsupported-features/dynamic-directive-arguments', rule, { } ] }) - diff --git a/tests/lib/rules/no-unsupported-features/slot-scope-attribute.js b/tests/lib/rules/no-unsupported-features/slot-scope-attribute.js index 137ed7c2a..f9be7a006 100644 --- a/tests/lib/rules/no-unsupported-features/slot-scope-attribute.js +++ b/tests/lib/rules/no-unsupported-features/slot-scope-attribute.js @@ -52,7 +52,10 @@ tester.run('no-unsupported-features/slot-scope-attribute', rule, { `, - options: buildOptions({ version: '^2.4.0', ignores: ['slot-scope-attribute'] }) + options: buildOptions({ + version: '^2.4.0', + ignores: ['slot-scope-attribute'] + }) } ], invalid: [ diff --git a/tests/lib/rules/no-unsupported-features/utils.js b/tests/lib/rules/no-unsupported-features/utils.js index 9c94f062c..0273961bd 100644 --- a/tests/lib/rules/no-unsupported-features/utils.js +++ b/tests/lib/rules/no-unsupported-features/utils.js @@ -16,8 +16,8 @@ module.exports = { * @param {string} defaultVersion default Vue.js version * @returns {function} the options builder */ - optionsBuilder (targetSyntax, defaultVersion) { - const baseIgnores = SYNTAXES.filter(s => s !== targetSyntax) + optionsBuilder(targetSyntax, defaultVersion) { + const baseIgnores = SYNTAXES.filter((s) => s !== targetSyntax) return (option) => { const ignores = [...baseIgnores] let version = defaultVersion diff --git a/tests/lib/rules/no-unsupported-features/v-bind-prop-modifier-shorthand.js b/tests/lib/rules/no-unsupported-features/v-bind-prop-modifier-shorthand.js index ac4662935..a49b61e93 100644 --- a/tests/lib/rules/no-unsupported-features/v-bind-prop-modifier-shorthand.js +++ b/tests/lib/rules/no-unsupported-features/v-bind-prop-modifier-shorthand.js @@ -8,7 +8,10 @@ const RuleTester = require('eslint').RuleTester const rule = require('../../../../lib/rules/no-unsupported-features') const utils = require('./utils') -const buildOptions = utils.optionsBuilder('v-bind-prop-modifier-shorthand', '^2.6.0') +const buildOptions = utils.optionsBuilder( + 'v-bind-prop-modifier-shorthand', + '^2.6.0' +) const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), parserOptions: { @@ -60,7 +63,8 @@ tester.run('no-unsupported-features/v-bind-prop-modifier-shorthand', rule, { `, errors: [ { - message: '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".', + message: + '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".', line: 3 } ] @@ -77,11 +81,11 @@ tester.run('no-unsupported-features/v-bind-prop-modifier-shorthand', rule, { `, errors: [ { - message: '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".', + message: + '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".', line: 3 } ] } ] }) - diff --git a/tests/lib/rules/no-unsupported-features/v-slot.js b/tests/lib/rules/no-unsupported-features/v-slot.js index 95b1eef13..6874731bd 100644 --- a/tests/lib/rules/no-unsupported-features/v-slot.js +++ b/tests/lib/rules/no-unsupported-features/v-slot.js @@ -74,7 +74,6 @@ tester.run('no-unsupported-features/v-slot', rule, { } ], invalid: [ - { code: ` `, - errors: [{ - message: "The 'users' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.", - line: 6 - }] + errors: [ + { + message: + "The 'users' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.", + line: 6 + } + ] }, { filename: 'test.vue', @@ -184,18 +205,24 @@ tester.run('no-use-v-if-with-v-for', rule, { `, - errors: [{ - message: "This 'v-if' should be moved to the wrapper element.", - line: 6 - }] + errors: [ + { + message: "This 'v-if' should be moved to the wrapper element.", + line: 6 + } + ] }, { filename: 'test.vue', - code: '', - errors: [{ - message: "The 'list' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.", - line: 1 - }] + code: + '', + errors: [ + { + message: + "The 'list' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.", + line: 1 + } + ] }, { filename: 'test.vue', @@ -212,10 +239,13 @@ tester.run('no-use-v-if-with-v-for', rule, { `, - errors: [{ - message: "The 'users' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.", - line: 6 - }] + errors: [ + { + message: + "The 'users' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.", + line: 6 + } + ] }, { filename: 'test.vue', @@ -232,26 +262,36 @@ tester.run('no-use-v-if-with-v-for', rule, { `, - errors: [{ - message: "This 'v-if' should be moved to the wrapper element.", - line: 6 - }] + errors: [ + { + message: "This 'v-if' should be moved to the wrapper element.", + line: 6 + } + ] }, { filename: 'test.vue', - code: '', - errors: [{ - message: "The 'list()' expression inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.", - line: 1 - }] + code: + '', + errors: [ + { + message: + "The 'list()' expression inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.", + line: 1 + } + ] }, { filename: 'test.vue', - code: '', - errors: [{ - message: "The '5' expression inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.", - line: 1 - }] + code: + '', + errors: [ + { + message: + "The '5' expression inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.", + line: 1 + } + ] } ] }) diff --git a/tests/lib/rules/no-v-model-argument.js b/tests/lib/rules/no-v-model-argument.js index 5cf5c5952..cba98ebe4 100644 --- a/tests/lib/rules/no-v-model-argument.js +++ b/tests/lib/rules/no-v-model-argument.js @@ -21,7 +21,6 @@ const ruleTester = new RuleTester({ }) ruleTester.run('no-v-model-argument', rule, { - valid: [ { filename: 'test.vue', @@ -32,12 +31,14 @@ ruleTester.run('no-v-model-argument', rule, { invalid: [ { filename: 'test.vue', - code: '', + code: + '', errors: ["'v-model' directives require no argument."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["'v-model' directives require no argument."] } ] diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js index 5290ec07d..eb932b299 100644 --- a/tests/lib/rules/prop-name-casing.js +++ b/tests/lib/rules/prop-name-casing.js @@ -22,7 +22,6 @@ const parserOptions = { const ruleTester = new RuleTester() ruleTester.run('prop-name-casing', rule, { - valid: [ { filename: 'test.vue', @@ -361,11 +360,13 @@ ruleTester.run('prop-name-casing', rule, { } `, parserOptions, - errors: [{ - message: 'Prop "greeting_text" is not in camelCase.', - type: 'Property', - line: 4 - }] + errors: [ + { + message: 'Prop "greeting_text" is not in camelCase.', + type: 'Property', + line: 4 + } + ] }, { filename: 'test.vue', @@ -378,11 +379,13 @@ ruleTester.run('prop-name-casing', rule, { `, options: ['camelCase'], parserOptions, - errors: [{ - message: 'Prop "greeting_text" is not in camelCase.', - type: 'Property', - line: 4 - }] + errors: [ + { + message: 'Prop "greeting_text" is not in camelCase.', + type: 'Property', + line: 4 + } + ] }, { filename: 'test.vue', @@ -393,11 +396,13 @@ ruleTester.run('prop-name-casing', rule, { `, options: ['camelCase'], parserOptions, - errors: [{ - message: 'Prop "greeting_text" is not in camelCase.', - type: 'Literal', - line: 3 - }] + errors: [ + { + message: 'Prop "greeting_text" is not in camelCase.', + type: 'Literal', + line: 3 + } + ] }, { filename: 'test.vue', @@ -410,11 +415,13 @@ ruleTester.run('prop-name-casing', rule, { `, options: ['snake_case'], parserOptions, - errors: [{ - message: 'Prop "greetingText" is not in snake_case.', - type: 'Property', - line: 4 - }] + errors: [ + { + message: 'Prop "greetingText" is not in snake_case.', + type: 'Property', + line: 4 + } + ] }, { filename: 'test.vue', @@ -427,11 +434,13 @@ ruleTester.run('prop-name-casing', rule, { `, options: ['camelCase'], parserOptions, - errors: [{ - message: 'Prop "greeting-text" is not in camelCase.', - type: 'Property', - line: 4 - }] + errors: [ + { + message: 'Prop "greeting-text" is not in camelCase.', + type: 'Property', + line: 4 + } + ] }, { filename: 'test.vue', @@ -444,11 +453,13 @@ ruleTester.run('prop-name-casing', rule, { `, options: ['snake_case'], parserOptions, - errors: [{ - message: 'Prop "greeting-text" is not in snake_case.', - type: 'Property', - line: 4 - }] + errors: [ + { + message: 'Prop "greeting-text" is not in snake_case.', + type: 'Property', + line: 4 + } + ] }, { filename: 'test.vue', @@ -460,11 +471,13 @@ ruleTester.run('prop-name-casing', rule, { } `, parserOptions, - errors: [{ - message: 'Prop "greeting_text" is not in camelCase.', - type: 'Property', - line: 4 - }] + errors: [ + { + message: 'Prop "greeting_text" is not in camelCase.', + type: 'Property', + line: 4 + } + ] }, { // computed property name @@ -477,11 +490,13 @@ ruleTester.run('prop-name-casing', rule, { } `, parserOptions, - errors: [{ - message: 'Prop "greeting-text" is not in camelCase.', - type: 'Property', - line: 4 - }] + errors: [ + { + message: 'Prop "greeting-text" is not in camelCase.', + type: 'Property', + line: 4 + } + ] }, { // shorthand @@ -494,11 +509,13 @@ ruleTester.run('prop-name-casing', rule, { } `, parserOptions, - errors: [{ - message: 'Prop "greeting_text" is not in camelCase.', - type: 'Property', - line: 4 - }] + errors: [ + { + message: 'Prop "greeting_text" is not in camelCase.', + type: 'Property', + line: 4 + } + ] }, { filename: 'test.vue', @@ -510,11 +527,13 @@ ruleTester.run('prop-name-casing', rule, { } `, parserOptions, - errors: [{ - message: 'Prop "abc-123-def" is not in camelCase.', - type: 'Property', - line: 4 - }] + errors: [ + { + message: 'Prop "abc-123-def" is not in camelCase.', + type: 'Property', + line: 4 + } + ] }, { // Parentheses computed property name @@ -527,11 +546,13 @@ ruleTester.run('prop-name-casing', rule, { } `, parserOptions, - errors: [{ - message: 'Prop "greeting-text" is not in camelCase.', - type: 'Property', - line: 4 - }] + errors: [ + { + message: 'Prop "greeting-text" is not in camelCase.', + type: 'Property', + line: 4 + } + ] }, { filename: 'test.vue', @@ -558,6 +579,5 @@ ruleTester.run('prop-name-casing', rule, { parserOptions, errors: ['Prop "_itemName" is not in snake_case.'] } - ] }) diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js index b544921bd..ce292774e 100644 --- a/tests/lib/rules/require-default-prop.js +++ b/tests/lib/rules/require-default-prop.js @@ -21,7 +21,6 @@ const parserOptions = { const ruleTester = new RuleTester({ parserOptions }) ruleTester.run('require-default-prop', rule, { - valid: [ { filename: 'test.vue', @@ -205,25 +204,32 @@ ruleTester.run('require-default-prop', rule, { } } `, - errors: [{ - message: `Prop 'a' requires default value to be set.`, - line: 4 - }, { - message: `Prop 'b' requires default value to be set.`, - line: 5 - }, { - message: `Prop 'c' requires default value to be set.`, - line: 6 - }, { - message: `Prop 'd' requires default value to be set.`, - line: 9 - }, { - message: `Prop 'e' requires default value to be set.`, - line: 13 - }, { - message: `Prop 'f' requires default value to be set.`, - line: 14 - }] + errors: [ + { + message: `Prop 'a' requires default value to be set.`, + line: 4 + }, + { + message: `Prop 'b' requires default value to be set.`, + line: 5 + }, + { + message: `Prop 'c' requires default value to be set.`, + line: 6 + }, + { + message: `Prop 'd' requires default value to be set.`, + line: 9 + }, + { + message: `Prop 'e' requires default value to be set.`, + line: 13 + }, + { + message: `Prop 'f' requires default value to be set.`, + line: 14 + } + ] }, { filename: 'test.vue', @@ -237,10 +243,12 @@ ruleTester.run('require-default-prop', rule, { }); `, parser: require.resolve('@typescript-eslint/parser'), - errors: [{ - message: `Prop 'a' requires default value to be set.`, - line: 4 - }] + errors: [ + { + message: `Prop 'a' requires default value to be set.`, + line: 4 + } + ] }, { filename: 'test.vue', @@ -254,10 +262,12 @@ ruleTester.run('require-default-prop', rule, { }); `, parser: require.resolve('@typescript-eslint/parser'), - errors: [{ - message: `Prop 'a' requires default value to be set.`, - line: 4 - }] + errors: [ + { + message: `Prop 'a' requires default value to be set.`, + line: 4 + } + ] }, // computed propertys @@ -273,19 +283,24 @@ ruleTester.run('require-default-prop', rule, { } }; `, - errors: [{ - message: `Prop 'a' requires default value to be set.`, - line: 4 - }, { - message: `Prop 'b' requires default value to be set.`, - line: 5 - }, { - message: `Prop 'c' requires default value to be set.`, - line: 6 - }, { - message: `Prop 'd' requires default value to be set.`, - line: 7 - }] + errors: [ + { + message: `Prop 'a' requires default value to be set.`, + line: 4 + }, + { + message: `Prop 'b' requires default value to be set.`, + line: 5 + }, + { + message: `Prop 'c' requires default value to be set.`, + line: 6 + }, + { + message: `Prop 'd' requires default value to be set.`, + line: 7 + } + ] }, // unknown static name { @@ -299,16 +314,20 @@ ruleTester.run('require-default-prop', rule, { } }; `, - errors: [{ - message: `Prop '[foo]' requires default value to be set.`, - line: 4 - }, { - message: `Prop '[bar()]' requires default value to be set.`, - line: 5 - }, { - message: `Prop '[baz.baz]' requires default value to be set.`, - line: 6 - }] + errors: [ + { + message: `Prop '[foo]' requires default value to be set.`, + line: 4 + }, + { + message: `Prop '[bar()]' requires default value to be set.`, + line: 5 + }, + { + message: `Prop '[baz.baz]' requires default value to be set.`, + line: 6 + } + ] }, { // https://github.com/vuejs/eslint-plugin-vue/issues/1040 @@ -322,7 +341,7 @@ ruleTester.run('require-default-prop', rule, { } } `, - errors: ['Prop \'foo\' requires default value to be set.'] + errors: ["Prop 'foo' requires default value to be set."] }, { filename: 'unknown-prop-details-test.vue', @@ -335,7 +354,7 @@ ruleTester.run('require-default-prop', rule, { } } `, - errors: ['Prop \'foo\' requires default value to be set.'] + errors: ["Prop 'foo' requires default value to be set."] } ] }) diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js index 0a084ab43..1db1edb2b 100644 --- a/tests/lib/rules/require-explicit-emits.js +++ b/tests/lib/rules/require-explicit-emits.js @@ -382,7 +382,8 @@ tester.run('require-explicit-emits', rule, { endColumn: 33, suggestions: [ { - desc: 'Add the `emits` option with array syntax and define "foo" event.', + desc: + 'Add the `emits` option with array syntax and define "foo" event.', output: ` `, - errors: [{ messageId: 'expectedLongform', data: { actual: 'v-slot', argument: 'default' }}], + errors: [ + { + messageId: 'expectedLongform', + data: { actual: 'v-slot', argument: 'default' } + } + ], options: [{ default: 'longform' }] }, { @@ -346,7 +396,12 @@ tester.run('v-slot-style', rule, { `, - errors: [{ messageId: 'expectedVSlot', data: { actual: '#default', argument: 'default' }}], + errors: [ + { + messageId: 'expectedVSlot', + data: { actual: '#default', argument: 'default' } + } + ], options: [{ default: 'v-slot' }] }, { @@ -364,7 +419,12 @@ tester.run('v-slot-style', rule, { `, - errors: [{ messageId: 'expectedVSlot', data: { actual: 'v-slot:default', argument: 'default' }}], + errors: [ + { + messageId: 'expectedVSlot', + data: { actual: 'v-slot:default', argument: 'default' } + } + ], options: [{ default: 'v-slot' }] }, @@ -383,7 +443,12 @@ tester.run('v-slot-style', rule, { `, - errors: [{ messageId: 'expectedShorthand', data: { actual: 'v-slot:foo', argument: 'foo' }}] + errors: [ + { + messageId: 'expectedShorthand', + data: { actual: 'v-slot:foo', argument: 'foo' } + } + ] }, { code: ` @@ -400,7 +465,12 @@ tester.run('v-slot-style', rule, { `, - errors: [{ messageId: 'expectedLongform', data: { actual: '#foo', argument: 'foo' }}], + errors: [ + { + messageId: 'expectedLongform', + data: { actual: '#foo', argument: 'foo' } + } + ], options: [{ named: 'longform' }] }, @@ -419,7 +489,12 @@ tester.run('v-slot-style', rule, { `, - errors: [{ messageId: 'expectedShorthand', data: { actual: 'v-slot:[foo]', argument: '[foo]' }}] + errors: [ + { + messageId: 'expectedShorthand', + data: { actual: 'v-slot:[foo]', argument: '[foo]' } + } + ] }, { code: ` @@ -436,7 +511,12 @@ tester.run('v-slot-style', rule, { `, - errors: [{ messageId: 'expectedLongform', data: { actual: '#[foo]', argument: '[foo]' }}], + errors: [ + { + messageId: 'expectedLongform', + data: { actual: '#[foo]', argument: '[foo]' } + } + ], options: [{ named: 'longform' }] } ] diff --git a/tests/lib/rules/valid-template-root.js b/tests/lib/rules/valid-template-root.js index 033188cca..212fefc8d 100644 --- a/tests/lib/rules/valid-template-root.js +++ b/tests/lib/rules/valid-template-root.js @@ -41,11 +41,13 @@ tester.run('valid-template-root', rule, { }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', @@ -57,7 +59,8 @@ tester.run('valid-template-root', rule, { }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', @@ -117,12 +120,16 @@ tester.run('valid-template-root', rule, { { filename: 'test.vue', code: '', - errors: ["The template root with 'src' attribute is required to be empty."] + errors: [ + "The template root with 'src' attribute is required to be empty." + ] }, { filename: 'test.vue', code: '', - errors: ["The template root with 'src' attribute is required to be empty."] + errors: [ + "The template root with 'src' attribute is required to be empty." + ] } ] }) diff --git a/tests/lib/rules/valid-v-bind-sync.js b/tests/lib/rules/valid-v-bind-sync.js index 4f939aa16..385f5d991 100644 --- a/tests/lib/rules/valid-v-bind-sync.js +++ b/tests/lib/rules/valid-v-bind-sync.js @@ -51,51 +51,63 @@ tester.run('valid-v-bind-sync', rule, { }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, // not .sync { @@ -128,12 +140,15 @@ tester.run('valid-v-bind-sync', rule, { `, - errors: [{ - message: "'.sync' modifiers require the attribute value which is valid as LHS.", - line: 3, - column: 24, - endColumn: 41 - }] + errors: [ + { + message: + "'.sync' modifiers require the attribute value which is valid as LHS.", + line: 3, + column: 24, + endColumn: 41 + } + ] }, { filename: 'test.vue', @@ -142,12 +157,15 @@ tester.run('valid-v-bind-sync', rule, { `, - errors: [{ - message: "'.sync' modifiers require the attribute value which is valid as LHS.", - line: 3, - column: 24, - endColumn: 47 - }] + errors: [ + { + message: + "'.sync' modifiers require the attribute value which is valid as LHS.", + line: 3, + column: 24, + endColumn: 47 + } + ] }, { filename: 'test.vue', @@ -156,12 +174,15 @@ tester.run('valid-v-bind-sync', rule, { `, - errors: [{ - message: "'.sync' modifiers aren't supported on non Vue-components.", - line: 3, - column: 18, - endColumn: 33 - }] + errors: [ + { + message: + "'.sync' modifiers aren't supported on non Vue-components.", + line: 3, + column: 18, + endColumn: 33 + } + ] }, { filename: 'test.vue', @@ -170,12 +191,15 @@ tester.run('valid-v-bind-sync', rule, { `, - errors: [{ - message: "'.sync' modifiers require the attribute value which is valid as LHS.", - line: 3, - column: 24, - endColumn: 41 - }] + errors: [ + { + message: + "'.sync' modifiers require the attribute value which is valid as LHS.", + line: 3, + column: 24, + endColumn: 41 + } + ] }, { filename: 'test.vue', @@ -184,12 +208,15 @@ tester.run('valid-v-bind-sync', rule, { `, - errors: [{ - message: "'.sync' modifiers require the attribute value which is valid as LHS.", - line: 3, - column: 24, - endColumn: 46 - }] + errors: [ + { + message: + "'.sync' modifiers require the attribute value which is valid as LHS.", + line: 3, + column: 24, + endColumn: 46 + } + ] }, { filename: 'test.vue', @@ -198,12 +225,15 @@ tester.run('valid-v-bind-sync', rule, { `, - errors: [{ - message: "'.sync' modifiers aren't supported on non Vue-components.", - line: 3, - column: 18, - endColumn: 39 - }] + errors: [ + { + message: + "'.sync' modifiers aren't supported on non Vue-components.", + line: 3, + column: 18, + endColumn: 39 + } + ] }, { filename: 'test.vue', @@ -214,12 +244,15 @@ tester.run('valid-v-bind-sync', rule, {
`, - errors: [{ - message: "'.sync' modifiers cannot update the iteration variable 'x' itself.", - line: 4, - column: 26, - endColumn: 39 - }] + errors: [ + { + message: + "'.sync' modifiers cannot update the iteration variable 'x' itself.", + line: 4, + column: 26, + endColumn: 39 + } + ] }, { filename: 'test.vue', @@ -230,12 +263,15 @@ tester.run('valid-v-bind-sync', rule, {
`, - errors: [{ - message: "'.sync' modifiers cannot update the iteration variable 'e' itself.", - line: 4, - column: 26, - endColumn: 45 - }] + errors: [ + { + message: + "'.sync' modifiers cannot update the iteration variable 'e' itself.", + line: 4, + column: 26, + endColumn: 45 + } + ] }, { filename: 'test.vue', @@ -250,10 +286,13 @@ tester.run('valid-v-bind-sync', rule, { `, - errors: [{ - message: "'.sync' modifiers cannot update the iteration variable 'e1' itself.", - line: 6 - }] + errors: [ + { + message: + "'.sync' modifiers cannot update the iteration variable 'e1' itself.", + line: 6 + } + ] }, { filename: 'test.vue', @@ -264,10 +303,13 @@ tester.run('valid-v-bind-sync', rule, { `, - errors: [{ - message: "'.sync' modifiers cannot update the iteration variable 'index' itself.", - line: 4 - }] + errors: [ + { + message: + "'.sync' modifiers cannot update the iteration variable 'index' itself.", + line: 4 + } + ] }, { filename: 'test.vue', @@ -276,17 +318,23 @@ tester.run('valid-v-bind-sync', rule, {
`, - errors: ["'.sync' modifiers aren't supported on
non Vue-components."] + errors: [ + "'.sync' modifiers aren't supported on
non Vue-components." + ] }, { filename: 'test.vue', code: '', - errors: ["'.sync' modifiers require the attribute value which is valid as LHS."] + errors: [ + "'.sync' modifiers require the attribute value which is valid as LHS." + ] }, { filename: 'test.vue', code: '', - errors: ["'.sync' modifiers require the attribute value which is valid as LHS."] + errors: [ + "'.sync' modifiers require the attribute value which is valid as LHS." + ] } ] }) diff --git a/tests/lib/rules/valid-v-else-if.js b/tests/lib/rules/valid-v-else-if.js index b0c18093f..778d07339 100644 --- a/tests/lib/rules/valid-v-else-if.js +++ b/tests/lib/rules/valid-v-else-if.js @@ -29,11 +29,13 @@ tester.run('valid-v-else-if', rule, { }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', @@ -43,57 +45,82 @@ tester.run('valid-v-else-if', rule, { invalid: [ { filename: 'test.vue', - code: '', - errors: ["'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + code: + '', + errors: [ + "'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', code: '', - errors: ["'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + errors: [ + "'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', code: '', - errors: ["'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + errors: [ + "'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + code: + '', + errors: [ + "'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + code: + '', + errors: [ + "'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + code: + '', + errors: [ + "'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["'v-else-if' and 'v-if' directives can't exist on the same element."] + code: + '', + errors: [ + "'v-else-if' and 'v-if' directives can't exist on the same element." + ] }, { filename: 'test.vue', - code: '', - errors: ["'v-else-if' and 'v-else' directives can't exist on the same element."] + code: + '', + errors: [ + "'v-else-if' and 'v-else' directives can't exist on the same element." + ] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["'v-else-if' directives require no argument."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["'v-else-if' directives require no modifier."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["'v-else-if' directives require that attribute value."] } ] diff --git a/tests/lib/rules/valid-v-else.js b/tests/lib/rules/valid-v-else.js index 7825e4dfb..1e235f9b5 100644 --- a/tests/lib/rules/valid-v-else.js +++ b/tests/lib/rules/valid-v-else.js @@ -29,11 +29,13 @@ tester.run('valid-v-else', rule, { }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', @@ -44,56 +46,79 @@ tester.run('valid-v-else', rule, { { filename: 'test.vue', code: '', - errors: ["'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + errors: [ + "'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', code: '', - errors: ["'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + errors: [ + "'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', code: '', - errors: ["'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + errors: [ + "'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', code: '', - errors: ["'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + errors: [ + "'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + code: + '', + errors: [ + "'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."] + code: + '', + errors: [ + "'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["'v-else' and 'v-if' directives can't exist on the same element. You may want 'v-else-if' directives."] + code: + '', + errors: [ + "'v-else' and 'v-if' directives can't exist on the same element. You may want 'v-else-if' directives." + ] }, { filename: 'test.vue', - code: '', - errors: ["'v-else' and 'v-else-if' directives can't exist on the same element."] + code: + '', + errors: [ + "'v-else' and 'v-else-if' directives can't exist on the same element." + ] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["'v-else' directives require no argument."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["'v-else' directives require no modifier."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["'v-else' directives require no attribute value."] } ] diff --git a/tests/lib/rules/valid-v-for.js b/tests/lib/rules/valid-v-for.js index d83b634a3..74a1ea614 100644 --- a/tests/lib/rules/valid-v-for.js +++ b/tests/lib/rules/valid-v-for.js @@ -37,55 +37,68 @@ tester.run('valid-v-for', rule, { }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', - code: '' + code: + '' }, { filename: 'test.vue', @@ -134,92 +147,123 @@ tester.run('valid-v-for', rule, { }, { filename: 'test.vue', - code: '', + code: + '', errors: ["Invalid alias ''."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["Invalid alias ''."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["Invalid alias ''."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["Invalid alias '{b,c}'."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["Invalid alias '{c,d}'."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["Custom elements in iteration require 'v-bind:key' directives."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["Custom elements in iteration require 'v-bind:key' directives."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["Custom elements in iteration require 'v-bind:key' directives."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["Custom elements in iteration require 'v-bind:key' directives."] }, { filename: 'test.vue', - code: '', - errors: ["Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."] + code: + '', + errors: [ + "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."] + code: + '', + errors: [ + "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."] + code: + '', + errors: [ + "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."] + code: + '', + errors: [ + "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive." + ] }, { filename: 'test.vue', - code: '', - errors: ["Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."] + code: + '', + errors: [ + "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive." + ] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["Custom elements in iteration require 'v-bind:key' directives."] }, { filename: 'test.vue', - code: '', + code: + '', errors: ["'v-for' directives require that attribute value."] }, { filename: 'test.vue', - code: '', - errors: ["Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."] + code: + '', + errors: [ + "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive." + ] }, { filename: 'test.vue', - errors: ["Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."], + errors: [ + "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive." + ], code: ` ` + }, + // empty value + { + filename: 'empty-value.vue', + code: '', + errors: ["'v-for' directives require that attribute value."] } ] }) diff --git a/tests/lib/rules/valid-v-html.js b/tests/lib/rules/valid-v-html.js index 9a30bcf48..afb312f74 100644 --- a/tests/lib/rules/valid-v-html.js +++ b/tests/lib/rules/valid-v-html.js @@ -30,6 +30,16 @@ tester.run('valid-v-html', rule, { { filename: 'test.vue', code: '' + }, + // parsing error + { + filename: 'parsing-error.vue', + code: '' + }, + // comment value (parsing error) + { + filename: 'comment-value.vue', + code: '' } ], invalid: [ @@ -47,6 +57,12 @@ tester.run('valid-v-html', rule, { filename: 'test.vue', code: '', errors: ["'v-html' directives require that attribute value."] + }, + // empty value + { + filename: 'empty-value.vue', + code: '', + errors: ["'v-html' directives require that attribute value."] } ] }) diff --git a/tests/lib/rules/valid-v-if.js b/tests/lib/rules/valid-v-if.js index effe04836..d3afe5780 100644 --- a/tests/lib/rules/valid-v-if.js +++ b/tests/lib/rules/valid-v-if.js @@ -30,6 +30,16 @@ tester.run('valid-v-if', rule, { { filename: 'test.vue', code: '' + }, + // parsing error + { + filename: 'parsing-error.vue', + code: '' + }, + // comment value (parsing error) + { + filename: 'comment-value.vue', + code: '' } ], invalid: [ @@ -62,6 +72,12 @@ tester.run('valid-v-if', rule, { filename: 'test.vue', code: '', errors: ["'v-if' directives require that attribute value."] + }, + // empty value + { + filename: 'empty-value.vue', + code: '', + errors: ["'v-if' directives require that attribute value."] } ] }) diff --git a/tests/lib/rules/valid-v-model.js b/tests/lib/rules/valid-v-model.js index c9c28585a..9e78ef47a 100644 --- a/tests/lib/rules/valid-v-model.js +++ b/tests/lib/rules/valid-v-model.js @@ -150,6 +150,16 @@ tester.run('valid-v-model', rule, { filename: 'test.vue', code: '' + }, + // parsing error + { + filename: 'parsing-error.vue', + code: '' + }, + // comment value (parsing error) + { + filename: 'comment-value.vue', + code: '' } ], invalid: [ @@ -216,6 +226,12 @@ tester.run('valid-v-model', rule, { errors: [ "'v-model' directives cannot update the iteration variable 'e' itself." ] + }, + // empty value + { + filename: 'empty-value.vue', + code: '', + errors: ["'v-model' directives require that attribute value."] } ] }) diff --git a/tests/lib/rules/valid-v-on.js b/tests/lib/rules/valid-v-on.js index 911039ea5..6f162e1df 100644 --- a/tests/lib/rules/valid-v-on.js +++ b/tests/lib/rules/valid-v-on.js @@ -96,6 +96,33 @@ tester.run('valid-v-on', rule, { filename: 'test.vue', code: '', options: [{ modifiers: ['bar', 'aaa'] }] + }, + // parsing error + { + filename: 'parsing-error.vue', + code: '' + }, + // comment value (valid) + { + filename: 'comment-value.vue', + code: '' + }, + { + filename: 'comment-value.vue', + code: '' + }, + { + filename: 'comment-value.vue', + code: '' + }, + { + filename: 'comment-value.vue', + code: '' + }, + // empty value + { + filename: 'empty-value.vue', + code: '' } ], invalid: [ @@ -139,6 +166,14 @@ tester.run('valid-v-on', rule, { filename: 'test.vue', code: '', errors: ['Avoid using JavaScript keyword as "v-on" value: "delete".'] + }, + // empty value + { + filename: 'empty-value.vue', + code: '', + errors: [ + "'v-on' directives require a value or verb modifier (like 'stop' or 'prevent')." + ] } ] }) diff --git a/tests/lib/rules/valid-v-once.js b/tests/lib/rules/valid-v-once.js index 0c6a71a20..c9f3750c3 100644 --- a/tests/lib/rules/valid-v-once.js +++ b/tests/lib/rules/valid-v-once.js @@ -47,6 +47,24 @@ tester.run('valid-v-once', rule, { filename: 'test.vue', code: '', errors: ["'v-once' directives require no attribute value."] + }, + // parsing error + { + filename: 'parsing-error.vue', + code: '', + errors: ["'v-once' directives require no attribute value."] + }, + // comment value + { + filename: 'comment-value.vue', + code: '', + errors: ["'v-once' directives require no attribute value."] + }, + // empty value + { + filename: 'comment-value.vue', + code: '', + errors: ["'v-once' directives require no attribute value."] } ] }) diff --git a/tests/lib/rules/valid-v-pre.js b/tests/lib/rules/valid-v-pre.js index ae9d38e6f..3d9c9d883 100644 --- a/tests/lib/rules/valid-v-pre.js +++ b/tests/lib/rules/valid-v-pre.js @@ -47,6 +47,24 @@ tester.run('valid-v-pre', rule, { filename: 'test.vue', code: '', errors: ["'v-pre' directives require no attribute value."] + }, + // parsing error + { + filename: 'parsing-error.vue', + code: '', + errors: ["'v-pre' directives require no attribute value."] + }, + // comment value + { + filename: 'comment-value.vue', + code: '', + errors: ["'v-pre' directives require no attribute value."] + }, + // empty value + { + filename: 'empty-value.vue', + code: '', + errors: ["'v-pre' directives require no attribute value."] } ] }) diff --git a/tests/lib/rules/valid-v-show.js b/tests/lib/rules/valid-v-show.js index 0471ccd9f..4220e8749 100644 --- a/tests/lib/rules/valid-v-show.js +++ b/tests/lib/rules/valid-v-show.js @@ -30,6 +30,16 @@ tester.run('valid-v-show', rule, { { filename: 'test.vue', code: '' + }, + // parsing error + { + filename: 'parsing-error.vue', + code: '' + }, + // comment value (parsing error) + { + filename: 'comment-value.vue', + code: '' } ], invalid: [ @@ -48,8 +58,9 @@ tester.run('valid-v-show', rule, { code: '', errors: ["'v-show' directives require that attribute value."] }, + // empty value { - filename: 'test.vue', + filename: 'empty-value.vue', code: '', errors: ["'v-show' directives require that attribute value."] } diff --git a/tests/lib/rules/valid-v-slot.js b/tests/lib/rules/valid-v-slot.js index ab8307fed..0043756f1 100644 --- a/tests/lib/rules/valid-v-slot.js +++ b/tests/lib/rules/valid-v-slot.js @@ -83,7 +83,13 @@ tester.run('valid-v-slot', rule, { - ` + `, + // parsing error + { + filename: 'parsing-error.vue', + code: + '' + } ], invalid: [ // Verify location. @@ -294,6 +300,26 @@ tester.run('valid-v-slot', rule, { `, errors: [{ messageId: 'requireAttributeValue' }] + }, + // comment value + { + filename: 'comment-value1.vue', + code: + '', + errors: [{ messageId: 'requireAttributeValue' }] + }, + { + filename: 'comment-value2.vue', + code: + '', + errors: [{ messageId: 'requireAttributeValue' }] + }, + // empty value + { + filename: 'empty-value.vue', + code: + '', + errors: [{ messageId: 'requireAttributeValue' }] } ] }) diff --git a/tests/lib/rules/valid-v-text.js b/tests/lib/rules/valid-v-text.js index 3956462ce..611b1a31a 100644 --- a/tests/lib/rules/valid-v-text.js +++ b/tests/lib/rules/valid-v-text.js @@ -34,6 +34,16 @@ tester.run('valid-v-text', rule, { { filename: 'test.vue', code: '' + }, + // parsing error + { + filename: 'parsing-error.vue', + code: '' + }, + // comment value (parsing error) + { + filename: 'parsing-error.vue', + code: '' } ], invalid: [ @@ -51,6 +61,12 @@ tester.run('valid-v-text', rule, { filename: 'test.vue', code: '', errors: ["'v-text' directives require that attribute value."] + }, + // empty value + { + filename: 'empty-value.vue', + code: '', + errors: ["'v-text' directives require that attribute value."] } ] }) From e5c835eb957340b153ea2047e3ba274b88ba01bb Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Fri, 5 Jun 2020 14:29:35 +0900 Subject: [PATCH 0107/1010] Add `vue/no-bare-strings-in-template` rule (#1185) --- docs/rules/README.md | 1 + docs/rules/no-bare-strings-in-template.md | 88 ++++++ lib/index.js | 1 + lib/rules/no-bare-strings-in-template.js | 279 +++++++++++++++++ lib/utils/regexp.js | 14 +- package.json | 2 + .../lib/rules/no-bare-strings-in-template.js | 290 ++++++++++++++++++ 7 files changed, 673 insertions(+), 2 deletions(-) create mode 100644 docs/rules/no-bare-strings-in-template.md create mode 100644 lib/rules/no-bare-strings-in-template.js create mode 100644 tests/lib/rules/no-bare-strings-in-template.js diff --git a/docs/rules/README.md b/docs/rules/README.md index f3e2bfdf6..55b3c4e0d 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -282,6 +282,7 @@ For example: | [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: | | [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: | | [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name | | +| [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in ` `, - options: [{ ignoreWhenEmpty: false, ignoreWhenNoAttributes: false }], output: ` `, + options: [{ ignoreWhenEmpty: false, ignoreWhenNoAttributes: false }], errors: [ 'Expected 1 line break after opening tag (`
`), but no line breaks found.' ] @@ -473,13 +473,13 @@ singleline element
`, - options: [{ ignoreWhenEmpty: false, ignoreWhenNoAttributes: false }], output: ` `, + options: [{ ignoreWhenEmpty: false, ignoreWhenNoAttributes: false }], errors: [ 'Expected 1 line break after opening tag (`
`), but no line breaks found.' ] diff --git a/tests/lib/rules/space-in-parens.js b/tests/lib/rules/space-in-parens.js index 8791652ed..ef6fe628e 100644 --- a/tests/lib/rules/space-in-parens.js +++ b/tests/lib/rules/space-in-parens.js @@ -94,13 +94,13 @@ tester.run('space-in-parens', rule, { @click="foo(arg)" /> `, - options: ['always'], output: ` `, + options: ['always'], errors: [ errorMessage({ messageId: 'missingOpeningSpace', @@ -143,13 +143,13 @@ tester.run('space-in-parens', rule, { :value="(1 + 2) + 3" > `, - options: ['always'], output: ` `, + options: ['always'], errors: [ errorMessage({ messageId: 'missingOpeningSpace', @@ -192,13 +192,13 @@ tester.run('space-in-parens', rule, { :[(1+2)]="(1 + 2) + 3" > `, - options: ['always'], output: ` `, + options: ['always'], errors: [ errorMessage({ messageId: 'missingOpeningSpace', diff --git a/tests/lib/rules/space-unary-ops.js b/tests/lib/rules/space-unary-ops.js index 6b3e7c25f..63539a5a7 100644 --- a/tests/lib/rules/space-unary-ops.js +++ b/tests/lib/rules/space-unary-ops.js @@ -50,8 +50,8 @@ tester.run('space-unary-ops', rule, { }, { code: '', - options: [{ nonwords: true }], output: '', + options: [{ nonwords: true }], errors: ["Unary operator '!' must be followed by whitespace."] }, diff --git a/tests/lib/rules/template-curly-spacing.js b/tests/lib/rules/template-curly-spacing.js index 6c4376969..0fe530511 100644 --- a/tests/lib/rules/template-curly-spacing.js +++ b/tests/lib/rules/template-curly-spacing.js @@ -41,14 +41,13 @@ tester.run('template-curly-spacing', rule, { }, // CSS vars injection - { - code: ` + ` ` - } + + ` ], invalid: [ { @@ -79,12 +78,12 @@ tester.run('template-curly-spacing', rule, {
`, - options: ['always'], output: ` `, + options: ['always'], errors: [ { message: "Expected space(s) after '${'.", diff --git a/tests/lib/rules/this-in-template.js b/tests/lib/rules/this-in-template.js index 904aa00c4..5686bf137 100644 --- a/tests/lib/rules/this-in-template.js +++ b/tests/lib/rules/this-in-template.js @@ -248,14 +248,14 @@ ruleTester.run('this-in-template', rule, { { code: ``, output: ``, - errors: ["Unexpected usage of 'this'."], - options: ['never'] + options: ['never'], + errors: ["Unexpected usage of 'this'."] }, { code: ``, output: ``, - errors: ["Unexpected usage of 'this'."], - options: ['never'] + options: ['never'], + errors: ["Unexpected usage of 'this'."] } ] }) diff --git a/tests/lib/rules/use-v-on-exact.js b/tests/lib/rules/use-v-on-exact.js index 73ac2408b..929fd746a 100644 --- a/tests/lib/rules/use-v-on-exact.js +++ b/tests/lib/rules/use-v-on-exact.js @@ -15,92 +15,48 @@ const ruleTester = new RuleTester({ ruleTester.run('use-v-on-exact', rule, { valid: [ - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: `` - }, - { - code: ` + ` ], invalid: [ diff --git a/tests/lib/rules/v-bind-style.js b/tests/lib/rules/v-bind-style.js index 2fd1ce7c2..0cb67f62f 100644 --- a/tests/lib/rules/v-bind-style.js +++ b/tests/lib/rules/v-bind-style.js @@ -67,44 +67,44 @@ tester.run('v-bind-style', rule, { }, { filename: 'test.vue', - options: ['shorthand'], code: '', output: '', + options: ['shorthand'], errors: ["Unexpected 'v-bind' before ':'."] }, { filename: 'test.vue', - options: ['longform'], code: '', output: '', + options: ['longform'], errors: ["Expected 'v-bind' before ':'."] }, { filename: 'test.vue', - options: ['longform'], code: '', output: '', + options: ['longform'], errors: ["Expected 'v-bind:' instead of '.'."] }, { filename: 'test.vue', - options: ['longform'], code: '', output: '', + options: ['longform'], errors: ["Expected 'v-bind:' instead of '.'."] }, { filename: 'test.vue', - options: ['longform'], code: '', output: '', + options: ['longform'], errors: ["Expected 'v-bind:' instead of '.'."] }, { filename: 'test.vue', - options: ['longform'], code: '', output: '', + options: ['longform'], errors: ["Expected 'v-bind:' instead of '.'."] } ] diff --git a/tests/lib/rules/v-for-delimiter-style.js b/tests/lib/rules/v-for-delimiter-style.js index b1500af50..23665a32c 100644 --- a/tests/lib/rules/v-for-delimiter-style.js +++ b/tests/lib/rules/v-for-delimiter-style.js @@ -94,9 +94,9 @@ tester.run('v-for-delimiter-style', rule, { }, { filename: 'test.vue', - options: ['in'], code: '', output: '', + options: ['in'], errors: [ { message: "Expected 'in' instead of 'of' in 'v-for'.", @@ -106,9 +106,9 @@ tester.run('v-for-delimiter-style', rule, { }, { filename: 'test.vue', - options: ['of'], code: '', output: '', + options: ['of'], errors: [ { message: "Expected 'of' instead of 'in' in 'v-for'.", diff --git a/tests/lib/rules/v-on-event-hyphenation.js b/tests/lib/rules/v-on-event-hyphenation.js index 087e5d00a..beaa5e3fa 100644 --- a/tests/lib/rules/v-on-event-hyphenation.js +++ b/tests/lib/rules/v-on-event-hyphenation.js @@ -73,12 +73,12 @@ tester.run('v-on-event-hyphenation', rule, { `, - options: ['always', { autofix: true }], output: ` `, + options: ['always', { autofix: true }], errors: [ { message: "v-on event '@customEvent' must be hyphenated.", @@ -95,12 +95,12 @@ tester.run('v-on-event-hyphenation', rule, { `, - options: ['never', { autofix: true }], output: ` `, + options: ['never', { autofix: true }], errors: ["v-on event 'v-on:custom-event' can't be hyphenated."] }, { @@ -110,13 +110,13 @@ tester.run('v-on-event-hyphenation', rule, { `, - options: ['always', { autofix: true }], output: ` `, + options: ['always', { autofix: true }], errors: ["v-on event '@update:modelValue' must be hyphenated."] }, { @@ -126,13 +126,13 @@ tester.run('v-on-event-hyphenation', rule, { `, - options: ['never', { autofix: true }], output: ` `, + options: ['never', { autofix: true }], errors: ["v-on event '@update:model-value' can't be hyphenated."] }, { @@ -144,7 +144,6 @@ tester.run('v-on-event-hyphenation', rule, { `, - options: ['always', { autofix: true }], output: ` `, + options: ['always', { autofix: true }], errors: [ "v-on event '@upDate:modelValue' must be hyphenated.", "v-on event '@up-date:modelValue' must be hyphenated.", @@ -168,7 +168,6 @@ tester.run('v-on-event-hyphenation', rule, { `, - options: ['never', { autofix: true }], output: ` `, + options: ['never', { autofix: true }], errors: [ "v-on event '@up-date:modelValue' can't be hyphenated.", "v-on event '@upDate:model-value' can't be hyphenated.", diff --git a/tests/lib/rules/v-on-function-call.js b/tests/lib/rules/v-on-function-call.js index d8486a448..d06bbe231 100644 --- a/tests/lib/rules/v-on-function-call.js +++ b/tests/lib/rules/v-on-function-call.js @@ -161,37 +161,37 @@ tester.run('v-on-function-call', rule, { filename: 'test.vue', code: '', output: null, + options: ['always'], errors: [ "Method calls inside of 'v-on' directives must have parentheses." - ], - options: ['always'] + ] }, { filename: 'test.vue', code: '', output: ``, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', code: '', output: ``, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', code: '', output: null, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', @@ -204,13 +204,13 @@ tester.run('v-on-function-call', rule, { ">
`, output: null, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses.", "Method calls without arguments inside of 'v-on' directives must not have parentheses.", "Method calls without arguments inside of 'v-on' directives must not have parentheses.", "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', @@ -222,10 +222,10 @@ tester.run('v-on-function-call', rule, { `, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', @@ -237,10 +237,10 @@ tester.run('v-on-function-call', rule, { `, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', @@ -254,11 +254,11 @@ tester.run('v-on-function-call', rule, {
`, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses.", "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', @@ -270,10 +270,10 @@ tester.run('v-on-function-call', rule, { `, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', @@ -285,19 +285,19 @@ tester.run('v-on-function-call', rule, { `, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', code: '', output: '', + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', @@ -319,10 +319,10 @@ tester.run('v-on-function-call', rule, { } } `, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] }, { filename: 'test.vue', @@ -344,10 +344,10 @@ tester.run('v-on-function-call', rule, { } } `, + options: ['never'], errors: [ "Method calls without arguments inside of 'v-on' directives must not have parentheses." - ], - options: ['never'] + ] } ] }) diff --git a/tests/lib/rules/v-on-handler-style.js b/tests/lib/rules/v-on-handler-style.js index e3afd05d5..314bbf8c8 100644 --- a/tests/lib/rules/v-on-handler-style.js +++ b/tests/lib/rules/v-on-handler-style.js @@ -78,12 +78,12 @@ tester.run('v-on-handler-style', rule, {