diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 000000000..e5b6d8d6a --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 000000000..43b72b358 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://unpkg.com/@changesets/config/schema.json", + "changelog": [ + "@svitejs/changesets-changelog-github-compact", + { + "repo": "vuejs/eslint-plugin-vue" + } + ], + "commit": false, + "linked": [], + "access": "public", + "baseBranch": "master", + "bumpVersionsWithWorkspaceProtocolOnly": true, + "ignore": [] +} diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 57aa5876d..90bd33ad5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -46,7 +46,7 @@ jobs: with: node-version: ${{ matrix.node }} - name: Install Packages - run: npm install -f + run: npm install - name: Install ESLint v${{ matrix.eslint }} run: npm install --save-dev eslint@${{ matrix.eslint }} -f - name: Test @@ -61,8 +61,23 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 - name: Install Packages - run: npm install -f + run: npm install - name: Uninstall @stylistic/eslint-plugin run: npm uninstall -D @stylistic/eslint-plugin - name: Test run: npm test + + test-with-typescript-eslint-v7: + name: Test with typescript-eslint v7 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Node.js + uses: actions/setup-node@v4 + - name: Install Packages + run: npm install + - name: Install @typescript-eslint/parser@7 + run: npm install -D @typescript-eslint/parser@7 -f + - name: Test + run: npm test diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml new file mode 100644 index 000000000..3653780d5 --- /dev/null +++ b/.github/workflows/Release.yml @@ -0,0 +1,35 @@ +name: Release + +on: + push: + branches: + - master + +permissions: {} + +jobs: + release: + # prevents this action from running on forks + if: github.repository == 'vuejs/eslint-plugin-vue' + permissions: + contents: write # to create release (changesets/action) + pull-requests: write # to create pull request (changesets/action) + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + - name: Install Dependencies + run: npm install + + - name: Create Release Pull Request or Publish to npm + id: changesets + uses: changesets/action@v1 + with: + version: npm run changeset:version + publish: npm run changeset:publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 797d0cbcd..d6fadf92c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,9 @@ yarn.lock yarn-error.log /docs/.vitepress/dist -/docs/.vitepress/build-system/shim/eslint.mjs -/docs/.vitepress/build-system/shim/assert.mjs +/docs/.vitepress/build-system/shim/vue-eslint-parser.mjs +/docs/.vitepress/build-system/shim/@typescript-eslint/parser.mjs /docs/.vitepress/.temp /docs/.vitepress/cache typings/eslint/lib/rules +eslint-typegen.d.ts diff --git a/.markdownlintignore b/.markdownlintignore index 3c3629e64..e7becf85b 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -1 +1,2 @@ node_modules +CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..d48439d99 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# eslint-plugin-vue + +## 10.3.0 + +### Minor Changes + +- Added [`@typescript-eslint/parser`](https://typescript-eslint.io/packages/parser) as an optional peer dependency ([#2775](https://github.com/vuejs/eslint-plugin-vue/pull/2775)) + +- Add TypeScript IntelliSense support via [eslint-typegen](https://github.com/antfu/eslint-typegen) ([#2770](https://github.com/vuejs/eslint-plugin-vue/pull/2770)) + +- [`vue/no-deprecated-slot-attribute`](https://eslint.vuejs.org/rules/no-deprecated-slot-attribute.html) `ignore` option now supports regex patterns ([#2773](https://github.com/vuejs/eslint-plugin-vue/pull/2773)) + +### Patch Changes + +- Fixed false negatives when using typescript-eslint v8 in [`vue/script-indent`](https://eslint.vuejs.org/rules/script-indent.html) rule ([#2775](https://github.com/vuejs/eslint-plugin-vue/pull/2775)) + +- Update resources ([#2752](https://github.com/vuejs/eslint-plugin-vue/pull/2752)) + +- [`vue/no-restricted-html-elements`](https://eslint.vuejs.org/rules/no-restricted-html-elements.html) now also checks SVG and MathML elements ([#2755](https://github.com/vuejs/eslint-plugin-vue/pull/2755)) + +## 10.2.0 + +### Minor Changes + +- [vue/no-restricted-html-elements](https://eslint.vuejs.org/rules/no-restricted-html-elements.html) now accepts multiple elements in each entry. ([#2750](https://github.com/vuejs/eslint-plugin-vue/pull/2750)) + +### Patch Changes + +- Updates resources ([#2747](https://github.com/vuejs/eslint-plugin-vue/pull/2747)) diff --git a/docs/.vitepress/build-system/build.mts b/docs/.vitepress/build-system/build.mts index 9dce53faf..edda69143 100644 --- a/docs/.vitepress/build-system/build.mts +++ b/docs/.vitepress/build-system/build.mts @@ -9,14 +9,38 @@ import { fileURLToPath } from 'url' const dirname = path.dirname(fileURLToPath(import.meta.url)) build( - path.join(dirname, './src/eslint.mjs'), - path.join(dirname, './shim/eslint.mjs'), - ['path', 'assert', 'util', 'esquery'] + path.join( + dirname, + '../../../node_modules/@typescript-eslint/parser/dist/index.js' + ), + path.join(dirname, './shim/@typescript-eslint/parser.mjs'), + [ + 'util', + 'node:util', + 'path', + 'node:path', + 'fs', + 'node:fs', + 'semver', + 'fast-glob', + 'debug' + ] ) + build( - path.join(dirname, '../../../node_modules/assert'), - path.join(dirname, './shim/assert.mjs'), - ['path'] + path.join(dirname, '../../../node_modules/vue-eslint-parser/index.js'), + path.join(dirname, './shim/vue-eslint-parser.mjs'), + [ + 'path', + 'debug', + 'semver', + 'assert', + 'module', + 'events', + 'esquery', + 'fs', + 'eslint' + ] ) function build(input: string, out: string, injects: string[] = []) { @@ -42,16 +66,22 @@ function bundle(entryPoint: string, externals: string[]) { } function transform(code: string, injects: string[]) { + const normalizeInjects = [ + ...new Set(injects.map((inject) => inject.replace(/^node:/u, ''))) + ] const newCode = code.replace(/"[a-z]+" = "[a-z]+";/u, '') return ` -${injects +${normalizeInjects .map( (inject) => - `import $inject_${inject.replace(/-/gu, '_')}$ from '${inject}';` + `import $inject_${inject.replace(/[\-:]/gu, '_')}$ from '${inject}';` ) .join('\n')} const $_injects_$ = {${injects - .map((inject) => `${inject.replace(/-/gu, '_')}:$inject_${inject}$`) + .map( + (inject) => + `"${inject}":$inject_${inject.replace(/^node:/u, '').replace(/[\-:]/gu, '_')}$` + ) .join(',\n')}}; function require(module, ...args) { return $_injects_$[module] || {} diff --git a/docs/.vitepress/build-system/shim/globby.mjs b/docs/.vitepress/build-system/shim/empty.mjs similarity index 100% rename from docs/.vitepress/build-system/shim/globby.mjs rename to docs/.vitepress/build-system/shim/empty.mjs diff --git a/docs/.vitepress/build-system/shim/eslint/use-at-your-own-risk.mjs b/docs/.vitepress/build-system/shim/eslint/use-at-your-own-risk.mjs deleted file mode 100644 index 0db0b01c2..000000000 --- a/docs/.vitepress/build-system/shim/eslint/use-at-your-own-risk.mjs +++ /dev/null @@ -1,3 +0,0 @@ -export default { - /* empty */ -} diff --git a/docs/.vitepress/build-system/shim/esquery.mjs b/docs/.vitepress/build-system/shim/esquery.mjs deleted file mode 100644 index 5652b09f1..000000000 --- a/docs/.vitepress/build-system/shim/esquery.mjs +++ /dev/null @@ -1,5 +0,0 @@ -import esquery from '../../../../node_modules/esquery/dist/esquery.esm.js' - -export const { parse, match, traverse, matches, query } = esquery - -export { default } from '../../../../node_modules/esquery/dist/esquery.esm.js' diff --git a/docs/.vitepress/build-system/shim/path.mjs b/docs/.vitepress/build-system/shim/path.mjs deleted file mode 100644 index 544792b00..000000000 --- a/docs/.vitepress/build-system/shim/path.mjs +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-nocheck -export const sep = '/' -export function basename(path, ext) { - const b = (/[^\/]*$/u.exec(path) || [''])[0] - return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b -} -export function extname(path) { - return (/[^.\/]*$/u.exec(path) || [''])[0] -} -export function isAbsolute() { - return false -} -export function join(...args) { - return args.length > 0 ? normalize(args.join('/')) : '.' -} - -function normalize(path) { - const result = [] - for (const part of path.replace(/\/+/gu, '/').split('/')) { - if (part === '..') { - if (result[0] && result[0] !== '..' && result[0] !== '.') result.shift() - } else if (part === '.' && result.length > 0) { - // noop - } else { - result.unshift(part) - } - } - return result.reverse().join('/') -} -const posix = { - sep, - basename, - extname, - isAbsolute, - join -} -posix.posix = posix -export default posix diff --git a/docs/.vitepress/build-system/src/eslint.mjs b/docs/.vitepress/build-system/src/eslint.mjs deleted file mode 100644 index ed193b58b..000000000 --- a/docs/.vitepress/build-system/src/eslint.mjs +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -/* eslint-disable unicorn/prefer-export-from -- exporting as named and default is less duplication without `export…from` */ - -import { Linter } from '../../../../node_modules/eslint/lib/linter/linter.js' -import SourceCode from '../../../../node_modules/eslint/lib/source-code/source-code.js' - -export { Linter, SourceCode } -export default { Linter, SourceCode } diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index afa635d7e..7d198d588 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -3,6 +3,7 @@ import { defineConfig } from 'vitepress' import path from 'pathe' import { fileURLToPath } from 'url' import { viteCommonjs, vitePluginRequireResolve } from './vite-plugin.mjs' +import eslint4b, { requireESLintUseAtYourOwnRisk4b } from 'vite-plugin-eslint4b' // Pre-build cjs packages that cannot be bundled well. import './build-system/build.mjs' @@ -142,24 +143,30 @@ export default async () => { vite: { publicDir: path.resolve(dirname, './public'), - plugins: [vitePluginRequireResolve(), viteCommonjs()], + plugins: [ + vitePluginRequireResolve(), + viteCommonjs(), + eslint4b() as any, + requireESLintUseAtYourOwnRisk4b() + ], resolve: { alias: { - 'eslint/use-at-your-own-risk': path.join( + 'vue-eslint-parser': path.join( + dirname, + './build-system/shim/vue-eslint-parser.mjs' + ), + '@typescript-eslint/parser': path.join( dirname, - './build-system/shim/eslint/use-at-your-own-risk.mjs' + './build-system/shim/@typescript-eslint/parser.mjs' ), - eslint: path.join(dirname, './build-system/shim/eslint.mjs'), - assert: path.join(dirname, './build-system/shim/assert.mjs'), - path: path.join(dirname, './build-system/shim/path.mjs'), tslib: path.join(dirname, '../../node_modules/tslib/tslib.es6.js'), - esquery: path.join(dirname, './build-system/shim/esquery.mjs'), - globby: path.join(dirname, './build-system/shim/globby.mjs') + globby: path.join(dirname, './build-system/shim/empty.mjs'), + 'fast-glob': path.join(dirname, './build-system/shim/empty.mjs'), + module: path.join(dirname, './build-system/shim/empty.mjs') } }, define: { - 'process.env.NODE_DEBUG': 'false', 'require.cache': '{}' } }, diff --git a/docs/.vitepress/theme/components/eslint-code-block.vue b/docs/.vitepress/theme/components/eslint-code-block.vue index 77d4fd7cf..12cd1ff8b 100644 --- a/docs/.vitepress/theme/components/eslint-code-block.vue +++ b/docs/.vitepress/theme/components/eslint-code-block.vue @@ -8,8 +8,6 @@ class="eslint-code-block" :filename="filename" :language="language" - :preprocess="preprocess" - :postprocess="postprocess" dark :format="format" :fix="fix" @@ -20,8 +18,6 @@ +``` + + + +The rule applies to both JavaScript and TypeScript props: + + + +```vue + +``` + + + +## :wrench: Options + +```js +{ + "vue/define-props-destructuring": ["error", { + "destructure": "always" | "never" + }] +} +``` + +- `destructure` - Sets the destructuring preference for props + - `"always"` (default) - Requires destructuring when using `defineProps` and warns against using `withDefaults` with destructuring + - `"never"` - Requires using a variable to store props and prohibits destructuring + +### `"destructure": "never"` + + + +```vue + +``` + + + +## :books: Further Reading + +- [Reactive Props Destructure](https://vuejs.org/guide/components/props.html#reactive-props-destructure) + +## :rocket: Version + +This rule was introduced in eslint-plugin-vue v10.1.0 + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-props-destructuring.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-props-destructuring.js) diff --git a/docs/rules/eqeqeq.md b/docs/rules/eqeqeq.md index 392deff7f..fb0133251 100644 --- a/docs/rules/eqeqeq.md +++ b/docs/rules/eqeqeq.md @@ -11,6 +11,7 @@ since: v5.2.0 > Require the use of `===` and `!==` in `` - :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fix-problems) can automatically fix some of the problems reported by this rule. +- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). This rule is the same rule as core [eqeqeq] rule but it applies to the expressions in ``. diff --git a/docs/rules/index.md b/docs/rules/index.md index 55c5b96c9..6423ef365 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -218,6 +218,7 @@ For example: | [vue/define-emits-declaration] | enforce declaration style of `defineEmits` | | :hammer: | | [vue/define-macros-order] | enforce order of compiler macros (`defineProps`, `defineEmits`, etc.) | :wrench::bulb: | :lipstick: | | [vue/define-props-declaration] | enforce declaration style of `defineProps` | | :hammer: | +| [vue/define-props-destructuring] | enforce consistent style for props destructuring | | :hammer: | | [vue/enforce-style-attribute] | enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags | | :hammer: | | [vue/html-button-has-type] | disallow usage of button without an explicit type attribute | | :hammer: | | [vue/html-comment-content-newline] | enforce unified line break in HTML comments | :wrench: | :lipstick: | @@ -244,7 +245,7 @@ For example: | [vue/no-restricted-component-names] | disallow specific component names | :bulb: | :hammer: | | [vue/no-restricted-component-options] | disallow specific component option | | :hammer: | | [vue/no-restricted-custom-event] | disallow specific custom event | :bulb: | :hammer: | -| [vue/no-restricted-html-elements] | disallow specific HTML elements | | :hammer: | +| [vue/no-restricted-html-elements] | disallow specific elements | | :hammer: | | [vue/no-restricted-props] | disallow specific props | :bulb: | :hammer: | | [vue/no-restricted-static-attribute] | disallow specific attribute | | :hammer: | | [vue/no-restricted-v-bind] | disallow specific argument in `v-bind` | | :hammer: | @@ -313,7 +314,7 @@ The following rules extend the rules provided by ESLint itself and apply them to | [vue/comma-style] | Enforce consistent comma style in `` | :wrench: | :lipstick: | | [vue/dot-location] | Enforce consistent newlines before and after dots in `` | :wrench: | :lipstick: | | [vue/dot-notation] | Enforce dot notation whenever possible in `` | :wrench: | :hammer: | -| [vue/eqeqeq] | Require the use of `===` and `!==` in `` | :wrench: | :hammer: | +| [vue/eqeqeq] | Require the use of `===` and `!==` in `` | :wrench::bulb: | :hammer: | | [vue/func-call-spacing] | Require or disallow spacing between function identifiers and their invocations in `` | :wrench: | :lipstick: | | [vue/key-spacing] | Enforce consistent spacing between property names and type annotations in types and interfaces in `` | :wrench: | :lipstick: | | [vue/keyword-spacing] | Enforce consistent spacing before and after keywords in `` | :wrench: | :lipstick: | @@ -323,7 +324,7 @@ The following rules extend the rules provided by ESLint itself and apply them to | [vue/no-constant-condition] | Disallow constant expressions in conditions in `` | | :warning: | | [vue/no-empty-pattern] | Disallow empty destructuring patterns in `` | | :warning: | | [vue/no-extra-parens] | Disallow unnecessary parentheses in `` | :wrench: | :lipstick: | -| [vue/no-implicit-coercion] | Disallow shorthand type conversions in `` | :wrench: | :hammer: | +| [vue/no-implicit-coercion] | Disallow shorthand type conversions in `` | :wrench::bulb: | :hammer: | | [vue/no-irregular-whitespace] | disallow irregular whitespace in `.vue` files | | :warning: | | [vue/no-loss-of-precision] | Disallow literal numbers that lose precision in `` | | :warning: | | [vue/no-restricted-syntax] | Disallow specified syntax in `` | | :hammer: | @@ -398,6 +399,7 @@ The following rules extend the rules provided by ESLint itself and apply them to [vue/define-emits-declaration]: ./define-emits-declaration.md [vue/define-macros-order]: ./define-macros-order.md [vue/define-props-declaration]: ./define-props-declaration.md +[vue/define-props-destructuring]: ./define-props-destructuring.md [vue/dot-location]: ./dot-location.md [vue/dot-notation]: ./dot-notation.md [vue/enforce-style-attribute]: ./enforce-style-attribute.md diff --git a/docs/rules/no-bare-strings-in-template.md b/docs/rules/no-bare-strings-in-template.md index df1fae123..23a23c116 100644 --- a/docs/rules/no-bare-strings-in-template.md +++ b/docs/rules/no-bare-strings-in-template.md @@ -12,7 +12,7 @@ since: v7.0.0 ## :book: Rule Details -This rule disallows the use of bare strings in ``. +This rule disallows the use of bare strings in ``. In order to be able to internationalize your application, you will need to avoid using plain strings in your templates. Instead, you would need to use a template helper specializing in translation. This rule was inspired by [no-bare-strings rule in ember-template-lint](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-bare-strings.md). @@ -50,7 +50,7 @@ This rule was inspired by [no-bare-strings rule in ember-template-lint](https:// :::tip -This rule does not check for string literals, in bindings and mustaches interpolation. This is because it looks like a conscious decision. +This rule does not check for string literals, in bindings and mustaches interpolation. This is because it looks like a conscious decision. If you want to report these string literals, enable the [vue/no-useless-v-bind] and [vue/no-useless-mustaches] rules and fix the useless string literals. ::: @@ -72,7 +72,7 @@ If you want to report these string literals, enable the [vue/no-useless-v-bind] } ``` -- `allowlist` ... An array of allowed strings. +- `allowlist` ... An array of allowed strings or regular expression patterns (e.g. `/\d+/` to allow numbers). - `attributes` ... An object whose keys are tag name or patterns and value is an array of attributes to check for that tag name. - `directives` ... An array of directive names to check literal value. diff --git a/docs/rules/no-deprecated-slot-attribute.md b/docs/rules/no-deprecated-slot-attribute.md index 64f2c5e00..df4575cc4 100644 --- a/docs/rules/no-deprecated-slot-attribute.md +++ b/docs/rules/no-deprecated-slot-attribute.md @@ -48,7 +48,7 @@ This rule reports deprecated `slot` attribute in Vue.js v2.6.0+. } ``` -- `"ignore"` (`string[]`) An array of tags that ignore this rules. This option will check both kebab-case and PascalCase versions of the given tag names. Default is empty. +- `"ignore"` (`string[]`) An array of tags or regular expression patterns (e.g. `/^custom-/`) that ignore these rules. This option will check both kebab-case and PascalCase versions of the given tag names. Default is empty. ### `"ignore": ["my-component"]` diff --git a/docs/rules/no-implicit-coercion.md b/docs/rules/no-implicit-coercion.md index 22698320c..751e189b9 100644 --- a/docs/rules/no-implicit-coercion.md +++ b/docs/rules/no-implicit-coercion.md @@ -11,6 +11,7 @@ since: v9.33.0 > Disallow shorthand type conversions in `` - :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fix-problems) can automatically fix some of the problems reported by this rule. +- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). This rule is the same rule as core [no-implicit-coercion] rule but it applies to the expressions in ``. diff --git a/docs/rules/no-multiple-template-root.md b/docs/rules/no-multiple-template-root.md index 90b5488fc..8a85c9201 100644 --- a/docs/rules/no-multiple-template-root.md +++ b/docs/rules/no-multiple-template-root.md @@ -61,7 +61,32 @@ This rule checks whether template contains single root element valid for Vue 2. ## :wrench: Options -Nothing. +```json +{ + "vue/no-multiple-template-root": ["error", { + "disallowComments": false + }] +} +``` + +- "disallowComments" (`boolean`) Enables there should not be any comments in the template root. Default is `false`. + +### "disallowComments": true + + + +```vue +/* ✗ BAD */ + + + + vue eslint plugin + + + +``` + + ## :rocket: Version diff --git a/docs/rules/no-restricted-html-elements.md b/docs/rules/no-restricted-html-elements.md index 2a6d6c997..9adc7a6ab 100644 --- a/docs/rules/no-restricted-html-elements.md +++ b/docs/rules/no-restricted-html-elements.md @@ -2,17 +2,17 @@ pageClass: rule-details sidebarDepth: 0 title: vue/no-restricted-html-elements -description: disallow specific HTML elements +description: disallow specific elements since: v8.6.0 --- # vue/no-restricted-html-elements -> disallow specific HTML elements +> disallow specific elements ## :book: Rule Details -This rule allows you to specify HTML elements that you don't want to use in your application. +This rule allows you to specify HTML, SVG, and MathML elements that you don't want to use in your application. @@ -33,20 +33,20 @@ This rule allows you to specify HTML elements that you don't want to use in your ## :wrench: Options -This rule takes a list of strings, where each string is an HTML element name to be restricted: +This rule takes a list of strings, where each string is an element name to be restricted: ```json { - "vue/no-restricted-html-elements": ["error", "button", "marquee"] + "vue/no-restricted-html-elements": ["error", "a", "marquee"] } ``` - + ```vue - + ``` @@ -60,8 +60,8 @@ Alternatively, the rule also accepts objects. "vue/no-restricted-html-elements": [ "error", { - "element": "button", - "message": "Prefer use of our custom component" + "element": ["a", "RouterLink"], + "message": "Prefer the use of component" }, { "element": "marquee", @@ -73,18 +73,18 @@ Alternatively, the rule also accepts objects. The following properties can be specified for the object. -- `element` ... Specify the html element. +- `element` ... Specify the element name or an array of element names. - `message` ... Specify an optional custom message. -### `{ "element": "marquee" }, { "element": "button" }` +### `{ "element": "marquee" }, { "element": "a" }` - + ```vue - + ``` diff --git a/docs/rules/padding-line-between-blocks.md b/docs/rules/padding-line-between-blocks.md index 4f3ee0290..645efeac7 100644 --- a/docs/rules/padding-line-between-blocks.md +++ b/docs/rules/padding-line-between-blocks.md @@ -14,7 +14,7 @@ since: v6.2.0 ## :book: Rule Details -This rule requires or disallows blank lines between the given 2 blocks. Properly blank lines help developers to understand the code. +This rule requires or disallows blank lines between blocks. Properly placed blank lines help developers understand the code. diff --git a/eslint.config.js b/eslint.config.mjs similarity index 85% rename from eslint.config.js rename to eslint.config.mjs index 73c3ba2a4..9cd54f623 100644 --- a/eslint.config.js +++ b/eslint.config.mjs @@ -1,13 +1,19 @@ -'use strict' +import globals from 'globals' +import eslintPluginEslintPlugin from 'eslint-plugin-eslint-plugin/configs/all' +import eslintPluginJsonc from 'eslint-plugin-jsonc' +import eslintPluginNodeDependencies from 'eslint-plugin-node-dependencies' +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' +import eslintPluginUnicorn from 'eslint-plugin-unicorn' +import vueEslintParser from 'vue-eslint-parser' +import noInvalidMeta from './eslint-internal-rules/no-invalid-meta.js' +import noInvalidMetaDocsCategories from './eslint-internal-rules/no-invalid-meta-docs-categories.js' +import requireEslintCommunity from './eslint-internal-rules/require-eslint-community.js' -const globals = require('globals') -const eslintPluginEslintPlugin = require('eslint-plugin-eslint-plugin/configs/all') -const eslintPluginJsonc = require('eslint-plugin-jsonc') -const eslintPluginNodeDependencies = require('eslint-plugin-node-dependencies') -const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended') -const eslintPluginUnicorn = require('eslint-plugin-unicorn') +// @ts-check +/// +import typegen from 'eslint-typegen' -module.exports = [ +export default typegen([ { ignores: [ '.nyc_output', @@ -18,9 +24,8 @@ module.exports = [ '!.vitepress', 'docs/.vitepress/dist', - 'docs/.vitepress/build-system/shim/eslint.mjs', - 'docs/.vitepress/build-system/shim/assert.mjs', - 'docs/.vitepress/build-system/shim/path.mjs', + 'docs/.vitepress/build-system/shim/vue-eslint-parser.mjs', + 'docs/.vitepress/build-system/shim/@typescript-eslint/parser.mjs', 'docs/.vitepress/.temp', 'docs/.vitepress/cache' ] @@ -34,9 +39,9 @@ module.exports = [ plugins: { internal: { rules: { - 'no-invalid-meta': require('./eslint-internal-rules/no-invalid-meta'), - 'no-invalid-meta-docs-categories': require('./eslint-internal-rules/no-invalid-meta-docs-categories'), - 'require-eslint-community': require('./eslint-internal-rules/require-eslint-community') + 'no-invalid-meta': noInvalidMeta, + 'no-invalid-meta-docs-categories': noInvalidMetaDocsCategories, + 'require-eslint-community': requireEslintCommunity } } } @@ -214,7 +219,7 @@ module.exports = [ languageOptions: { ecmaVersion: 'latest', sourceType: 'module', - parser: require('vue-eslint-parser') + parser: vueEslintParser } }, { @@ -242,4 +247,4 @@ module.exports = [ 'prettier/prettier': 'off' } } -] +]) diff --git a/lib/index.d.ts b/lib/index.d.ts index 8cbff659f..b6d658852 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,3 +1,4 @@ +/// import type { Linter } from 'eslint' declare const vue: { diff --git a/lib/index.js b/lib/index.js index 834e5f28b..e511536fa 100644 --- a/lib/index.js +++ b/lib/index.js @@ -58,6 +58,7 @@ const plugin = { 'define-emits-declaration': require('./rules/define-emits-declaration'), 'define-macros-order': require('./rules/define-macros-order'), 'define-props-declaration': require('./rules/define-props-declaration'), + 'define-props-destructuring': require('./rules/define-props-destructuring'), 'dot-location': require('./rules/dot-location'), 'dot-notation': require('./rules/dot-notation'), 'enforce-style-attribute': require('./rules/enforce-style-attribute'), diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js index 65d096cd4..45e7ca687 100644 --- a/lib/rules/attribute-hyphenation.js +++ b/lib/rules/attribute-hyphenation.js @@ -6,7 +6,7 @@ const utils = require('../utils') const casing = require('../utils/casing') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const svgAttributes = require('../utils/svg-attributes-weird-case.json') /** @@ -79,11 +79,7 @@ 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 isIgnoredTagName = toRegExpGroupMatcher(optionsPayload?.ignoreTags) const ignoredAttributes = ['data-', 'aria-', 'slot-scope', ...svgAttributes] if (optionsPayload && optionsPayload.ignore) { @@ -142,11 +138,6 @@ 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 diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js index d330f60da..c7267cd49 100644 --- a/lib/rules/component-name-in-template-casing.js +++ b/lib/rules/component-name-in-template-casing.js @@ -6,7 +6,7 @@ const utils = require('../utils') const casing = require('../utils/casing') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const allowedCaseOptions = ['PascalCase', 'kebab-case'] const defaultCase = 'PascalCase' @@ -81,8 +81,7 @@ module.exports = { const caseType = allowedCaseOptions.includes(caseOption) ? caseOption : defaultCase - /** @type {RegExp[]} */ - const ignores = (options.ignores || []).map(toRegExp) + const isIgnored = toRegExpGroupMatcher(options.ignores) /** @type {string[]} */ const globals = (options.globals || []).map(casing.pascalCase) const registeredComponentsOnly = options.registeredComponentsOnly !== false @@ -116,7 +115,7 @@ module.exports = { * @returns {boolean} `true` if the given node is the verification target node. */ function isVerifyTarget(node) { - if (ignores.some((re) => re.test(node.rawName))) { + if (isIgnored(node.rawName)) { // ignore return false } diff --git a/lib/rules/custom-event-name-casing.js b/lib/rules/custom-event-name-casing.js index aff4609b5..c63b4e9d9 100644 --- a/lib/rules/custom-event-name-casing.js +++ b/lib/rules/custom-event-name-casing.js @@ -7,7 +7,7 @@ const { findVariable } = require('@eslint-community/eslint-utils') const utils = require('../utils') const casing = require('../utils/casing') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') /** * @typedef {import('../utils').VueObjectData} VueObjectData @@ -92,8 +92,7 @@ module.exports = { const caseType = context.options[0] || DEFAULT_CASE const objectOption = context.options[1] || {} const caseChecker = casing.getChecker(caseType) - /** @type {RegExp[]} */ - const ignores = (objectOption.ignores || []).map(toRegExp) + const isIgnored = toRegExpGroupMatcher(objectOption.ignores) /** * Check whether the given event name is valid. @@ -109,7 +108,7 @@ module.exports = { */ function verify(nameWithLoc) { const name = nameWithLoc.name - if (isValidEventName(name) || ignores.some((re) => re.test(name))) { + if (isValidEventName(name) || isIgnored(name)) { return } context.report({ diff --git a/lib/rules/define-props-destructuring.js b/lib/rules/define-props-destructuring.js new file mode 100644 index 000000000..65ec1dcd7 --- /dev/null +++ b/lib/rules/define-props-destructuring.js @@ -0,0 +1,79 @@ +/** + * @author Wayne Zhang + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'enforce consistent style for props destructuring', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/define-props-destructuring.html' + }, + fixable: null, + schema: [ + { + type: 'object', + properties: { + destructure: { + enum: ['always', 'never'] + } + }, + additionalProperties: false + } + ], + messages: { + preferDestructuring: 'Prefer destructuring from `defineProps` directly.', + avoidDestructuring: 'Avoid destructuring from `defineProps`.', + avoidWithDefaults: 'Avoid using `withDefaults` with destructuring.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const options = context.options[0] || {} + const destructurePreference = options.destructure || 'always' + + return utils.compositingVisitors( + utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(node, props) { + const hasNoArgs = props.filter((prop) => prop.propName).length === 0 + if (hasNoArgs) { + return + } + + const hasDestructure = utils.isUsingPropsDestructure(node) + const hasWithDefaults = utils.hasWithDefaults(node) + + if (destructurePreference === 'never') { + if (hasDestructure) { + context.report({ + node, + messageId: 'avoidDestructuring' + }) + } + return + } + + if (!hasDestructure) { + context.report({ + node, + messageId: 'preferDestructuring' + }) + return + } + + if (hasWithDefaults) { + context.report({ + node: node.parent.callee, + messageId: 'avoidWithDefaults' + }) + } + } + }) + ) + } +} diff --git a/lib/rules/no-bare-strings-in-template.js b/lib/rules/no-bare-strings-in-template.js index 1c75f5092..8e4acc96d 100644 --- a/lib/rules/no-bare-strings-in-template.js +++ b/lib/rules/no-bare-strings-in-template.js @@ -149,17 +149,33 @@ module.exports = { */ const opts = context.options[0] || {} /** @type {string[]} */ - const allowlist = opts.allowlist || DEFAULT_ALLOWLIST + const rawAllowlist = opts.allowlist || DEFAULT_ALLOWLIST const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES) const directives = opts.directives || DEFAULT_DIRECTIVES - const allowlistRe = new RegExp( - allowlist - .map((w) => regexp.escape(w)) - .sort((a, b) => b.length - a.length) - .join('|'), - 'gu' - ) + /** @type {string[]} */ + const stringAllowlist = [] + /** @type {RegExp[]} */ + const regexAllowlist = [] + + for (const item of rawAllowlist) { + if (regexp.isRegExp(item)) { + regexAllowlist.push(regexp.toRegExp(item)) + } else { + stringAllowlist.push(item) + } + } + + const allowlistRe = + stringAllowlist.length > 0 + ? new RegExp( + stringAllowlist + .map((w) => regexp.escape(w)) + .sort((a, b) => b.length - a.length) + .join('|'), + 'gu' + ) + : null /** @type {ElementStack | null} */ let elementStack = null @@ -168,7 +184,21 @@ module.exports = { * @param {string} str */ function getBareString(str) { - return str.trim().replace(allowlistRe, '').trim() + let result = str.trim() + + if (allowlistRe) { + result = result.replace(allowlistRe, '') + } + + for (const regex of regexAllowlist) { + const flags = regex.flags.includes('g') + ? regex.flags + : `${regex.flags}g` + const globalRegex = new RegExp(regex.source, flags) + result = result.replace(globalRegex, '') + } + + return result.trim() } /** diff --git a/lib/rules/no-multiple-template-root.js b/lib/rules/no-multiple-template-root.js index 45c22389f..524b7a2b1 100644 --- a/lib/rules/no-multiple-template-root.js +++ b/lib/rules/no-multiple-template-root.js @@ -6,6 +6,21 @@ const utils = require('../utils') +/** + * Get all comments that need to be reported + * @param {(HTMLComment | HTMLBogusComment | Comment)[]} comments + * @param {Range[]} elementRanges + * @returns {(HTMLComment | HTMLBogusComment | Comment)[]} + */ +function getReportComments(comments, elementRanges) { + return comments.filter( + (comment) => + !elementRanges.some( + (range) => range[0] <= comment.range[0] && comment.range[1] <= range[1] + ) + ) +} + module.exports = { meta: { type: 'problem', @@ -15,8 +30,19 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/no-multiple-template-root.html' }, fixable: null, - schema: [], + schema: [ + { + type: 'object', + properties: { + disallowComments: { + type: 'boolean' + } + }, + additionalProperties: false + } + ], messages: { + commentRoot: 'The template root disallows comments.', multipleRoot: 'The template root requires exactly one element.', textRoot: 'The template root requires an element rather than texts.', disallowedElement: "The template root disallows '<{{name}}>' elements.", @@ -28,6 +54,8 @@ module.exports = { * @returns {RuleListener} AST event handlers. */ create(context) { + const options = context.options[0] || {} + const disallowComments = options.disallowComments const sourceCode = context.getSourceCode() return { @@ -37,6 +65,18 @@ module.exports = { return } + const comments = element.comments + const elementRanges = element.children.map((child) => child.range) + if (disallowComments && comments.length > 0) { + for (const comment of getReportComments(comments, elementRanges)) { + context.report({ + node: comment, + loc: comment.loc, + messageId: 'commentRoot' + }) + } + } + const rootElements = [] let extraText = null let extraElement = null diff --git a/lib/rules/no-restricted-block.js b/lib/rules/no-restricted-block.js index 87b4bda3e..000e2c7b8 100644 --- a/lib/rules/no-restricted-block.js +++ b/lib/rules/no-restricted-block.js @@ -12,30 +12,16 @@ const regexp = require('../utils/regexp') * @property {string} [message] */ -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} /** * @param {any} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test(block) { - return matcher(block.rawName) + return matcher.test(block.rawName) } } } diff --git a/lib/rules/no-restricted-call-after-await.js b/lib/rules/no-restricted-call-after-await.js index 180dfe413..6754ad0d4 100644 --- a/lib/rules/no-restricted-call-after-await.js +++ b/lib/rules/no-restricted-call-after-await.js @@ -21,7 +21,7 @@ function safeRequireResolve(id) { if (fs.statSync(id).isDirectory()) { return require.resolve(id) } - } catch (_error) { + } catch { // ignore } return id diff --git a/lib/rules/no-restricted-class.js b/lib/rules/no-restricted-class.js index 41d30df2d..1a74fd249 100644 --- a/lib/rules/no-restricted-class.js +++ b/lib/rules/no-restricted-class.js @@ -12,20 +12,10 @@ const regexp = require('../utils/regexp') * @param {string} className * @param {*} node * @param {RuleContext} context - * @param {Set} forbiddenClasses - * @param {Array} forbiddenClassesRegexps + * @param {(name: string) => boolean} isForbiddenClass */ -const reportForbiddenClass = ( - className, - node, - context, - forbiddenClasses, - forbiddenClassesRegexps -) => { - if ( - forbiddenClasses.has(className) || - forbiddenClassesRegexps.some((re) => re.test(className)) - ) { +const reportForbiddenClass = (className, node, context, isForbiddenClass) => { + if (isForbiddenClass(className)) { const loc = node.value ? node.value.loc : node.loc context.report({ node, @@ -123,10 +113,8 @@ module.exports = { /** @param {RuleContext} context */ create(context) { - const forbiddenClasses = new Set(context.options || []) - const forbiddenClassesRegexps = (context.options || []) - .filter((cl) => regexp.isRegExp(cl)) - .map((cl) => regexp.toRegExp(cl)) + const { options = [] } = context + const isForbiddenClass = regexp.toRegExpGroupMatcher(options) return utils.defineTemplateBodyVisitor(context, { /** @@ -134,13 +122,7 @@ module.exports = { */ 'VAttribute[directive=false][key.name="class"][value!=null]'(node) { for (const className of node.value.value.split(/\s+/)) { - reportForbiddenClass( - className, - node, - context, - forbiddenClasses, - forbiddenClassesRegexps - ) + reportForbiddenClass(className, node, context, isForbiddenClass) } }, @@ -155,13 +137,7 @@ module.exports = { for (const { className, reportNode } of extractClassNames( /** @type {Expression} */ (node.expression) )) { - reportForbiddenClass( - className, - reportNode, - context, - forbiddenClasses, - forbiddenClassesRegexps - ) + reportForbiddenClass(className, reportNode, context, isForbiddenClass) } } }) diff --git a/lib/rules/no-restricted-component-names.js b/lib/rules/no-restricted-component-names.js index e5111a748..df5ad4a23 100644 --- a/lib/rules/no-restricted-component-names.js +++ b/lib/rules/no-restricted-component-names.js @@ -22,7 +22,7 @@ const { isRegExp, toRegExp } = require('../utils/regexp') */ function buildMatcher(str) { if (isRegExp(str)) { - const regex = toRegExp(str) + const regex = toRegExp(str, { remove: 'g' }) return (s) => regex.test(s) } return (s) => s === casing.pascalCase(str) || s === casing.kebabCase(str) diff --git a/lib/rules/no-restricted-component-options.js b/lib/rules/no-restricted-component-options.js index b8563a92b..227a89382 100644 --- a/lib/rules/no-restricted-component-options.js +++ b/lib/rules/no-restricted-component-options.js @@ -23,21 +23,6 @@ const regexp = require('../utils/regexp') * @typedef { (node: Property | SpreadElement) => (MatchResult | null) } Tester */ -/** - * @param {string} str - * @returns {Matcher} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} - /** * @param {string | string[] | { name: string | string[], message?: string } } option * @returns {ParsedOption} @@ -65,7 +50,8 @@ function parseOption(option) { if (name === '*') { steps.push({ wildcard: true }) } else { - steps.push({ test: buildMatcher(name) }) + const matcher = regexp.toRegExp(name, { remove: 'g' }) + steps.push({ test: (value) => matcher.test(value) }) } } const message = option.message diff --git a/lib/rules/no-restricted-custom-event.js b/lib/rules/no-restricted-custom-event.js index 5ddda037f..93c1aa764 100644 --- a/lib/rules/no-restricted-custom-event.js +++ b/lib/rules/no-restricted-custom-event.js @@ -15,30 +15,16 @@ const regexp = require('../utils/regexp') * @property {string|undefined} [suggest] */ -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} /** * @param {string|{event: string, message?: string, suggest?: string}} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test(name) { - return matcher(name) + return matcher.test(name) } } } diff --git a/lib/rules/no-restricted-html-elements.js b/lib/rules/no-restricted-html-elements.js index e906d86f2..ab52abde3 100644 --- a/lib/rules/no-restricted-html-elements.js +++ b/lib/rules/no-restricted-html-elements.js @@ -10,7 +10,7 @@ module.exports = { meta: { type: 'suggestion', docs: { - description: 'disallow specific HTML elements', + description: 'disallow specific elements', categories: undefined, url: 'https://eslint.vuejs.org/rules/no-restricted-html-elements.html' }, @@ -23,7 +23,12 @@ module.exports = { { type: 'object', properties: { - element: { type: 'string' }, + element: { + oneOf: [ + { type: 'string' }, + { type: 'array', items: { type: 'string' } } + ] + }, message: { type: 'string', minLength: 1 } }, required: ['element'], @@ -35,7 +40,7 @@ module.exports = { minItems: 0 }, messages: { - forbiddenElement: 'Unexpected use of forbidden HTML element {{name}}.', + forbiddenElement: 'Unexpected use of forbidden element {{name}}.', // eslint-disable-next-line eslint-plugin/report-message-format customMessage: '{{message}}' } @@ -50,14 +55,21 @@ module.exports = { * @param {VElement} node */ VElement(node) { - if (!utils.isHtmlElementNode(node)) { + if ( + !utils.isHtmlElementNode(node) && + !utils.isSvgElementNode(node) && + !utils.isMathElementNode(node) + ) { return } for (const option of context.options) { - const element = option.element || option + const restrictedItem = option.element || option + const elementsToRestrict = Array.isArray(restrictedItem) + ? restrictedItem + : [restrictedItem] - if (element === node.rawName) { + if (elementsToRestrict.includes(node.rawName)) { context.report({ messageId: option.message ? 'customMessage' : 'forbiddenElement', data: { @@ -66,6 +78,8 @@ module.exports = { }, node: node.startTag }) + + return } } } diff --git a/lib/rules/no-restricted-props.js b/lib/rules/no-restricted-props.js index 2d2f74bb0..e0684393d 100644 --- a/lib/rules/no-restricted-props.js +++ b/lib/rules/no-restricted-props.js @@ -18,30 +18,16 @@ const regexp = require('../utils/regexp') * @property {string|undefined} [suggest] */ -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} /** * @param {string|{name:string, message?: string, suggest?:string}} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test(name) { - return matcher(name) + return matcher.test(name) } } } diff --git a/lib/rules/no-restricted-static-attribute.js b/lib/rules/no-restricted-static-attribute.js index d9241620b..d7223044a 100644 --- a/lib/rules/no-restricted-static-attribute.js +++ b/lib/rules/no-restricted-static-attribute.js @@ -15,30 +15,16 @@ const regexp = require('../utils/regexp') * @property {string} [message] */ -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} /** * @param {any} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test({ key }) { - return matcher(key.rawName) + return matcher.test(key.rawName) } } } @@ -53,25 +39,25 @@ function parseOption(option) { return node.value == null || node.value.value === node.key.rawName } } else { - const valueMatcher = buildMatcher(option.value) + const valueMatcher = regexp.toRegExp(option.value, { remove: 'g' }) parsed.test = (node) => { if (!keyTest(node)) { return false } - return node.value != null && valueMatcher(node.value.value) + return node.value != null && valueMatcher.test(node.value.value) } } parsed.useValue = true } if (option.element) { const argTest = parsed.test - const tagMatcher = buildMatcher(option.element) + const tagMatcher = regexp.toRegExp(option.element, { remove: 'g' }) parsed.test = (node) => { if (!argTest(node)) { return false } const element = node.parent.parent - return tagMatcher(element.rawName) + return tagMatcher.test(element.rawName) } parsed.useElement = true } diff --git a/lib/rules/no-restricted-v-bind.js b/lib/rules/no-restricted-v-bind.js index f9bf5462b..e16622174 100644 --- a/lib/rules/no-restricted-v-bind.js +++ b/lib/rules/no-restricted-v-bind.js @@ -22,33 +22,19 @@ const DEFAULT_OPTIONS = [ } ] -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} /** * @param {any} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test(key) { return Boolean( key.argument && key.argument.type === 'VIdentifier' && - matcher(key.argument.rawName) + matcher.test(key.argument.rawName) ) }, modifiers: [] @@ -77,13 +63,13 @@ function parseOption(option) { } if (option.element) { const argTest = parsed.test - const tagMatcher = buildMatcher(option.element) + const tagMatcher = regexp.toRegExp(option.element, { remove: 'g' }) parsed.test = (key) => { if (!argTest(key)) { return false } const element = key.parent.parent.parent - return tagMatcher(element.rawName) + return tagMatcher.test(element.rawName) } parsed.useElement = true } diff --git a/lib/rules/no-restricted-v-on.js b/lib/rules/no-restricted-v-on.js index 2379df349..893d511a6 100644 --- a/lib/rules/no-restricted-v-on.js +++ b/lib/rules/no-restricted-v-on.js @@ -15,34 +15,19 @@ const regexp = require('../utils/regexp') * @property {string} [message] */ -/** - * @param {string} str - * @returns {(str: string) => boolean} - */ -function buildMatcher(str) { - if (regexp.isRegExp(str)) { - const re = regexp.toRegExp(str) - return (s) => { - re.lastIndex = 0 - return re.test(s) - } - } - return (s) => s === str -} - /** * @param {any} option * @returns {ParsedOption} */ function parseOption(option) { if (typeof option === 'string') { - const matcher = buildMatcher(option) + const matcher = regexp.toRegExp(option, { remove: 'g' }) return { test(key) { return Boolean( key.argument && key.argument.type === 'VIdentifier' && - matcher(key.argument.rawName) + matcher.test(key.argument.rawName) ) } } @@ -70,12 +55,12 @@ function parseOption(option) { } if (option.element) { const argTest = parsed.test - const tagMatcher = buildMatcher(option.element) + const tagMatcher = regexp.toRegExp(option.element, { remove: 'g' }) parsed.test = (key) => { if (!argTest(key)) { return false } - return tagMatcher(key.parent.parent.parent.rawName) + return tagMatcher.test(key.parent.parent.parent.rawName) } parsed.useElement = true } diff --git a/lib/rules/no-undef-properties.js b/lib/rules/no-undef-properties.js index 711c2ed22..3ff49bd83 100644 --- a/lib/rules/no-undef-properties.js +++ b/lib/rules/no-undef-properties.js @@ -6,7 +6,7 @@ const utils = require('../utils') const reserved = require('../utils/vue-reserved.json') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const { getStyleVariablesContext } = require('../utils/style-variables') const { definePropertyReferenceExtractor @@ -106,9 +106,8 @@ module.exports = { /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} - const ignores = /** @type {string[]} */ ( - options.ignores || [String.raw`/^\$/`] - ).map(toRegExp) + const { ignores = [String.raw`/^\$/`] } = options + const isIgnored = toRegExpGroupMatcher(ignores) const propertyReferenceExtractor = definePropertyReferenceExtractor(context) const programNode = context.getSourceCode().ast /** @@ -190,7 +189,7 @@ module.exports = { report(node, name, messageId = 'undef') { if ( reserved.includes(name) || - ignores.some((ignore) => ignore.test(name)) || + isIgnored(name) || propertiesDefinedByStoreHelpers.has(name) ) { return diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js index d5ff8b48a..dc50c3ecc 100644 --- a/lib/rules/no-unsupported-features.js +++ b/lib/rules/no-unsupported-features.js @@ -57,7 +57,7 @@ function getSemverRange(x) { if (!ret) { try { ret = new semver.Range(s) - } catch (_error) { + } catch { // Ignore parsing error. } cache.set(s, ret) diff --git a/lib/rules/prefer-true-attribute-shorthand.js b/lib/rules/prefer-true-attribute-shorthand.js index 817525d1d..3f5446bda 100644 --- a/lib/rules/prefer-true-attribute-shorthand.js +++ b/lib/rules/prefer-true-attribute-shorthand.js @@ -4,7 +4,7 @@ */ 'use strict' -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const utils = require('../utils') /** @@ -99,8 +99,7 @@ module.exports = { create(context) { /** @type {'always' | 'never'} */ const option = context.options[0] || 'always' - /** @type {RegExp[]} */ - const exceptReg = (context.options[1]?.except || []).map(toRegExp) + const exceptMatcher = toRegExpGroupMatcher(context.options[1]?.except) /** * @param {VAttribute | VDirective} node @@ -155,7 +154,7 @@ module.exports = { const name = getAttributeName(node) if (name === null) return - const isExcepted = exceptReg.some((re) => re.test(name)) + const isExcepted = exceptMatcher(name) if (shouldConvertToLongForm(node, isExcepted, option)) { const key = /** @type {VIdentifier} */ (node.key) diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js index fd4f0dc31..7121c66c6 100644 --- a/lib/rules/prop-name-casing.js +++ b/lib/rules/prop-name-casing.js @@ -6,7 +6,7 @@ const utils = require('../utils') const casing = require('../utils/casing') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const allowedCaseOptions = ['camelCase', 'snake_case'] /** @@ -16,8 +16,7 @@ const allowedCaseOptions = ['camelCase', 'snake_case'] /** @param {RuleContext} context */ function create(context) { const options = context.options[0] - /** @type {RegExp[]} */ - const ignoreProps = (context.options[1]?.ignoreProps || []).map(toRegExp) + const isIgnoredProp = toRegExpGroupMatcher(context.options[1]?.ignoreProps) const caseType = allowedCaseOptions.includes(options) ? options : 'camelCase' const checker = casing.getChecker(caseType) @@ -30,7 +29,7 @@ function create(context) { if (propName == null) { continue } - if (!checker(propName) && !ignoreProps.some((re) => re.test(propName))) { + if (!checker(propName) && !isIgnoredProp(propName)) { context.report({ node: item.node, messageId: 'invalidCase', diff --git a/lib/rules/restricted-component-names.js b/lib/rules/restricted-component-names.js index 636224db6..89d04bfad 100644 --- a/lib/rules/restricted-component-names.js +++ b/lib/rules/restricted-component-names.js @@ -5,7 +5,7 @@ 'use strict' const utils = require('../utils') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') const htmlElements = require('../utils/html-elements.json') const deprecatedHtmlElements = require('../utils/deprecated-html-elements.json') @@ -51,12 +51,11 @@ module.exports = { /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} - /** @type {RegExp[]} */ - const allow = (options.allow || []).map(toRegExp) + const isAllowed = toRegExpGroupMatcher(options.allow) /** @param {string} name */ function isAllowedTarget(name) { - return reservedNames.has(name) || allow.some((re) => re.test(name)) + return reservedNames.has(name) || isAllowed(name) } return utils.defineTemplateBodyVisitor(context, { diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js index 27087cb37..8d854230c 100644 --- a/lib/rules/syntaxes/slot-attribute.js +++ b/lib/rules/syntaxes/slot-attribute.js @@ -5,6 +5,7 @@ 'use strict' const canConvertToVSlot = require('./utils/can-convert-to-v-slot') +const regexp = require('../../utils/regexp') const casing = require('../../utils/casing') module.exports = { @@ -12,9 +13,10 @@ module.exports = { supported: '<3.0.0', /** @param {RuleContext} context @returns {TemplateListener} */ createTemplateBodyVisitor(context) { + /** @type {{ ignore: string[] }} */ const options = context.options[0] || {} - /** @type {Set} */ - const ignore = new Set(options.ignore) + const { ignore = [] } = options + const isAnyIgnored = regexp.toRegExpGroupMatcher(ignore) const sourceCode = context.getSourceCode() const tokenStore = @@ -122,10 +124,13 @@ module.exports = { */ function reportSlot(slotAttr) { const componentName = slotAttr.parent.parent.rawName + if ( - ignore.has(componentName) || - ignore.has(casing.pascalCase(componentName)) || - ignore.has(casing.kebabCase(componentName)) + isAnyIgnored( + componentName, + casing.pascalCase(componentName), + casing.kebabCase(componentName) + ) ) { return } diff --git a/lib/rules/v-on-event-hyphenation.js b/lib/rules/v-on-event-hyphenation.js index c9fac76e8..056890fa0 100644 --- a/lib/rules/v-on-event-hyphenation.js +++ b/lib/rules/v-on-event-hyphenation.js @@ -2,7 +2,7 @@ const utils = require('../utils') const casing = require('../utils/casing') -const { toRegExp } = require('../utils/regexp') +const { toRegExpGroupMatcher } = require('../utils/regexp') module.exports = { meta: { @@ -63,11 +63,7 @@ module.exports = { const useHyphenated = option !== 'never' /** @type {string[]} */ const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || [] - /** @type {RegExp[]} */ - const ignoredTagsRegexps = ( - (optionsPayload && optionsPayload.ignoreTags) || - [] - ).map(toRegExp) + const isIgnoredTag = toRegExpGroupMatcher(optionsPayload?.ignoreTags) const autofix = Boolean(optionsPayload && optionsPayload.autofix) const caseConverter = casing.getConverter( @@ -111,17 +107,12 @@ 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) { const element = node.parent.parent if ( !utils.isCustomComponent(element) || - isIgnoredTagName(element.rawName) + isIgnoredTag(element.rawName) ) { return } diff --git a/lib/utils/indent-ts.js b/lib/utils/indent-ts.js index 314858c9a..8db40869f 100644 --- a/lib/utils/indent-ts.js +++ b/lib/utils/indent-ts.js @@ -189,9 +189,12 @@ function defineVisitor({ tokenStore.getFirstToken(node.id || node) ) } - if (node.superTypeParameters != null && node.superClass != null) { + const superTypeArguments = + node.superTypeArguments || + /** @type {any} for old parser */ (node).superTypeParameters + if (superTypeArguments != null && node.superClass != null) { setOffset( - tokenStore.getFirstToken(node.superTypeParameters), + tokenStore.getFirstToken(superTypeArguments), 1, tokenStore.getFirstToken(node.superClass) ) @@ -600,9 +603,11 @@ function defineVisitor({ TSMappedType(node) { // {[key in foo]: bar} const leftBraceToken = tokenStore.getFirstToken(node) - const leftBracketToken = tokenStore.getTokenBefore(node.typeParameter) + const leftBracketToken = tokenStore.getTokenBefore( + node.key || node.typeParameter + ) const rightBracketToken = tokenStore.getTokenAfter( - node.nameType || node.typeParameter + node.nameType || node.constraint || node.typeParameter ) setOffset( [ @@ -613,11 +618,21 @@ function defineVisitor({ leftBraceToken ) processNodeList( - [node.typeParameter, node.nameType], + [node.key || node.typeParameter, node.nameType], leftBracketToken, rightBracketToken, 1 ) + if (node.constraint) { + setOffset( + [ + ...tokenStore.getTokensBetween(node.key, node.constraint), + tokenStore.getFirstToken(node.constraint) + ], + 1, + tokenStore.getFirstToken(node.key) + ) + } const rightBraceToken = tokenStore.getLastToken(node) if (node.typeAnnotation) { const typeAnnotationToken = tokenStore.getFirstToken( @@ -795,9 +810,12 @@ function defineVisitor({ * @param {TSInterfaceHeritage | TSClassImplements} node */ 'TSClassImplements, TSInterfaceHeritage'(node) { - if (node.typeParameters) { + const typeArguments = + node.typeArguments || + /** @type {any} for old parser */ (node).typeParameters + if (typeArguments) { setOffset( - tokenStore.getFirstToken(node.typeParameters), + tokenStore.getFirstToken(typeArguments), 1, tokenStore.getFirstToken(node) ) @@ -823,6 +841,15 @@ function defineVisitor({ const leftBraceToken = tokenStore.getTokenAfter(idTokens.lastToken) const rightBraceToken = tokenStore.getLastToken(node) setOffset(leftBraceToken, 0, firstToken) + if (node.body) { + return + } + // For old parser + processNodeList(node.members, leftBraceToken, rightBraceToken, 1) + }, + TSEnumBody(node) { + const leftBraceToken = tokenStore.getFirstToken(node) + const rightBraceToken = tokenStore.getLastToken(node) processNodeList(node.members, leftBraceToken, rightBraceToken, 1) }, TSModuleDeclaration(node) { diff --git a/lib/utils/index.js b/lib/utils/index.js index 769362966..f83b000f7 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -301,7 +301,7 @@ function wrapContextToOverrideTokenMethods(context, tokenStore, options) { }) containerScopes.set(exprContainer, scope) return scope - } catch (error) { + } catch { // ignore // console.log(error) } diff --git a/lib/utils/regexp.js b/lib/utils/regexp.js index 3ee40ae41..006568067 100644 --- a/lib/utils/regexp.js +++ b/lib/utils/regexp.js @@ -22,14 +22,25 @@ function escape(string) { * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`. * * @param {string} string The string to convert. + * @param {{add?: string, remove?: string}} [flags] The flags to add or remove. + * - `add`: Flags to add to the `RegExp` (e.g. `'i'` for case-insensitive). + * - `remove`: Flags to remove from the `RegExp` (e.g. `'g'` to remove global matching). * @returns {RegExp} Returns the `RegExp`. */ -function toRegExp(string) { +function toRegExp(string, flags = {}) { const parts = RE_REGEXP_STR.exec(string) + const { add: forceAddFlags = '', remove: forceRemoveFlags = '' } = + typeof flags === 'object' ? flags : {} // Avoid issues when this is called directly from array.map if (parts) { - return new RegExp(parts[1], parts[2]) + return new RegExp( + parts[1], + parts[2].replace( + new RegExp(`[${forceAddFlags}${forceRemoveFlags}]`, 'g'), + '' + ) + forceAddFlags + ) } - return new RegExp(`^${escape(string)}$`) + return new RegExp(`^${escape(string)}$`, forceAddFlags) } /** @@ -41,8 +52,32 @@ function isRegExp(string) { return RE_REGEXP_STR.test(string) } +/** + * Converts an array of strings to a singular function to match any of them. + * This function converts each string to a `RegExp` and returns a function that checks all of them. + * + * @param {string[]} [patterns] The strings or regular expression strings to match. + * @returns {(...toCheck: string[]) => boolean} Returns a function that checks if any string matches any of the given patterns. + */ +function toRegExpGroupMatcher(patterns = []) { + if (patterns.length === 0) { + return () => false + } + + // In the future, we could optimize this by joining expressions with identical flags. + const regexps = patterns.map((pattern) => toRegExp(pattern, { remove: 'g' })) + + if (regexps.length === 1) { + return (...toCheck) => toCheck.some((str) => regexps[0].test(str)) + } + + return (...toCheck) => + regexps.some((regexp) => toCheck.some((str) => regexp.test(str))) +} + module.exports = { escape, toRegExp, - isRegExp + isRegExp, + toRegExpGroupMatcher } diff --git a/lib/utils/selector.js b/lib/utils/selector.js index b5aa1adfc..dfcfc3265 100644 --- a/lib/utils/selector.js +++ b/lib/utils/selector.js @@ -23,7 +23,7 @@ function parseSelector(selector, context) { let astSelector try { astSelector = parser().astSync(selector) - } catch (error) { + } catch { context.report({ loc: { line: 0, column: 0 }, message: `Cannot parse selector: ${selector}.` @@ -554,7 +554,7 @@ function parseNth(pseudoNode) { .trim() try { return nthCheck(argument) - } catch (error) { + } catch { throw new SelectorError( `Cannot parse An+B micro syntax (:nth-xxx() argument): '${argument}'.` ) diff --git a/lib/utils/svg-elements.json b/lib/utils/svg-elements.json index eedbf0d07..f214aad24 100644 --- a/lib/utils/svg-elements.json +++ b/lib/utils/svg-elements.json @@ -7,7 +7,6 @@ "clipPath", "defs", "desc", - "discard", "ellipse", "feBlend", "feColorMatrix", diff --git a/lib/utils/vue3-export-names.json b/lib/utils/vue3-export-names.json index 349779da1..395090026 100644 --- a/lib/utils/vue3-export-names.json +++ b/lib/utils/vue3-export-names.json @@ -235,6 +235,7 @@ "AsyncComponentOptions", "defineAsyncComponent", "useModel", + "TemplateRef", "useTemplateRef", "useId", "h", @@ -263,8 +264,8 @@ "devtools", "setDevtoolsHook", "DeprecationTypes", - "WatchOptionsBase", "createElementVNode", + "WatchOptionsBase", "TransitionProps", "Transition", "TransitionGroupProps", diff --git a/package.json b/package.json index 5eb6e8113..a473deb46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-vue", - "version": "10.0.1", + "version": "10.3.0", "description": "Official ESLint plugin for Vue.js", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -18,12 +18,16 @@ "lint:fix": "eslint . --fix && markdownlint \"**/*.md\" --fix", "tsc": "tsc", "preversion": "npm test && git add .", - "version": "env-cmd -e version npm run update && npm run lint -- --fix && git add .", + "version": "npm run generate:version && git add .", "update": "node ./tools/update.js", "update-resources": "node ./tools/update-resources.js", + "typegen": "node ./tools/generate-typegen.mjs", "docs:watch": "vitepress dev docs", "predocs:build": "npm run update", - "docs:build": "vitepress build docs" + "docs:build": "vitepress build docs", + "generate:version": "env-cmd -e version npm run update && npm run lint -- --fix", + "changeset:version": "changeset version && npm run generate:version && git add --all", + "changeset:publish": "npm run typegen && changeset publish" }, "files": [ "lib" @@ -55,7 +59,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "vue-eslint-parser": "^10.0.0" + "vue-eslint-parser": "^10.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/parser": { + "optional": true + } }, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", @@ -66,19 +76,21 @@ "xml-name-validator": "^4.0.0" }, "devDependencies": { + "@changesets/cli": "^2.29.2", "@ota-meshi/site-kit-eslint-editor-vue": "^0.2.4", "@stylistic/eslint-plugin": "^2.12.1", + "@svitejs/changesets-changelog-github-compact": "^1.2.0", "@types/eslint": "^8.56.2", "@types/natural-compare": "^1.4.3", - "@types/node": "^14.18.63", + "@types/node": "^24.0.8", "@types/semver": "^7.5.8", "@types/xml-name-validator": "^4.0.3", - "@typescript-eslint/parser": "^7.18.0", - "@typescript-eslint/types": "^7.18.0", + "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/types": "^8.35.1", "assert": "^2.1.0", "env-cmd": "^10.1.0", "esbuild": "^0.24.0", - "eslint": "^8.57.0", + "eslint": "^9.30.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-eslint-plugin": "~6.4.0", "eslint-plugin-import": "^2.31.0", @@ -87,8 +99,9 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-unicorn": "^56.0.0", "eslint-plugin-vue": "file:.", + "eslint-typegen": "^2.2.0", "eslint-visitor-keys": "^4.2.0", - "espree": "^9.6.1", + "espree": "^10.4.0", "events": "^3.3.0", "globals": "^15.14.0", "jsdom": "^22.0.0", @@ -98,6 +111,7 @@ "pathe": "^1.1.2", "prettier": "^3.3.3", "typescript": "^5.7.2", + "vite-plugin-eslint4b": "^0.5.1", "vitepress": "^1.4.1", "vue-eslint-parser": "^10.0.0" } diff --git a/tests/fixtures/typescript/src/test01.ts b/tests/fixtures/typescript/src/test01.ts index d14550843..917643c67 100644 --- a/tests/fixtures/typescript/src/test01.ts +++ b/tests/fixtures/typescript/src/test01.ts @@ -18,3 +18,8 @@ export type Props2 = { h?: string[] i?: readonly string[] } + +export type Slots1 = { + default(props: { msg: string }): any + foo(props: { msg: string }): any +} diff --git a/tests/integrations/flat-config/a.vue b/tests/integrations/flat-config/a.vue index 0470d17d3..03610f638 100644 --- a/tests/integrations/flat-config/a.vue +++ b/tests/integrations/flat-config/a.vue @@ -1,4 +1,3 @@ - Hello diff --git a/tests/lib/rules/define-props-destructuring.js b/tests/lib/rules/define-props-destructuring.js new file mode 100644 index 000000000..ec24b4328 --- /dev/null +++ b/tests/lib/rules/define-props-destructuring.js @@ -0,0 +1,212 @@ +/** + * @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/define-props-destructuring') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2015, + sourceType: 'module' + } +}) + +tester.run('define-props-destructuring', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + } + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }], + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + } + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'preferDestructuring', + line: 3, + column: 21 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'preferDestructuring', + line: 3, + column: 34 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'avoidWithDefaults', + line: 3, + column: 23 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + errors: [ + { + messageId: 'preferDestructuring', + line: 3, + column: 34 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + errors: [ + { + messageId: 'avoidWithDefaults', + line: 3, + column: 23 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }], + errors: [ + { + messageId: 'avoidDestructuring', + line: 3, + column: 23 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }], + errors: [ + { + messageId: 'avoidDestructuring', + line: 3, + column: 36 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ destructure: 'never' }], + languageOptions: { + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + errors: [ + { + messageId: 'avoidDestructuring', + line: 3, + column: 23 + } + ] + } + ] +}) diff --git a/tests/lib/rules/eqeqeq.js b/tests/lib/rules/eqeqeq.js index afd458248..8089ebaaa 100644 --- a/tests/lib/rules/eqeqeq.js +++ b/tests/lib/rules/eqeqeq.js @@ -3,7 +3,8 @@ */ 'use strict' -const RuleTester = require('../../eslint-compat').RuleTester +const semver = require('semver') +const { RuleTester, ESLint } = require('../../eslint-compat') const rule = require('../../../lib/rules/eqeqeq') const tester = new RuleTester({ @@ -24,7 +25,19 @@ tester.run('eqeqeq', rule, { invalid: [ { code: '', - errors: ["Expected '===' and instead saw '=='."] + errors: [ + { + message: "Expected '===' and instead saw '=='.", + suggestions: semver.gte(ESLint.version, '9.26.0') + ? [ + { + desc: "Use '===' instead of '=='.", + output: `` + } + ] + : null + } + ] }, // CSS vars injection { @@ -34,7 +47,24 @@ tester.run('eqeqeq', rule, { color: v-bind(a == 1 ? 'red' : 'blue') } `, - errors: ["Expected '===' and instead saw '=='."] + errors: [ + { + message: "Expected '===' and instead saw '=='.", + suggestions: semver.gte(ESLint.version, '9.26.0') + ? [ + { + desc: "Use '===' instead of '=='.", + output: ` + ` + } + ] + : null + } + ] } ] }) diff --git a/tests/lib/rules/no-bare-strings-in-template.js b/tests/lib/rules/no-bare-strings-in-template.js index 4c6c89c7c..6706aaae0 100644 --- a/tests/lib/rules/no-bare-strings-in-template.js +++ b/tests/lib/rules/no-bare-strings-in-template.js @@ -132,6 +132,32 @@ tester.run('no-bare-strings-in-template', rule, { `, options: [{ allowlist: ['@@'] }] + }, + // regex + { + code: ` + + 123 321 + + `, + options: [{ allowlist: [String.raw`/\d+/g`] }] + }, + { + code: ` + + $foo + $bar + + `, + options: [{ allowlist: [String.raw`/\$\w+/`] }] + }, + { + code: ` + + foo123foo + + `, + options: [{ allowlist: [String.raw`/\d+/`, 'foo'] }] } ], invalid: [ @@ -316,6 +342,40 @@ tester.run('no-bare-strings-in-template', rule, { endColumn: 34 } ] + }, + { + code: ` + + 123, foo is invalid, 321 + + `, + options: [{ allowlist: [String.raw`/^\d+$/g`] }], + errors: [ + { + messageId: 'unexpected', + line: 3, + column: 13, + endLine: 3, + endColumn: 37 + } + ] + }, + { + code: ` + + foo123bar + + `, + options: [{ allowlist: [String.raw`/\d+/`, 'foo'] }], + errors: [ + { + messageId: 'unexpected', + line: 3, + column: 13, + endLine: 3, + endColumn: 22 + } + ] } ] }) diff --git a/tests/lib/rules/no-deprecated-slot-attribute.js b/tests/lib/rules/no-deprecated-slot-attribute.js index 2fd5fa401..e420788ce 100644 --- a/tests/lib/rules/no-deprecated-slot-attribute.js +++ b/tests/lib/rules/no-deprecated-slot-attribute.js @@ -55,6 +55,18 @@ tester.run('no-deprecated-slot-attribute', rule, { `, options: [{ ignore: ['one', 'two', 'my-component'] }] + }, + { + code: ` + + + + + + + + `, + options: [{ ignore: ['/one/', '/^Two$/i', '/^my-.*/i'] }] } ], invalid: [ @@ -644,6 +656,82 @@ tester.run('no-deprecated-slot-attribute', rule, { ], errors: ['`slot` attributes are deprecated.'] }, + { + code: ` + + + + A + + + B + + + `, + output: ` + + + + A + + \n + B + \n + + `, + options: [ + { + ignore: ['/one/'] + } + ], + errors: [ + { + message: '`slot` attributes are deprecated.', + line: 7, + endLine: 7, + column: 16, + endColumn: 20 + } + ] + }, + { + code: ` + + + + A + + + B + + + `, + output: ` + + + + A + + \n + B + \n + + `, + options: [ + { + ignore: ['/^one$/'] + } + ], + errors: [ + { + message: '`slot` attributes are deprecated.', + line: 7, + endLine: 7, + column: 16, + endColumn: 20 + } + ] + }, { code: ` diff --git a/tests/lib/rules/no-multiple-template-root.js b/tests/lib/rules/no-multiple-template-root.js index 7ab80af7e..c52c1c202 100644 --- a/tests/lib/rules/no-multiple-template-root.js +++ b/tests/lib/rules/no-multiple-template-root.js @@ -62,6 +62,66 @@ ruleTester.run('no-multiple-template-root', rule, { ` + }, + { + filename: 'test.vue', + code: ` + + + 12333 + + + `, + options: [{ disallowComments: false }] + }, + { + filename: 'test.vue', + code: ` + + + + + 12333 + + + `, + options: [{ disallowComments: false }] + }, + { + filename: 'test.vue', + code: ` + + + + 12333 + + + 12333 + + + + `, + options: [{ disallowComments: true }] + }, + { + filename: 'test.vue', + code: ` + + + + 12333 + + + 12333 + + + + + 12333 + + + `, + options: [{ disallowComments: true }] } ], invalid: [ @@ -104,6 +164,132 @@ ruleTester.run('no-multiple-template-root', rule, { filename: 'test.vue', code: '', errors: ["The template root disallows '' elements."] + }, + { + code: ` + + + 12333 + + + `, + options: [{ disallowComments: true }], + errors: [ + { + message: 'The template root disallows comments.', + line: 3 + }, + { + message: 'The template root disallows comments.', + line: 5 + } + ] + }, + { + code: ` + + + + 12333 + + + + `, + options: [{ disallowComments: true }], + errors: [ + { + message: 'The template root disallows comments.', + line: 3 + } + ] + }, + { + code: ` + + + + + 12333 + + + 12333 + + + + + + 12333 + + + + `, + options: [{ disallowComments: true }], + errors: [ + { + message: 'The template root disallows comments.', + line: 3 + }, + { + message: 'The template root disallows comments.', + line: 12 + }, + { + message: 'The template root disallows comments.', + line: 17 + } + ] + }, + { + code: ` + + + 12333 + + + + + `, + options: [{ disallowComments: true }], + errors: [ + { + message: 'The template root disallows comments.', + line: 7 + } + ] + }, + { + code: ` + + + + + + `, + options: [{ disallowComments: true }], + errors: [ + { + message: 'The template root disallows comments.', + line: 4 + }, + { + message: 'The template root requires exactly one element.', + line: 5 + } + ] + }, + { + code: ` + + + + 12333 + + + + `, + options: [{ disallowComments: true }], + errors: ['The template root disallows comments.'] } ] }) diff --git a/tests/lib/rules/no-restricted-html-elements.js b/tests/lib/rules/no-restricted-html-elements.js index c3e3e9eeb..f89f58a11 100644 --- a/tests/lib/rules/no-restricted-html-elements.js +++ b/tests/lib/rules/no-restricted-html-elements.js @@ -31,6 +31,23 @@ tester.run('no-restricted-html-elements', rule, { filename: 'test.vue', code: '', options: ['button'] + }, + { + filename: 'test.vue', + code: '', + options: [{ element: ['div', 'span'] }] + }, + // SVG + { + filename: 'test.vue', + code: '', + options: ['circle'] + }, + // Math + { + filename: 'test.vue', + code: '2', + options: ['mi'] } ], invalid: [ @@ -40,7 +57,7 @@ tester.run('no-restricted-html-elements', rule, { options: ['button'], errors: [ { - message: 'Unexpected use of forbidden HTML element button.', + message: 'Unexpected use of forbidden element button.', line: 1, column: 16 } @@ -52,7 +69,7 @@ tester.run('no-restricted-html-elements', rule, { options: ['div'], errors: [ { - message: 'Unexpected use of forbidden HTML element div.', + message: 'Unexpected use of forbidden element div.', line: 1, column: 11 } @@ -69,6 +86,85 @@ tester.run('no-restricted-html-elements', rule, { column: 11 } ] + }, + { + filename: 'test.vue', + code: '', + options: [ + { + element: ['a', 'RouterLink'], + message: 'Prefer the use of component' + } + ], + errors: [ + { + message: 'Prefer the use of component', + line: 1, + column: 11 + }, + { + message: 'Prefer the use of component', + line: 1, + column: 18 + } + ] + }, + // SVG + { + filename: 'test.vue', + code: '', + options: ['circle'], + errors: [ + { + message: 'Unexpected use of forbidden element circle.', + line: 1, + column: 16 + } + ] + }, + { + filename: 'test.vue', + code: '', + options: [ + { element: ['rect', 'path'], message: 'Use simplified shapes instead' } + ], + errors: [ + { + message: 'Use simplified shapes instead', + line: 1, + column: 16 + }, + { + message: 'Use simplified shapes instead', + line: 1, + column: 54 + } + ] + }, + // Math + { + filename: 'test.vue', + code: '12', + options: ['mfrac'], + errors: [ + { + message: 'Unexpected use of forbidden element mfrac.', + line: 1, + column: 17 + } + ] + }, + { + filename: 'test.vue', + code: 'x=5', + options: [{ element: 'mo', message: 'Avoid using operators directly' }], + errors: [ + { + message: 'Avoid using operators directly', + line: 1, + column: 27 + } + ] } ] }) diff --git a/tests/lib/rules/require-explicit-slots.js b/tests/lib/rules/require-explicit-slots.js index f99614119..afee739a6 100644 --- a/tests/lib/rules/require-explicit-slots.js +++ b/tests/lib/rules/require-explicit-slots.js @@ -6,6 +6,9 @@ const RuleTester = require('../../eslint-compat').RuleTester const rule = require('../../../lib/rules/require-explicit-slots') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const tester = new RuleTester({ languageOptions: { @@ -276,6 +279,21 @@ tester.run('require-explicit-slots', rule, { }) ` }, + { + filename: 'test.vue', + code: ` + + + + + + + `, + ...getTypeScriptFixtureTestOptions() + }, { filename: 'test.vue', code: ` @@ -656,6 +674,28 @@ tester.run('require-explicit-slots', rule, { } ] }, + { + filename: 'test.vue', + code: ` + + + + + + + `, + errors: [ + { + message: 'Slots must be explicitly defined.', + line: 5, + column: 11 + } + ], + ...getTypeScriptFixtureTestOptions() + }, { // ignore attribute binding except string literal filename: 'test.vue', diff --git a/tests/lib/utils/regexp.js b/tests/lib/utils/regexp.js index 830fa2a11..e27a0596f 100644 --- a/tests/lib/utils/regexp.js +++ b/tests/lib/utils/regexp.js @@ -1,6 +1,10 @@ 'use strict' -const { escape, toRegExp } = require('../../../lib/utils/regexp') +const { + escape, + toRegExp, + toRegExpGroupMatcher +} = require('../../../lib/utils/regexp') const assert = require('assert') const ESCAPED = '\\^\\$\\.\\*\\+\\?\\(\\)\\[\\]\\{\\}\\|\\\\' @@ -35,4 +39,120 @@ describe('toRegExp()', () => { assert.deepEqual(toRegExp(`${/^bar/i}`), /^bar/i) assert.deepEqual(toRegExp(`${/[\sA-Z]+/u}`), /[\sA-Z]+/u) }) + + it('should handle simple patterns', () => { + const regex = toRegExp('foo') + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), false) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), false) + }) + + it('should handle simple patterns with added flags', () => { + const regex = toRegExp('foo', { add: 'i' }) + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), false) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), true) + }) + + it('should handle regexp patterns', () => { + const regex = toRegExp('/^foo/') + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), true) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), false) + }) + + it('should handle regexp patterns with attached flags', () => { + const regex = toRegExp('/^foo/i') + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), true) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), true) + }) + + it('should handle regexp patterns with added flags', () => { + const regex = toRegExp('/^foo/', { add: 'i' }) + assert.deepEqual(regex, /^foo/i) + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), true) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), true) + }) + + it('should handle regexp patterns with removed flags', () => { + const regex = toRegExp('/^foo/i', { remove: 'i' }) + assert.deepEqual(regex, /^foo/) + assert.strictEqual(regex.test('foo'), true) + assert.strictEqual(regex.test('bar'), false) + assert.strictEqual(regex.test('foobar'), true) + assert.strictEqual(regex.test('afoo'), false) + assert.strictEqual(regex.test('afoobar'), false) + assert.strictEqual(regex.test('Foo'), false) + }) +}) + +describe('toRegExpGroupMatcher()', () => { + it('should return a function missing input', () => { + const groupMatcher = toRegExpGroupMatcher() + assert.strictEqual(groupMatcher(''), false) + assert.strictEqual(groupMatcher('foo'), false) + assert.strictEqual(groupMatcher('bar'), false) + }) + + it('should return a function for empty array', () => { + const groupMatcher = toRegExpGroupMatcher([]) + assert.strictEqual(groupMatcher(''), false) + assert.strictEqual(groupMatcher('foo'), false) + assert.strictEqual(groupMatcher('bar'), false) + }) + + it('should return a function for single simple pattern', () => { + const groupMatcher = toRegExpGroupMatcher(['foo']) + assert.strictEqual(groupMatcher(''), false) + assert.strictEqual(groupMatcher('foo'), true) + assert.strictEqual(groupMatcher('foo', 'early'), true) + assert.strictEqual(groupMatcher('late', 'matches', 'foo'), true) + assert.strictEqual(groupMatcher('foobar'), false) + assert.strictEqual(groupMatcher('afoo', 'fooa', 'afooa', 'bar'), false) + }) + + it('should return a function for multiple simple patterns', () => { + const groupMatcher = toRegExpGroupMatcher(['foo', 'bar']) + assert.strictEqual(groupMatcher('foo'), true) + assert.strictEqual(groupMatcher('bar', 'early'), true) + assert.strictEqual(groupMatcher('late', 'matches', 'foo'), true) + assert.strictEqual(groupMatcher('foobar'), false) + assert.strictEqual(groupMatcher('afoo', 'fooa', 'afooa'), false) + }) + + it('should return a function for single regexp pattern', () => { + const groupMatcher = toRegExpGroupMatcher(['/^foo/g']) + assert.strictEqual(groupMatcher(''), false) + assert.strictEqual(groupMatcher('foo'), true) + assert.strictEqual(groupMatcher('fooa', 'early'), true) + assert.strictEqual(groupMatcher('late', 'matches', 'fooa'), true) + assert.strictEqual(groupMatcher('barfoo'), false) + assert.strictEqual(groupMatcher('afoo', 'afooa', 'bar'), false) + }) + + it('should return a function for multiple regexp patterns', () => { + const groupMatcher = toRegExpGroupMatcher(['/^foo/', '/bar$/gi']) + assert.strictEqual(groupMatcher('foo'), true) + assert.strictEqual(groupMatcher('Bar', 'early'), true) + assert.strictEqual(groupMatcher('late', 'matches', 'foo'), true) + assert.strictEqual(groupMatcher('barfoo'), false) + assert.strictEqual(groupMatcher('afoo', 'afooa', 'bara'), false) + }) }) diff --git a/tools/generate-typegen.mjs b/tools/generate-typegen.mjs new file mode 100644 index 000000000..5bcb325a5 --- /dev/null +++ b/tools/generate-typegen.mjs @@ -0,0 +1,9 @@ +import fs from 'node:fs/promises' +import { pluginsToRulesDTS } from 'eslint-typegen/core' +import plugin from '../lib/index.js' + +const dts = await pluginsToRulesDTS({ + vue: plugin +}) + +await fs.writeFile('lib/eslint-typegen.d.ts', dts)