From 774f4f3ae2a8acbd2dea24b4edf3f72d756bef71 Mon Sep 17 00:00:00 2001 From: Alexandr Orel <4aorel@mail.ru> Date: Thu, 16 Jan 2020 17:58:50 +0300 Subject: [PATCH 1/9] Update match-component-file-name docs (#1032) --- docs/rules/match-component-file-name.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/rules/match-component-file-name.md b/docs/rules/match-component-file-name.md index 961937b19..bbf84be72 100644 --- a/docs/rules/match-component-file-name.md +++ b/docs/rules/match-component-file-name.md @@ -9,8 +9,7 @@ description: require component name property to match its file name This rule reports if a component `name` property does not match its file name. -You can define an array of file extensions this rule should verify for -the component's name. +You can define an array of file extensions this rule should verify for the component's name. ## :book: Rule Details @@ -27,7 +26,7 @@ This rule has some options. By default this rule will only verify components in a file with a `.jsx` extension. -You can use any combination of `".jsx"`, `".vue"` and `".js"` extensions. +You can use any combination of `".js"`, `".jsx"`, `".ts"`, `".tsx"`, and `".vue"` extensions. You can also enforce same case between the component's name and its file name. From fe190dca0df27397a40bb69ed7a265cdb134e210 Mon Sep 17 00:00:00 2001 From: Federico Gorga <53117838+federico-solveit@users.noreply.github.com> Date: Fri, 17 Jan 2020 17:05:30 +0800 Subject: [PATCH 2/9] README.md-VSCode ESlint Auto Fix (#1037) Update README.md-VSCode: eslint.validate object type is now deprecated. Use string format https://github.com/microsoft/vscode-eslint/commit/7b5c40536131aa94eccd8a175e37104f5b785071#diff-b9cfc7f2cdf78a7f4b91a753d10865a2R223 --- docs/user-guide/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md index e8fededdb..01710c07a 100644 --- a/docs/user-guide/README.md +++ b/docs/user-guide/README.md @@ -140,7 +140,7 @@ Example **.vscode/settings.json**: "eslint.validate": [ "javascript", "javascriptreact", - { "language": "vue", "autoFix": true } + "vue" ] } ``` From 7608deafd74ba6032dce169c1bdcd4e6069d2731 Mon Sep 17 00:00:00 2001 From: Loren Date: Sun, 16 Feb 2020 04:42:32 -0500 Subject: [PATCH 3/9] 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 4/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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 `