From 4500389633f8d4dc3fe416e65867a3d3ea280de4 Mon Sep 17 00:00:00 2001 From: ntnyq Date: Mon, 18 Nov 2024 19:23:44 +0800 Subject: [PATCH 01/11] fix(prefer-use-template-ref): add support for `shallowRef` (#2608) --- docs/rules/index.md | 2 +- docs/rules/prefer-use-template-ref.md | 18 ++++++---- lib/rules/prefer-use-template-ref.js | 18 +++++++--- tests/lib/rules/prefer-use-template-ref.js | 40 ++++++++++++++++++++++ 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/docs/rules/index.md b/docs/rules/index.md index e359f47ab..51515e233 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: | 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 ``` @@ -47,14 +49,16 @@ This rule skips `ref` template function refs as these should be used to allow cu ```vue ``` diff --git a/lib/rules/prefer-use-template-ref.js b/lib/rules/prefer-use-template-ref.js index 8dcdccb38..916583e9f 100644 --- a/lib/rules/prefer-use-template-ref.js +++ b/lib/rules/prefer-use-template-ref.js @@ -8,8 +8,12 @@ const utils = require('../utils') /** @param expression {Expression | null} */ function expressionIsRef(expression) { - // @ts-ignore - return expression?.callee?.name === 'ref' + return ( + // @ts-ignore + expression?.callee?.name === 'ref' || + // @ts-ignore + expression?.callee?.name === 'shallowRef' + ) } /** @type {import("eslint").Rule.RuleModule} */ @@ -18,13 +22,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 */ @@ -79,7 +83,11 @@ module.exports = { context.report({ node: scriptRef.node, - messageId: 'preferUseTemplateRef' + messageId: 'preferUseTemplateRef', + data: { + // @ts-ignore + name: scriptRef.node?.callee?.name + } }) } } diff --git a/tests/lib/rules/prefer-use-template-ref.js b/tests/lib/rules/prefer-use-template-ref.js index 49a2f0759..fe1ee2d2f 100644 --- a/tests/lib/rules/prefer-use-template-ref.js +++ b/tests/lib/rules/prefer-use-template-ref.js @@ -214,6 +214,9 @@ tester.run('prefer-use-template-ref', rule, { errors: [ { messageId: 'preferUseTemplateRef', + data: { + name: 'ref' + }, line: 7, column: 22 } @@ -235,6 +238,9 @@ tester.run('prefer-use-template-ref', rule, { errors: [ { messageId: 'preferUseTemplateRef', + data: { + name: 'ref' + }, line: 9, column: 22 } @@ -256,11 +262,17 @@ 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 } @@ -288,6 +300,9 @@ tester.run('prefer-use-template-ref', rule, { errors: [ { messageId: 'preferUseTemplateRef', + data: { + name: 'ref' + }, line: 14, column: 33 } @@ -314,10 +329,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 + } + ] } ] }) From 86a813887db9ed7b796b9394e96a6c7ea4d00a8e Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Wed, 27 Nov 2024 13:53:43 +0800 Subject: [PATCH 02/11] feat(no-duplicate-attr-inheritance): ignore multi root (#2598) Co-authored-by: Flo Edelmann --- docs/rules/no-duplicate-attr-inheritance.md | 38 +++++- lib/rules/no-duplicate-attr-inheritance.js | 76 ++++++++++-- .../rules/no-duplicate-attr-inheritance.js | 112 ++++++++++++++++++ 3 files changed, 214 insertions(+), 12 deletions(-) diff --git a/docs/rules/no-duplicate-attr-inheritance.md b/docs/rules/no-duplicate-attr-inheritance.md index 0f1b60340..fe3cd37bf 100644 --- a/docs/rules/no-duplicate-attr-inheritance.md +++ b/docs/rules/no-duplicate-attr-inheritance.md @@ -13,9 +13,9 @@ since: v7.0.0 ## :book: Rule Details This rule aims to prevent duplicate attribute inheritance. -This rule to warn to apply `inheritAttrs: false` when it detects `v-bind="$attrs"` being used. +This rule suggests applying `inheritAttrs: false` when it detects `v-bind="$attrs"` being used. - + ```vue `, 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 + } + ] } ] }) From 9ddf3e5a6d8d38ee3e7c07ee692740218bf1be8e Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Wed, 27 Nov 2024 14:06:14 +0800 Subject: [PATCH 04/11] feat: add `ignoreTags` option (#2609) Co-authored-by: Flo Edelmann --- docs/rules/attribute-hyphenation.md | 26 +++++++-- docs/rules/v-on-event-hyphenation.md | 26 +++++++-- lib/rules/attribute-hyphenation.js | 22 ++++++- lib/rules/v-on-event-hyphenation.js | 25 +++++++- tests/lib/rules/attribute-hyphenation.js | 66 +++++++++++++++++++++ tests/lib/rules/v-on-event-hyphenation.js | 70 +++++++++++++++++++++++ 6 files changed, 224 insertions(+), 11 deletions(-) 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/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/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/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/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/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 + } + ] } ] }) From bed816bdb803c93c6a1140773c634e59a4cb3452 Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Wed, 27 Nov 2024 14:14:17 +0800 Subject: [PATCH 05/11] feat: add `restricted-component-names` rule (#2611) Co-authored-by: Flo Edelmann --- docs/rules/index.md | 1 + docs/rules/no-restricted-component-names.md | 4 + docs/rules/restricted-component-names.md | 66 +++++++++++++++ lib/index.js | 1 + lib/rules/restricted-component-names.js | 80 +++++++++++++++++++ tests/lib/rules/restricted-component-names.js | 78 ++++++++++++++++++ 6 files changed, 230 insertions(+) create mode 100644 docs/rules/restricted-component-names.md create mode 100644 lib/rules/restricted-component-names.js create mode 100644 tests/lib/rules/restricted-component-names.js diff --git a/docs/rules/index.md b/docs/rules/index.md index 51515e233..d280011f1 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -281,6 +281,7 @@ 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 in component names | | :warning: | | [vue/script-indent](./script-indent.md) | enforce consistent indentation in ` ` + }, + { + filename: 'ref-in-block.vue', + code: ` + + + ` + }, + { + filename: 'ref-in-block-setup-fn.vue', + code: ` + + + ` } ], invalid: [ @@ -278,36 +333,6 @@ tester.run('prefer-use-template-ref', rule, { } ] }, - { - filename: 'ref-in-block.vue', - code: ` - - - `, - errors: [ - { - messageId: 'preferUseTemplateRef', - data: { - name: 'ref' - }, - line: 14, - column: 33 - } - ] - }, { filename: 'setup-function-only-refs.vue', code: ` From a270df82fef9d3f0e421b2ed27edd42afb25a0a6 Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Wed, 27 Nov 2024 17:33:58 +0800 Subject: [PATCH 08/11] feat: add slot-name-casing rule (#2620) Co-authored-by: Flo Edelmann --- docs/rules/index.md | 3 +- docs/rules/restricted-component-names.md | 4 +- docs/rules/slot-name-casing.md | 88 ++++++++++++++ lib/index.js | 1 + lib/rules/slot-name-casing.js | 82 +++++++++++++ tests/lib/rules/slot-name-casing.js | 148 +++++++++++++++++++++++ 6 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 docs/rules/slot-name-casing.md create mode 100644 lib/rules/slot-name-casing.js create mode 100644 tests/lib/rules/slot-name-casing.js diff --git a/docs/rules/index.md b/docs/rules/index.md index d280011f1..074f4bd46 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -281,8 +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 in component names | | :warning: | +| [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 `` }, + { + 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/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 From dc0653520db30b804108c9ffd7202a3a840a9b4a Mon Sep 17 00:00:00 2001 From: "Ghislain B." Date: Wed, 27 Nov 2024 10:01:18 -0500 Subject: [PATCH 10/11] docs: add example config with typescript-eslint and Prettier (#2522) Co-authored-by: Flo Edelmann --- docs/user-guide/index.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) 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: . From 4cbcad602c8e4ad534ab779a340654f6b3bc5600 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 30 Nov 2024 14:55:31 +0900 Subject: [PATCH 11/11] 9.32.0 --- docs/rules/restricted-component-names.md | 7 +++++-- docs/rules/slot-name-casing.md | 7 +++++-- package.json | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/rules/restricted-component-names.md b/docs/rules/restricted-component-names.md index e38ed7a7b..55e9883b8 100644 --- a/docs/rules/restricted-component-names.md +++ b/docs/rules/restricted-component-names.md @@ -3,14 +3,13 @@ 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 -- :exclamation: _**This rule has not been released yet.**_ - ## :book: Rule Details This rule enforces consistency in component names. @@ -60,6 +59,10 @@ This rule enforces consistency in component names. - [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) diff --git a/docs/rules/slot-name-casing.md b/docs/rules/slot-name-casing.md index 07d793ffe..63884fe86 100644 --- a/docs/rules/slot-name-casing.md +++ b/docs/rules/slot-name-casing.md @@ -3,14 +3,13 @@ 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 -- :exclamation: _**This rule has not been released yet.**_ - ## :book: Rule Details This rule enforces proper casing of slot names in Vue components. @@ -82,6 +81,10 @@ This rule enforces proper casing of slot names in Vue components. +## :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) diff --git a/package.json b/package.json index 503f711f1..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",