Skip to content

Commit ccb6488

Browse files
ymichalsnik
y
authored andcommitted
New: Add vue/prop-name-casing rule (#289)
1 parent c6d25af commit ccb6488

File tree

5 files changed

+332
-0
lines changed

5 files changed

+332
-0
lines changed

docs/rules/prop-name-casing.md

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# enforce specific casing for the Prop name in Vue components(prop-name-casing)
2+
3+
This rule would enforce proper casing of props in vue components(camelCase).
4+
5+
## :book: Rule Details
6+
7+
(https://vuejs.org/v2/style-guide/#Prop-name-casing-strongly-recommended).
8+
9+
:+1: Examples of **correct** code for `camelCase`:
10+
11+
```js
12+
export default {
13+
props: {
14+
greetingText: String
15+
}
16+
}
17+
```
18+
19+
:-1: Examples of **incorrect** code for `camelCase`:
20+
21+
```js
22+
export default {
23+
props: {
24+
'greeting-text': String
25+
}
26+
}
27+
```
28+
29+
## :wrench: Options
30+
31+
Default casing is set to `camelCase`.
32+
33+
```
34+
"vue/prop-name-casing": ["error", "camelCase|snake_case"]
35+
```

lib/rules/prop-name-casing.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @fileoverview Requires specific casing for the Prop name in Vue components
3+
* @author Yu Kimura
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
const casing = require('../utils/casing')
9+
const allowedCaseOptions = ['camelCase', 'snake_case']
10+
11+
// ------------------------------------------------------------------------------
12+
// Rule Definition
13+
// ------------------------------------------------------------------------------
14+
15+
function create (context) {
16+
const options = context.options[0]
17+
const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'camelCase'
18+
const converter = casing.getConverter(caseType)
19+
20+
// ----------------------------------------------------------------------
21+
// Public
22+
// ----------------------------------------------------------------------
23+
24+
return utils.executeOnVue(context, (obj) => {
25+
const node = obj.properties.find(p =>
26+
p.type === 'Property' &&
27+
p.key.type === 'Identifier' &&
28+
p.key.name === 'props' &&
29+
(p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression')
30+
)
31+
32+
if (!node) return
33+
34+
const items = node.value.type === 'ObjectExpression' ? node.value.properties : node.value.elements
35+
for (const item of items) {
36+
if (item.type !== 'Property') {
37+
return
38+
}
39+
40+
const propName = item.key.type === 'Literal' ? item.key.value : item.key.name
41+
const convertedName = converter(propName)
42+
if (convertedName !== propName) {
43+
context.report({
44+
node: item,
45+
message: 'Prop "{{name}}" is not in {{caseType}}.',
46+
data: {
47+
name: propName,
48+
caseType: caseType
49+
}
50+
})
51+
}
52+
}
53+
})
54+
}
55+
56+
// ------------------------------------------------------------------------------
57+
// Rule Definition
58+
// ------------------------------------------------------------------------------
59+
60+
module.exports = {
61+
meta: {
62+
docs: {
63+
description: 'enforce specific casing for the Prop name in Vue components',
64+
category: undefined // 'strongly-recommended'
65+
},
66+
fixable: null, // or "code" or "whitespace"
67+
schema: [
68+
{
69+
enum: allowedCaseOptions
70+
}
71+
]
72+
},
73+
74+
create
75+
}

lib/utils/casing.js

+13
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ function kebabCase (str) {
1414
.toLowerCase()
1515
}
1616

17+
/**
18+
* Convert text to snake_case
19+
* @param {string} str Text to be converted
20+
* @return {string}
21+
*/
22+
function snakeCase (str) {
23+
return str
24+
.replace(/([a-z])([A-Z])/g, match => match[0] + '_' + match[1])
25+
.replace(invalidChars, '_')
26+
.toLowerCase()
27+
}
28+
1729
/**
1830
* Convert text to camelCase
1931
* @param {string} str Text to be converted
@@ -42,6 +54,7 @@ function pascalCase (str) {
4254

4355
const convertersMap = {
4456
'kebab-case': kebabCase,
57+
'snake_case': snakeCase,
4558
'camelCase': camelCase,
4659
'PascalCase': pascalCase
4760
}

tests/lib/rules/prop-name-casing.js

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* @fileoverview Define a style for the name property casing for consistency purposes
3+
* @author Yu Kimura
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const rule = require('../../../lib/rules/prop-name-casing')
12+
const RuleTester = require('eslint').RuleTester
13+
14+
// ------------------------------------------------------------------------------
15+
// Tests
16+
// ------------------------------------------------------------------------------
17+
18+
const parserOptions = {
19+
ecmaVersion: 6,
20+
sourceType: 'module',
21+
ecmaFeatures: { experimentalObjectRestSpread: true }
22+
}
23+
24+
const ruleTester = new RuleTester()
25+
ruleTester.run('prop-name-casing', rule, {
26+
27+
valid: [
28+
{
29+
filename: 'test.vue',
30+
code: `
31+
export default {
32+
props: ['greetingText']
33+
}
34+
`,
35+
parserOptions
36+
},
37+
{
38+
filename: 'test.vue',
39+
code: `
40+
export default {
41+
props: some_props
42+
}
43+
`,
44+
parserOptions
45+
},
46+
{
47+
filename: 'test.vue',
48+
code: `
49+
export default {
50+
props: {
51+
...some_props,
52+
}
53+
}
54+
`,
55+
parserOptions
56+
},
57+
{
58+
filename: 'test.vue',
59+
code: `
60+
export default {
61+
props: ['greetingText']
62+
}
63+
`,
64+
options: ['camelCase'],
65+
parserOptions
66+
},
67+
{
68+
filename: 'test.vue',
69+
code: `
70+
export default {
71+
props: ['greetingText']
72+
}
73+
`,
74+
options: ['snake_case'],
75+
parserOptions
76+
},
77+
{
78+
filename: 'test.vue',
79+
code: `
80+
export default {
81+
props: {
82+
greetingText: String
83+
}
84+
}
85+
`,
86+
parserOptions
87+
},
88+
{
89+
filename: 'test.vue',
90+
code: `
91+
export default {
92+
props: {
93+
greetingText: String
94+
}
95+
}
96+
`,
97+
options: ['camelCase'],
98+
parserOptions
99+
},
100+
{
101+
filename: 'test.vue',
102+
code: `
103+
export default {
104+
props: {
105+
greeting_text: String
106+
}
107+
}
108+
`,
109+
options: ['snake_case'],
110+
parserOptions
111+
}
112+
],
113+
114+
invalid: [
115+
{
116+
filename: 'test.vue',
117+
code: `
118+
export default {
119+
props: {
120+
greeting_text: String
121+
}
122+
}
123+
`,
124+
parserOptions,
125+
errors: [{
126+
message: 'Prop "greeting_text" is not in camelCase.',
127+
type: 'Property',
128+
line: 4
129+
}]
130+
},
131+
{
132+
filename: 'test.vue',
133+
code: `
134+
export default {
135+
props: {
136+
greeting_text: String
137+
}
138+
}
139+
`,
140+
options: ['camelCase'],
141+
parserOptions,
142+
errors: [{
143+
message: 'Prop "greeting_text" is not in camelCase.',
144+
type: 'Property',
145+
line: 4
146+
}]
147+
},
148+
{
149+
filename: 'test.vue',
150+
code: `
151+
export default {
152+
props: {
153+
greetingText: String
154+
}
155+
}
156+
`,
157+
options: ['snake_case'],
158+
parserOptions,
159+
errors: [{
160+
message: 'Prop "greetingText" is not in snake_case.',
161+
type: 'Property',
162+
line: 4
163+
}]
164+
},
165+
{
166+
filename: 'test.vue',
167+
code: `
168+
export default {
169+
props: {
170+
'greeting-text': String
171+
}
172+
}
173+
`,
174+
options: ['camelCase'],
175+
parserOptions,
176+
errors: [{
177+
message: 'Prop "greeting-text" is not in camelCase.',
178+
type: 'Property',
179+
line: 4
180+
}]
181+
},
182+
{
183+
filename: 'test.vue',
184+
code: `
185+
export default {
186+
props: {
187+
'greeting-text': String
188+
}
189+
}
190+
`,
191+
options: ['snake_case'],
192+
parserOptions,
193+
errors: [{
194+
message: 'Prop "greeting-text" is not in snake_case.',
195+
type: 'Property',
196+
line: 4
197+
}]
198+
}
199+
]
200+
})

tests/lib/utils/casing.js

+9
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,13 @@ describe('getConverter()', () => {
3232
assert.ok(converter('FooBar') === 'foo-bar')
3333
assert.ok(converter('Foo1Bar') === 'foo1bar')
3434
})
35+
36+
it('should conver string to snake_case', () => {
37+
const converter = casing.getConverter('snake_case')
38+
39+
assert.ok(converter('fooBar') === 'foo_bar')
40+
assert.ok(converter('foo-bar') === 'foo_bar')
41+
assert.ok(converter('FooBar') === 'foo_bar')
42+
assert.ok(converter('Foo1Bar') === 'foo1bar')
43+
})
3544
})

0 commit comments

Comments
 (0)