Skip to content

Commit 56e1422

Browse files
committed
Add rules: no-dupe-keys and no-reserved-keys.
fixes vuejs#86
1 parent 49b40d6 commit 56e1422

10 files changed

+694
-3
lines changed

docs/rules/no-dupe-keys.md

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Prevents duplication of field names (no-dupe-keys)
2+
3+
This rule prevents to use duplicated names.
4+
5+
## :book: Rule Details
6+
7+
This rule is aimed at preventing duplicated property names.
8+
9+
:-1: Examples of **incorrect** code for this rule:
10+
11+
```js
12+
export default {
13+
props: {
14+
foo: String
15+
},
16+
computed: {
17+
foo: {
18+
get () {
19+
}
20+
}
21+
},
22+
data: {
23+
foo: null
24+
},
25+
methods: {
26+
foo () {
27+
}
28+
}
29+
}
30+
```
31+
32+
:+1: Examples of **correct** code for this rule:
33+
34+
```js
35+
export default {
36+
props: ['foo'],
37+
computed: {
38+
bar () {
39+
}
40+
},
41+
data () {
42+
return {
43+
dat: null
44+
}
45+
},
46+
methods: {
47+
test () {
48+
}
49+
}
50+
}
51+
```
52+
53+
## :wrench: Options
54+
55+
This rule has an object option:
56+
57+
`"groups"`: [] (default) array of additional groups to search for duplicates.
58+
59+
### Example:
60+
61+
```
62+
vue/no-dupe-keys: [2, {
63+
groups: ['asyncComputed']
64+
}]
65+
```
66+
67+
:-1: Examples of **incorrect** code for this configuration
68+
69+
```js
70+
export default {
71+
computed: {
72+
foo () {}
73+
},
74+
asyncComputed: {
75+
foo () {}
76+
}
77+
}
78+
```

docs/rules/no-reservered-keys.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Prevent overwrite reserved keys (no-reservered-keys)
2+
3+
This rule prevents to use reserved names from to avoid conflicts and unexpected behavior.
4+
5+
## Rule Details
6+
7+
:-1: Examples of **incorrect** code for this rule:
8+
9+
```js
10+
export default {
11+
props: {
12+
$el: String
13+
},
14+
computed: {
15+
$on: {
16+
get () {
17+
}
18+
}
19+
},
20+
data: {
21+
_foo: null
22+
},
23+
methods: {
24+
$nextTick () {
25+
}
26+
}
27+
}
28+
```
29+
30+
## :wrench: Options
31+
32+
This rule has an object option:
33+
34+
`"reserved"`: [] (default) array of dissalowed names inside `groups`.
35+
36+
`"groups"`: [] (default) array of additional groups to search for duplicates.
37+
38+
### Example:
39+
40+
```
41+
vue/no-dupe-keys: [2, {
42+
reserved: ['foo']
43+
}]
44+
```
45+
46+
:-1: Examples of **incorrect** code for this configuration
47+
48+
```js
49+
export default {
50+
computed: {
51+
foo () {}
52+
}
53+
}
54+
```

lib/rules/no-dupe-keys.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @fileoverview Prevents duplication of field names.
3+
* @author Armano
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
// ------------------------------------------------------------------------------
10+
// Rule Definition
11+
// ------------------------------------------------------------------------------
12+
13+
const GROUP_NAMES = ['props', 'computed', 'data', 'methods']
14+
15+
function create (context) {
16+
const usedNames = []
17+
18+
const options = context.options[0] || {}
19+
const groups = new Set(GROUP_NAMES.concat(options.groups || []))
20+
21+
// ----------------------------------------------------------------------
22+
// Public
23+
// ----------------------------------------------------------------------
24+
25+
return utils.executeOnVue(context, (obj) => {
26+
const properties = utils.iterateProperties(obj, groups)
27+
for (const o of properties) {
28+
if (usedNames.indexOf(o.name) !== -1) {
29+
context.report({
30+
node: o.node,
31+
message: "Duplicated key '{{name}}'.",
32+
data: {
33+
name: o.name
34+
}
35+
})
36+
}
37+
usedNames.push(o.name)
38+
}
39+
})
40+
}
41+
42+
// ------------------------------------------------------------------------------
43+
// Rule Definition
44+
// ------------------------------------------------------------------------------
45+
46+
module.exports = {
47+
meta: {
48+
docs: {
49+
description: 'Prevents duplication of field names.',
50+
category: 'Possible Errors',
51+
recommended: false
52+
},
53+
fixable: null, // or "code" or "whitespace"
54+
schema: [
55+
{
56+
type: 'object',
57+
properties: {
58+
groups: {
59+
type: 'array'
60+
}
61+
},
62+
additionalProperties: false
63+
}
64+
]
65+
},
66+
67+
create
68+
}

