Skip to content

Fix: Add registeredComponentsOnly option to component-name-in-template-casing rule #714

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

Merged
merged 7 commits into from
Jan 3, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 70 additions & 15 deletions docs/rules/component-name-in-template-casing.md
Original file line number Diff line number Diff line change
@@ -20,30 +20,49 @@ This rule aims to warn the tag names other than the configured casing in Vue.js

```json
{
"vue/component-name-in-template-casing": ["error", "PascalCase" | "kebab-case", {
"vue/component-name-in-template-casing": ["error", "PascalCase" | "kebab-case", {
"registeredComponentsOnly": true,
"ignores": []
}]
}
```

- `"PascalCase"` (default) ... enforce tag names to pascal case. E.g. `<CoolComponent>`. This is consistent with the JSX practice.
- `"kebab-case"` ... enforce tag names to kebab case: E.g. `<cool-component>`. This is consistent with the HTML practice which is case-insensitive originally.
- `ignores` (`string[]`) ... The element names to ignore. Sets the element name to allow. For example, a custom element or a non-Vue component.
- `registeredComponentsOnly` ... If `true`, only registered components (in PascalCase) are checked. If `false`, check all.
default `true`
- `ignores` (`string[]`) ... The element names to ignore. Sets the element name to allow. For example, custom elements or Vue components with special name. You can set the regexp by writing it like `"/^name/"`.

### `"PascalCase"`
### `"PascalCase", { registeredComponentsOnly: true }` (default)

<eslint-code-block fix :rules="{'vue/component-name-in-template-casing': ['error']}">

```vue
<template>
<!-- ✓ GOOD -->
<TheComponent />
<CoolComponent />
<!-- ✗ BAD -->
<the-component />
<theComponent />
<The-component />
<cool-component />
<coolComponent />
<Cool-component />
<!-- ignore -->
<UnregisteredComponent />
<unregistered-component />
<registered-in-kebab-case />
<registeredInCamelCase />
</template>
<script>
export default {
components: {
CoolComponent,
'registered-in-kebab-case': VueComponent1,
'registeredInCamelCase': VueComponent2
}
}
</script>
```

</eslint-code-block>
@@ -55,28 +74,64 @@ This rule aims to warn the tag names other than the configured casing in Vue.js
```vue
<template>
<!-- ✓ GOOD -->
<the-component />
<cool-component />
<!-- ✗ BAD -->
<TheComponent />
<theComponent />
<Thecomponent />
<The-component />
<CoolComponent />
<coolComponent />
<Cool-component />
<!-- ignore -->
<unregistered-component />
<UnregisteredComponent />
</template>
<script>
export default {
components: {
CoolComponent
}
}
</script>
```

</eslint-code-block>

### `"PascalCase", { registeredComponentsOnly: false }`

<eslint-code-block fix :rules="{'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: false }]}">

```vue
<template>
<!-- ✓ GOOD -->
<CoolComponent />
<UnregisteredComponent />
<!-- ✗ BAD -->
<cool-component />
<unregistered-component />
</template>
<script>
export default {
components: {
CoolComponent
}
}
</script>
```

</eslint-code-block>

### `"PascalCase", { ignores: ["custom-element"] }`
### `"PascalCase", { ignores: ["/^custom-/"], registeredComponentsOnly: false }`

<eslint-code-block fix :rules="{'vue/component-name-in-template-casing': ['error', 'PascalCase', {ignores: ['custom-element']}]}">
<eslint-code-block fix :rules="{'vue/component-name-in-template-casing': ['error', 'PascalCase', {ignores: ['/^custom-/'], registeredComponentsOnly: false}]}">

