diff --git a/docs/rules/attribute-hyphenation.md b/docs/rules/attribute-hyphenation.md
index d5fba2e31..89442fceb 100644
--- a/docs/rules/attribute-hyphenation.md
+++ b/docs/rules/attribute-hyphenation.md
@@ -36,7 +36,8 @@ This rule enforces using hyphenated attribute names on custom components in Vue
```json
{
"vue/attribute-hyphenation": ["error", "always" | "never", {
- "ignore": []
+ "ignore": [],
+ "ignoreTags": []
}]
}
```
@@ -44,9 +45,10 @@ This rule enforces using hyphenated attribute names on custom components in Vue
Default casing is set to `always`. By default the following attributes are ignored: `data-`, `aria-`, `slot-scope`,
and all the [SVG attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute) with either an upper case letter or an hyphen.
-- `"always"` (default) ... Use hyphenated name.
-- `"never"` ... Don't use hyphenated name except the ones that are ignored.
-- `"ignore"` ... Array of ignored names
+- `"always"` (default) ... Use hyphenated attribute name.
+- `"never"` ... Don't use hyphenated attribute name.
+- `"ignore"` ... Array of attribute names that don't need to follow the specified casing.
+- `"ignoreTags"` ... Array of tag names whose attributes don't need to follow the specified casing.
### `"always"`
@@ -109,6 +111,22 @@ Don't use hyphenated name but allow custom attributes
+### `"never", { "ignoreTags": ["/^custom-/"] }`
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
## :couple: Related Rules
- [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md)
diff --git a/docs/rules/index.md b/docs/rules/index.md
index e359f47ab..074f4bd46 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -270,7 +270,7 @@ For example:
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: |
| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
-| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` instead of `ref` for template refs | | :hammer: |
+| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs | | :hammer: |
| [vue/require-default-export](./require-default-export.md) | require components to be the default export | | :warning: |
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | :hammer: |
| [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: |
@@ -281,7 +281,9 @@ For example:
| [vue/require-prop-comment](./require-prop-comment.md) | require props to have a comment | | :hammer: |
| [vue/require-typed-object-prop](./require-typed-object-prop.md) | enforce adding type declarations to object props | :bulb: | :hammer: |
| [vue/require-typed-ref](./require-typed-ref.md) | require `ref` and `shallowRef` functions to be strongly typed | | :hammer: |
+| [vue/restricted-component-names](./restricted-component-names.md) | enforce using only specific component names | | :warning: |
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `
```
-
+
```vue
@@ -41,17 +42,46 @@ export default {
/* ✗ BAD */
// inheritAttrs: true (default)
}
+
```
## :wrench: Options
-Nothing.
+```json
+{
+ "vue/no-duplicate-attr-inheritance": ["error", {
+ "checkMultiRootNodes": false,
+ }]
+}
+```
+
+- `"checkMultiRootNodes"`: If set to `true`, also suggest applying `inheritAttrs: false` to components with multiple root nodes (where `inheritAttrs: false` is the implicit default, see [attribute inheritance on multiple root nodes](https://vuejs.org/guide/components/attrs.html#attribute-inheritance-on-multiple-root-nodes)), whenever it detects `v-bind="$attrs"` being used. Default is `false`, which will ignore components with multiple root nodes.
+
+### `"checkMultiRootNodes": true`
+
+
+
+```vue
+
+
+
+
+
+```
+
+
## :books: Further Reading
- [API - inheritAttrs](https://vuejs.org/api/options-misc.html#inheritattrs)
+- [Fallthrough Attributes](https://vuejs.org/guide/components/attrs.html#attribute-inheritance-on-multiple-root-nodes)
## :rocket: Version
diff --git a/docs/rules/no-restricted-component-names.md b/docs/rules/no-restricted-component-names.md
index 82f350ca9..689c73aa1 100644
--- a/docs/rules/no-restricted-component-names.md
+++ b/docs/rules/no-restricted-component-names.md
@@ -84,6 +84,10 @@ export default {
+## :couple: Related Rules
+
+- [vue/restricted-component-names](./restricted-component-names.md)
+
## :rocket: Version
This rule was introduced in eslint-plugin-vue v9.15.0
diff --git a/docs/rules/no-v-text-v-html-on-component.md b/docs/rules/no-v-text-v-html-on-component.md
index 8e504d859..7d75eb9c9 100644
--- a/docs/rules/no-v-text-v-html-on-component.md
+++ b/docs/rules/no-v-text-v-html-on-component.md
@@ -25,11 +25,15 @@ If you use v-text / v-html on a component, it will overwrite the component's con
+
+
{{ content }}
+
+
```
@@ -39,14 +43,15 @@ If you use v-text / v-html on a component, it will overwrite the component's con
```json
{
- "vue/no-v-text-v-html-on-component": [
- "error",
- { "allow": ["router-link", "nuxt-link"] }
- ]
+ "vue/no-v-text-v-html-on-component": ["error", {
+ "allow": ["router-link", "nuxt-link"],
+ "ignoreElementNamespaces": false
+ }]
}
```
- `allow` (`string[]`) ... Specify a list of custom components for which the rule should not apply.
+- `ignoreElementNamespaces` (`boolean`) ... If `true`, always treat SVG and MathML tag names as HTML elements, even if they are not used inside a SVG/MathML root element. Default is `false`.
### `{ "allow": ["router-link", "nuxt-link"] }`
@@ -65,6 +70,20 @@ If you use v-text / v-html on a component, it will overwrite the component's con
+### `{ "ignoreElementNamespaces": true }`
+
+
+
+```vue
+
+
+
+
+
+```
+
+
+
## :rocket: Version
This rule was introduced in eslint-plugin-vue v8.4.0
diff --git a/docs/rules/prefer-use-template-ref.md b/docs/rules/prefer-use-template-ref.md
index 553e99bf1..1b1b40385 100644
--- a/docs/rules/prefer-use-template-ref.md
+++ b/docs/rules/prefer-use-template-ref.md
@@ -2,31 +2,32 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/prefer-use-template-ref
-description: require using `useTemplateRef` instead of `ref` for template refs
+description: require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs
since: v9.31.0
---
# vue/prefer-use-template-ref
-> require using `useTemplateRef` instead of `ref` for template refs
+> require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs
## :book: Rule Details
Vue 3.5 introduced a new way of obtaining template refs via
the [`useTemplateRef()`](https://vuejs.org/guide/essentials/template-refs.html#accessing-the-refs) API.
-This rule enforces using the new `useTemplateRef` function instead of `ref` for template refs.
+This rule enforces using the new `useTemplateRef` function instead of `ref`/`shallowRef` for template refs.
```vue
Submit
+ Cancel
Close
```
@@ -47,14 +49,16 @@ This rule skips `ref` template function refs as these should be used to allow cu
```vue
- Content
+ Submit
+ Cancel
```
diff --git a/docs/rules/restricted-component-names.md b/docs/rules/restricted-component-names.md
new file mode 100644
index 000000000..55e9883b8
--- /dev/null
+++ b/docs/rules/restricted-component-names.md
@@ -0,0 +1,69 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/restricted-component-names
+description: enforce using only specific component names
+since: v9.32.0
+---
+
+# vue/restricted-component-names
+
+> enforce using only specific component names
+
+## :book: Rule Details
+
+This rule enforces consistency in component names.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/restricted-component-names": ["error", {
+ "allow": []
+ }]
+}
+```
+
+### `"allow"`
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
+## :couple: Related Rules
+
+- [vue/no-restricted-component-names](./no-restricted-component-names.md)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.32.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/restricted-component-names.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/restricted-component-names.js)
diff --git a/docs/rules/slot-name-casing.md b/docs/rules/slot-name-casing.md
new file mode 100644
index 000000000..63884fe86
--- /dev/null
+++ b/docs/rules/slot-name-casing.md
@@ -0,0 +1,91 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/slot-name-casing
+description: enforce specific casing for slot names
+since: v9.32.0
+---
+
+# vue/slot-name-casing
+
+> enforce specific casing for slot names
+
+## :book: Rule Details
+
+This rule enforces proper casing of slot names in Vue components.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/slot-name-casing": ["error", "camelCase" | "kebab-case" | "singleword"]
+}
+```
+
+- `"camelCase"` (default) ... Enforce slot name to be in camel case.
+- `"kebab-case"` ... Enforce slot name to be in kebab case.
+- `"singleword"` ... Enforce slot name to be a single word.
+
+### `"kebab-case"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `"singleword"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.32.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/slot-name-casing.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/slot-name-casing.js)
diff --git a/docs/rules/v-on-event-hyphenation.md b/docs/rules/v-on-event-hyphenation.md
index 811b37437..493a9dac9 100644
--- a/docs/rules/v-on-event-hyphenation.md
+++ b/docs/rules/v-on-event-hyphenation.md
@@ -39,14 +39,16 @@ This rule enforces using hyphenated v-on event names on custom components in Vue
{
"vue/v-on-event-hyphenation": ["error", "always" | "never", {
"autofix": false,
- "ignore": []
+ "ignore": [],
+ "ignoreTags": []
}]
}
```
-- `"always"` (default) ... Use hyphenated name.
-- `"never"` ... Don't use hyphenated name.
-- `"ignore"` ... Array of ignored names
+- `"always"` (default) ... Use hyphenated event name.
+- `"never"` ... Don't use hyphenated event name.
+- `"ignore"` ... Array of event names that don't need to follow the specified casing.
+- `"ignoreTags"` ... Array of tag names whose events don't need to follow the specified casing.
- `"autofix"` ... If `true`, enable autofix. If you are using Vue 2, we recommend that you do not use it due to its side effects.
### `"always"`
@@ -104,6 +106,22 @@ Don't use hyphenated name but allow custom event names
+### `"never", { "ignoreTags": ["/^custom-/"] }`
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
## :couple: Related Rules
- [vue/custom-event-name-casing](./custom-event-name-casing.md)
diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md
index ba4a7e3fd..4cf65c928 100644
--- a/docs/user-guide/index.md
+++ b/docs/user-guide/index.md
@@ -67,6 +67,44 @@ You can use the following configs by adding them to `eslint.config.js`.
By default, all rules from **base** and **essential** categories report ESLint errors. Other rules - because they're not covering potential bugs in the application - report warnings. What does it mean? By default - nothing, but if you want - you can set up a threshold and break the build after a certain amount of warnings, instead of any. More information [here](https://eslint.org/docs/user-guide/command-line-interface#handling-warnings).
:::
+#### Example configuration with [typescript-eslint](https://typescript-eslint.io/) and [Prettier](https://prettier.io/)
+
+```bash
+npm install --save-dev eslint eslint-config-prettier eslint-plugin-vue globals typescript-eslint
+```
+
+```ts
+import eslint from '@eslint/js';
+import eslintConfigPrettier from 'eslint-config-prettier';
+import eslintPluginVue from 'eslint-plugin-vue';
+import globals from 'globals';
+import typescriptEslint from 'typescript-eslint';
+
+export default tseslint.config(
+ { ignores: ['*.d.ts', '**/coverage', '**/dist'] },
+ {
+ extends: [
+ eslint.configs.recommended,
+ ...typescriptEslint.configs.recommended,
+ ...eslintPluginVue.configs['flat/recommended'],
+ ],
+ files: ['**/*.{ts,vue}'],
+ languageOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ globals: globals.browser,
+ parserOptions: {
+ parser: typescriptEslint.parser,
+ },
+ },
+ rules: {
+ // your rules
+ },
+ },
+ eslintConfigPrettier
+);
+```
+
### Configuration (`.eslintrc`)
Use `.eslintrc.*` file to configure rules in ESLint < v9. See also: .
diff --git a/lib/index.js b/lib/index.js
index b09e247d5..ce562f458 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -231,11 +231,13 @@ const plugin = {
'require-typed-ref': require('./rules/require-typed-ref'),
'require-v-for-key': require('./rules/require-v-for-key'),
'require-valid-default-prop': require('./rules/require-valid-default-prop'),
+ 'restricted-component-names': require('./rules/restricted-component-names'),
'return-in-computed-property': require('./rules/return-in-computed-property'),
'return-in-emits-validator': require('./rules/return-in-emits-validator'),
'script-indent': require('./rules/script-indent'),
'script-setup-uses-vars': require('./rules/script-setup-uses-vars'),
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
+ 'slot-name-casing': require('./rules/slot-name-casing'),
'sort-keys': require('./rules/sort-keys'),
'space-in-parens': require('./rules/space-in-parens'),
'space-infix-ops': require('./rules/space-infix-ops'),
diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js
index 35519d231..65d096cd4 100644
--- a/lib/rules/attribute-hyphenation.js
+++ b/lib/rules/attribute-hyphenation.js
@@ -6,6 +6,7 @@
const utils = require('../utils')
const casing = require('../utils/casing')
+const { toRegExp } = require('../utils/regexp')
const svgAttributes = require('../utils/svg-attributes-weird-case.json')
/**
@@ -56,6 +57,12 @@ module.exports = {
},
uniqueItems: true,
additionalItems: false
+ },
+ ignoreTags: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true,
+ additionalItems: false
}
},
additionalProperties: false
@@ -72,6 +79,11 @@ module.exports = {
const option = context.options[0]
const optionsPayload = context.options[1]
const useHyphenated = option !== 'never'
+ /** @type {RegExp[]} */
+ const ignoredTagsRegexps = (
+ (optionsPayload && optionsPayload.ignoreTags) ||
+ []
+ ).map(toRegExp)
const ignoredAttributes = ['data-', 'aria-', 'slot-scope', ...svgAttributes]
if (optionsPayload && optionsPayload.ignore) {
@@ -130,11 +142,17 @@ module.exports = {
return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
}
+ /** @param {string} name */
+ function isIgnoredTagName(name) {
+ return ignoredTagsRegexps.some((re) => re.test(name))
+ }
+
return utils.defineTemplateBodyVisitor(context, {
VAttribute(node) {
+ const element = node.parent.parent
if (
- !utils.isCustomComponent(node.parent.parent) &&
- node.parent.parent.name !== 'slot'
+ (!utils.isCustomComponent(element) && element.name !== 'slot') ||
+ isIgnoredTagName(element.rawName)
)
return
diff --git a/lib/rules/no-duplicate-attr-inheritance.js b/lib/rules/no-duplicate-attr-inheritance.js
index 654929bd1..ba20a52e5 100644
--- a/lib/rules/no-duplicate-attr-inheritance.js
+++ b/lib/rules/no-duplicate-attr-inheritance.js
@@ -6,6 +6,33 @@
const utils = require('../utils')
+/** @param {VElement[]} elements */
+function isConditionalGroup(elements) {
+ if (elements.length < 2) {
+ return false
+ }
+
+ const firstElement = elements[0]
+ const lastElement = elements[elements.length - 1]
+ const inBetweenElements = elements.slice(1, -1)
+
+ return (
+ utils.hasDirective(firstElement, 'if') &&
+ (utils.hasDirective(lastElement, 'else-if') ||
+ utils.hasDirective(lastElement, 'else')) &&
+ inBetweenElements.every((element) => utils.hasDirective(element, 'else-if'))
+ )
+}
+
+/** @param {VElement[]} elements */
+function isMultiRootNodes(elements) {
+ if (elements.length > 1 && !isConditionalGroup(elements)) {
+ return true
+ }
+
+ return false
+}
+
module.exports = {
meta: {
type: 'suggestion',
@@ -17,15 +44,30 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/no-duplicate-attr-inheritance.html'
},
fixable: null,
- schema: [],
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ checkMultiRootNodes: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
+ }
+ ],
messages: {
noDuplicateAttrInheritance: 'Set "inheritAttrs" to false.'
}
},
/** @param {RuleContext} context */
create(context) {
+ const options = context.options[0] || {}
+ const checkMultiRootNodes = options.checkMultiRootNodes === true
+
/** @type {string | number | boolean | RegExp | BigInt | null} */
let inheritsAttrs = true
+ /** @type {VReference[]} */
+ const attrsRefs = []
/** @param {ObjectExpression} node */
function processOptions(node) {
@@ -54,7 +96,7 @@ module.exports = {
if (!inheritsAttrs) {
return
}
- const attrsRef = node.references.find((reference) => {
+ const reference = node.references.find((reference) => {
if (reference.variable != null) {
// Not vm reference
return false
@@ -62,14 +104,32 @@ module.exports = {
return reference.id.name === '$attrs'
})
- if (attrsRef) {
- context.report({
- node: attrsRef.id,
- messageId: 'noDuplicateAttrInheritance'
- })
+ if (reference) {
+ attrsRefs.push(reference)
}
}
- })
+ }),
+ {
+ 'Program:exit'(program) {
+ const element = program.templateBody
+ if (element == null) {
+ return
+ }
+
+ const rootElements = element.children.filter(utils.isVElement)
+
+ if (!checkMultiRootNodes && isMultiRootNodes(rootElements)) return
+
+ if (attrsRefs.length > 0) {
+ for (const attrsRef of attrsRefs) {
+ context.report({
+ node: attrsRef.id,
+ messageId: 'noDuplicateAttrInheritance'
+ })
+ }
+ }
+ }
+ }
)
}
}
diff --git a/lib/rules/no-v-text-v-html-on-component.js b/lib/rules/no-v-text-v-html-on-component.js
index 50ef9c76e..e3f1f5409 100644
--- a/lib/rules/no-v-text-v-html-on-component.js
+++ b/lib/rules/no-v-text-v-html-on-component.js
@@ -26,6 +26,9 @@ module.exports = {
type: 'string'
},
uniqueItems: true
+ },
+ ignoreElementNamespaces: {
+ type: 'boolean'
}
},
additionalProperties: false
@@ -41,6 +44,8 @@ module.exports = {
const options = context.options[0] || {}
/** @type {Set} */
const allow = new Set(options.allow)
+ /** @type {boolean} */
+ const ignoreElementNamespaces = options.ignoreElementNamespaces === true
/**
* Check whether the given node is an allowed component or not.
@@ -62,7 +67,10 @@ module.exports = {
*/
function verify(node) {
const element = node.parent.parent
- if (utils.isCustomComponent(element) && !isAllowedComponent(element)) {
+ if (
+ utils.isCustomComponent(element, ignoreElementNamespaces) &&
+ !isAllowedComponent(element)
+ ) {
context.report({
node,
loc: node.loc,
diff --git a/lib/rules/prefer-use-template-ref.js b/lib/rules/prefer-use-template-ref.js
index 8dcdccb38..7d01958b7 100644
--- a/lib/rules/prefer-use-template-ref.js
+++ b/lib/rules/prefer-use-template-ref.js
@@ -6,10 +6,42 @@
const utils = require('../utils')
-/** @param expression {Expression | null} */
-function expressionIsRef(expression) {
- // @ts-ignore
- return expression?.callee?.name === 'ref'
+/**
+ * @typedef ScriptRef
+ * @type {{node: Expression, ref: string}}
+ */
+
+/**
+ * @param declarator {VariableDeclarator}
+ * @returns {ScriptRef}
+ * */
+function convertDeclaratorToScriptRef(declarator) {
+ return {
+ // @ts-ignore
+ node: declarator.init,
+ // @ts-ignore
+ ref: declarator.id.name
+ }
+}
+
+/**
+ * @param body {(Statement | ModuleDeclaration)[]}
+ * @returns {ScriptRef[]}
+ * */
+function getScriptRefsFromSetupFunction(body) {
+ /** @type {VariableDeclaration[]} */
+ const variableDeclarations = body.filter(
+ (child) => child.type === 'VariableDeclaration'
+ )
+ const variableDeclarators = variableDeclarations.map(
+ (declaration) => declaration.declarations[0]
+ )
+ const refDeclarators = variableDeclarators.filter((declarator) =>
+ // @ts-ignore
+ ['ref', 'shallowRef'].includes(declarator.init?.callee?.name)
+ )
+
+ return refDeclarators.map(convertDeclaratorToScriptRef)
}
/** @type {import("eslint").Rule.RuleModule} */
@@ -18,13 +50,13 @@ module.exports = {
type: 'suggestion',
docs: {
description:
- 'require using `useTemplateRef` instead of `ref` for template refs',
+ 'require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/prefer-use-template-ref.html'
},
schema: [],
messages: {
- preferUseTemplateRef: "Replace 'ref' with 'useTemplateRef'."
+ preferUseTemplateRef: "Replace '{{name}}' with 'useTemplateRef'."
}
},
/** @param {RuleContext} context */
@@ -32,40 +64,33 @@ module.exports = {
/** @type Set */
const templateRefs = new Set()
- /**
- * @typedef ScriptRef
- * @type {{node: Expression, ref: string}}
- */
-
/**
* @type ScriptRef[] */
const scriptRefs = []
return utils.compositingVisitors(
- utils.defineTemplateBodyVisitor(
- context,
- {
- 'VAttribute[directive=false]'(node) {
- if (node.key.name === 'ref' && node.value?.value) {
- templateRefs.add(node.value.value)
- }
+ utils.defineTemplateBodyVisitor(context, {
+ 'VAttribute[directive=false]'(node) {
+ if (node.key.name === 'ref' && node.value?.value) {
+ templateRefs.add(node.value.value)
}
- },
- {
- VariableDeclarator(declarator) {
- if (!expressionIsRef(declarator.init)) {
- return
- }
+ }
+ }),
+ utils.defineVueVisitor(context, {
+ onSetupFunctionEnter(node) {
+ // @ts-ignore
+ const newScriptRefs = getScriptRefsFromSetupFunction(node.body.body)
- scriptRefs.push({
- // @ts-ignore
- node: declarator.init,
- // @ts-ignore
- ref: declarator.id.name
- })
- }
+ scriptRefs.push(...newScriptRefs)
+ }
+ }),
+ utils.defineScriptSetupVisitor(context, {
+ Program(node) {
+ const newScriptRefs = getScriptRefsFromSetupFunction(node.body)
+
+ scriptRefs.push(...newScriptRefs)
}
- ),
+ }),
{
'Program:exit'() {
for (const templateRef of templateRefs) {
@@ -79,7 +104,11 @@ module.exports = {
context.report({
node: scriptRef.node,
- messageId: 'preferUseTemplateRef'
+ messageId: 'preferUseTemplateRef',
+ data: {
+ // @ts-ignore
+ name: scriptRef.node?.callee?.name
+ }
})
}
}
diff --git a/lib/rules/require-explicit-slots.js b/lib/rules/require-explicit-slots.js
index f87503bb7..5298e598c 100644
--- a/lib/rules/require-explicit-slots.js
+++ b/lib/rules/require-explicit-slots.js
@@ -98,30 +98,22 @@ module.exports = {
return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
- onDefineSlotsEnter(node) {
- const typeArguments =
- 'typeArguments' in node ? node.typeArguments : node.typeParameters
- const param = /** @type {TypeNode|undefined} */ (
- typeArguments?.params[0]
- )
- if (!param) return
-
- if (param.type === 'TSTypeLiteral') {
- for (const memberNode of param.members) {
- const slotName = getSlotsName(memberNode)
- if (!slotName) continue
-
- if (slotsDefined.has(slotName)) {
- context.report({
- node: memberNode,
- messageId: 'alreadyDefinedSlot',
- data: {
- slotName
- }
- })
- } else {
- slotsDefined.add(slotName)
- }
+ onDefineSlotsEnter(_node, slots) {
+ for (const slot of slots) {
+ if (!slot.slotName) {
+ continue
+ }
+
+ if (slotsDefined.has(slot.slotName)) {
+ context.report({
+ node: slot.node,
+ messageId: 'alreadyDefinedSlot',
+ data: {
+ slotName: slot.slotName
+ }
+ })
+ } else {
+ slotsDefined.add(slot.slotName)
}
}
}
diff --git a/lib/rules/restricted-component-names.js b/lib/rules/restricted-component-names.js
new file mode 100644
index 000000000..636224db6
--- /dev/null
+++ b/lib/rules/restricted-component-names.js
@@ -0,0 +1,80 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const { toRegExp } = require('../utils/regexp')
+
+const htmlElements = require('../utils/html-elements.json')
+const deprecatedHtmlElements = require('../utils/deprecated-html-elements.json')
+const svgElements = require('../utils/svg-elements.json')
+const vue2builtinComponents = require('../utils/vue2-builtin-components')
+const vue3builtinComponents = require('../utils/vue3-builtin-components')
+
+const reservedNames = new Set([
+ ...htmlElements,
+ ...deprecatedHtmlElements,
+ ...svgElements,
+ ...vue2builtinComponents,
+ ...vue3builtinComponents
+])
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce using only specific component names',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/restricted-component-names.html'
+ },
+ fixable: null,
+ schema: [
+ {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ allow: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true,
+ additionalItems: false
+ }
+ }
+ }
+ ],
+ messages: {
+ invalidName: 'Component name "{{name}}" is not allowed.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const options = context.options[0] || {}
+ /** @type {RegExp[]} */
+ const allow = (options.allow || []).map(toRegExp)
+
+ /** @param {string} name */
+ function isAllowedTarget(name) {
+ return reservedNames.has(name) || allow.some((re) => re.test(name))
+ }
+
+ return utils.defineTemplateBodyVisitor(context, {
+ VElement(node) {
+ const name = node.rawName
+ if (isAllowedTarget(name)) {
+ return
+ }
+
+ context.report({
+ node,
+ loc: node.loc,
+ messageId: 'invalidName',
+ data: {
+ name
+ }
+ })
+ }
+ })
+ }
+}
diff --git a/lib/rules/slot-name-casing.js b/lib/rules/slot-name-casing.js
new file mode 100644
index 000000000..6d98d8d82
--- /dev/null
+++ b/lib/rules/slot-name-casing.js
@@ -0,0 +1,82 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+/**
+ * @typedef { 'camelCase' | 'kebab-case' | 'singleword' } OptionType
+ * @typedef { (str: string) => boolean } CheckerType
+ */
+
+/**
+ * Checks whether the given string is a single word.
+ * @param {string} str
+ * @return {boolean}
+ */
+function isSingleWord(str) {
+ return /^[a-z]+$/u.test(str)
+}
+
+/** @type {OptionType[]} */
+const allowedCaseOptions = ['camelCase', 'kebab-case', 'singleword']
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'enforce specific casing for slot names',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/slot-name-casing.html'
+ },
+ fixable: null,
+ schema: [
+ {
+ enum: allowedCaseOptions
+ }
+ ],
+ messages: {
+ invalidCase: 'Slot name "{{name}}" is not {{caseType}}.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const option = context.options[0]
+
+ /** @type {OptionType} */
+ const caseType = allowedCaseOptions.includes(option) ? option : 'camelCase'
+
+ /** @type {CheckerType} */
+ const checker =
+ caseType === 'singleword' ? isSingleWord : casing.getChecker(caseType)
+
+ /** @param {VAttribute} node */
+ function processSlotNode(node) {
+ const name = node.value?.value
+ if (name && !checker(name)) {
+ context.report({
+ node,
+ loc: node.loc,
+ messageId: 'invalidCase',
+ data: {
+ name,
+ caseType
+ }
+ })
+ }
+ }
+
+ return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VElement} node */
+ "VElement[name='slot']"(node) {
+ const slotName = utils.getAttribute(node, 'name')
+ if (slotName) {
+ processSlotNode(slotName)
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/v-on-event-hyphenation.js b/lib/rules/v-on-event-hyphenation.js
index f99a45fdc..c9fac76e8 100644
--- a/lib/rules/v-on-event-hyphenation.js
+++ b/lib/rules/v-on-event-hyphenation.js
@@ -2,6 +2,7 @@
const utils = require('../utils')
const casing = require('../utils/casing')
+const { toRegExp } = require('../utils/regexp')
module.exports = {
meta: {
@@ -35,6 +36,12 @@ module.exports = {
},
uniqueItems: true,
additionalItems: false
+ },
+ ignoreTags: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true,
+ additionalItems: false
}
},
additionalProperties: false
@@ -56,6 +63,11 @@ module.exports = {
const useHyphenated = option !== 'never'
/** @type {string[]} */
const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || []
+ /** @type {RegExp[]} */
+ const ignoredTagsRegexps = (
+ (optionsPayload && optionsPayload.ignoreTags) ||
+ []
+ ).map(toRegExp)
const autofix = Boolean(optionsPayload && optionsPayload.autofix)
const caseConverter = casing.getConverter(
@@ -99,9 +111,20 @@ module.exports = {
return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
}
+ /** @param {string} name */
+ function isIgnoredTagName(name) {
+ return ignoredTagsRegexps.some((re) => re.test(name))
+ }
+
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='on']"(node) {
- if (!utils.isCustomComponent(node.parent.parent)) return
+ const element = node.parent.parent
+ if (
+ !utils.isCustomComponent(element) ||
+ isIgnoredTagName(element.rawName)
+ ) {
+ return
+ }
if (!node.key.argument || node.key.argument.type !== 'VIdentifier') {
return
}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 58cd32689..167edf208 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -26,6 +26,10 @@ const { getScope } = require('./scope')
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeEmit} ComponentInferTypeEmit
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownEmit} ComponentUnknownEmit
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentEmit} ComponentEmit
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeSlot} ComponentTypeSlot
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeSlot} ComponentInferTypeSlot
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownSlot} ComponentUnknownSlot
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentSlot} ComponentSlot
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentModelName} ComponentModelName
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentModel} ComponentModel
*/
@@ -70,6 +74,7 @@ const {
const {
getComponentPropsFromTypeDefine,
getComponentEmitsFromTypeDefine,
+ getComponentSlotsFromTypeDefine,
isTypeNode
} = require('./ts-utils')
@@ -941,19 +946,30 @@ module.exports = {
/**
* Check whether the given node is a custom component or not.
* @param {VElement} node The start tag node to check.
+ * @param {boolean} [ignoreElementNamespaces=false] If `true`, ignore element namespaces.
* @returns {boolean} `true` if the node is a custom component.
*/
- isCustomComponent(node) {
- return (
- (this.isHtmlElementNode(node) &&
- !this.isHtmlWellKnownElementName(node.rawName)) ||
- (this.isSvgElementNode(node) &&
- !this.isSvgWellKnownElementName(node.rawName)) ||
- (this.isMathElementNode(node) &&
- !this.isMathWellKnownElementName(node.rawName)) ||
+ isCustomComponent(node, ignoreElementNamespaces = false) {
+ if (
hasAttribute(node, 'is') ||
hasDirective(node, 'bind', 'is') ||
hasDirective(node, 'is')
+ ) {
+ return true
+ }
+
+ const isHtmlName = this.isHtmlWellKnownElementName(node.rawName)
+ const isSvgName = this.isSvgWellKnownElementName(node.rawName)
+ const isMathName = this.isMathWellKnownElementName(node.rawName)
+
+ if (ignoreElementNamespaces) {
+ return !isHtmlName && !isSvgName && !isMathName
+ }
+
+ return (
+ (this.isHtmlElementNode(node) && !isHtmlName) ||
+ (this.isSvgElementNode(node) && !isSvgName) ||
+ (this.isMathElementNode(node) && !isMathName)
)
},
@@ -1424,7 +1440,7 @@ module.exports = {
'onDefineSlotsEnter',
'onDefineSlotsExit',
(candidateMacro, node) => candidateMacro === node,
- () => undefined
+ getComponentSlotsFromDefineSlots
),
new MacroListener(
'defineExpose',
@@ -3361,6 +3377,28 @@ function getComponentEmitsFromDefineEmits(context, node) {
}
]
}
+
+/**
+ * Get all slots from `defineSlots` call expression.
+ * @param {RuleContext} context The rule context object.
+ * @param {CallExpression} node `defineSlots` call expression
+ * @return {ComponentSlot[]} Array of component slots
+ */
+function getComponentSlotsFromDefineSlots(context, node) {
+ const typeArguments =
+ 'typeArguments' in node ? node.typeArguments : node.typeParameters
+ if (typeArguments && typeArguments.params.length > 0) {
+ return getComponentSlotsFromTypeDefine(context, typeArguments.params[0])
+ }
+ return [
+ {
+ type: 'unknown',
+ slotName: null,
+ node: null
+ }
+ ]
+}
+
/**
* Get model info from `defineModel` call expression.
* @param {RuleContext} _context The rule context object.
@@ -3403,6 +3441,7 @@ function getComponentModelFromDefineModel(_context, node) {
typeNode: null
}
}
+
/**
* Get all props by looking at all component's properties
* @param {ObjectExpression|ArrayExpression} propsNode Object with props definition
diff --git a/lib/utils/ts-utils/index.js b/lib/utils/ts-utils/index.js
index 8b6c53b26..3db610d1c 100644
--- a/lib/utils/ts-utils/index.js
+++ b/lib/utils/ts-utils/index.js
@@ -5,11 +5,13 @@ const {
isTSTypeLiteralOrTSFunctionType,
extractRuntimeEmits,
flattenTypeNodes,
- isTSInterfaceBody
+ isTSInterfaceBody,
+ extractRuntimeSlots
} = require('./ts-ast')
const {
getComponentPropsFromTypeDefineTypes,
- getComponentEmitsFromTypeDefineTypes
+ getComponentEmitsFromTypeDefineTypes,
+ getComponentSlotsFromTypeDefineTypes
} = require('./ts-types')
/**
@@ -22,12 +24,16 @@ const {
* @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit
* @typedef {import('../index').ComponentInferTypeEmit} ComponentInferTypeEmit
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
+ * @typedef {import('../index').ComponentTypeSlot} ComponentTypeSlot
+ * @typedef {import('../index').ComponentInferTypeSlot} ComponentInferTypeSlot
+ * @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
*/
module.exports = {
isTypeNode,
getComponentPropsFromTypeDefine,
- getComponentEmitsFromTypeDefine
+ getComponentEmitsFromTypeDefine,
+ getComponentSlotsFromTypeDefine
}
/**
@@ -86,3 +92,30 @@ function getComponentEmitsFromTypeDefine(context, emitsNode) {
}
return result
}
+
+/**
+ * Get all slots by looking at all component's properties
+ * @param {RuleContext} context The ESLint rule context object.
+ * @param {TypeNode} slotsNode Type with slots definition
+ * @return {(ComponentTypeSlot|ComponentInferTypeSlot|ComponentUnknownSlot)[]} Array of component slots
+ */
+function getComponentSlotsFromTypeDefine(context, slotsNode) {
+ /** @type {(ComponentTypeSlot|ComponentInferTypeSlot|ComponentUnknownSlot)[]} */
+ const result = []
+ for (const defNode of flattenTypeNodes(
+ context,
+ /** @type {TSESTreeTypeNode} */ (slotsNode)
+ )) {
+ if (isTSInterfaceBody(defNode) || isTSTypeLiteral(defNode)) {
+ result.push(...extractRuntimeSlots(defNode))
+ } else {
+ result.push(
+ ...getComponentSlotsFromTypeDefineTypes(
+ context,
+ /** @type {TypeNode} */ (defNode)
+ )
+ )
+ }
+ }
+ return result
+}
diff --git a/lib/utils/ts-utils/ts-ast.js b/lib/utils/ts-utils/ts-ast.js
index ddbb9de05..1021b4baf 100644
--- a/lib/utils/ts-utils/ts-ast.js
+++ b/lib/utils/ts-utils/ts-ast.js
@@ -15,6 +15,8 @@ const { inferRuntimeTypeFromTypeNode } = require('./ts-types')
* @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp
* @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
+ * @typedef {import('../index').ComponentTypeSlot} ComponentTypeSlot
+ * @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
*/
const noop = Function.prototype
@@ -26,7 +28,8 @@ module.exports = {
isTSTypeLiteral,
isTSTypeLiteralOrTSFunctionType,
extractRuntimeProps,
- extractRuntimeEmits
+ extractRuntimeEmits,
+ extractRuntimeSlots
}
/**
@@ -209,6 +212,38 @@ function* extractRuntimeEmits(node) {
}
}
+/**
+ * @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody} node
+ * @returns {IterableIterator}
+ */
+function* extractRuntimeSlots(node) {
+ const members = node.type === 'TSTypeLiteral' ? node.members : node.body
+ for (const member of members) {
+ if (
+ member.type === 'TSPropertySignature' ||
+ member.type === 'TSMethodSignature'
+ ) {
+ if (member.key.type !== 'Identifier' && member.key.type !== 'Literal') {
+ yield {
+ type: 'unknown',
+ slotName: null,
+ node: /** @type {Expression} */ (member.key)
+ }
+ continue
+ }
+ yield {
+ type: 'type',
+ key: /** @type {Identifier | Literal} */ (member.key),
+ slotName:
+ member.key.type === 'Identifier'
+ ? member.key.name
+ : `${member.key.value}`,
+ node: /** @type {TSPropertySignature | TSMethodSignature} */ (member)
+ }
+ }
+ }
+}
+
/**
* @param {TSESTreeParameter} eventName
* @param {TSCallSignatureDeclaration | TSFunctionType} member
diff --git a/lib/utils/ts-utils/ts-types.js b/lib/utils/ts-utils/ts-types.js
index abb303862..2fe354c2c 100644
--- a/lib/utils/ts-utils/ts-types.js
+++ b/lib/utils/ts-utils/ts-types.js
@@ -24,11 +24,14 @@ const {
* @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp
* @typedef {import('../index').ComponentInferTypeEmit} ComponentInferTypeEmit
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
+ * @typedef {import('../index').ComponentInferTypeSlot} ComponentInferTypeSlot
+ * @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
*/
module.exports = {
getComponentPropsFromTypeDefineTypes,
getComponentEmitsFromTypeDefineTypes,
+ getComponentSlotsFromTypeDefineTypes,
inferRuntimeTypeFromTypeNode
}
@@ -122,6 +125,34 @@ function getComponentEmitsFromTypeDefineTypes(context, emitsNode) {
return [...extractRuntimeEmits(type, tsNode, emitsNode, services)]
}
+/**
+ * Get all slots by looking at all component's properties
+ * @param {RuleContext} context The ESLint rule context object.
+ * @param {TypeNode} slotsNode Type with slots definition
+ * @return {(ComponentInferTypeSlot|ComponentUnknownSlot)[]} Array of component slots
+ */
+function getComponentSlotsFromTypeDefineTypes(context, slotsNode) {
+ const services = getTSParserServices(context)
+ const tsNode = services && services.tsNodeMap.get(slotsNode)
+ const type = tsNode && services.checker.getTypeAtLocation(tsNode)
+ if (
+ !type ||
+ isAny(type) ||
+ isUnknown(type) ||
+ isNever(type) ||
+ isNull(type)
+ ) {
+ return [
+ {
+ type: 'unknown',
+ slotName: null,
+ node: slotsNode
+ }
+ ]
+ }
+ return [...extractRuntimeSlots(type, slotsNode)]
+}
+
/**
* @param {RuleContext} context The ESLint rule context object.
* @param {TypeNode|Expression} node
@@ -259,6 +290,23 @@ function* extractRuntimeEmits(type, tsNode, emitsNode, services) {
}
}
+/**
+ * @param {Type} type
+ * @param {TypeNode} slotsNode Type with slots definition
+ * @returns {IterableIterator}
+ */
+function* extractRuntimeSlots(type, slotsNode) {
+ for (const property of type.getProperties()) {
+ const name = property.getName()
+
+ yield {
+ type: 'infer-type',
+ slotName: name,
+ node: slotsNode
+ }
+ }
+}
+
/**
* @param {Type} type
* @returns {Iterable}
diff --git a/package.json b/package.json
index 75d83695f..18d136ea5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-vue",
- "version": "9.31.0",
+ "version": "9.32.0",
"description": "Official ESLint plugin for Vue.js",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@@ -67,7 +67,7 @@
},
"devDependencies": {
"@ota-meshi/site-kit-eslint-editor-vue": "^0.2.4",
- "@stylistic/eslint-plugin": "^2.9.0",
+ "@stylistic/eslint-plugin": "~2.10.0",
"@types/eslint": "^8.56.2",
"@types/eslint-visitor-keys": "^3.3.2",
"@types/natural-compare": "^1.4.3",
diff --git a/tests/lib/rules/attribute-hyphenation.js b/tests/lib/rules/attribute-hyphenation.js
index 18d60e19c..738d59ae9 100644
--- a/tests/lib/rules/attribute-hyphenation.js
+++ b/tests/lib/rules/attribute-hyphenation.js
@@ -85,6 +85,26 @@ ruleTester.run('attribute-hyphenation', rule, {
filename: 'test.vue',
code: '
',
options: ['never']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: ['never', { ignoreTags: ['VueComponent', '/^custom-/'] }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: ['always', { ignoreTags: ['VueComponent', '/^custom-/'] }]
}
],
@@ -450,6 +470,52 @@ ruleTester.run('attribute-hyphenation', rule, {
line: 1
}
]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ output: `
+
+
+
+
+ `,
+ options: ['never', { ignoreTags: ['CustomComponent'] }],
+ errors: [
+ {
+ message: "Attribute 'my-prop' can't be hyphenated.",
+ type: 'VIdentifier',
+ line: 3,
+ column: 17
+ }
+ ]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ output: `
+
+
+
+
+ `,
+ options: ['always', { ignoreTags: ['CustomComponent'] }],
+ errors: [
+ {
+ message: "Attribute 'myProp' must be hyphenated.",
+ type: 'VIdentifier',
+ line: 3,
+ column: 17
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/no-duplicate-attr-inheritance.js b/tests/lib/rules/no-duplicate-attr-inheritance.js
index e38711a54..41e9f1522 100644
--- a/tests/lib/rules/no-duplicate-attr-inheritance.js
+++ b/tests/lib/rules/no-duplicate-attr-inheritance.js
@@ -43,6 +43,57 @@ ruleTester.run('no-duplicate-attr-inheritance', rule, {
`
},
+ // ignore multi root by default
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ options: [{ checkMultiRootNodes: false }]
+ },
{
filename: 'test.vue',
code: `
@@ -151,6 +202,67 @@ ruleTester.run('no-duplicate-attr-inheritance', rule, {
line: 5
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
`,
+ options: [{ checkMultiRootNodes: true }],
+ errors: [{ message: 'Set "inheritAttrs" to false.' }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ options: [{ checkMultiRootNodes: true }],
+ errors: [{ message: 'Set "inheritAttrs" to false.' }]
+ },
+ // condition group as a single root node
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [{ message: 'Set "inheritAttrs" to false.' }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+ `,
+ errors: [{ message: 'Set "inheritAttrs" to false.' }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ errors: [{ message: 'Set "inheritAttrs" to false.' }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `,
+ errors: [{ message: 'Set "inheritAttrs" to false.' }]
}
]
})
diff --git a/tests/lib/rules/no-v-text-v-html-on-component.js b/tests/lib/rules/no-v-text-v-html-on-component.js
index ebf2901ba..bb403489e 100644
--- a/tests/lib/rules/no-v-text-v-html-on-component.js
+++ b/tests/lib/rules/no-v-text-v-html-on-component.js
@@ -59,6 +59,26 @@ tester.run('no-v-text-v-html-on-component', rule, {
`,
options: [{ allow: ['RouterLink', 'nuxt-link'] }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ options: [{ ignoreElementNamespaces: true }]
}
],
invalid: [
@@ -167,6 +187,28 @@ tester.run('no-v-text-v-html-on-component', rule, {
column: 22
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: [{ ignoreElementNamespaces: false }],
+ errors: [
+ {
+ message: "Using v-text on component may break component's content.",
+ line: 3,
+ column: 12
+ },
+ {
+ message: "Using v-text on component may break component's content.",
+ line: 4,
+ column: 13
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/prefer-use-template-ref.js b/tests/lib/rules/prefer-use-template-ref.js
index 49a2f0759..77020cdcf 100644
--- a/tests/lib/rules/prefer-use-template-ref.js
+++ b/tests/lib/rules/prefer-use-template-ref.js
@@ -197,6 +197,61 @@ tester.run('prefer-use-template-ref', rule, {
const button = ref();
`
+ },
+ {
+ filename: 'ref-in-block.vue',
+ code: `
+
+
+
+ Morning
+ Afternoon
+ Evening
+
+
+
+
+ `
+ },
+ {
+ filename: 'ref-in-block-setup-fn.vue',
+ code: `
+
+
+
+ Morning
+ Afternoon
+ Evening
+
+
+
+
+ `
}
],
invalid: [
@@ -214,6 +269,9 @@ tester.run('prefer-use-template-ref', rule, {
errors: [
{
messageId: 'preferUseTemplateRef',
+ data: {
+ name: 'ref'
+ },
line: 7,
column: 22
}
@@ -235,6 +293,9 @@ tester.run('prefer-use-template-ref', rule, {
errors: [
{
messageId: 'preferUseTemplateRef',
+ data: {
+ name: 'ref'
+ },
line: 9,
column: 22
}
@@ -256,43 +317,22 @@ tester.run('prefer-use-template-ref', rule, {
errors: [
{
messageId: 'preferUseTemplateRef',
+ data: {
+ name: 'ref'
+ },
line: 8,
column: 25
},
{
messageId: 'preferUseTemplateRef',
+ data: {
+ name: 'ref'
+ },
line: 9,
column: 22
}
]
},
- {
- filename: 'ref-in-block.vue',
- code: `
-
-
-
- Morning
- Afternoon
- Evening
-
-
-
-
- `,
- errors: [
- {
- messageId: 'preferUseTemplateRef',
- line: 14,
- column: 33
- }
- ]
- },
{
filename: 'setup-function-only-refs.vue',
code: `
@@ -314,10 +354,35 @@ tester.run('prefer-use-template-ref', rule, {
errors: [
{
messageId: 'preferUseTemplateRef',
+ data: {
+ name: 'ref'
+ },
line: 12,
column: 28
}
]
+ },
+ {
+ filename: 'single-shallowRef.vue',
+ code: `
+
+
+
+
+ `,
+ errors: [
+ {
+ messageId: 'preferUseTemplateRef',
+ data: {
+ name: 'shallowRef'
+ },
+ line: 7,
+ column: 22
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/require-explicit-slots.js b/tests/lib/rules/require-explicit-slots.js
index 92d1a1334..f99614119 100644
--- a/tests/lib/rules/require-explicit-slots.js
+++ b/tests/lib/rules/require-explicit-slots.js
@@ -34,6 +34,36 @@ tester.run('require-explicit-slots', rule, {
}>()
`
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
{
filename: 'test.vue',
code: `
@@ -48,6 +78,36 @@ tester.run('require-explicit-slots', rule, {
}>()
`
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
{
filename: 'test.vue',
code: `
@@ -62,6 +122,36 @@ tester.run('require-explicit-slots', rule, {
}>()
`
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
{
filename: 'test.vue',
code: `
@@ -76,6 +166,36 @@ tester.run('require-explicit-slots', rule, {
}>()
`
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
{
filename: 'test.vue',
code: `
@@ -90,6 +210,36 @@ tester.run('require-explicit-slots', rule, {
}>()
`
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
{
filename: 'test.vue',
code: `
@@ -178,6 +328,40 @@ tester.run('require-explicit-slots', rule, {
}>()
`
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+ `
+ },
{
filename: 'test.vue',
code: `
@@ -191,6 +375,36 @@ tester.run('require-explicit-slots', rule, {
default(props: { msg: string }): any
}>()
`
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `
}
],
invalid: [
@@ -261,6 +475,46 @@ tester.run('require-explicit-slots', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slots must be explicitly defined.'
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slots must be explicitly defined.'
+ }
+ ]
+ },
{
filename: 'test.vue',
code: `
@@ -280,6 +534,46 @@ tester.run('require-explicit-slots', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slots must be explicitly defined.'
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slots must be explicitly defined.'
+ }
+ ]
+ },
{
filename: 'test.vue',
code: `
@@ -299,6 +593,46 @@ tester.run('require-explicit-slots', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slots must be explicitly defined.'
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slots must be explicitly defined.'
+ }
+ ]
+ },
{
filename: 'test.vue',
code: `
@@ -342,6 +676,48 @@ tester.run('require-explicit-slots', rule, {
}
]
},
+ {
+ // ignore attribute binding except string literal
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slots must be explicitly defined.'
+ }
+ ]
+ },
+ {
+ // ignore attribute binding except string literal
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slots must be explicitly defined.'
+ }
+ ]
+ },
{
filename: 'test.vue',
code: `
@@ -362,6 +738,48 @@ tester.run('require-explicit-slots', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slot foo is already defined.'
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slot foo is already defined.'
+ }
+ ]
+ },
{
filename: 'test.vue',
code: `
@@ -384,6 +802,56 @@ tester.run('require-explicit-slots', rule, {
message: 'Slot foo is already defined.'
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slot foo is already defined.'
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Slot foo is already defined.'
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/restricted-component-names.js b/tests/lib/rules/restricted-component-names.js
new file mode 100644
index 000000000..db87ca804
--- /dev/null
+++ b/tests/lib/rules/restricted-component-names.js
@@ -0,0 +1,78 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/restricted-component-names')
+
+const tester = new RuleTester({
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+})
+
+tester.run('restricted-component-names', rule, {
+ valid: [
+ ' ',
+ ' ',
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: [{ allow: ['/^foo-/', '/-bar$/'] }]
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ errors: [
+ {
+ messageId: 'invalidName',
+ data: { name: 'Button' },
+ line: 3
+ },
+ {
+ messageId: 'invalidName',
+ data: { name: 'foo-button' },
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: [{ allow: ['/^foo-/', 'bar'] }],
+ errors: [
+ {
+ messageId: 'invalidName',
+ data: { name: 'bar-button' },
+ line: 3
+ },
+ {
+ messageId: 'invalidName',
+ data: { name: 'foo' },
+ line: 4
+ }
+ ]
+ }
+ ]
+})
diff --git a/tests/lib/rules/slot-name-casing.js b/tests/lib/rules/slot-name-casing.js
new file mode 100644
index 000000000..ea8b72aab
--- /dev/null
+++ b/tests/lib/rules/slot-name-casing.js
@@ -0,0 +1,148 @@
+/**
+ * @author WayneZhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/slot-name-casing')
+
+const tester = new RuleTester({
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+})
+
+tester.run('slot-name-casing', rule, {
+ valid: [
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ options: ['kebab-case']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: ['singleword']
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ errors: [
+ {
+ messageId: 'invalidCase',
+ data: {
+ name: 'foo-bar',
+ caseType: 'camelCase'
+ },
+ line: 3,
+ column: 17
+ },
+ {
+ messageId: 'invalidCase',
+ data: {
+ name: 'foo-Bar_baz',
+ caseType: 'camelCase'
+ },
+ line: 4,
+ column: 17
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: ['kebab-case'],
+ errors: [
+ {
+ messageId: 'invalidCase',
+ data: {
+ name: 'fooBar',
+ caseType: 'kebab-case'
+ },
+ line: 3,
+ column: 17
+ },
+ {
+ messageId: 'invalidCase',
+ data: {
+ name: 'foo-Bar_baz',
+ caseType: 'kebab-case'
+ },
+ line: 4,
+ column: 17
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ options: ['singleword'],
+ errors: [
+ {
+ messageId: 'invalidCase',
+ data: {
+ name: 'foo-bar',
+ caseType: 'singleword'
+ },
+ line: 3,
+ column: 17
+ },
+ {
+ messageId: 'invalidCase',
+ data: {
+ name: 'fooBar',
+ caseType: 'singleword'
+ },
+ line: 4,
+ column: 17
+ },
+ {
+ messageId: 'invalidCase',
+ data: {
+ name: 'foo-Bar_baz',
+ caseType: 'singleword'
+ },
+ line: 5,
+ column: 17
+ }
+ ]
+ }
+ ]
+})
diff --git a/tests/lib/rules/v-on-event-hyphenation.js b/tests/lib/rules/v-on-event-hyphenation.js
index 54d2ec435..3f58ce1f0 100644
--- a/tests/lib/rules/v-on-event-hyphenation.js
+++ b/tests/lib/rules/v-on-event-hyphenation.js
@@ -44,6 +44,32 @@ tester.run('v-on-event-hyphenation', rule, {
`,
options: ['never', { ignore: ['custom'] }]
+ },
+ {
+ code: `
+
+
+
+ `,
+ options: ['never', { ignore: ['custom-event'] }]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ options: ['never', { ignoreTags: ['/^Vue/', 'custom-component'] }]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ options: ['always', { ignoreTags: ['/^Vue/', 'custom-component'] }]
}
],
invalid: [
@@ -179,6 +205,50 @@ tester.run('v-on-event-hyphenation', rule, {
"v-on event '@upDate:model-value' can't be hyphenated.",
"v-on event '@up-date:model-value' can't be hyphenated."
]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ output: `
+
+
+
+
+ `,
+ options: ['never', { autofix: true, ignoreTags: ['CustomComponent'] }],
+ errors: [
+ {
+ message: "v-on event 'v-on:custom-event' can't be hyphenated.",
+ line: 3,
+ column: 23
+ }
+ ]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ output: `
+
+
+
+
+ `,
+ options: ['always', { autofix: true, ignoreTags: ['CustomComponent'] }],
+ errors: [
+ {
+ message: "v-on event 'v-on:customEvent' must be hyphenated.",
+ line: 3,
+ column: 23
+ }
+ ]
}
]
})
diff --git a/tests/lib/utils/ts-utils/index/get-component-slots.js b/tests/lib/utils/ts-utils/index/get-component-slots.js
new file mode 100644
index 000000000..410021b93
--- /dev/null
+++ b/tests/lib/utils/ts-utils/index/get-component-slots.js
@@ -0,0 +1,115 @@
+/**
+ * Test for getComponentSlotsFromTypeDefineTypes
+ */
+'use strict'
+
+const path = require('path')
+const fs = require('fs')
+const Linter = require('../../../../eslint-compat').Linter
+const parser = require('vue-eslint-parser')
+const tsParser = require('@typescript-eslint/parser')
+const utils = require('../../../../../lib/utils/index')
+const assert = require('assert')
+
+const FIXTURES_ROOT = path.resolve(
+ __dirname,
+ '../../../../fixtures/utils/ts-utils'
+)
+const TSCONFIG_PATH = path.resolve(FIXTURES_ROOT, './tsconfig.json')
+const SRC_TS_TEST_PATH = path.join(FIXTURES_ROOT, './src/test.ts')
+
+function extractComponentSlots(code, tsFileCode) {
+ const linter = new Linter()
+ const result = []
+ const config = {
+ files: ['**/*.vue'],
+ languageOptions: {
+ parser,
+ ecmaVersion: 2020,
+ parserOptions: {
+ parser: tsParser,
+ project: [TSCONFIG_PATH],
+ extraFileExtensions: ['.vue']
+ }
+ },
+ plugins: {
+ test: {
+ rules: {
+ test: {
+ create(context) {
+ return utils.defineScriptSetupVisitor(context, {
+ onDefineSlotsEnter(_node, slots) {
+ result.push(
+ ...slots.map((prop) => ({
+ type: prop.type,
+ name: prop.slotName
+ }))
+ )
+ }
+ })
+ }
+ }
+ }
+ }
+ },
+ rules: {
+ 'test/test': 'error'
+ }
+ }
+ fs.writeFileSync(SRC_TS_TEST_PATH, tsFileCode || '', 'utf8')
+ // clean './src/test.ts' cache
+ tsParser.clearCaches()
+ assert.deepStrictEqual(
+ linter.verify(code, config, path.join(FIXTURES_ROOT, './src/test.vue')),
+ []
+ )
+ // reset
+ fs.writeFileSync(SRC_TS_TEST_PATH, '', 'utf8')
+ return result
+}
+
+describe('getComponentSlotsFromTypeDefineTypes', () => {
+ for (const { scriptCode, tsFileCode, slots: expected } of [
+ {
+ scriptCode: `
+ defineSlots<{
+ default(props: { msg: string }): any
+ }>()
+ `,
+ slots: [{ type: 'type', name: 'default' }]
+ },
+ {
+ scriptCode: `
+ interface Slots {
+ default(props: { msg: string }): any
+ }
+ defineSlots()
+ `,
+ slots: [{ type: 'type', name: 'default' }]
+ },
+ {
+ scriptCode: `
+ type Slots = {
+ default(props: { msg: string }): any
+ }
+ defineSlots()
+ `,
+ slots: [{ type: 'type', name: 'default' }]
+ }
+ ]) {
+ const code = `
+
+ `
+ it(`should return expected slots with :${code}`, () => {
+ const slots = extractComponentSlots(code, tsFileCode)
+
+ assert.deepStrictEqual(
+ slots,
+ expected,
+ `\n${JSON.stringify(slots)}\n === \n${JSON.stringify(expected)}`
+ )
+ })
+ }
+})
diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts
index 3e9184262..ebe9933d3 100644
--- a/typings/eslint-plugin-vue/util-types/utils.ts
+++ b/typings/eslint-plugin-vue/util-types/utils.ts
@@ -42,8 +42,8 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase {
onDefineEmitsExit?(node: CallExpression, emits: ComponentEmit[]): void
onDefineOptionsEnter?(node: CallExpression): void
onDefineOptionsExit?(node: CallExpression): void
- onDefineSlotsEnter?(node: CallExpression): void
- onDefineSlotsExit?(node: CallExpression): void
+ onDefineSlotsEnter?(node: CallExpression, slots: ComponentSlot[]): void
+ onDefineSlotsExit?(node: CallExpression, slots: ComponentSlot[]): void
onDefineExposeEnter?(node: CallExpression): void
onDefineExposeExit?(node: CallExpression): void
onDefineModelEnter?(node: CallExpression, model: ComponentModel): void
@@ -52,6 +52,7 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase {
| ((node: VAST.ParamNode) => void)
| ((node: CallExpression, props: ComponentProp[]) => void)
| ((node: CallExpression, emits: ComponentEmit[]) => void)
+ | ((node: CallExpression, slots: ComponentSlot[]) => void)
| ((node: CallExpression, model: ComponentModel) => void)
| undefined
}
@@ -191,6 +192,30 @@ export type ComponentEmit =
| ComponentInferTypeEmit
| ComponentUnknownEmit
+export type ComponentUnknownSlot = {
+ type: 'unknown'
+ slotName: null
+ node: Expression | SpreadElement | TypeNode | null
+}
+
+export type ComponentTypeSlot = {
+ type: 'type'
+ key: Identifier | Literal
+ slotName: string
+ node: TSPropertySignature | TSMethodSignature
+}
+
+export type ComponentInferTypeSlot = {
+ type: 'infer-type'
+ slotName: string
+ node: TypeNode
+}
+
+export type ComponentSlot =
+ | ComponentTypeSlot
+ | ComponentInferTypeSlot
+ | ComponentUnknownSlot
+
export type ComponentModelName = {
modelName: string
node: Literal | null