From 0849a271ca7a6a0592df9183dc0f972f7b30bd9b Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sat, 9 May 2020 18:13:29 +0900 Subject: [PATCH] Add `disallowVueBuiltInComponents` and `disallowVue3BuiltInComponents` option that reports Vue built-in component names to the `vue/no-reserved-component-names` rule. (#1116) --- docs/rules/no-reserved-component-names.md | 45 +++++- lib/rules/no-reserved-component-names.js | 85 ++++++++--- .../lib/rules/no-reserved-component-names.js | 136 +++++++++++++++++- 3 files changed, 241 insertions(+), 25 deletions(-) diff --git a/docs/rules/no-reserved-component-names.md b/docs/rules/no-reserved-component-names.md index 3aa5f5db3..2d11b2339 100644 --- a/docs/rules/no-reserved-component-names.md +++ b/docs/rules/no-reserved-component-names.md @@ -9,7 +9,7 @@ description: disallow the use of reserved names in component definitions ## :book: Rule Details -This rule prevents name collisions between vue components and standard html elements. +This rule prevents name collisions between Vue components and standard HTML elements and built-in components. @@ -26,7 +26,47 @@ export default { ## :wrench: Options -Nothing. +```json +{ + "vue/no-reserved-component-names": ["error", { + "disallowVueBuiltInComponents": false, + "disallowVue3BuiltInComponents": false + }] +} +``` + +- `disallowVueBuiltInComponents` (`boolean`) ... If `true`, disallow Vue.js 2.x built-in component names. Default is `false`. +- `disallowVue3BuiltInComponents` (`boolean`) ... If `true`, disallow Vue.js 3.x built-in component names. Default is `false`. + +### `"disallowVueBuiltInComponents": true` + + + +```vue + +``` + + + +### `"disallowVue3BuiltInComponents": true` + + + +```vue + +``` + + ## :books: Further reading @@ -34,6 +74,7 @@ Nothing. - [List of SVG elements](https://developer.mozilla.org/en-US/docs/Web/SVG/Element) - [Kebab case elements](https://stackoverflow.com/questions/22545621/do-custom-elements-require-a-dash-in-their-name/22545622#22545622) - [Valid custom element name](https://w3c.github.io/webcomponents/spec/custom/#valid-custom-element-name) +- [API - Built-In Components](https://vuejs.org/v2/api/index.html#Built-In-Components) ## :mag: Implementation diff --git a/lib/rules/no-reserved-component-names.js b/lib/rules/no-reserved-component-names.js index 75e535567..c566684e6 100644 --- a/lib/rules/no-reserved-component-names.js +++ b/lib/rules/no-reserved-component-names.js @@ -22,20 +22,44 @@ const kebabCaseElements = [ 'missing-glyph' ] +// https://vuejs.org/v2/api/index.html#Built-In-Components +const vueBuiltInComponents = [ + 'component', + 'transition', + 'transition-group', + 'keep-alive', + 'slot' +] + +const vue3BuiltInComponents = [ + 'teleport', + 'suspense' +] + const isLowercase = (word) => /^[a-z]*$/.test(word) const capitalizeFirstLetter = (word) => word[0].toUpperCase() + word.substring(1, word.length) -const RESERVED_NAMES = new Set( - [ - ...kebabCaseElements, - ...kebabCaseElements.map(casing.pascalCase), - ...htmlElements, - ...htmlElements.map(capitalizeFirstLetter), - ...deprecatedHtmlElements, - ...deprecatedHtmlElements.map(capitalizeFirstLetter), - ...svgElements, - ...svgElements.filter(isLowercase).map(capitalizeFirstLetter) - ]) +const RESERVED_NAMES_IN_HTML = new Set([ + ...htmlElements, + ...htmlElements.map(capitalizeFirstLetter) +]) +const RESERVED_NAMES_IN_VUE = new Set([ + ...vueBuiltInComponents, + ...vueBuiltInComponents.map(casing.pascalCase) +]) +const RESERVED_NAMES_IN_VUE3 = new Set([ + ...RESERVED_NAMES_IN_VUE, + ...vue3BuiltInComponents, + ...vue3BuiltInComponents.map(casing.pascalCase) +]) +const RESERVED_NAMES_IN_OTHERS = new Set([ + ...deprecatedHtmlElements, + ...deprecatedHtmlElements.map(capitalizeFirstLetter), + ...kebabCaseElements, + ...kebabCaseElements.map(casing.pascalCase), + ...svgElements, + ...svgElements.filter(isLowercase).map(capitalizeFirstLetter) +]) // ------------------------------------------------------------------------------ // Rule Definition @@ -46,14 +70,41 @@ module.exports = { type: 'suggestion', docs: { description: 'disallow the use of reserved names in component definitions', - categories: undefined, // 'essential' + categories: undefined, url: 'https://eslint.vuejs.org/rules/no-reserved-component-names.html' }, fixable: null, - schema: [] + schema: [{ + type: 'object', + properties: { + disallowVueBuiltInComponents: { + type: 'boolean' + }, + disallowVue3BuiltInComponents: { + type: 'boolean' + } + } + }], + messages: { + reserved: 'Name "{{name}}" is reserved.', + reservedInHtml: 'Name "{{name}}" is reserved in HTML.', + reservedInVue: 'Name "{{name}}" is reserved in Vue.js.', + reservedInVue3: 'Name "{{name}}" is reserved in Vue.js 3.x.' + } }, create (context) { + const options = context.options[0] || {} + const disallowVueBuiltInComponents = options.disallowVueBuiltInComponents === true + const disallowVue3BuiltInComponents = options.disallowVue3BuiltInComponents === true + + const reservedNames = new Set([ + ...RESERVED_NAMES_IN_HTML, + ...(disallowVueBuiltInComponents ? RESERVED_NAMES_IN_VUE : []), + ...(disallowVue3BuiltInComponents ? RESERVED_NAMES_IN_VUE3 : []), + ...RESERVED_NAMES_IN_OTHERS + ]) + function canVerify (node) { return node.type === 'Literal' || ( node.type === 'TemplateLiteral' && @@ -70,7 +121,7 @@ module.exports = { } else { name = node.value } - if (RESERVED_NAMES.has(name)) { + if (reservedNames.has(name)) { report(node, name) } } @@ -78,7 +129,9 @@ module.exports = { function report (node, name) { context.report({ node: node, - message: 'Name "{{name}}" is reserved.', + messageId: RESERVED_NAMES_IN_HTML.has(name) ? 'reservedInHtml' + : RESERVED_NAMES_IN_VUE.has(name) ? 'reservedInVue' + : RESERVED_NAMES_IN_VUE3.has(name) ? 'reservedInVue3' : 'reserved', data: { name: name } @@ -98,7 +151,7 @@ module.exports = { utils.executeOnVue(context, (obj) => { // Report if a component has been registered locally with a reserved name. utils.getRegisteredComponents(obj) - .filter(({ name }) => RESERVED_NAMES.has(name)) + .filter(({ name }) => reservedNames.has(name)) .forEach(({ node, name }) => report(node, name)) const node = obj.properties diff --git a/tests/lib/rules/no-reserved-component-names.js b/tests/lib/rules/no-reserved-component-names.js index 0585b5c92..a3bc3bb01 100644 --- a/tests/lib/rules/no-reserved-component-names.js +++ b/tests/lib/rules/no-reserved-component-names.js @@ -11,6 +11,12 @@ const rule = require('../../../lib/rules/no-reserved-component-names') const RuleTester = require('eslint').RuleTester +const htmlElements = require('../../../lib/utils/html-elements.json') +const RESERVED_NAMES_IN_HTML = new Set([ + ...htmlElements, + ...htmlElements.map((word) => word[0].toUpperCase() + word.substring(1, word.length)) +]) + // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ @@ -234,6 +240,18 @@ const invalidElements = [ 'xmp', 'Xmp' ] +const vue2BuiltInComponents = [ + 'component', 'Component', + 'transition', 'Transition', + 'transition-group', 'TransitionGroup', + 'keep-alive', 'KeepAlive' +] + +const vue3BuiltInComponents = [ + 'teleport', 'Teleport', + 'suspense', 'Suspense' +] + const parserOptions = { ecmaVersion: 2018, sourceType: 'module' @@ -269,6 +287,16 @@ ruleTester.run('no-reserved-component-names', rule, { `, parserOptions }, + { + filename: 'test.vue', + code: ` + export default { + name: 'FooBar' + } + `, + options: [{ disallowVueBuiltInComponents: true, disallowVue3BuiltInComponents: true }], + parserOptions + }, { filename: 'test.vue', code: `Vue.component('FooBar', {})`, @@ -322,7 +350,41 @@ ruleTester.run('no-reserved-component-names', rule, { filename: 'test.js', code: `fn1(component.data)`, parserOptions - } + }, + ...vue2BuiltInComponents.map(name => { + return { + filename: `${name}.vue`, + code: ` + export default { + name: '${name}' + } + `, + parserOptions + } + }), + ...vue3BuiltInComponents.map(name => { + return { + filename: `${name}.vue`, + code: ` + export default { + name: '${name}' + } + `, + parserOptions + } + }), + ...vue3BuiltInComponents.map(name => { + return { + filename: `${name}.vue`, + code: ` + export default { + name: '${name}' + } + `, + parserOptions, + options: [{ disallowVueBuiltInComponents: true }] + } + }) ], invalid: [ @@ -336,7 +398,8 @@ ruleTester.run('no-reserved-component-names', rule, { `, parserOptions, errors: [{ - message: `Name "${name}" is reserved.`, + messageId: RESERVED_NAMES_IN_HTML.has(name) ? 'reservedInHtml' : 'reserved', + data: { name }, type: 'Literal', line: 3 }] @@ -348,7 +411,8 @@ ruleTester.run('no-reserved-component-names', rule, { code: `Vue.component('${name}', component)`, parserOptions, errors: [{ - message: `Name "${name}" is reserved.`, + messageId: RESERVED_NAMES_IN_HTML.has(name) ? 'reservedInHtml' : 'reserved', + data: { name }, type: 'Literal', line: 1 }] @@ -360,7 +424,8 @@ ruleTester.run('no-reserved-component-names', rule, { code: `app.component('${name}', component)`, parserOptions, errors: [{ - message: `Name "${name}" is reserved.`, + messageId: RESERVED_NAMES_IN_HTML.has(name) ? 'reservedInHtml' : 'reserved', + data: { name }, type: 'Literal', line: 1 }] @@ -372,7 +437,8 @@ ruleTester.run('no-reserved-component-names', rule, { code: `Vue.component(\`${name}\`, {})`, parserOptions, errors: [{ - message: `Name "${name}" is reserved.`, + messageId: RESERVED_NAMES_IN_HTML.has(name) ? 'reservedInHtml' : 'reserved', + data: { name }, type: 'TemplateLiteral', line: 1 }] @@ -384,7 +450,8 @@ ruleTester.run('no-reserved-component-names', rule, { code: `app.component(\`${name}\`, {})`, parserOptions, errors: [{ - message: `Name "${name}" is reserved.`, + messageId: RESERVED_NAMES_IN_HTML.has(name) ? 'reservedInHtml' : 'reserved', + data: { name }, type: 'TemplateLiteral', line: 1 }] @@ -400,11 +467,66 @@ ruleTester.run('no-reserved-component-names', rule, { }`, parserOptions, errors: [{ - message: `Name "${name}" is reserved.`, + messageId: RESERVED_NAMES_IN_HTML.has(name) ? 'reservedInHtml' : 'reserved', + data: { name }, type: 'Property', line: 3 }] } + }), + ...vue2BuiltInComponents.map(name => { + return { + filename: `${name}.vue`, + code: ` + export default { + name: '${name}' + } + `, + parserOptions, + options: [{ disallowVueBuiltInComponents: true }], + errors: [{ + messageId: 'reservedInVue', + data: { name }, + type: 'Literal', + line: 3 + }] + } + }), + ...vue2BuiltInComponents.map(name => { + return { + filename: `${name}.vue`, + code: ` + export default { + name: '${name}' + } + `, + parserOptions, + options: [{ disallowVue3BuiltInComponents: true }], + errors: [{ + messageId: 'reservedInVue', + data: { name }, + type: 'Literal', + line: 3 + }] + } + }), + ...vue3BuiltInComponents.map(name => { + return { + filename: `${name}.vue`, + code: ` + export default { + name: '${name}' + } + `, + parserOptions, + options: [{ disallowVue3BuiltInComponents: true }], + errors: [{ + messageId: 'reservedInVue3', + data: { name }, + type: 'Literal', + line: 3 + }] + } }) ] })