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
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy