Skip to content

Commit a3d64c7

Browse files
armano2michalsnik
authored andcommitted
Add rule vue/require-valid-default-prop. (#119)
* Add rule `vue/require-valid-default-prop`. fixes #117 * Update doc & error message & add more tests
1 parent dfae686 commit a3d64c7

File tree

3 files changed

+600
-0
lines changed

3 files changed

+600
-0
lines changed

Diff for: docs/rules/require-valid-default-prop.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Enforces props default values to be valid (require-valid-default-prop)
2+
3+
This rule checks whether the default value of each prop is valid for the given type. It should report an error when default value for type `Array` or `Object` is not returned using function.
4+
5+
## :book: Rule Details
6+
7+
:-1: Examples of **incorrect** code for this rule:
8+
9+
```js
10+
Vue.component('example', {
11+
props: {
12+
propA: {
13+
type: String,
14+
default: {}
15+
},
16+
propB: {
17+
type: String,
18+
default: []
19+
},
20+
propC: {
21+
type: Object,
22+
default: []
23+
},
24+
propD: {
25+
type: Array,
26+
default: []
27+
},
28+
propE: {
29+
type: Object,
30+
default: { message: 'hello' }
31+
}
32+
}
33+
})
34+
```
35+
36+
:+1: Examples of **correct** code for this rule:
37+
38+
```js
39+
Vue.component('example', {
40+
props: {
41+
// basic type check (`null` means accept any type)
42+
propA: Number,
43+
// multiple possible types
44+
propB: [String, Number],
45+
// a number with default value
46+
propD: {
47+
type: Number,
48+
default: 100
49+
},
50+
// object/array defaults should be returned from a factory function
51+
propE: {
52+
type: Object,
53+
default: function () {
54+
return { message: 'hello' }
55+
}
56+
}
57+
}
58+
})
59+
```
60+
61+
## :wrench: Options
62+
63+
Nothing.

Diff for: lib/rules/require-valid-default-prop.js

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* @fileoverview Enforces props default values to be valid.
3+
* @author Armano
4+
*/
5+
'use strict'
6+
const utils = require('../utils')
7+
8+
const NATIVE_TYPES = new Set([
9+
'String',
10+
'Number',
11+
'Boolean',
12+
'Function',
13+
'Object',
14+
'Array',
15+
'Symbol'
16+
])
17+
18+
// ------------------------------------------------------------------------------
19+
// Rule Definition
20+
// ------------------------------------------------------------------------------
21+
22+
module.exports = {
23+
meta: {
24+
docs: {
25+
description: 'Enforces props default values to be valid.',
26+
category: 'Possible Errors',
27+
recommended: false
28+
},
29+
fixable: null,
30+
schema: []
31+
},
32+
33+
create (context) {
34+
// ----------------------------------------------------------------------
35+
// Helpers
36+
// ----------------------------------------------------------------------
37+
38+
function isPropertyIdentifier (node) {
39+
return node.type === 'Property' && node.key.type === 'Identifier'
40+
}
41+
42+
function getPropertyNode (obj, name) {
43+
return obj.properties.find(p =>
44+
isPropertyIdentifier(p) &&
45+
p.key.name === name
46+
)
47+
}
48+
49+
function getTypes (node) {
50+
if (node.type === 'Identifier') {
51+
return [node.name]
52+
} else if (node.type === 'ArrayExpression') {
53+
return node.elements
54+
.filter(item => item.type === 'Identifier')
55+
.map(item => item.name)
56+
}
57+
return []
58+
}
59+
60+
function ucFirst (text) {
61+
return text[0].toUpperCase() + text.slice(1)
62+
}
63+
64+
function getValueType (node) {
65+
if (node.type === 'CallExpression') { // Symbol(), Number() ...
66+
if (node.callee.type === 'Identifier' && NATIVE_TYPES.has(node.callee.name)) {
67+
return node.callee.name
68+
}
69+
} else if (node.type === 'TemplateLiteral') { // String
70+
return 'String'
71+
} else if (node.type === 'Literal') { // String, Boolean, Number
72+
if (node.value === null) return null
73+
const type = ucFirst(typeof node.value)
74+
if (NATIVE_TYPES.has(type)) {
75+
return type
76+
}
77+
} else if (node.type === 'ArrayExpression') { // Array
78+
return 'Array'
79+
} else if (node.type === 'ObjectExpression') { // Object
80+
return 'Object'
81+
}
82+
// FunctionExpression, ArrowFunctionExpression
83+
return null
84+
}
85+
86+
// ----------------------------------------------------------------------
87+
// Public
88+
// ----------------------------------------------------------------------
89+
90+
return utils.executeOnVue(context, obj => {
91+
const props = obj.properties.find(p =>
92+
isPropertyIdentifier(p) &&
93+
p.key.name === 'props' &&
94+
p.value.type === 'ObjectExpression'
95+
)
96+
if (!props) return
97+
98+
const properties = props.value.properties.filter(p =>
99+
isPropertyIdentifier(p) &&
100+
p.value.type === 'ObjectExpression'
101+
)
102+
103+
for (const prop of properties) {
104+
const type = getPropertyNode(prop.value, 'type')
105+
if (!type) {
106+
return
107+
}
108+
109+
const typeNames = new Set(getTypes(type.value)
110+
.map(item => item === 'Object' || item === 'Array' ? 'Function' : item) // Object and Array require function
111+
.filter(item => NATIVE_TYPES.has(item)))
112+
113+
if (typeNames.size === 0) { // There is no native types detected
114+
return
115+
}
116+
117+
const def = getPropertyNode(prop.value, 'default')
118+
if (!def) return
119+
120+
const defType = getValueType(def.value)
121+
if (typeNames.has(defType)) return
122+
123+
context.report({
124+
node: def,
125+
message: "Type of the default value for '{{name}}' prop must be a {{types}}.",
126+
data: {
127+
name: prop.key.name,
128+
types: Array.from(typeNames).join(' or ').toLowerCase()
129+
}
130+
})
131+
}
132+
})
133+
}
134+
}

0 commit comments

Comments
 (0)