From 4a08c070d94754f3be6fb131ba207c369d3f9b36 Mon Sep 17 00:00:00 2001 From: horitaka Date: Sat, 7 Jan 2023 21:49:39 +0900 Subject: [PATCH] fix: [vue/component-name-in-template-casing] False positive for dynamic component in template (#2069) --- .../component-name-in-template-casing.js | 29 +++- .../component-name-in-template-casing.js | 134 +++++++++++++++++- 2 files changed, 160 insertions(+), 3 deletions(-) diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js index df06a0b1c..b457ffa61 100644 --- a/lib/rules/component-name-in-template-casing.js +++ b/lib/rules/component-name-in-template-casing.js @@ -11,6 +11,30 @@ const { toRegExp } = require('../utils/regexp') const allowedCaseOptions = ['PascalCase', 'kebab-case'] const defaultCase = 'PascalCase' +/** + * Checks whether the given variable is the type-only import object. + * @param {Variable} variable + * @returns {boolean} `true` if the given variable is the type-only import. + */ +function isTypeOnlyImport(variable) { + if (variable.defs.length === 0) return false + + return variable.defs.every((def) => { + if (def.type !== 'ImportBinding') { + return false + } + if (def.parent.importKind === 'type') { + // check for `import type Foo from './xxx'` + return true + } + if (def.node.type === 'ImportSpecifier' && def.node.importKind === 'type') { + // check for `import { type Foo } from './xxx'` + return true + } + return false + }) +} + module.exports = { meta: { type: 'suggestion', @@ -75,10 +99,13 @@ module.exports = { (scope) => scope.type === 'module' ) for (const variable of (moduleScope && moduleScope.variables) || []) { - registeredComponents.add(variable.name) + if (!isTypeOnlyImport(variable)) { + registeredComponents.add(variable.name) + } } } } + /** * Checks whether the given node is the verification target node. * @param {VElement} node element node diff --git a/tests/lib/rules/component-name-in-template-casing.js b/tests/lib/rules/component-name-in-template-casing.js index d57a5ffbf..7b3f012c8 100644 --- a/tests/lib/rules/component-name-in-template-casing.js +++ b/tests/lib/rules/component-name-in-template-casing.js @@ -4,6 +4,7 @@ 'use strict' const rule = require('../../../lib/rules/component-name-in-template-casing') +const semver = require('semver') const RuleTester = require('eslint').RuleTester const tester = new RuleTester({ @@ -193,7 +194,43 @@ tester.run('component-name-in-template-casing', rule, { `, options: ['kebab-case', { globals: ['RouterView', 'router-link'] }] - } + }, + + // type-only imports + ...(semver.gte( + require('@typescript-eslint/parser/package.json').version, + '5.0.0' + ) + ? [ + { + code: ` + + + + `, + options: ['PascalCase', { registeredComponentsOnly: true }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + } + ] + : []) ], invalid: [ { @@ -939,6 +976,99 @@ tester.run('component-name-in-template-casing', rule, { column: 11 } ] - } + }, + // type-only imports + ...(semver.gte( + require('@typescript-eslint/parser/package.json').version, + '5.0.0' + ) + ? [ + { + code: ` + + + + `, + options: ['PascalCase', { registeredComponentsOnly: false }], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + output: ` + + + + `, + errors: [ + { + message: 'Component name "foo" is not PascalCase.', + line: 13, + column: 17 + }, + { + message: 'Component name "hello-world1" is not PascalCase.', + line: 14, + column: 17 + }, + { + message: 'Component name "hello-world2" is not PascalCase.', + line: 15, + column: 17 + }, + { + message: 'Component name "hello-world3" is not PascalCase.', + line: 16, + column: 17 + }, + { + message: 'Component name "hello-world4" is not PascalCase.', + line: 17, + column: 17 + }, + { + message: 'Component name "hello-world5" is not PascalCase.', + line: 18, + column: 17 + }, + { + message: 'Component name "component" is not PascalCase.', + line: 19, + column: 17 + } + ] + } + ] + : []) ] })