```vue
<template>
<!-- ✓ GOOD -->
<TheComponent/>
<CoolComponent/>
<custom-element></custom-element>
<custom-button></custom-button>
<custom-input />
<!-- ✗ BAD -->
<magic-element></magic-element>
71 changes: 57 additions & 14 deletions lib/rules/component-name-in-template-casing.js
Original file line number Diff line number Diff line change
@@ -10,6 +10,11 @@

const utils = require('../utils')
const casing = require('../utils/casing')
const { toRegExp } = require('../utils/regexp')

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

const allowedCaseOptions = ['PascalCase', 'kebab-case']
const defaultCase = 'PascalCase'
@@ -39,6 +44,9 @@ module.exports = {
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
},
registeredComponentsOnly: {
type: 'boolean'
}
},
additionalProperties: false
@@ -50,9 +58,43 @@ module.exports = {
const caseOption = context.options[0]
const options = context.options[1] || {}
const caseType = allowedCaseOptions.indexOf(caseOption) !== -1 ? caseOption : defaultCase
const ignores = options.ignores || []
const ignores = (options.ignores || []).map(toRegExp)
const registeredComponentsOnly = options.registeredComponentsOnly !== false
const tokens = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()

const registeredComponents = []

/**
* Checks whether the given node is the verification target node.
* @param {VElement} node element node
* @returns {boolean} `true` if the given node is the verification target node.
*/
function isVerifyTarget (node) {
if (ignores.some(re => re.test(node.rawName))) {
// ignore
return false
}

if (!registeredComponentsOnly) {
// If the user specifies registeredComponentsOnly as false, it checks all component tags.
if ((!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
utils.isHtmlWellKnownElementName(node.rawName) ||
utils.isSvgWellKnownElementName(node.rawName)
) {
return false
}
return true
}
// 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
.some(name => node.rawName === name || casing.pascalCase(node.rawName) === name)) {
return true
}

return false
}

let hasInvalidEOF = false

return utils.defineTemplateBodyVisitor(context, {
@@ -61,18 +103,11 @@ module.exports = {
return
}

if (
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
utils.isHtmlWellKnownElementName(node.rawName) ||
utils.isSvgWellKnownElementName(node.rawName)
) {
if (!isVerifyTarget(node)) {
return
}

const name = node.rawName
if (ignores.indexOf(name) >= 0) {
return
}
const casingName = casing.getConverter(caseType)(name)
if (casingName !== name) {
const startTag = node.startTag
@@ -100,10 +135,18 @@ module.exports = {
})
}
}
}, {
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
})
},
Object.assign(
{
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
},
registeredComponentsOnly
? utils.executeOnVue(context, (obj) => {
registeredComponents.push(...utils.getRegisteredComponents(obj).map(n => n.name))
})
: {}
))
}
}
38 changes: 38 additions & 0 deletions lib/utils/regexp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const RE_REGEXP_CHAR = /[\\^$.*+?()[\]{}|]/gu
const RE_HAS_REGEXP_CHAR = new RegExp(RE_REGEXP_CHAR.source)

const RE_REGEXP_STR = /^\/(.+)\/(.*)$/u

/**
* Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+",
* "?", "(", ")", "[", "]", "{", "}", and "|" in `string`.
*
* @param {string} string The string to escape.
* @returns {string} Returns the escaped string.
*/
function escape (string) {
return (string && RE_HAS_REGEXP_CHAR.test(string))
? string.replace(RE_REGEXP_CHAR, '\\$&')
: string
}

/**
* Convert a string to the `RegExp`.
* Normal strings (e.g. `"foo"`) is converted to `/^foo$/` of `RegExp`.
* Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`.
*
* @param {string} string The string to convert.
* @returns {string} Returns the `RegExp`.
*/
function toRegExp (string) {
const parts = RE_REGEXP_STR.exec(string)
if (parts) {
return new RegExp(parts[1], parts[2])
}
return new RegExp(`^${escape(string)}$`)
}

module.exports = {
escape,
toRegExp
}
4 changes: 2 additions & 2 deletions tests/lib/autofix.js
Original file line number Diff line number Diff line change
@@ -95,7 +95,7 @@ describe('Complex autofix test cases', () => {
'component': 'never'
}
}],
'vue/component-name-in-template-casing': ['error', 'kebab-case']
'vue/component-name-in-template-casing': ['error', 'kebab-case', { registeredComponentsOnly: false }]
}})

const pascalConfig = Object.assign({}, baseConfig, { 'rules': {
@@ -104,7 +104,7 @@ describe('Complex autofix test cases', () => {
'component': 'never'
}
}],
'vue/component-name-in-template-casing': ['error']
'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: false }]
}})

it('Even if set kebab-case, the output should be as expected.', () => {
392 changes: 362 additions & 30 deletions tests/lib/rules/component-name-in-template-casing.js

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions tests/lib/utils/regexp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict'

const { escape, toRegExp } = require('../../../lib/utils/regexp')
const chai = require('chai')

const assert = chai.assert

const ESCAPED = '\\^\\$\\.\\*\\+\\?\\(\\)\\[\\]\\{\\}\\|\\\\'
const UNESCAPED = '^$.*+?()[]{}|\\'

describe('escape()', () => {
it('should escape values', () => {
assert.strictEqual(escape(UNESCAPED), ESCAPED)
assert.strictEqual(escape(UNESCAPED + UNESCAPED), ESCAPED + ESCAPED)
})

it('should handle strings with nothing to escape', () => {
assert.strictEqual(escape('abc'), 'abc')
})

it('should return an empty string for empty values', () => {
assert.strictEqual(escape(null), null)
assert.strictEqual(escape(undefined), undefined)
assert.strictEqual(escape(''), '')
})
})

describe('toRegExp()', () => {
it('should be convert to RegExp', () => {
assert.deepEqual(toRegExp('foo'), /^foo$/)
assert.deepEqual(toRegExp(UNESCAPED), new RegExp(`^${ESCAPED}$`))
})

it('RegExp like string should be convert to RegExp', () => {
assert.deepEqual(toRegExp('/^foo/i'), /^foo/i)
assert.deepEqual(toRegExp('/.*/iu'), /.*/iu)
assert.deepEqual(toRegExp(`${/^bar/i}`), /^bar/i)
assert.deepEqual(toRegExp(`${/[\sA-Z]+/u}`), /[\sA-Z]+/u)
})
})