Skip to content

Commit b8d11de

Browse files
armano2mysticatea
authored andcommitted
New: add require-prop-types rule (fixes #19)(#85)
1 parent 49b40d6 commit b8d11de

File tree

6 files changed

+377
-4
lines changed

6 files changed

+377
-4
lines changed

docs/rules/require-prop-types.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Prop definitions should be detailed (require-prop-types)
2+
3+
In committed code, prop definitions should always be as detailed as possible, specifying at least type(s).
4+
5+
## :book: Rule Details
6+
7+
This rule enforces that a `props` statement contains type definition.
8+
9+
:-1: Examples of **incorrect** code for this rule:
10+
11+
```js
12+
export default {
13+
props: ['status']
14+
}
15+
```
16+
17+
:+1: Examples of **correct** code for this rule:
18+
19+
```js
20+
export default {
21+
props: {
22+
status: String
23+
}
24+
}
25+
```
26+
27+
```js
28+
export default {
29+
props: {
30+
status: {
31+
type: String,
32+
required: true,
33+
validate: function (value) {
34+
return ['syncing', 'synced', 'version-conflict', 'error'].indexOf(value) !== -1
35+
}
36+
}
37+
}
38+
}
39+
```
40+
## :wrench: Options
41+
42+
Nothing.

lib/rules/name-property-casing.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ function create (context) {
2121

2222
return utils.executeOnVue(context, (obj) => {
2323
const node = obj.properties
24-
.filter(item => (
24+
.find(item => (
2525
item.type === 'Property' &&
2626
item.key.name === 'name' &&
2727
item.value.type === 'Literal'
28-
))[0]
28+
))
2929

3030
if (!node) return
3131

lib/rules/require-prop-types.js

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* @fileoverview Prop definitions should be detailed
3+
* @author Armano
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
function create (context) {
10+
// ----------------------------------------------------------------------
11+
// Helpers
12+
// ----------------------------------------------------------------------
13+
14+
function objectHasType (node) {
15+
const typeProperty = node.properties
16+
.find(p =>
17+
utils.getStaticPropertyName(p.key) === 'type' &&
18+
(
19+
p.value.type !== 'ArrayExpression' ||
20+
p.value.elements.length > 0
21+
)
22+
)
23+
return Boolean(typeProperty)
24+
}
25+
26+
function checkProperties (items) {
27+
for (const cp of items) {
28+
if (cp.type !== 'Property') {
29+
return
30+
}
31+
let hasType = true
32+
if (cp.value.type === 'ObjectExpression') { // foo: {
33+
hasType = objectHasType(cp.value)
34+
} else if (cp.value.type === 'ArrayExpression') { // foo: [
35+
hasType = cp.value.elements.length > 0
36+
} else if (cp.value.type === 'FunctionExpression' || cp.value.type === 'ArrowFunctionExpression') {
37+
hasType = false
38+
}
39+
if (!hasType) {
40+
context.report({
41+
node: cp,
42+
message: 'Prop "{{name}}" should define at least it\'s type.',
43+
data: {
44+
name: cp.key.name
45+
}
46+
})
47+
}
48+
}
49+
}
50+
51+
// ----------------------------------------------------------------------
52+
// Public
53+
// ----------------------------------------------------------------------
54+
55+
return utils.executeOnVue(context, (obj) => {
56+
const node = obj.properties
57+
.find(p =>
58+
p.type === 'Property' &&
59+
p.key.type === 'Identifier' &&
60+
p.key.name === 'props'
61+
)
62+
63+
if (!node) return
64+
65+
if (node.value.type === 'ObjectExpression') {
66+
checkProperties(node.value.properties)
67+
} else {
68+
context.report({
69+
node,
70+
message: 'Props should at least define their types.'
71+
})
72+
}
73+
})
74+
}
75+
76+
// ------------------------------------------------------------------------------
77+
// Rule Definition
78+
// ------------------------------------------------------------------------------
79+
80+
module.exports = {
81+
meta: {
82+
docs: {
83+
description: 'Prop definitions should be detailed',
84+
category: 'Best Practices',
85+
recommended: false
86+
},
87+
fixable: null, // or "code" or "whitespace"
88+
schema: [
89+
// fill in your schema
90+
]
91+
},
92+
93+
create
94+
}

lib/utils/index.js

+44-2
Original file line numberDiff line numberDiff line change
@@ -271,18 +271,60 @@ 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
277319
* @return {Array} Array of computed properties in format: [{key: String, value: ASTNode}]
278320
*/
279321
getComputedProperties (componentObject) {
280322
const computedPropertiesNode = componentObject.properties
281-
.filter(p =>
323+
.find(p =>
282324
p.key.type === 'Identifier' &&
283325
p.key.name === 'computed' &&
284326
p.value.type === 'ObjectExpression'
285-
)[0]
327+
)
286328

287329
if (!computedPropertiesNode) { return [] }
288330

tests/lib/rules/require-prop-types.js

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* @fileoverview Prop definitions should be detailed
3+
* @author Armano
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const rule = require('../../../lib/rules/require-prop-types')
12+
13+
const RuleTester = require('eslint').RuleTester
14+
15+
// ------------------------------------------------------------------------------
16+
// Tests
17+
// ------------------------------------------------------------------------------
18+
19+
var ruleTester = new RuleTester()
20+
ruleTester.run('require-prop-types', rule, {
21+
22+
valid: [
23+
{
24+
filename: 'test.vue',
25+
code: `
26+
export default {
27+
props: {
28+
...test(),
29+
foo: String
30+
}
31+
}
32+
`,
33+
parserOptions: { ecmaVersion: 6, sourceType: 'module', ecmaFeatures: { experimentalObjectRestSpread: true }}
34+
},
35+
{
36+
filename: 'test.vue',
37+
code: `
38+
export default {
39+
props: {
40+
foo: [String, Number]
41+
}
42+
}
43+
`,
44+
parserOptions: { ecmaVersion: 6, sourceType: 'module' }
45+
},
46+
{
47+
filename: 'test.vue',
48+
code: `
49+
export default {
50+
props: {
51+
foo: {
52+
type: String
53+
}
54+
}
55+
}
56+
`,
57+
parserOptions: { ecmaVersion: 6, sourceType: 'module' }
58+
},
59+
{
60+
filename: 'test.vue',
61+
code: `
62+
export default {
63+
props: {
64+
foo: {
65+
['type']: String
66+
}
67+
}
68+
}
69+
`,
70+
parserOptions: { ecmaVersion: 6, sourceType: 'module' }
71+
}
72+
],
73+
74+
invalid: [
75+
{
76+
filename: 'test.vue',
77+
code: `
78+
export default {
79+
props: ['foo']
80+
}
81+
`,
82+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
83+
errors: [{
84+
message: 'Props should at least define their types.',
85+
line: 3
86+
}]
87+
},
88+
{
89+
filename: 'test.js',
90+
code: `
91+
new Vue({
92+
props: ['foo']
93+
})
94+
`,
95+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
96+
errors: [{
97+
message: 'Props should at least define their types.',
98+
line: 3
99+
}]
100+
},
101+
{
102+
filename: 'test.vue',
103+
code: `
104+
export default {
105+
props: {
106+
foo: {
107+
}
108+
}
109+
}
110+
`,
111+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
112+
errors: [{
113+
message: 'Prop "foo" should define at least it\'s type.',
114+
line: 4
115+
}]
116+
},
117+
{
118+
filename: 'test.vue',
119+
code: `
120+
export default {
121+
props: {
122+
foo: {
123+
type: []
124+
}
125+
}
126+
}
127+
`,
128+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
129+
errors: [{
130+
message: 'Prop "foo" should define at least it\'s type.',
131+
line: 4
132+
}]
133+
},
134+
{
135+
filename: 'test.vue',
136+
code: `
137+
export default {
138+
props: {
139+
foo() {}
140+
}
141+
}
142+
`,
143+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
144+
errors: [{
145+
message: 'Prop "foo" should define at least it\'s type.',
146+
line: 4
147+
}]
148+
}
149+
]
150+
})

0 commit comments

Comments
 (0)