Skip to content

Commit f03b2d8

Browse files
ota-meshiarmano2
andauthored
Fix some casing issues (#1152)
* Fix casing of unicode characters * Japanese and Chinese has no lower/upper letters * Add support for node < 10 * lint fix * Simplify casing regexp * fix: make casing ignores language specific characters * Fix some casing issues. * Add testcase Co-authored-by: Armano <armano2@users.noreply.github.com>
1 parent e80c2f0 commit f03b2d8

13 files changed

+423
-92
lines changed

lib/rules/attribute-hyphenation.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ module.exports = {
5656
ignoredAttributes = ignoredAttributes.concat(optionsPayload.ignore)
5757
}
5858

59-
const caseConverter = casing.getConverter(useHyphenated ? 'kebab-case' : 'camelCase')
59+
const caseConverter = casing.getExactConverter(useHyphenated ? 'kebab-case' : 'camelCase')
6060

6161
function reportIssue (node, name) {
6262
const text = sourceCode.getText(node.key)

lib/rules/component-definition-name-casing.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,15 @@ module.exports = {
4848
range = node.range
4949
}
5050

51-
const value = casing.getConverter(caseType)(nodeValue)
52-
if (value !== nodeValue) {
51+
if (!casing.getChecker(caseType)(nodeValue)) {
5352
context.report({
5453
node: node,
5554
message: 'Property name "{{value}}" is not {{caseType}}.',
5655
data: {
5756
value: nodeValue,
5857
caseType: caseType
5958
},
60-
fix: fixer => fixer.replaceTextRange([range[0] + 1, range[1] - 1], value)
59+
fix: fixer => fixer.replaceTextRange([range[0] + 1, range[1] - 1], casing.getExactConverter(caseType)(nodeValue))
6160
})
6261
}
6362
}

lib/rules/component-name-in-template-casing.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ module.exports = {
8787
}
8888
// We only verify the components registered in the component.
8989
if (registeredComponents
90-
.filter(name => casing.pascalCase(name) === name) // When defining a component with PascalCase, you can use either case
90+
.filter(name => casing.isPascalCase(name)) // When defining a component with PascalCase, you can use either case
9191
.some(name => node.rawName === name || casing.pascalCase(node.rawName) === name)) {
9292
return true
9393
}
@@ -108,11 +108,10 @@ module.exports = {
108108
}
109109

110110
const name = node.rawName
111-
const casingName = casing.getConverter(caseType)(name)
112-
if (casingName !== name) {
111+
if (!casing.getChecker(caseType)(name)) {
113112
const startTag = node.startTag
114113
const open = tokens.getFirstToken(startTag)
115-
114+
const casingName = casing.getExactConverter(caseType)(name)
116115
context.report({
117116
node: open,
118117
loc: open.loc,

lib/rules/name-property-casing.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ module.exports = {
4848

4949
if (!node) return
5050

51-
const value = casing.getConverter(caseType)(node.value.value)
52-
if (value !== node.value.value) {
51+
if (!casing.getChecker(caseType)(node.value.value)) {
52+
const value = casing.getExactConverter(caseType)(node.value.value)
5353
context.report({
5454
node: node.value,
5555
message: 'Property name "{{value}}" is not {{caseType}}.',

lib/rules/no-unregistered-components.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,7 @@ module.exports = {
128128

129129
// Component registered as `foo-bar` cannot be used as `FooBar`
130130
if (
131-
name.indexOf('-') === -1 &&
132-
name === casing.pascalCase(name) &&
131+
casing.isPascalCase(name) &&
133132
componentsRegisteredAsKebabCase.indexOf(kebabCaseName) !== -1
134133
) {
135134
return true

lib/rules/no-unused-components.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ module.exports = {
8787
// it can be used in various of ways inside template,
8888
// like "theComponent", "The-component" etc.
8989
// but except snake_case
90-
if (casing.pascalCase(name) === name || casing.camelCase(name) === name) {
90+
if (casing.isPascalCase(name) || casing.isCamelCase(name)) {
9191
return ![...usedComponents].some(n => {
9292
return n.indexOf('_') === -1 && (name === casing.pascalCase(n) || casing.camelCase(n) === name)
9393
})

lib/rules/prop-name-casing.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const allowedCaseOptions = ['camelCase', 'snake_case']
1515
function create (context) {
1616
const options = context.options[0]
1717
const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'camelCase'
18-
const converter = casing.getConverter(caseType)
18+
const checker = casing.getChecker(caseType)
1919

2020
// ----------------------------------------------------------------------
2121
// Public
@@ -31,8 +31,7 @@ function create (context) {
3131
// (boolean | null | number | RegExp) Literal
3232
continue
3333
}
34-
const convertedName = converter(propName)
35-
if (convertedName !== propName) {
34+
if (!checker(propName)) {
3635
context.report({
3736
node: item.node,
3837
message: 'Prop "{{name}}" is not in {{caseType}}.',

lib/utils/casing.js

Lines changed: 143 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
const assert = require('assert')
22

3-
const invalidChars = /[^a-zA-Z0-9:]+/g
3+
// ------------------------------------------------------------------------------
4+
// Helpers
5+
// ------------------------------------------------------------------------------
6+
7+
/**
8+
* Capitalize a string.
9+
*/
10+
function capitalize (str) {
11+
return str.charAt(0).toUpperCase() + str.slice(1)
12+
}
13+
/**
14+
* Checks whether the given string has symbols.
15+
*/
16+
function hasSymbols (str) {
17+
return /[!"#%&'()*+,./:;<=>?@[\\\]^`{|}]/u.exec(str) // without " ", "$", "-" and "_"
18+
}
19+
/**
20+
* Checks whether the given string has upper.
21+
*/
22+
function hasUpper (str) {
23+
return /[A-Z]/u.exec(str)
24+
}
425

526
/**
627
* Convert text to kebab-case
@@ -9,39 +30,76 @@ const invalidChars = /[^a-zA-Z0-9:]+/g
930
*/
1031
function kebabCase (str) {
1132
return str
12-
.replace(/[A-Z]/g, match => '-' + match)
13-
.replace(/([^a-zA-Z])-([A-Z])/g, match => match[0] + match[2])
14-
.replace(/^-/, '')
15-
.replace(invalidChars, '-')
33+
.replace(/_/gu, '-')
34+
.replace(/\B([A-Z])/gu, '-$1')
1635
.toLowerCase()
1736
}
1837

38+
/**
39+
* Checks whether the given string is kebab-case.
40+
*/
41+
function isKebabCase (str) {
42+
if (
43+
hasUpper(str) ||
44+
hasSymbols(str) ||
45+
/^-/u.exec(str) || // starts with hyphen is not kebab-case
46+
/_|--|\s/u.exec(str)
47+
) {
48+
return false
49+
}
50+
return true
51+
}
52+
1953
/**
2054
* Convert text to snake_case
2155
* @param {string} str Text to be converted
2256
* @return {string}
2357
*/
2458
function snakeCase (str) {
2559
return str
26-
.replace(/[A-Z]/g, match => '_' + match)
27-
.replace(/([^a-zA-Z])_([A-Z])/g, match => match[0] + match[2])
28-
.replace(/^_/, '')
29-
.replace(invalidChars, '_')
60+
.replace(/\B([A-Z])/gu, '_$1')
61+
.replace(/-/gu, '_')
3062
.toLowerCase()
3163
}
3264

65+
/**
66+
* Checks whether the given string is snake_case.
67+
*/
68+
function isSnakeCase (str) {
69+
if (
70+
hasUpper(str) ||
71+
hasSymbols(str) ||
72+
/-|__|\s/u.exec(str)
73+
) {
74+
return false
75+
}
76+
return true
77+
}
78+
3379
/**
3480
* Convert text to camelCase
3581
* @param {string} str Text to be converted
3682
* @return {string} Converted string
3783
*/
3884
function camelCase (str) {
39-
return str
40-
.replace(/_/g, (_, index) => index === 0 ? _ : '-')
41-
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) =>
42-
index === 0 ? letter.toLowerCase() : letter.toUpperCase()
43-
)
44-
.replace(invalidChars, '')
85+
if (isPascalCase(str)) {
86+
return str.charAt(0).toLowerCase() + str.slice(1)
87+
}
88+
return str.replace(/[-_](\w)/gu, (_, c) => c ? c.toUpperCase() : '')
89+
}
90+
91+
/**
92+
* Checks whether the given string is camelCase.
93+
*/
94+
function isCamelCase (str) {
95+
if (
96+
hasSymbols(str) ||
97+
/^[A-Z]/u.exec(str) ||
98+
/-|_|\s/u.exec(str) // kebab or snake or space
99+
) {
100+
return false
101+
}
102+
return true
45103
}
46104

47105
/**
@@ -50,10 +108,21 @@ function camelCase (str) {
50108
* @return {string} Converted string
51109
*/
52110
function pascalCase (str) {
53-
return str
54-
.replace(/_/g, (_, index) => index === 0 ? _ : '-')
55-
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => letter.toUpperCase())
56-
.replace(invalidChars, '')
111+
return capitalize(camelCase(str))
112+
}
113+
114+
/**
115+
* Checks whether the given string is PascalCase.
116+
*/
117+
function isPascalCase (str) {
118+
if (
119+
hasSymbols(str) ||
120+
/^[a-z]/u.exec(str) ||
121+
/-|_|\s/u.exec(str) // kebab or snake or space
122+
) {
123+
return false
124+
}
125+
return true
57126
}
58127

59128
const convertersMap = {
@@ -63,6 +132,34 @@ const convertersMap = {
63132
'PascalCase': pascalCase
64133
}
65134

135+
const checkersMap = {
136+
'kebab-case': isKebabCase,
137+
'snake_case': isSnakeCase,
138+
'camelCase': isCamelCase,
139+
'PascalCase': isPascalCase
140+
}
141+
/**
142+
* Return case checker
143+
* @param {string} name type of checker to return ('camelCase', 'kebab-case', 'PascalCase')
144+
* @return {isKebabCase|isCamelCase|isPascalCase}
145+
*/
146+
function getChecker (name) {
147+
assert(typeof name === 'string')
148+
149+
return checkersMap[name] || isPascalCase
150+
}
151+
152+
/**
153+
* Return case converter
154+
* @param {string} name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
155+
* @return {kebabCase|camelCase|pascalCase}
156+
*/
157+
function getConverter (name) {
158+
assert(typeof name === 'string')
159+
160+
return convertersMap[name] || pascalCase
161+
}
162+
66163
module.exports = {
67164
allowedCaseOptions: [
68165
'camelCase',
@@ -75,14 +172,37 @@ module.exports = {
75172
* @param {string} name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
76173
* @return {kebabCase|camelCase|pascalCase}
77174
*/
78-
getConverter (name) {
79-
assert(typeof name === 'string')
175+
getConverter,
176+
177+
/**
178+
* Return case checker
179+
* @param {string} name type of checker to return ('camelCase', 'kebab-case', 'PascalCase')
180+
* @return {isKebabCase|isCamelCase|isPascalCase}
181+
*/
182+
getChecker,
80183

81-
return convertersMap[name] || pascalCase
184+
/**
185+
* Return case exact converter.
186+
* If the converted result is not the correct case, the original value is returned.
187+
* @param {string} name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
188+
* @return {kebabCase|camelCase|pascalCase}
189+
*/
190+
getExactConverter (name) {
191+
const converter = getConverter(name)
192+
const checker = getChecker(name)
193+
return (str) => {
194+
const result = converter(str)
195+
return checker(result) ? result : str/* cannot convert */
196+
}
82197
},
83198

84199
camelCase,
85200
pascalCase,
86201
kebabCase,
87-
snakeCase
202+
snakeCase,
203+
204+
isCamelCase,
205+
isPascalCase,
206+
isKebabCase,
207+
isSnakeCase
88208
}

tests/lib/rules/component-definition-name-casing.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,7 @@ ruleTester.run('component-definition-name-casing', rule, {
184184
name: 'foo bar'
185185
}
186186
`,
187-
output: `
188-
export default {
189-
name: 'FooBar'
190-
}
191-
`,
187+
output: null,
192188
parserOptions,
193189
errors: [{
194190
message: 'Property name "foo bar" is not PascalCase.',
@@ -203,11 +199,7 @@ ruleTester.run('component-definition-name-casing', rule, {
203199
name: 'foo!bar'
204200
}
205201
`,
206-
output: `
207-
export default {
208-
name: 'FooBar'
209-
}
210-
`,
202+
output: null,
211203
parserOptions,
212204
errors: [{
213205
message: 'Property name "foo!bar" is not PascalCase.',
@@ -222,11 +214,7 @@ ruleTester.run('component-definition-name-casing', rule, {
222214
name: 'foo!bar'
223215
})
224216
`,
225-
output: `
226-
new Vue({
227-
name: 'FooBar'
228-
})
229-
`,
217+
output: null,
230218
parserOptions: { ecmaVersion: 6 },
231219
errors: [{
232220
message: 'Property name "foo!bar" is not PascalCase.',

tests/lib/rules/component-name-in-template-casing.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,31 @@ tester.run('component-name-in-template-casing', rule, {
686686
'Component name "the-component" is not PascalCase.',
687687
'Component name "the-component" is not PascalCase.'
688688
]
689+
},
690+
{
691+
code: `
692+
<template>
693+
<foo--bar />
694+
<Foo--Bar />
695+
<FooBar />
696+
<FooBar_Baz-qux />
697+
</template>`,
698+
output: `
699+
<template>
700+
<foo--bar />
701+
<Foo--Bar />
702+
<foo-bar />
703+
<foo-bar-baz-qux />
704+
</template>`,
705+
options: ['kebab-case', {
706+
registeredComponentsOnly: false
707+
}],
708+
errors: [
709+
'Component name "foo--bar" is not kebab-case.',
710+
'Component name "Foo--Bar" is not kebab-case.',
711+
'Component name "FooBar" is not kebab-case.',
712+
'Component name "FooBar_Baz-qux" is not kebab-case.'
713+
]
689714
}
690715
]
691716
})

0 commit comments

Comments
 (0)