Skip to content

Commit

Permalink
Fix some casing issues (#1152)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
ota-meshi and armano2 committed May 21, 2020
1 parent e80c2f0 commit f03b2d8
Show file tree
Hide file tree
Showing 13 changed files with 423 additions and 92 deletions.
2 changes: 1 addition & 1 deletion lib/rules/attribute-hyphenation.js
Expand Up @@ -56,7 +56,7 @@ module.exports = {
ignoredAttributes = ignoredAttributes.concat(optionsPayload.ignore)
}

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

function reportIssue (node, name) {
const text = sourceCode.getText(node.key)
Expand Down
5 changes: 2 additions & 3 deletions lib/rules/component-definition-name-casing.js
Expand Up @@ -48,16 +48,15 @@ module.exports = {
range = node.range
}

const value = casing.getConverter(caseType)(nodeValue)
if (value !== nodeValue) {
if (!casing.getChecker(caseType)(nodeValue)) {
context.report({
node: node,
message: 'Property name "{{value}}" is not {{caseType}}.',
data: {
value: nodeValue,
caseType: caseType
},
fix: fixer => fixer.replaceTextRange([range[0] + 1, range[1] - 1], value)
fix: fixer => fixer.replaceTextRange([range[0] + 1, range[1] - 1], casing.getExactConverter(caseType)(nodeValue))
})
}
}
Expand Down
7 changes: 3 additions & 4 deletions lib/rules/component-name-in-template-casing.js
Expand Up @@ -87,7 +87,7 @@ module.exports = {
}
// We only verify the components registered in the component.
if (registeredComponents
.filter(name => casing.pascalCase(name) === name) // When defining a component with PascalCase, you can use either case
.filter(name => casing.isPascalCase(name)) // When defining a component with PascalCase, you can use either case
.some(name => node.rawName === name || casing.pascalCase(node.rawName) === name)) {
return true
}
Expand All @@ -108,11 +108,10 @@ module.exports = {
}

const name = node.rawName
const casingName = casing.getConverter(caseType)(name)
if (casingName !== name) {
if (!casing.getChecker(caseType)(name)) {
const startTag = node.startTag
const open = tokens.getFirstToken(startTag)

const casingName = casing.getExactConverter(caseType)(name)
context.report({
node: open,
loc: open.loc,
Expand Down
4 changes: 2 additions & 2 deletions lib/rules/name-property-casing.js
Expand Up @@ -48,8 +48,8 @@ module.exports = {

if (!node) return

const value = casing.getConverter(caseType)(node.value.value)
if (value !== node.value.value) {
if (!casing.getChecker(caseType)(node.value.value)) {
const value = casing.getExactConverter(caseType)(node.value.value)
context.report({
node: node.value,
message: 'Property name "{{value}}" is not {{caseType}}.',
Expand Down
3 changes: 1 addition & 2 deletions lib/rules/no-unregistered-components.js
Expand Up @@ -128,8 +128,7 @@ module.exports = {

// Component registered as `foo-bar` cannot be used as `FooBar`
if (
name.indexOf('-') === -1 &&
name === casing.pascalCase(name) &&
casing.isPascalCase(name) &&
componentsRegisteredAsKebabCase.indexOf(kebabCaseName) !== -1
) {
return true
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/no-unused-components.js
Expand Up @@ -87,7 +87,7 @@ module.exports = {
// it can be used in various of ways inside template,
// like "theComponent", "The-component" etc.
// but except snake_case
if (casing.pascalCase(name) === name || casing.camelCase(name) === name) {
if (casing.isPascalCase(name) || casing.isCamelCase(name)) {
return ![...usedComponents].some(n => {
return n.indexOf('_') === -1 && (name === casing.pascalCase(n) || casing.camelCase(n) === name)
})
Expand Down
5 changes: 2 additions & 3 deletions lib/rules/prop-name-casing.js
Expand Up @@ -15,7 +15,7 @@ const allowedCaseOptions = ['camelCase', 'snake_case']
function create (context) {
const options = context.options[0]
const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'camelCase'
const converter = casing.getConverter(caseType)
const checker = casing.getChecker(caseType)

// ----------------------------------------------------------------------
// Public
Expand All @@ -31,8 +31,7 @@ function create (context) {
// (boolean | null | number | RegExp) Literal
continue
}
const convertedName = converter(propName)
if (convertedName !== propName) {
if (!checker(propName)) {
context.report({
node: item.node,
message: 'Prop "{{name}}" is not in {{caseType}}.',
Expand Down
166 changes: 143 additions & 23 deletions lib/utils/casing.js
@@ -1,6 +1,27 @@
const assert = require('assert')

const invalidChars = /[^a-zA-Z0-9:]+/g
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

/**
* Capitalize a string.
*/
function capitalize (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
* Checks whether the given string has symbols.
*/
function hasSymbols (str) {
return /[!"#%&'()*+,./:;<=>?@[\\\]^`{|}]/u.exec(str) // without " ", "$", "-" and "_"
}
/**
* Checks whether the given string has upper.
*/
function hasUpper (str) {
return /[A-Z]/u.exec(str)
}

/**
* Convert text to kebab-case
Expand All @@ -9,39 +30,76 @@ const invalidChars = /[^a-zA-Z0-9:]+/g
*/
function kebabCase (str) {
return str
.replace(/[A-Z]/g, match => '-' + match)
.replace(/([^a-zA-Z])-([A-Z])/g, match => match[0] + match[2])
.replace(/^-/, '')
.replace(invalidChars, '-')
.replace(/_/gu, '-')
.replace(/\B([A-Z])/gu, '-$1')
.toLowerCase()
}

/**
* Checks whether the given string is kebab-case.
*/
function isKebabCase (str) {
if (
hasUpper(str) ||
hasSymbols(str) ||
/^-/u.exec(str) || // starts with hyphen is not kebab-case
/_|--|\s/u.exec(str)
) {
return false
}
return true
}

/**
* Convert text to snake_case
* @param {string} str Text to be converted
* @return {string}
*/
function snakeCase (str) {
return str
.replace(/[A-Z]/g, match => '_' + match)
.replace(/([^a-zA-Z])_([A-Z])/g, match => match[0] + match[2])
.replace(/^_/, '')
.replace(invalidChars, '_')
.replace(/\B([A-Z])/gu, '_$1')
.replace(/-/gu, '_')
.toLowerCase()
}

/**
* Checks whether the given string is snake_case.
*/
function isSnakeCase (str) {
if (
hasUpper(str) ||
hasSymbols(str) ||
/-|__|\s/u.exec(str)
) {
return false
}
return true
}

/**
* Convert text to camelCase
* @param {string} str Text to be converted
* @return {string} Converted string
*/
function camelCase (str) {
return str
.replace(/_/g, (_, index) => index === 0 ? _ : '-')
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) =>
index === 0 ? letter.toLowerCase() : letter.toUpperCase()
)
.replace(invalidChars, '')
if (isPascalCase(str)) {
return str.charAt(0).toLowerCase() + str.slice(1)
}
return str.replace(/[-_](\w)/gu, (_, c) => c ? c.toUpperCase() : '')
}

/**
* Checks whether the given string is camelCase.
*/
function isCamelCase (str) {
if (
hasSymbols(str) ||
/^[A-Z]/u.exec(str) ||
/-|_|\s/u.exec(str) // kebab or snake or space
) {
return false
}
return true
}

/**
Expand All @@ -50,10 +108,21 @@ function camelCase (str) {
* @return {string} Converted string
*/
function pascalCase (str) {
return str
.replace(/_/g, (_, index) => index === 0 ? _ : '-')
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => letter.toUpperCase())
.replace(invalidChars, '')
return capitalize(camelCase(str))
}

/**
* Checks whether the given string is PascalCase.
*/
function isPascalCase (str) {
if (
hasSymbols(str) ||
/^[a-z]/u.exec(str) ||
/-|_|\s/u.exec(str) // kebab or snake or space
) {
return false
}
return true
}

const convertersMap = {
Expand All @@ -63,6 +132,34 @@ const convertersMap = {
'PascalCase': pascalCase
}

const checkersMap = {
'kebab-case': isKebabCase,
'snake_case': isSnakeCase,
'camelCase': isCamelCase,
'PascalCase': isPascalCase
}
/**
* Return case checker
* @param {string} name type of checker to return ('camelCase', 'kebab-case', 'PascalCase')
* @return {isKebabCase|isCamelCase|isPascalCase}
*/
function getChecker (name) {
assert(typeof name === 'string')

return checkersMap[name] || isPascalCase
}

/**
* Return case converter
* @param {string} name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
* @return {kebabCase|camelCase|pascalCase}
*/
function getConverter (name) {
assert(typeof name === 'string')

return convertersMap[name] || pascalCase
}

module.exports = {
allowedCaseOptions: [
'camelCase',
Expand All @@ -75,14 +172,37 @@ module.exports = {
* @param {string} name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
* @return {kebabCase|camelCase|pascalCase}
*/
getConverter (name) {
assert(typeof name === 'string')
getConverter,

/**
* Return case checker
* @param {string} name type of checker to return ('camelCase', 'kebab-case', 'PascalCase')
* @return {isKebabCase|isCamelCase|isPascalCase}
*/
getChecker,

return convertersMap[name] || pascalCase
/**
* Return case exact converter.
* If the converted result is not the correct case, the original value is returned.
* @param {string} name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
* @return {kebabCase|camelCase|pascalCase}
*/
getExactConverter (name) {
const converter = getConverter(name)
const checker = getChecker(name)
return (str) => {
const result = converter(str)
return checker(result) ? result : str/* cannot convert */
}
},

camelCase,
pascalCase,
kebabCase,
snakeCase
snakeCase,

isCamelCase,
isPascalCase,
isKebabCase,
isSnakeCase
}
18 changes: 3 additions & 15 deletions tests/lib/rules/component-definition-name-casing.js
Expand Up @@ -184,11 +184,7 @@ ruleTester.run('component-definition-name-casing', rule, {
name: 'foo bar'
}
`,
output: `
export default {
name: 'FooBar'
}
`,
output: null,
parserOptions,
errors: [{
message: 'Property name "foo bar" is not PascalCase.',
Expand All @@ -203,11 +199,7 @@ ruleTester.run('component-definition-name-casing', rule, {
name: 'foo!bar'
}
`,
output: `
export default {
name: 'FooBar'
}
`,
output: null,
parserOptions,
errors: [{
message: 'Property name "foo!bar" is not PascalCase.',
Expand All @@ -222,11 +214,7 @@ ruleTester.run('component-definition-name-casing', rule, {
name: 'foo!bar'
})
`,
output: `
new Vue({
name: 'FooBar'
})
`,
output: null,
parserOptions: { ecmaVersion: 6 },
errors: [{
message: 'Property name "foo!bar" is not PascalCase.',
Expand Down
25 changes: 25 additions & 0 deletions tests/lib/rules/component-name-in-template-casing.js
Expand Up @@ -686,6 +686,31 @@ tester.run('component-name-in-template-casing', rule, {
'Component name "the-component" is not PascalCase.',
'Component name "the-component" is not PascalCase.'
]
},
{
code: `
<template>
<foo--bar />
<Foo--Bar />
<FooBar />
<FooBar_Baz-qux />
</template>`,
output: `
<template>
<foo--bar />
<Foo--Bar />
<foo-bar />
<foo-bar-baz-qux />
</template>`,
options: ['kebab-case', {
registeredComponentsOnly: false
}],
errors: [
'Component name "foo--bar" is not kebab-case.',
'Component name "Foo--Bar" is not kebab-case.',
'Component name "FooBar" is not kebab-case.',
'Component name "FooBar_Baz-qux" is not kebab-case.'
]
}
]
})

0 comments on commit f03b2d8

Please sign in to comment.