Skip to content

Commit ad07deb

Browse files
Fix false positives for circular references in <script setup> in vue/no-undef-components (#2073)
Co-authored-by: Mr.Hope <mister-hope@outlook.com>
1 parent 5e0bd2c commit ad07deb

File tree

2 files changed

+160
-62
lines changed

2 files changed

+160
-62
lines changed

lib/rules/no-undef-components.js

+117-62
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
'use strict'
66

7+
const path = require('path')
78
const utils = require('../utils')
89
const casing = require('../utils/casing')
910

@@ -17,6 +18,93 @@ function camelize(str) {
1718
return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
1819
}
1920

21+
class DefinedInSetupComponents {
22+
constructor() {
23+
/**
24+
* Component names
25+
* @type {Set<string>}
26+
*/
27+
this.names = new Set()
28+
}
29+
30+
/**
31+
* @param {string[]} names
32+
*/
33+
addName(...names) {
34+
for (const name of names) {
35+
this.names.add(name)
36+
}
37+
}
38+
39+
/**
40+
* @see https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L334
41+
* @param {string} rawName
42+
*/
43+
isDefinedComponent(rawName) {
44+
if (this.names.has(rawName)) {
45+
return true
46+
}
47+
const camelName = camelize(rawName)
48+
if (this.names.has(camelName)) {
49+
return true
50+
}
51+
const pascalName = casing.capitalize(camelName)
52+
if (this.names.has(pascalName)) {
53+
return true
54+
}
55+
// Check namespace
56+
// https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L305
57+
const dotIndex = rawName.indexOf('.')
58+
if (dotIndex > 0 && this.isDefinedComponent(rawName.slice(0, dotIndex))) {
59+
return true
60+
}
61+
return false
62+
}
63+
}
64+
65+
class DefinedInOptionComponents {
66+
constructor() {
67+
/**
68+
* Component names
69+
* @type {Set<string>}
70+
*/
71+
this.names = new Set()
72+
/**
73+
* Component names, transformed to kebab-case
74+
* @type {Set<string>}
75+
*/
76+
this.kebabCaseNames = new Set()
77+
}
78+
79+
/**
80+
* @param {string[]} names
81+
*/
82+
addName(...names) {
83+
for (const name of names) {
84+
this.names.add(name)
85+
this.kebabCaseNames.add(casing.kebabCase(name))
86+
}
87+
}
88+
89+
/**
90+
* @param {string} rawName
91+
*/
92+
isDefinedComponent(rawName) {
93+
if (this.names.has(rawName)) {
94+
return true
95+
}
96+
const kebabCaseName = casing.kebabCase(rawName)
97+
if (
98+
this.kebabCaseNames.has(kebabCaseName) &&
99+
!casing.isPascalCase(rawName)
100+
) {
101+
// Component registered as `foo-bar` cannot be used as `FooBar`
102+
return true
103+
}
104+
return false
105+
}
106+
}
107+
20108
module.exports = {
21109
meta: {
22110
type: 'suggestion',
@@ -109,13 +197,15 @@ module.exports = {
109197

110198
if (utils.isScriptSetup(context)) {
111199
// For <script setup>
200+
const definedInSetupComponents = new DefinedInSetupComponents()
201+
const definedInOptionComponents = new DefinedInOptionComponents()
202+
112203
/** @type {Set<string>} */
113-
const scriptVariableNames = new Set()
114204
const scriptTypeOnlyNames = new Set()
115205
const globalScope = context.getSourceCode().scopeManager.globalScope
116206
if (globalScope) {
117207
for (const variable of globalScope.variables) {
118-
scriptVariableNames.add(variable.name)
208+
definedInSetupComponents.addName(variable.name)
119209
}
120210
const moduleScope = globalScope.childScopes.find(
121211
(scope) => scope.type === 'module'
@@ -146,39 +236,37 @@ module.exports = {
146236
) {
147237
scriptTypeOnlyNames.add(variable.name)
148238
} else {
149-
scriptVariableNames.add(variable.name)
239+
definedInSetupComponents.addName(variable.name)
150240
}
151241
}
152242
}
153-
/**
154-
* @see https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L334
155-
* @param {string} name
156-
*/
157-
const existsSetupReference = (name) => {
158-
if (scriptVariableNames.has(name)) {
159-
return true
160-
}
161-
const camelName = camelize(name)
162-
if (scriptVariableNames.has(camelName)) {
163-
return true
164-
}
165-
const pascalName = casing.capitalize(camelName)
166-
if (scriptVariableNames.has(pascalName)) {
167-
return true
243+
244+
// For circular references
245+
const fileName = context.getFilename()
246+
const selfComponentName = path.basename(fileName, path.extname(fileName))
247+
definedInSetupComponents.addName(selfComponentName)
248+
scriptVisitor = utils.defineVueVisitor(context, {
249+
onVueObjectEnter(node, { type }) {
250+
if (type !== 'export') return
251+
const nameProperty = utils.findProperty(node, 'name')
252+
253+
if (nameProperty && utils.isStringLiteral(nameProperty.value)) {
254+
const name = utils.getStringLiteralValue(nameProperty.value)
255+
if (name) {
256+
definedInOptionComponents.addName(name)
257+
}
258+
}
168259
}
169-
return false
170-
}
260+
})
261+
171262
verifyName = (rawName, reportNode) => {
172263
if (!isVerifyTargetComponent(rawName)) {
173264
return
174265
}
175-
if (existsSetupReference(rawName)) {
266+
if (definedInSetupComponents.isDefinedComponent(rawName)) {
176267
return
177268
}
178-
// Check namespace
179-
// https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L305
180-
const dotIndex = rawName.indexOf('.')
181-
if (dotIndex > 0 && existsSetupReference(rawName.slice(0, dotIndex))) {
269+
if (definedInOptionComponents.isDefinedComponent(rawName)) {
182270
return
183271
}
184272

@@ -192,26 +280,10 @@ module.exports = {
192280
}
193281
} else {
194282
// For Options API
195-
196-
/**
197-
* All registered components
198-
* @type {string[]}
199-
*/
200-
const registeredComponentNames = []
201-
/**
202-
* All registered components, transformed to kebab-case
203-
* @type {string[]}
204-
*/
205-
const registeredComponentKebabCaseNames = []
206-
207-
/**
208-
* All registered components using kebab-case syntax
209-
* @type {string[]}
210-
*/
211-
const componentsRegisteredAsKebabCase = []
283+
const definedInOptionComponents = new DefinedInOptionComponents()
212284

213285
scriptVisitor = utils.executeOnVue(context, (obj) => {
214-
registeredComponentNames.push(
286+
definedInOptionComponents.addName(
215287
...utils.getRegisteredComponents(obj).map(({ name }) => name)
216288
)
217289

@@ -220,33 +292,16 @@ module.exports = {
220292
if (nameProperty && utils.isStringLiteral(nameProperty.value)) {
221293
const name = utils.getStringLiteralValue(nameProperty.value)
222294
if (name) {
223-
registeredComponentNames.push(name)
295+
definedInOptionComponents.addName(name)
224296
}
225297
}
226-
227-
registeredComponentKebabCaseNames.push(
228-
...registeredComponentNames.map((name) => casing.kebabCase(name))
229-
)
230-
componentsRegisteredAsKebabCase.push(
231-
...registeredComponentNames.filter(
232-
(name) => name === casing.kebabCase(name)
233-
)
234-
)
235298
})
236299

237300
verifyName = (rawName, reportNode) => {
238301
if (!isVerifyTargetComponent(rawName)) {
239302
return
240303
}
241-
if (registeredComponentNames.includes(rawName)) {
242-
return
243-
}
244-
const kebabCaseName = casing.kebabCase(rawName)
245-
if (
246-
registeredComponentKebabCaseNames.includes(kebabCaseName) &&
247-
!casing.isPascalCase(rawName)
248-
) {
249-
// Component registered as `foo-bar` cannot be used as `FooBar`
304+
if (definedInOptionComponents.isDefinedComponent(rawName)) {
250305
return
251306
}
252307

tests/lib/rules/no-undef-components.js

+43
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,49 @@ tester.run('no-undef-components', rule, {
108108
}
109109
]
110110
},
111+
// circular references
112+
{
113+
filename: 'test.vue',
114+
code: `
115+
<script setup>
116+
</script>
117+
118+
<template>
119+
<test />
120+
</template>
121+
`
122+
},
123+
{
124+
filename: 'FooBar.vue',
125+
code: `
126+
<script setup>
127+
</script>
128+
129+
<template>
130+
<FooBar />
131+
<foo-bar />
132+
</template>
133+
`
134+
},
135+
{
136+
filename: 'FooBar.vue',
137+
code: `
138+
<script>
139+
export default {
140+
name: 'BarFoo'
141+
}
142+
</script>
143+
<script setup>
144+
</script>
145+
146+
<template>
147+
<FooBar />
148+
<foo-bar />
149+
<BarFoo />
150+
<bar-foo />
151+
</template>
152+
`
153+
},
111154

112155
// options API
113156
{

0 commit comments

Comments
 (0)