Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix some casing issues #1152

Merged
merged 9 commits into from May 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.'
]
}
]
})