-
-
Notifications
You must be signed in to change notification settings - Fork 681
New Rule vue/sort-keys #997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
49b4486
055f26d
6b5be10
273c68a
bd071ea
50404e9
3630bf4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
--- | ||
pageClass: rule-details | ||
sidebarDepth: 0 | ||
title: vue/sort-keys | ||
description: enforce sort-keys in a manner that is compatible with order-in-components | ||
--- | ||
# vue/sort-keys | ||
> enforce sort-keys within components after the top level details | ||
|
||
This rule is almost the same rule as core [sorts-keys] rule but it will not error on top component properties allowing that order to be enforced with `order-in-components`. | ||
|
||
## Options | ||
|
||
```json | ||
{ | ||
"sort-keys": ["error", "asc", {"caseSensitive": true, "natural": false, "minKeys": 2}] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you added properties be included in the example? |
||
} | ||
``` | ||
|
||
The 1st option is `"asc"` or `"desc"`. | ||
|
||
* `"asc"` (default) - enforce properties to be in ascending order. | ||
* `"desc"` - enforce properties to be in descending order. | ||
|
||
The 2nd option is an object which has 5 properties. | ||
|
||
* `caseSensitive` - if `true`, enforce properties to be in case-sensitive order. Default is `true`. | ||
* `ignoreChildrenOf` - an array of properties to ignore the children of. Default is `['model']` | ||
* `ignoreGrandchildrenOf` - an array of properties to ignore the grandchildren sort order. Default is `['computed', 'directives', 'inject', 'props', 'watch']` | ||
* `minKeys` - Specifies the minimum number of keys that an object should have in order for the object's unsorted keys to produce an error. Default is `2`, which means by default all objects with unsorted keys will result in lint errors. | ||
* `natural` - if `true`, enforce properties to be in natural order. Default is `false`. Natural Order compares strings containing combination of letters and numbers in the way a human being would sort. It basically sorts numerically, instead of sorting alphabetically. So the number 10 comes after the number 3 in Natural Sorting. | ||
|
||
While using this rule, you may disable the normal `sort-keys` rule. This rule will apply to plain js files as well as Vue component scripts. | ||
|
||
## :book: Rule Details | ||
|
||
This rule forces many dictionary properties to be in alphabetical order while allowing `order-in-components`, property details, | ||
and other similar fields not to be in alphabetical order. | ||
|
||
<eslint-code-block fix :rules="{'vue/sort-keys': ['error']}"> | ||
|
||
```vue | ||
<script> | ||
/* ✓ GOOD */ | ||
export default { | ||
name: 'app', | ||
model: { | ||
prop: 'checked', | ||
event: 'change' | ||
}, | ||
props: { | ||
propA: { | ||
type: String, | ||
default: 'abc', | ||
}, | ||
propB: { | ||
type: String, | ||
default: 'abc', | ||
}, | ||
}, | ||
} | ||
</script> | ||
``` | ||
|
||
</eslint-code-block> | ||
|
||
<eslint-code-block fix :rules="{'vue/sort-keys': ['error']}"> | ||
|
||
```vue | ||
<script> | ||
/* ✗ BAD */ | ||
export default { | ||
name: 'app', | ||
model: { | ||
prop: 'checked', | ||
event: 'change' | ||
}, | ||
props: { | ||
propB: { | ||
type: String, | ||
default: 'abc', | ||
}, | ||
propA: { | ||
type: String, | ||
default: 'abc', | ||
}, | ||
}, | ||
} | ||
</script> | ||
``` | ||
|
||
</eslint-code-block> | ||
|
||
## :books: Further reading | ||
|
||
- [sorts-keys] | ||
|
||
[sorts-keys]: https://eslint.org/docs/rules/sorts-keys | ||
loren138 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## :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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
/** | ||
* @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 reportErrors = (isVue) => { | ||
if (isVue) { | ||
errors = errors.filter((error) => { | ||
let filter = error.hasUpper | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Vue component objects may not be top-level objects. Excluding it with hasUpper can cause false positives. const obj = {
foo() {
Vue.component('my-component', {
name: 'app',
data() {}
})
}
} |
||
if (!error.grandparentName) { | ||
filter = filter && !ignoreChildrenOf.includes(error.parentName) | ||
} else if (!error.greatGrandparentName) { | ||
filter = filter && !ignoreGrandchildrenOf.includes(error.grandparentName) | ||
} | ||
return filter | ||
}) | ||
} | ||
errors.forEach((error) => context.report(error)) | ||
errors = [] | ||
} | ||
|
||
const sortTests = { | ||
ObjectExpression (node) { | ||
if (!stack) { | ||
reportErrors(false) | ||
} | ||
stack = { | ||
upper: stack, | ||
prevName: null, | ||
numKeys: node.properties.length | ||
} | ||
}, | ||
'ObjectExpression:exit' () { | ||
stack = stack.upper | ||
}, | ||
SpreadElement (node) { | ||
if (node.parent.type === 'ObjectExpression') { | ||
stack.prevName = 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 | ||
} | ||
|
||
if (prevName === null || thisName === null || numKeys < minKeys) { | ||
return | ||
} | ||
|
||
if (!isValidOrder(prevName, thisName)) { | ||
errors.push({ | ||
node, | ||
hasUpper: !!stack.upper, | ||
loc: node.key.loc, | ||
message: "Expected object keys to be in {{natual}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'.", | ||
parentName: stack.upper && stack.upper.prevName, | ||
grandparentName: stack.upper && stack.upper.upper && stack.upper.upper.prevName, | ||
greatGrandparentName: stack.upper && stack.upper.upper && stack.upper.upper.upper && stack.upper.upper.upper.prevName, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may false negatives if the objects are not chained. export default {
computed: {
foo () {
return {
b,
a
}
}
}
} |
||
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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EsLint 6.7.0 changes eslintignore parsing. The previous one seems to have ignored the whole folder which I'm now explicitly doing. Otherwise, you get errors about eslint-plugin-ignore not being installed when running
npm run lint
after a clean install.