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/.github/workflows/check-for-resources-update.yml b/.github/workflows/check-for-resources-update.yml
index 613125fc3..31f881b1d 100644
--- a/.github/workflows/check-for-resources-update.yml
+++ b/.github/workflows/check-for-resources-update.yml
@@ -5,12 +5,13 @@ on:
- cron: 0 0 * * 0 # At 00:00 on Sunday, see https://crontab.guru/#0_0_*_*_0
permissions:
- contents: write
- pull-requests: write
+ contents: write
+ pull-requests: write
jobs:
check-for-resources-update:
runs-on: ubuntu-latest
+ if: ${{ github.repository == 'vuejs/eslint-plugin-vue' }}
steps:
- name: Checkout
uses: actions/checkout@v4
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/README.md b/README.md
index 95e0c08be..bdeb5e167 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://npmjs.org/package/eslint-plugin-vue)
[](https://npmjs.org/package/eslint-plugin-vue)
-[](https://circleci.com/gh/vuejs/eslint-plugin-vue)
+[](https://github.com/vuejs/eslint-plugin-vue/actions/workflows/CI.yml)
[](https://github.com/vuejs/eslint-plugin-vue/blob/master/LICENSE)
> Official ESLint plugin for Vue.js
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/docs/user-guide/index.md b/docs/user-guide/index.md
index fcd7ff45c..103471f61 100644
--- a/docs/user-guide/index.md
+++ b/docs/user-guide/index.md
@@ -11,7 +11,7 @@ npm install --save-dev eslint eslint-plugin-vue
Via [yarn](https://yarnpkg.com/):
```bash
-yarn add -D eslint eslint-plugin-vue globals
+yarn add -D eslint eslint-plugin-vue vue-eslint-parser globals
```
::: tip Requirements
@@ -166,8 +166,8 @@ module.exports = {
extends: [
// add more generic rulesets here, such as:
// 'eslint:recommended',
- 'plugin:vue/vue3-recommended',
- // 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.
+ 'plugin:vue/recommended',
+ // 'plugin:vue/vue2-recommended' // Use this if you are using Vue.js 2.x.
],
rules: {
// override/add rules settings here, such as:
@@ -185,13 +185,13 @@ You can use the following configs by adding them to `extends`.
- `"plugin:vue/base"` ... Settings and rules to enable correct ESLint parsing.
- Configurations for using Vue.js 3.x:
- - `"plugin:vue/vue3-essential"` ... `base`, plus rules to prevent errors or unintended behavior.
- - `"plugin:vue/vue3-strongly-recommended"` ... Above, plus rules to considerably improve code readability and/or dev experience.
- - `"plugin:vue/vue3-recommended"` ... Above, plus rules to enforce subjective community defaults to ensure consistency.
-- Configurations for using Vue.js 2.x:
- `"plugin:vue/essential"` ... `base`, plus rules to prevent errors or unintended behavior.
- `"plugin:vue/strongly-recommended"` ... Above, plus rules to considerably improve code readability and/or dev experience.
- - `"plugin:vue/recommended"` ... Above, plus rules to enforce subjective community defaults to ensure consistency
+ - `"plugin:vue/recommended"` ... Above, plus rules to enforce subjective community defaults to ensure consistency.
+- Configurations for using Vue.js 2.x:
+ - `"plugin:vue/vue2-essential"` ... `base`, plus rules to prevent errors or unintended behavior.
+ - `"plugin:vue/vue2-strongly-recommended"` ... Above, plus rules to considerably improve code readability and/or dev experience.
+ - `"plugin:vue/vue2-recommended"` ... Above, plus rules to enforce subjective community defaults to ensure consistency.
:::warning Reporting rules
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).
@@ -264,7 +264,7 @@ Full example:
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
- "plugin:vue/vue3-recommended"
+ "plugin:vue/recommended"
],
"parser": "vue-eslint-parser",
"parserOptions": {
@@ -436,7 +436,7 @@ Most `eslint-plugin-vue` rules require `vue-eslint-parser` to check ``
Make sure you have one of the following settings in your **.eslintrc**:
-- `"extends": ["plugin:vue/vue3-recommended"]`
+- `"extends": ["plugin:vue/recommended"]`
- `"extends": ["plugin:vue/base"]`
If you already use another parser (e.g. `"parser": "@typescript-eslint/parser"`), please move it into `parserOptions`, so it doesn't collide with the `vue-eslint-parser` used by this plugin's configuration:
@@ -482,7 +482,7 @@ module.exports = {
// ...
// 'eslint:recommended',
// ...
- 'plugin:vue/vue3-recommended',
+ 'plugin:vue/recommended',
// ...
'prettier'
// Make sure "prettier" is the last element in this list.
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 19bbc8a9d..b6d658852 100644
--- a/lib/index.d.ts
+++ b/lib/index.d.ts
@@ -1,3 +1,4 @@
+///
import type { Linter } from 'eslint'
declare const vue: {
@@ -9,9 +10,9 @@ declare const vue: {
'vue2-strongly-recommended': Linter.LegacyConfig
'vue2-recommended': Linter.LegacyConfig
- 'vue3-essential': Linter.LegacyConfig
- 'vue3-strongly-recommended': Linter.LegacyConfig
- 'vue3-recommended': Linter.LegacyConfig
+ essential: Linter.LegacyConfig
+ 'strongly-recommended': Linter.LegacyConfig
+ recommended: Linter.LegacyConfig
'flat/base': Linter.FlatConfig[]
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/html-self-closing.js b/lib/rules/html-self-closing.js
index 5b98d8cf7..c31c9ab70 100644
--- a/lib/rules/html-self-closing.js
+++ b/lib/rules/html-self-closing.js
@@ -150,8 +150,7 @@ module.exports = {
isEmpty(node, sourceCode)
) {
context.report({
- node,
- loc: node.loc,
+ node: node.endTag || node,
messageId: 'requireSelfClosing',
data: {
elementType: ELEMENT_TYPE_MESSAGES[elementType],
@@ -175,7 +174,13 @@ module.exports = {
if (mode === 'never' && node.startTag.selfClosing) {
context.report({
node,
- loc: node.loc,
+ loc: {
+ start: {
+ line: node.loc.end.line,
+ column: node.loc.end.column - 2
+ },
+ end: node.loc.end
+ },
messageId: 'disallowSelfClosing',
data: {
elementType: ELEMENT_TYPE_MESSAGES[elementType],
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-dupe-keys.js b/lib/rules/no-dupe-keys.js
index 01b85d9f5..ecfa787cf 100644
--- a/lib/rules/no-dupe-keys.js
+++ b/lib/rules/no-dupe-keys.js
@@ -58,6 +58,33 @@ function isInsideInitializer(node, references) {
)
}
+/**
+ * Collects all renamed props from a pattern
+ * @param {Pattern | null} pattern - The destructuring pattern
+ * @returns {Set} - Set of prop names that have been renamed
+ */
+function collectRenamedProps(pattern) {
+ const renamedProps = new Set()
+
+ if (!pattern || pattern.type !== 'ObjectPattern') {
+ return renamedProps
+ }
+
+ for (const prop of pattern.properties) {
+ if (prop.type !== 'Property') continue
+
+ if (
+ prop.key.type === 'Identifier' &&
+ prop.value.type === 'Identifier' &&
+ prop.key.name !== prop.value.name
+ ) {
+ renamedProps.add(prop.key.name)
+ }
+ }
+
+ return renamedProps
+}
+
module.exports = {
meta: {
type: 'problem',
@@ -115,9 +142,15 @@ module.exports = {
node
]
+ const renamedProps = collectRenamedProps(propsNode)
+
for (const prop of props) {
if (!prop.propName) continue
+ if (renamedProps.has(prop.propName)) {
+ continue
+ }
+
const variable = findVariable(
utils.getScope(context, node),
prop.propName
diff --git a/lib/rules/no-duplicate-attr-inheritance.js b/lib/rules/no-duplicate-attr-inheritance.js
index 1f0fb0fd3..31aef7e44 100644
--- a/lib/rules/no-duplicate-attr-inheritance.js
+++ b/lib/rules/no-duplicate-attr-inheritance.js
@@ -63,7 +63,7 @@ module.exports = {
const options = context.options[0] || {}
const checkMultiRootNodes = options.checkMultiRootNodes === true
- /** @type {string | number | boolean | RegExp | BigInt | null} */
+ /** @type {Literal['value']} */
let inheritsAttrs = true
/** @type {VReference[]} */
const attrsRefs = []
diff --git a/lib/rules/no-export-in-script-setup.js b/lib/rules/no-export-in-script-setup.js
index 66286375a..98d41ae38 100644
--- a/lib/rules/no-export-in-script-setup.js
+++ b/lib/rules/no-export-in-script-setup.js
@@ -28,8 +28,11 @@ module.exports = {
},
/** @param {RuleContext} context */
create(context) {
- /** @param {ExportAllDeclaration | ExportDefaultDeclaration | ExportNamedDeclaration} node */
- function verify(node) {
+ /**
+ * @param {ExportAllDeclaration | ExportDefaultDeclaration | ExportNamedDeclaration} node
+ * @param {SourceLocation} loc
+ */
+ function verify(node, loc) {
const tsNode =
/** @type {TSESTreeExportAllDeclaration | TSESTreeExportDefaultDeclaration | TSESTreeExportNamedDeclaration} */ (
node
@@ -46,14 +49,24 @@ module.exports = {
}
context.report({
node,
+ loc,
messageId: 'forbidden'
})
}
return utils.defineScriptSetupVisitor(context, {
- ExportAllDeclaration: verify,
- ExportDefaultDeclaration: verify,
- ExportNamedDeclaration: verify
+ ExportAllDeclaration: (node) => verify(node, node.loc),
+ ExportDefaultDeclaration: (node) => verify(node, node.loc),
+ ExportNamedDeclaration: (node) => {
+ // export let foo = 'foo', export class Foo {}, export function foo() {}
+ if (node.declaration) {
+ verify(node, context.getSourceCode().getFirstToken(node).loc)
+ }
+ // export { foo }, export { foo } from 'bar'
+ else {
+ verify(node, node.loc)
+ }
+ }
})
}
}
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-ref-as-operand.js b/lib/rules/no-ref-as-operand.js
index db92f19e4..b1a9e12a8 100644
--- a/lib/rules/no-ref-as-operand.js
+++ b/lib/rules/no-ref-as-operand.js
@@ -233,7 +233,7 @@ module.exports = {
},
// `${refValue}`
/** @param {Identifier} node */
- 'TemplateLiteral>Identifier'(node) {
+ ':not(TaggedTemplateExpression)>TemplateLiteral>Identifier'(node) {
reportIfRefWrapped(node)
},
// refValue.x
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/no-unused-refs.js b/lib/rules/no-unused-refs.js
index 4896ce25a..6c5407e02 100644
--- a/lib/rules/no-unused-refs.js
+++ b/lib/rules/no-unused-refs.js
@@ -237,8 +237,10 @@ module.exports = {
CallExpression(callExpression) {
const firstArgument = callExpression.arguments[0]
if (
+ callExpression.callee.type !== 'Identifier' ||
callExpression.callee.name !== 'useTemplateRef' ||
- !firstArgument
+ !firstArgument ||
+ !utils.isStringLiteral(firstArgument)
) {
return
}
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/require-default-prop.js b/lib/rules/require-default-prop.js
index b5d2564e8..acceacf08 100644
--- a/lib/rules/require-default-prop.js
+++ b/lib/rules/require-default-prop.js
@@ -200,8 +200,8 @@ module.exports = {
processProps(props, (prop) => {
if (prop.type === 'type') {
- if (!hasWithDefaults) {
- // If don't use withDefaults(), exclude it from the report.
+ if (!hasWithDefaults && !isUsingPropsDestructure) {
+ // If don't use withDefaults() and props destructure, exclude it from the report.
return true
}
if (defaultsByWithDefaults[prop.propName]) {
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/html-elements.json b/lib/utils/html-elements.json
index 829b6f841..ff9cdf313 100644
--- a/lib/utils/html-elements.json
+++ b/lib/utils/html-elements.json
@@ -74,7 +74,6 @@
"output",
"p",
"picture",
- "portal",
"pre",
"progress",
"q",
@@ -87,6 +86,7 @@
"search",
"section",
"select",
+ "selectedcontent",
"slot",
"small",
"source",
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 8d0dfa80d..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)
}
@@ -1380,7 +1380,7 @@ module.exports = {
* @param {any[]} args
*/
function callVisitor(key, node, ...args) {
- if (visitor[key] && inScriptSetup(node)) {
+ if (visitor[key] && (node.type === 'Program' || inScriptSetup(node))) {
// @ts-expect-error
visitor[key](node, ...args)
}
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/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 475c24e60..a473deb46 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-vue",
- "version": "10.0.0",
+ "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,20 +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/eslint-visitor-keys": "^3.3.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",
@@ -88,7 +99,9 @@
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unicorn": "^56.0.0",
"eslint-plugin-vue": "file:.",
- "espree": "^9.6.1",
+ "eslint-typegen": "^2.2.0",
+ "eslint-visitor-keys": "^4.2.0",
+ "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-dupe-keys.js b/tests/lib/rules/no-dupe-keys.js
index 124442ec2..2df95908c 100644
--- a/tests/lib/rules/no-dupe-keys.js
+++ b/tests/lib/rules/no-dupe-keys.js
@@ -466,7 +466,7 @@ ruleTester.run('no-dupe-keys', rule, {
{
filename: 'test.vue',
code: `
-
+
`,
@@ -475,7 +475,7 @@ ruleTester.run('no-dupe-keys', rule, {
{
filename: 'test.vue',
code: `
-
+
`,
@@ -500,6 +500,17 @@ ruleTester.run('no-dupe-keys', rule, {
parser: require('vue-eslint-parser'),
parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
}
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: require('vue-eslint-parser') }
}
],
@@ -1105,6 +1116,24 @@ ruleTester.run('no-dupe-keys', rule, {
line: 5
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: require('vue-eslint-parser') },
+ errors: [
+ {
+ message:
+ "Duplicate key 'bar'. May cause name collision in script or template tag.",
+ line: 5
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/no-export-in-script-setup.js b/tests/lib/rules/no-export-in-script-setup.js
index 8703dc74a..bf24b8682 100644
--- a/tests/lib/rules/no-export-in-script-setup.js
+++ b/tests/lib/rules/no-export-in-script-setup.js
@@ -92,20 +92,62 @@ ruleTester.run('no-export-in-script-setup', rule, {
export * from 'foo'
export default {}
export class A {}
+ export const test = '123'
+ export function foo() {}
+ const a = 1
+ export { a }
+ export { fao } from 'bar'
`,
errors: [
{
message: '`
`,
+ `
+
+ `,
+ `
+
+ `,
`
`
+ },
+ {
+ filename: 'multiple-scripts-setup-first.vue',
+ code: `
+
+
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'multiple-scripts-setup-last.vue',
+ code: `
+
+
+
+
+
+
+
+ `
}
],
invalid: [
@@ -420,6 +454,60 @@ tester.run('prefer-use-template-ref', rule, {
column: 28
}
]
+ },
+ {
+ filename: 'multiple-scripts-setup-first.vue',
+ code: `
+
+
+
+
+
+
+
+ `,
+ errors: [
+ {
+ messageId: 'preferUseTemplateRef',
+ data: {
+ name: 'ref'
+ },
+ line: 8,
+ column: 20
+ }
+ ]
+ },
+ {
+ filename: 'multiple-scripts-setup-last.vue',
+ code: `
+
+
+
+
+
+
+
+ `,
+ errors: [
+ {
+ messageId: 'preferUseTemplateRef',
+ data: {
+ name: 'ref'
+ },
+ line: 12,
+ column: 20
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js
index e352eddf3..43160dfda 100644
--- a/tests/lib/rules/require-default-prop.js
+++ b/tests/lib/rules/require-default-prop.js
@@ -388,6 +388,31 @@ ruleTester.run('require-default-prop', rule, {
parser: require('vue-eslint-parser'),
...languageOptions
}
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ...languageOptions
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ...languageOptions,
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ }
}
],
@@ -700,6 +725,26 @@ ruleTester.run('require-default-prop', rule, {
line: 3
}
]
+ },
+ {
+ // https://github.com/vuejs/eslint-plugin-vue/issues/2725
+ filename: 'type-with-props-destructure.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ...languageOptions,
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ },
+ errors: [
+ {
+ message: "Prop 'foo' requires default value to be set.",
+ line: 3
+ }
+ ]
}
]
})
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)