lib/rules/no-reservered-keys.js

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @fileoverview Prevent overwrite reserved keys
3+
* @author Armano
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
// ------------------------------------------------------------------------------
10+
// Rule Definition
11+
// ------------------------------------------------------------------------------
12+
13+
const RESERVED_KEYS = require('../utils/vue-reserved.json')
14+
const GROUP_NAMES = ['props', 'computed', 'data', 'methods']
15+
16+
function create (context) {
17+
const options = context.options[0] || {}
18+
const reservedKeys = new Set(RESERVED_KEYS.concat(options.reserved || []))
19+
const groups = new Set(GROUP_NAMES.concat(options.groups || []))
20+
21+
// ----------------------------------------------------------------------
22+
// Public
23+
// ----------------------------------------------------------------------
24+
25+
return utils.executeOnVue(context, (obj) => {
26+
const properties = utils.iterateProperties(obj, groups)
27+
for (const o of properties) {
28+
if (o.groupName === 'data' && o.name[0] === '_') {
29+
context.report({
30+
node: o.node,
31+
message: "Keys starting with with '_' are reserved in '{{name}}' group.",
32+
data: {
33+
name: o.name
34+
}
35+
})
36+
} else if (reservedKeys.has(o.name)) {
37+
context.report({
38+
node: o.node,
39+
message: "Key '{{name}}' is reserved.",
40+
data: {
41+
name: o.name
42+
}
43+
})
44+
}
45+
}
46+
})
47+
}
48+
49+
module.exports = {
50+
meta: {
51+
docs: {
52+
description: 'Prevent overwrite reserved keys.',
53+
category: 'Possible Errors',
54+
recommended: false
55+
},
56+
fixable: null, // or "code" or "whitespace"
57+
schema: [
58+
{
59+
type: 'object',
60+
properties: {
61+
reserved: {
62+
type: 'array'
63+
},
64+
groups: {
65+
type: 'array'
66+
}
67+
},
68+
additionalProperties: false
69+
}
70+
]
71+
},
72+
73+
create
74+
}

lib/utils/index.js

+109
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,48 @@ module.exports = {
271271
return members.reverse()
272272
},
273273

274+
/**
275+
* Gets the property name of a given node.
276+
* @param {ASTNode} node - The node to get.
277+
* @return {string|null} The property name if static. Otherwise, null.
278+
*/
279+
getStaticPropertyName (node) {
280+
let prop
281+
switch (node && node.type) {
282+
case 'Property':
283+
case 'MethodDefinition':
284+
prop = node.key
285+
break
286+
case 'MemberExpression':
287+
prop = node.property
288+
break
289+
case 'Literal':
290+
case 'TemplateLiteral':
291+
case 'Identifier':
292+
prop = node
293+
break
294+
// no default
295+
}
296+
297+
switch (prop && prop.type) {
298+
case 'Literal':
299+
return String(prop.value)
300+
case 'TemplateLiteral':
301+
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
302+
return prop.quasis[0].value.cooked
303+
}
304+
break
305+
case 'Identifier':
306+
if (!node.computed) {
307+
return prop.name
308+
}
309+
break
310+
// no default
311+
}
312+
313+
return null
314+
},
315+
274316
/**
275317
* Get all computed properties by looking at all component's properties
276318
* @param {ObjectExpression} Object with component definition
@@ -397,5 +439,72 @@ module.exports = {
397439
cb(node.arguments.slice(-1)[0])
398440
}
399441
}
442+
},
443+
444+
/**
445+
* Return generator with all properties
446+
* @param {ASTNode} node Node to check
447+
* @param {string} groupName Name of parent group
448+
*/
449+
* iterateProperties (node, groups) {
450+
const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(this.getStaticPropertyName(p.key)))
451+
for (const item of nodes) {
452+
const name = this.getStaticPropertyName(item.key)
453+
if (item.value.type === 'ArrayExpression') {
454+
yield * this.iterateArrayExpression(item.value, name)
455+
} else if (item.value.type === 'ObjectExpression') {
456+
yield * this.iterateObjectExpression(item.value, name)
457+
} else if (item.value.type === 'FunctionExpression') {
458+
yield * this.iterateFunctionExpression(item.value, name)
459+
}
460+
}
461+
},
462+
463+
/**
464+
* Return generator with all elements inside ArrayExpression
465+
* @param {ASTNode} node Node to check
466+
* @param {string} groupName Name of parent group
467+
*/
468+
* iterateArrayExpression (node, groupName) {
469+
assert(node.type === 'ArrayExpression')
470+
for (const item of node.elements) {
471+
const name = this.getStaticPropertyName(item)
472+
if (name) {
473+
const obj = { name, groupName, node: item }
474+
yield obj
475+
}
476+
}
477+
},
478+
479+
/**
480+
* Return generator with all elements inside ObjectExpression
481+
* @param {ASTNode} node Node to check
482+
* @param {string} groupName Name of parent group
483+
*/
484+
* iterateObjectExpression (node, groupName) {
485+
assert(node.type === 'ObjectExpression')
486+
for (const item of node.properties) {
487+
const name = this.getStaticPropertyName(item)
488+
if (name) {
489+
const obj = { name, groupName, node: item.key }
490+
yield obj
491+
}
492+
}
493+
},
494+
495+
/**
496+
* Return generator with all elements inside FunctionExpression
497+
* @param {ASTNode} node Node to check
498+
* @param {string} groupName Name of parent group
499+
*/
500+
* iterateFunctionExpression (node, groupName) {
501+
assert(node.type === 'FunctionExpression')
502+
if (node.body.type === 'BlockStatement') {
503+
for (const item of node.body.body) {
504+
if (item.type === 'ReturnStatement' && item.argument.type === 'ObjectExpression') {
505+
yield * this.iterateObjectExpression(item.argument, groupName)
506+
}
507+
}
508+
}
400509
}
401510
}

0 commit comments

Comments
 (0)