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", 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