Skip to content

Commit e867c5c

Browse files
authored
Merge branch 'master' into feat/add-auto-fix
2 parents 3ca9212 + e13089e commit e867c5c

9 files changed

+232
-82
lines changed

docs/rules/no-reserved-component-names.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@ export default {
3535
{
3636
"vue/no-reserved-component-names": ["error", {
3737
"disallowVueBuiltInComponents": false,
38-
"disallowVue3BuiltInComponents": false
38+
"disallowVue3BuiltInComponents": false,
39+
"htmlElementCaseSensitive": false,
3940
}]
4041
}
4142
```
4243

4344
- `disallowVueBuiltInComponents` (`boolean`) ... If `true`, disallow Vue.js 2.x built-in component names. Default is `false`.
4445
- `disallowVue3BuiltInComponents` (`boolean`) ... If `true`, disallow Vue.js 3.x built-in component names. Default is `false`.
46+
- `htmlElementCaseSensitive` (`boolean`) ... If `true`, component names must exactly match the case of an HTML element to be considered conflicting. Default is `false` (i.e. case-insensitve comparison).
4547

4648
### `"disallowVueBuiltInComponents": true`
4749

@@ -73,6 +75,34 @@ export default {
7375

7476
</eslint-code-block>
7577

78+
### `"htmlElementCaseSensitive": true`
79+
80+
<eslint-code-block :rules="{'vue/no-reserved-component-names': ['error', {htmlElementCaseSensitive: true}]}">
81+
82+
```vue
83+
<script>
84+
/* ✓ GOOD */
85+
export default {
86+
name: 'Button'
87+
}
88+
</script>
89+
```
90+
91+
</eslint-code-block>
92+
93+
<eslint-code-block :rules="{'vue/no-reserved-component-names': ['error', {htmlElementCaseSensitive: true}]}">
94+
95+
```vue
96+
<script>
97+
/* ✗ BAD */
98+
export default {
99+
name: 'button'
100+
}
101+
</script>
102+
```
103+
104+
</eslint-code-block>
105+
76106
## :couple: Related Rules
77107

78108
- [vue/multi-word-component-names](./multi-word-component-names.md)

lib/rules/define-macros-order.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function isUseStrictStatement(node) {
5353
function getTargetStatementPosition(scriptSetup, program) {
5454
const skipStatements = new Set([
5555
'ImportDeclaration',
56+
'TSModuleDeclaration',
5657
'TSInterfaceDeclaration',
5758
'TSTypeAliasDeclaration',
5859
'DebuggerStatement',

lib/rules/no-reserved-component-names.js

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,6 @@ function isLowercase(word) {
3434
return /^[a-z]*$/.test(word)
3535
}
3636

37-
const RESERVED_NAMES_IN_HTML = new Set([
38-
...htmlElements,
39-
...htmlElements.map(casing.capitalize)
40-
])
41-
const RESERVED_NAMES_IN_OTHERS = new Set([
42-
...deprecatedHtmlElements,
43-
...deprecatedHtmlElements.map(casing.capitalize),
44-
...kebabCaseElements,
45-
...kebabCaseElements.map(casing.pascalCase),
46-
...svgElements,
47-
...svgElements.filter(isLowercase).map(casing.capitalize)
48-
])
49-
5037
/**
5138
* @param {Expression | SpreadElement} node
5239
* @returns {node is (Literal | TemplateLiteral)}
@@ -61,14 +48,14 @@ function canVerify(node) {
6148
}
6249

6350
/**
64-
* @param {string} name
65-
* @returns {string}
51+
* @template T
52+
* @param {Set<T>} set
53+
* @param {Iterable<T>} iterable
6654
*/
67-
function getMessageId(name) {
68-
if (RESERVED_NAMES_IN_HTML.has(name)) return 'reservedInHtml'
69-
if (RESERVED_NAMES_IN_VUE.has(name)) return 'reservedInVue'
70-
if (RESERVED_NAMES_IN_VUE3.has(name)) return 'reservedInVue3'
71-
return 'reserved'
55+
function addAll(set, iterable) {
56+
for (const element of iterable) {
57+
set.add(element)
58+
}
7259
}
7360

7461
module.exports = {
@@ -90,6 +77,9 @@ module.exports = {
9077
},
9178
disallowVue3BuiltInComponents: {
9279
type: 'boolean'
80+
},
81+
htmlElementCaseSensitive: {
82+
type: 'boolean'
9383
}
9484
},
9585
additionalProperties: false
@@ -109,6 +99,23 @@ module.exports = {
10999
options.disallowVueBuiltInComponents === true
110100
const disallowVue3BuiltInComponents =
111101
options.disallowVue3BuiltInComponents === true
102+
const htmlElementCaseSensitive = options.htmlElementCaseSensitive === true
103+
104+
const RESERVED_NAMES_IN_HTML = new Set(htmlElements)
105+
const RESERVED_NAMES_IN_OTHERS = new Set([
106+
...deprecatedHtmlElements,
107+
...kebabCaseElements,
108+
...svgElements
109+
])
110+
111+
if (!htmlElementCaseSensitive) {
112+
addAll(RESERVED_NAMES_IN_HTML, htmlElements.map(casing.capitalize))
113+
addAll(RESERVED_NAMES_IN_OTHERS, [
114+
...deprecatedHtmlElements.map(casing.capitalize),
115+
...kebabCaseElements.map(casing.pascalCase),
116+
...svgElements.filter(isLowercase).map(casing.capitalize)
117+
])
118+
}
112119

113120
const reservedNames = new Set([
114121
...RESERVED_NAMES_IN_HTML,
@@ -117,6 +124,17 @@ module.exports = {
117124
...RESERVED_NAMES_IN_OTHERS
118125
])
119126

127+
/**
128+
* @param {string} name
129+
* @returns {string}
130+
*/
131+
function getMessageId(name) {
132+
if (RESERVED_NAMES_IN_HTML.has(name)) return 'reservedInHtml'
133+
if (RESERVED_NAMES_IN_VUE.has(name)) return 'reservedInVue'
134+
if (RESERVED_NAMES_IN_VUE3.has(name)) return 'reservedInVue3'
135+
return 'reserved'
136+
}
137+
120138
/**
121139
* @param {Literal | TemplateLiteral} node
122140
*/

lib/rules/no-unused-emit-declarations.js

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,6 @@ function hasReferenceId(value, setupContext) {
5656
)
5757
}
5858

59-
/**
60-
* Check if the given name matches emitReferenceIds variable name
61-
* @param {string} name
62-
* @param {Set<Identifier>} emitReferenceIds
63-
* @returns {boolean}
64-
*/
65-
function isEmitVariableName(name, emitReferenceIds) {
66-
if (emitReferenceIds.size === 0) return false
67-
const emitVariable = emitReferenceIds.values().next().value.name
68-
return emitVariable === name
69-
}
70-
7159
module.exports = {
7260
meta: {
7361
type: 'suggestion',
@@ -91,6 +79,7 @@ module.exports = {
9179
/** @type {Map<ObjectExpression | Program, SetupContext>} */
9280
const setupContexts = new Map()
9381
const programNode = context.getSourceCode().ast
82+
let emitParamName = ''
9483

9584
/**
9685
* @param {CallExpression} node
@@ -204,14 +193,6 @@ module.exports = {
204193

205194
const { contextReferenceIds, emitReferenceIds } = setupContext
206195

207-
// verify defineEmits variable in template
208-
if (
209-
callee.type === 'Identifier' &&
210-
isEmitVariableName(callee.name, emitReferenceIds)
211-
) {
212-
addEmitCall(node)
213-
}
214-
215196
// verify setup(props,{emit}) {emit()}
216197
addEmitCallByReference(callee, emitReferenceIds, node)
217198
if (emit && emit.name === 'emit') {
@@ -229,8 +210,11 @@ module.exports = {
229210
}
230211
}
231212

232-
// verify $emit() in template
233-
if (callee.type === 'Identifier' && callee.name === '$emit') {
213+
// verify $emit() and defineEmits variable in template
214+
if (
215+
callee.type === 'Identifier' &&
216+
(callee.name === '$emit' || callee.name === emitParamName)
217+
) {
234218
addEmitCall(node)
235219
}
236220
}
@@ -316,10 +300,15 @@ module.exports = {
316300
}
317301

318302
const emitParam = node.parent.id
319-
const variable =
320-
emitParam.type === 'Identifier'
321-
? findVariable(utils.getScope(context, emitParam), emitParam)
322-
: null
303+
if (emitParam.type !== 'Identifier') {
304+
return
305+
}
306+
307+
emitParamName = emitParam.name
308+
const variable = findVariable(
309+
utils.getScope(context, emitParam),
310+
emitParam
311+
)
323312
if (!variable) {
324313
return
325314
}

lib/rules/require-explicit-emits.js

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -71,22 +71,6 @@ function getNameParamNode(node) {
7171
return null
7272
}
7373

74-
/**
75-
* Check if the given name matches defineEmitsNode variable name
76-
* @param {string} name
77-
* @param {CallExpression | undefined} defineEmitsNode
78-
* @returns {boolean}
79-
*/
80-
function isEmitVariableName(name, defineEmitsNode) {
81-
const node = defineEmitsNode?.parent
82-
83-
if (node?.type === 'VariableDeclarator' && node.id.type === 'Identifier') {
84-
return name === node.id.name
85-
}
86-
87-
return false
88-
}
89-
9074
module.exports = {
9175
meta: {
9276
type: 'suggestion',
@@ -128,6 +112,7 @@ module.exports = {
128112
const vueEmitsDeclarations = new Map()
129113
/** @type {Map<ObjectExpression | Program, ComponentProp[]>} */
130114
const vuePropsDeclarations = new Map()
115+
let emitParamName = ''
131116

132117
/**
133118
* @typedef {object} VueTemplateDefineData
@@ -271,11 +256,7 @@ module.exports = {
271256
// e.g. $emit() / emit() in template
272257
if (
273258
callee.type === 'Identifier' &&
274-
(callee.name === '$emit' ||
275-
isEmitVariableName(
276-
callee.name,
277-
vueTemplateDefineData.defineEmits
278-
))
259+
(callee.name === '$emit' || callee.name === emitParamName)
279260
) {
280261
verifyEmit(
281262
vueTemplateDefineData.emits,
@@ -308,10 +289,15 @@ module.exports = {
308289
}
309290

310291
const emitParam = node.parent.id
311-
const variable =
312-
emitParam.type === 'Identifier'
313-
? findVariable(utils.getScope(context, emitParam), emitParam)
314-
: null
292+
if (emitParam.type !== 'Identifier') {
293+
return
294+
}
295+
296+
emitParamName = emitParam.name
297+
const variable = findVariable(
298+
utils.getScope(context, emitParam),
299+
emitParam
300+
)
315301
if (!variable) {
316302
return
317303
}

lib/rules/require-explicit-slots.js

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ function getSlotsName(node) {
3535
return null
3636
}
3737

38+
/**
39+
* @param {VElement} node
40+
* @return {VAttribute | VDirective | undefined}
41+
*/
42+
function getSlotNameNode(node) {
43+
return node.startTag.attributes.find(
44+
(node) =>
45+
(!node.directive && node.key.name === 'name') ||
46+
(node.directive &&
47+
node.key.name.name === 'bind' &&
48+
node.key.argument?.type === 'VIdentifier' &&
49+
node.key.argument?.name === 'name')
50+
)
51+
}
52+
3853
module.exports = {
3954
meta: {
4055
type: 'problem',
@@ -68,6 +83,19 @@ module.exports = {
6883
}
6984
const slotsDefined = new Set()
7085

86+
/**
87+
* @param {VElement} node
88+
* @param {string | undefined} slotName
89+
*/
90+
function reportMissingSlot(node, slotName) {
91+
if (!slotsDefined.has(slotName)) {
92+
context.report({
93+
node,
94+
messageId: 'requireExplicitSlots'
95+
})
96+
}
97+
}
98+
7199
return utils.compositingVisitors(
72100
utils.defineScriptSetupVisitor(context, {
73101
onDefineSlotsEnter(node) {
@@ -137,20 +165,27 @@ module.exports = {
137165
}
138166
}),
139167
utils.defineTemplateBodyVisitor(context, {
168+
/** @param {VElement} node */
140169
"VElement[name='slot']"(node) {
141-
let slotName = 'default'
142-
143-
const slotNameAttr = utils.getAttribute(node, 'name')
170+
const nameNode = getSlotNameNode(node)
144171

145-
if (slotNameAttr?.value) {
146-
slotName = slotNameAttr.value.value
172+
// if no slot name is declared, default to 'default'
173+
if (!nameNode) {
174+
reportMissingSlot(node, 'default')
175+
return
147176
}
148177

149-
if (!slotsDefined.has(slotName)) {
150-
context.report({
151-
node,
152-
messageId: 'requireExplicitSlots'
153-
})
178+
if (nameNode.directive) {
179+
const expression = nameNode.value?.expression
180+
// ignore attribute binding except string literal
181+
if (!expression || !utils.isStringLiteral(expression)) {
182+
return
183+
}
184+
185+
const name = utils.getStringLiteralValue(expression) || undefined
186+
reportMissingSlot(node, name)
187+
} else {
188+
reportMissingSlot(node, nameNode.value?.value)
154189
}
155190
}
156191
})

tests/lib/rules/define-macros-order.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ tester.run('define-macros-order', rule, {
126126
code: `
127127
<script setup lang="ts">
128128
import { bar } from 'foo'
129+
declare global {}
130+
declare namespace Namespace {}
129131
export interface Props {
130132
msg?: string
131133
labels?: string[]

0 commit comments

Comments
 (0)