This is a documentation for version {{docVersion}}. Also, this documentation may contain content that has not yet been released.
- To check version 6.2.2go here.
- To check previous releases go here.
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/README.md b/docs/README.md
deleted file mode 100644
index 953b51b5b..000000000
--- a/docs/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-sidebarDepth: 0
----
-
-# Introduction
-
-Official ESLint plugin for Vue.js.
-
-This plugin allows us to check the `` and `
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+Specify the block name for the key of the option object.
+You can use the object as a value and use the following properties:
+
+- `lang` ... Specifies the available value for the `lang` attribute of the block. If multiple languages are available, specify them as an array. If you do not specify it, will disallow any language.
+- `allowNoLang` ... If `true`, allows the `lang` attribute not to be specified (allows the use of the default language of block).
+
+::: warning Note
+If the default language is specified for `lang` option of ``, `
+```
+
+
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+...
+```
+
+
+
+### `{ "order": ["template", "script", "style"] }`
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+
+
+```vue
+
+
+...
+
+```
+
+
+
+### `{ "order": ["docs", "template", "script", "style"] }`
+
+
+
+```vue
+
+ documentation
+...
+
+
+```
+
+
+
+
+
+```vue
+
+...
+
+ documentation
+
+```
+
+
+
+### `{ 'order': ['template', 'script:not([setup])', 'script[setup]'] }`
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+### `{ 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }`
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+### `{ 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }`
+
+
+
+```vue
+
+...
+/* ... */
+/* ... */
+```
+
+
+
+
+
+```vue
+
+...
+/* ... */
+/* ... */
+```
+
+
+
+## :books: Further Reading
+
+- [Style guide - Single-file component top-level element order](https://vuejs.org/style-guide/rules-recommended.html#single-file-component-top-level-element-order)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.16.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/block-order.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/block-order.js)
diff --git a/docs/rules/block-spacing.md b/docs/rules/block-spacing.md
index 16d5b3f51..e9f16b8d0 100644
--- a/docs/rules/block-spacing.md
+++ b/docs/rules/block-spacing.md
@@ -2,24 +2,38 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/block-spacing
-description: disallow or enforce spaces inside of blocks after opening block and before closing block
+description: Disallow or enforce spaces inside of blocks after opening block and before closing block in ``
+since: v5.2.0
---
+
# vue/block-spacing
-> disallow or enforce spaces inside of blocks after opening block and before closing block
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> Disallow or enforce spaces inside of blocks after opening block and before closing block 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.
+
+This rule is the same rule as [@stylistic/block-spacing] rule but it applies to the expressions in ``.
-This rule is the same rule as core [block-spacing] rule but it applies to the expressions in ``.
+This rule extends the rule that [@stylistic/eslint-plugin] has, but if [@stylistic/eslint-plugin] is not installed, this rule extracts and extends the same rule from ESLint core.
+However, if neither is found, the rule cannot be used.
-## :books: Further reading
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
+## :books: Further Reading
+
+- [@stylistic/block-spacing]
- [block-spacing]
+[@stylistic/block-spacing]: https://eslint.style/rules/default/block-spacing
[block-spacing]: https://eslint.org/docs/rules/block-spacing
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v5.2.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/block-spacing.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/block-spacing.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/block-spacing)
+Taken with ❤️ [from ESLint Stylistic](https://eslint.style/rules/ts/block-spacing)
diff --git a/docs/rules/block-tag-newline.md b/docs/rules/block-tag-newline.md
new file mode 100644
index 000000000..8e336b284
--- /dev/null
+++ b/docs/rules/block-tag-newline.md
@@ -0,0 +1,168 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/block-tag-newline
+description: enforce line breaks after opening and before closing block-level tags
+since: v7.1.0
+---
+
+# vue/block-tag-newline
+
+> enforce line breaks after opening and before closing block-level tags
+
+- :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.
+
+## :book: Rule Details
+
+This rule enforces a line break (or no line break) after opening and before closing block tags.
+
+
+
+```vue
+
+
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/block-tag-newline": ["error", {
+ "singleline": "always" | "never" | "consistent" | "ignore",
+ "multiline": "always" | "never" | "consistent" | "ignore",
+ "maxEmptyLines": 0,
+ "blocks": {
+ "template": {
+ "singleline": "always" | "never" | "consistent" | "ignore",
+ "multiline": "always" | "never" | "consistent" | "ignore",
+ "maxEmptyLines": 0,
+ },
+ "script": {
+ "singleline": "always" | "never" | "consistent" | "ignore",
+ "multiline": "always" | "never" | "consistent" | "ignore",
+ "maxEmptyLines": 0,
+ },
+ "my-block": {
+ "singleline": "always" | "never" | "consistent" | "ignore",
+ "multiline": "always" | "never" | "consistent" | "ignore",
+ "maxEmptyLines": 0,
+ }
+ }
+ }]
+}
+```
+
+- `singleline` ... the configuration for single-line blocks.
+ - `"consistent"` ... (default) requires consistent usage of line breaks for each pair of tags. It reports an error if one tag in the pair has a linebreak inside it and the other tag does not.
+ - `"always"` ... require one line break after opening and before closing block tags.
+ - `"never"` ... disallow line breaks after opening and before closing block tags.
+- `multiline` ... the configuration for multi-line blocks.
+ - `"consistent"` ... requires consistent usage of line breaks for each pair of tags. It reports an error if one tag in the pair has a linebreak inside it and the other tag does not.
+ - `"always"` ... (default) require one line break after opening and before closing block tags.
+ - `"never"` ... disallow line breaks after opening and before closing block tags.
+- `maxEmptyLines` ... specifies the maximum number of empty lines allowed. default 0.
+- `blocks` ... specifies for each block name.
+
+### `{ "singleline": "never", "multiline": "always" }`
+
+
+
+```vue
+
+
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+
+
+
+```
+
+
+
+### `{ "singleline": "always", "multiline": "always", "maxEmptyLines": 1 }`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.1.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/block-tag-newline.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/block-tag-newline.js)
diff --git a/docs/rules/brace-style.md b/docs/rules/brace-style.md
index a0466e6fe..1727f42fb 100644
--- a/docs/rules/brace-style.md
+++ b/docs/rules/brace-style.md
@@ -2,24 +2,38 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/brace-style
-description: enforce consistent brace style for blocks
+description: Enforce consistent brace style for blocks in ``
+since: v5.2.0
---
+
# vue/brace-style
-> enforce consistent brace style for blocks
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> Enforce consistent brace style for blocks 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.
+
+This rule is the same rule as [@stylistic/brace-style] rule but it applies to the expressions in ``.
-This rule is the same rule as core [brace-style] rule but it applies to the expressions in ``.
+This rule extends the rule that [@stylistic/eslint-plugin] has, but if [@stylistic/eslint-plugin] is not installed, this rule extracts and extends the same rule from ESLint core.
+However, if neither is found, the rule cannot be used.
-## :books: Further reading
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
+## :books: Further Reading
+
+- [@stylistic/brace-style]
- [brace-style]
+[@stylistic/brace-style]: https://eslint.style/rules/default/brace-style
[brace-style]: https://eslint.org/docs/rules/brace-style
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v5.2.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/brace-style.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/brace-style.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/brace-style)
+Taken with ❤️ [from ESLint Stylistic](https://eslint.style/rules/ts/brace-style)
diff --git a/docs/rules/camelcase.md b/docs/rules/camelcase.md
index e0e6d15f5..6ed97876a 100644
--- a/docs/rules/camelcase.md
+++ b/docs/rules/camelcase.md
@@ -2,22 +2,29 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/camelcase
-description: enforce camelcase naming convention
+description: Enforce camelcase naming convention in ``
+since: v5.2.0
---
+
# vue/camelcase
-> enforce camelcase naming convention
+
+> Enforce camelcase naming convention in ``
This rule is the same rule as core [camelcase] rule but it applies to the expressions in ``.
-## :books: Further reading
+## :books: Further Reading
- [camelcase]
[camelcase]: https://eslint.org/docs/rules/camelcase
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v5.2.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/camelcase.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/camelcase.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/camelcase)
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/latest/rules/camelcase)
diff --git a/docs/rules/comma-dangle.md b/docs/rules/comma-dangle.md
index 960776664..aceaa6dc1 100644
--- a/docs/rules/comma-dangle.md
+++ b/docs/rules/comma-dangle.md
@@ -2,24 +2,38 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/comma-dangle
-description: require or disallow trailing commas
+description: Require or disallow trailing commas in ``
+since: v5.2.0
---
+
# vue/comma-dangle
-> require or disallow trailing commas
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> Require or disallow trailing commas 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.
+
+This rule is the same rule as [@stylistic/comma-dangle] rule but it applies to the expressions in ``.
-This rule is the same rule as core [comma-dangle] rule but it applies to the expressions in ``.
+This rule extends the rule that [@stylistic/eslint-plugin] has, but if [@stylistic/eslint-plugin] is not installed, this rule extracts and extends the same rule from ESLint core.
+However, if neither is found, the rule cannot be used.
-## :books: Further reading
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
+## :books: Further Reading
+
+- [@stylistic/comma-dangle]
- [comma-dangle]
+[@stylistic/comma-dangle]: https://eslint.style/rules/default/comma-dangle
[comma-dangle]: https://eslint.org/docs/rules/comma-dangle
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v5.2.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/comma-dangle.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/comma-dangle.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/comma-dangle)
+Taken with ❤️ [from ESLint Stylistic](https://eslint.style/rules/ts/comma-dangle)
diff --git a/docs/rules/comma-spacing.md b/docs/rules/comma-spacing.md
index 9f2b5e9b2..b585fadf7 100644
--- a/docs/rules/comma-spacing.md
+++ b/docs/rules/comma-spacing.md
@@ -2,24 +2,38 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/comma-spacing
-description: enforce consistent spacing before and after commas
+description: Enforce consistent spacing before and after commas in ``
+since: v7.0.0
---
+
# vue/comma-spacing
-> enforce consistent spacing before and after commas
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> Enforce consistent spacing before and after commas 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.
+
+This rule is the same rule as [@stylistic/comma-spacing] rule but it applies to the expressions in ``.
-This rule is the same rule as core [comma-spacing] rule but it applies to the expressions in ``.
+This rule extends the rule that [@stylistic/eslint-plugin] has, but if [@stylistic/eslint-plugin] is not installed, this rule extracts and extends the same rule from ESLint core.
+However, if neither is found, the rule cannot be used.
-## :books: Further reading
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
+## :books: Further Reading
+
+- [@stylistic/comma-spacing]
- [comma-spacing]
+[@stylistic/comma-spacing]: https://eslint.style/rules/default/comma-spacing
[comma-spacing]: https://eslint.org/docs/rules/comma-spacing
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/comma-spacing.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/comma-spacing.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/comma-spacing)
+Taken with ❤️ [from ESLint Stylistic](https://eslint.style/rules/ts/comma-spacing)
diff --git a/docs/rules/comma-style.md b/docs/rules/comma-style.md
index 6b393a56a..9e08fdaaf 100644
--- a/docs/rules/comma-style.md
+++ b/docs/rules/comma-style.md
@@ -2,24 +2,38 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/comma-style
-description: enforce consistent comma style
+description: Enforce consistent comma style in ``
+since: v7.0.0
---
+
# vue/comma-style
-> enforce consistent comma style
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> Enforce consistent comma style 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.
+
+This rule is the same rule as [@stylistic/comma-style] rule but it applies to the expressions in ``.
-This rule is the same rule as core [comma-style] rule but it applies to the expressions in ``.
+This rule extends the rule that [@stylistic/eslint-plugin] has, but if [@stylistic/eslint-plugin] is not installed, this rule extracts and extends the same rule from ESLint core.
+However, if neither is found, the rule cannot be used.
-## :books: Further reading
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
+## :books: Further Reading
+
+- [@stylistic/comma-style]
- [comma-style]
+[@stylistic/comma-style]: https://eslint.style/rules/default/comma-style
[comma-style]: https://eslint.org/docs/rules/comma-style
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/comma-style.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/comma-style.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/comma-style)
+Taken with ❤️ [from ESLint Stylistic](https://eslint.style/rules/js/comma-style)
diff --git a/docs/rules/comment-directive.md b/docs/rules/comment-directive.md
index 01ad51826..294dcc15c 100644
--- a/docs/rules/comment-directive.md
+++ b/docs/rules/comment-directive.md
@@ -3,11 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/comment-directive
description: support comment-directives in ``
+since: v4.1.0
---
+
# vue/comment-directive
+
> support comment-directives in ``
-- :gear: This rule is included in all of `"plugin:vue/base"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-essential"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/base"`, `*.configs["flat/base"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-recommended"`, `*.configs["flat/vue2-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
Sole purpose of this rule is to provide `eslint-disable` functionality in the `` and in the block level.
It supports usage of the following comments:
@@ -121,12 +124,16 @@ The `eslint-disable`-like comments can include descriptions to explain why the c
Unused reports cannot be suppressed with `eslint-disable` HTML comments.
:::
-## :books: Further reading
+## :books: Further Reading
- [Disabling rules with inline comments]
[Disabling rules with inline comments]: https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v4.1.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/comment-directive.js)
diff --git a/docs/rules/component-api-style.md b/docs/rules/component-api-style.md
new file mode 100644
index 000000000..7a81176c9
--- /dev/null
+++ b/docs/rules/component-api-style.md
@@ -0,0 +1,149 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/component-api-style
+description: enforce component API style
+since: v7.18.0
+---
+
+# vue/component-api-style
+
+> enforce component API style
+
+## :book: Rule Details
+
+This rule aims to make the API style you use to define Vue components consistent in your project.
+
+For example, if you want to allow only `
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/component-api-style": ["error",
+ ["script-setup", "composition"] // "script-setup", "composition", "composition-vue2", or "options"
+ ]
+}
+```
+
+- Array options ... Defines the API styles you want to allow. Default is `["script-setup", "composition"]`. You can use the following values.
+ - `"script-setup"` ... If set, allows [`
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.18.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/component-api-style.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/component-api-style.js)
diff --git a/docs/rules/component-definition-name-casing.md b/docs/rules/component-definition-name-casing.md
index 44d543f64..18c625970 100644
--- a/docs/rules/component-definition-name-casing.md
+++ b/docs/rules/component-definition-name-casing.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/component-definition-name-casing
description: enforce specific casing for component definition name
+since: v7.0.0
---
+
# vue/component-definition-name-casing
+
> enforce specific casing for component definition name
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
Define a style for component definition name casing for consistency purposes.
@@ -29,7 +32,7 @@ Default casing is set to `PascalCase`.
- `"PascalCase"` (default) ... enforce component definition names to pascal case.
- `"kebab-case"` ... enforce component definition names to kebab case.
-### `"PascalCase" (default)
+### `"PascalCase"` (default)
@@ -61,19 +64,15 @@ export default {
```js
/* ✓ GOOD */
-Vue.component('MyComponent', {
-
-})
+Vue.component('MyComponent', {})
/* ✗ BAD */
-Vue.component('my-component', {
-
-})
+Vue.component('my-component', {})
```
-### `"kebab-case"
+### `"kebab-case"`
@@ -105,21 +104,21 @@ export default {
```js
/* ✓ GOOD */
-Vue.component('my-component', {
-
-})
+Vue.component('my-component', {})
/* ✗ BAD */
-Vue.component('MyComponent', {
-
-})
+Vue.component('MyComponent', {})
```
-## :books: Further reading
+## :books: Further Reading
+
+- [Style guide - Component name casing in JS/JSX](https://vuejs.org/style-guide/rules-strongly-recommended.html#component-name-casing-in-js-jsx)
+
+## :rocket: Version
-- [Style guide - Component name casing in JS/JSX](https://vuejs.org/v2/style-guide/#Component-name-casing-in-JS-JSX-strongly-recommended)
+This rule was introduced in eslint-plugin-vue v7.0.0
## :mag: Implementation
diff --git a/docs/rules/component-name-in-template-casing.md b/docs/rules/component-name-in-template-casing.md
index ffe6c39cd..dd59d40e4 100644
--- a/docs/rules/component-name-in-template-casing.md
+++ b/docs/rules/component-name-in-template-casing.md
@@ -3,11 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/component-name-in-template-casing
description: enforce specific casing for the component naming style in template
+since: v5.0.0
---
+
# vue/component-name-in-template-casing
+
> enforce specific casing for the component naming style in template
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :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.
Define a style for the component name in template casing for consistency purposes.
@@ -31,6 +34,7 @@ This rule aims to warn the tag names other than the configured casing in Vue.js
- `registeredComponentsOnly` ... If `true`, only registered components (in PascalCase) are checked. If `false`, check all.
default `true`
- `ignores` (`string[]`) ... The element names to ignore. Sets the element name to allow. For example, custom elements or Vue components with special name. You can set the regexp by writing it like `"/^name/"`.
+- `globals` (`string[]`) ... Globally registered component names to check. For example, `RouterView` and `RouterLink` are globally registered by `vue-router` and can't be detected as registered in a SFC file.
### `"PascalCase", { registeredComponentsOnly: true }` (default)
@@ -40,7 +44,7 @@ This rule aims to warn the tag names other than the configured casing in Vue.js
-
+
@@ -58,7 +62,7 @@ export default {
components: {
CoolComponent,
'registered-in-kebab-case': VueComponent1,
- 'registeredInCamelCase': VueComponent2
+ registeredInCamelCase: VueComponent2
}
}
@@ -104,7 +108,7 @@ export default {
-
+
@@ -127,11 +131,11 @@ export default {
```vue
-
+
-
+
@@ -139,9 +143,29 @@ export default {
-## :books: Further reading
+### `"PascalCase", { globals: ["RouterView"] }`
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
+## :books: Further Reading
+
+- [Style guide - Component name casing in templates](https://vuejs.org/style-guide/rules-strongly-recommended.html#component-name-casing-in-templates)
+
+## :rocket: Version
-- [Style guide - Component name casing in templates](https://vuejs.org/v2/style-guide/#Component-name-casing-in-templates-strongly-recommended)
+This rule was introduced in eslint-plugin-vue v5.0.0
## :mag: Implementation
diff --git a/docs/rules/component-options-name-casing.md b/docs/rules/component-options-name-casing.md
new file mode 100644
index 000000000..469865478
--- /dev/null
+++ b/docs/rules/component-options-name-casing.md
@@ -0,0 +1,170 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/component-options-name-casing
+description: enforce the casing of component name in `components` options
+since: v8.2.0
+---
+
+# vue/component-options-name-casing
+
+> enforce the casing of component name in `components` options
+
+- :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).
+
+## :book: Rule Details
+
+This rule aims to enforce casing of the component names in `components` options.
+
+## :wrench: Options
+
+```json
+{
+ "vue/component-options-name-casing": ["error", "PascalCase" | "kebab-case" | "camelCase"]
+}
+```
+
+This rule has an option which can be one of these values:
+
+- `"PascalCase"` (default) ... enforce component names to pascal case.
+- `"kebab-case"` ... enforce component names to kebab case.
+- `"camelCase"` ... enforce component names to camel case.
+
+Please note that if you use kebab case in `components` options,
+you can **only** use kebab case in template;
+and if you use camel case in `components` options,
+you **can't** use pascal case in template.
+
+For demonstration, the code example is invalid:
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+### `"PascalCase"` (default)
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+### `"kebab-case"`
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+### `"camelCase"`
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v8.2.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/component-options-name-casing.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/component-options-name-casing.js)
diff --git a/docs/rules/component-tags-order.md b/docs/rules/component-tags-order.md
index 50d43b83c..a5037cdee 100644
--- a/docs/rules/component-tags-order.md
+++ b/docs/rules/component-tags-order.md
@@ -3,15 +3,18 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/component-tags-order
description: enforce order of component top-level elements
+since: v6.1.0
---
+
# vue/component-tags-order
+
> enforce order of component top-level elements
-- :gear: This rule is included in `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+- :no_entry: This rule was **removed** in eslint-plugin-vue v10.0.0 and replaced by [vue/block-order](block-order.md) rule.
## :book: Rule Details
-This rule warns about the order of the `
@@ -98,21 +101,97 @@ This rule warns about the order of the `
- documents
+ documentation
```
-## :books: Further reading
+### `{ 'order': ['template', 'script:not([setup])', 'script[setup]'] }`
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+### `{ 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }`
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+### `{ 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }`
+
+
+
+```vue
+
+...
+/* ... */
+/* ... */
+```
+
+
+
+
+
+```vue
+
+...
+/* ... */
+/* ... */
+```
+
+
+
+## :books: Further Reading
+
+- [Style guide - Single-file component top-level element order](https://vuejs.org/style-guide/rules-recommended.html#single-file-component-top-level-element-order)
+
+## :rocket: Version
-- [Style guide - Single-file component top-level element order](https://vuejs.org/v2/style-guide/#Single-file-component-top-level-element-order-recommended)
+This rule was introduced in eslint-plugin-vue v6.1.0
## :mag: Implementation
diff --git a/docs/rules/custom-event-name-casing.md b/docs/rules/custom-event-name-casing.md
index 52ddc7b5b..a57e0eb80 100644
--- a/docs/rules/custom-event-name-casing.md
+++ b/docs/rules/custom-event-name-casing.md
@@ -2,25 +2,83 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/custom-event-name-casing
-description: enforce custom event names always use "kebab-case"
+description: enforce specific casing for custom event name
+since: v7.0.0
---
+
# vue/custom-event-name-casing
-> enforce custom event names always use "kebab-case"
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+> enforce specific casing for custom event name
+
+Define a style for custom event name casing for consistency purposes.
## :book: Rule Details
-This rule enforces using kebab-case custom event names.
+This rule aims to warn the custom event names other than the configured casing. (Default is **camelCase**.)
+
+Vue 2 recommends using kebab-case for custom event names.
> Event names will never be used as variable or property names in JavaScript, so there’s no reason to use camelCase or PascalCase. Additionally, `v-on` event listeners inside DOM templates will be automatically transformed to lowercase (due to HTML’s case-insensitivity), so `v-on:myEvent` would become `v-on:myevent` – making `myEvent` impossible to listen to.
>
> For these reasons, we recommend you **always use kebab-case for event names**.
+See [Guide (for v2) - Custom Events] for more details.
+
+In Vue 3, using either camelCase or kebab-case for your custom event name does not limit its use in v-on. However, following JavaScript conventions, camelCase is more natural.
+
See [Guide - Custom Events] for more details.
+This rule enforces camelCase by default.
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/custom-event-name-casing": ["error",
+ "camelCase" | "kebab-case",
+ {
+ "ignores": []
+ }
+ ]
+}
+```
+
+- `"camelCase"` (default) ... Enforce custom event names to camelCase.
+- `"kebab-case"` ... Enforce custom event names to kebab-case.
+- `ignores` (`string[]`) ... The event names to ignore. Sets the event name to allow. For example, custom event names, Vue components event with special name, or Vue library component event name. You can set the regexp by writing it like `"/^name/"` or `click:row` or `fooBar`.
+
+### `"kebab-case"`
+
+
+
```vue
@@ -32,10 +90,9 @@ See [Guide - Custom Events] for more details.
+```
-Nothing.
+
+
+### `"ignores": ["foo-bar", "/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u"]`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :couple: Related Rules
+
+- [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md)
+- [vue/prop-name-casing](./prop-name-casing.md)
## :books: Further Reading
- [Guide - Custom Events]
+- [Guide (for v2) - Custom Events]
+
+[Guide - Custom Events]: https://vuejs.org/guide/components/events.html
+[Guide (for v2) - Custom Events]: https://v2.vuejs.org/v2/guide/components-custom-events.html
+
+## :rocket: Version
-[Guide - Custom Events]: https://vuejs.org/v2/guide/components-custom-events.html
+This rule was introduced in eslint-plugin-vue v7.0.0
## :mag: Implementation
diff --git a/docs/rules/define-emits-declaration.md b/docs/rules/define-emits-declaration.md
new file mode 100644
index 000000000..c24f5ec62
--- /dev/null
+++ b/docs/rules/define-emits-declaration.md
@@ -0,0 +1,133 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/define-emits-declaration
+description: enforce declaration style of `defineEmits`
+since: v9.5.0
+---
+
+# vue/define-emits-declaration
+
+> enforce declaration style of `defineEmits`
+
+## :book: Rule Details
+
+This rule enforces `defineEmits` typing style which you should use `type-based`, strict `type-literal`
+(introduced in Vue 3.3), or `runtime` declaration.
+
+This rule only works in setup script and `lang="ts"`.
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+```json
+ "vue/define-emits-declaration": ["error", "type-based" | "type-literal" | "runtime"]
+```
+
+- `type-based` (default) enforces type based declaration
+- `type-literal` enforces strict "type literal" type based declaration
+- `runtime` enforces runtime declaration
+
+### `runtime`
+
+
+
+```vue
+
+```
+
+
+
+### `type-literal`
+
+
+
+```vue
+
+```
+
+
+
+## :couple: Related Rules
+
+- [vue/define-props-declaration](./define-props-declaration.md)
+- [vue/valid-define-emits](./valid-define-emits.md)
+
+## :books: Further Reading
+
+- [`defineEmits`](https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)
+- [Typescript-only-features of `defineEmits`](https://vuejs.org/api/sfc-script-setup.html#typescript-only-features)
+- [Guide - Typing-component-emits](https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.5.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-emits-declaration.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-emits-declaration.js)
diff --git a/docs/rules/define-macros-order.md b/docs/rules/define-macros-order.md
new file mode 100644
index 000000000..14729f991
--- /dev/null
+++ b/docs/rules/define-macros-order.md
@@ -0,0 +1,191 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/define-macros-order
+description: enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)
+since: v8.7.0
+---
+
+# vue/define-macros-order
+
+> enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)
+
+- :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).
+
+## :book: Rule Details
+
+This rule reports compiler macros (like `defineProps` or `defineEmits` but also custom ones) when they are not the first statements in `
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+### `{ "order": ["defineOptions", "defineModel", "defineProps", "defineEmits", "defineSlots"] }`
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+### `{ "order": ["definePage", "defineModel", "defineCustom", "defineEmits", "defineSlots"] }`
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+### `{ "defineExposeLast": true }`
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v8.7.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-macros-order.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-macros-order.js)
diff --git a/docs/rules/define-props-declaration.md b/docs/rules/define-props-declaration.md
new file mode 100644
index 000000000..40c5ea0b0
--- /dev/null
+++ b/docs/rules/define-props-declaration.md
@@ -0,0 +1,88 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/define-props-declaration
+description: enforce declaration style of `defineProps`
+since: v9.5.0
+---
+
+# vue/define-props-declaration
+
+> enforce declaration style of `defineProps`
+
+## :book: Rule Details
+
+This rule enforces `defineProps` typing style which you should use `type-based` or `runtime` declaration.
+
+This rule only works in setup script and `lang="ts"`.
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+```json
+ "vue/define-props-declaration": ["error", "type-based" | "runtime"]
+```
+
+- `type-based` (default) enforces type-based declaration
+- `runtime` enforces runtime declaration
+
+### `"runtime"`
+
+
+
+```vue
+
+```
+
+
+
+## :couple: Related Rules
+
+- [vue/define-emits-declaration](./define-emits-declaration.md)
+- [vue/valid-define-props](./valid-define-props.md)
+
+## :books: Further Reading
+
+- [`defineProps`](https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)
+- [Typescript-only-features of `defineProps`](https://vuejs.org/api/sfc-script-setup.html#typescript-only-features)
+- [Guide - Typing-component-props](https://vuejs.org/guide/typescript/composition-api.html#typing-component-props)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.5.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-props-declaration.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-props-declaration.js)
diff --git a/docs/rules/define-props-destructuring.md b/docs/rules/define-props-destructuring.md
new file mode 100644
index 000000000..e3c2b2745
--- /dev/null
+++ b/docs/rules/define-props-destructuring.md
@@ -0,0 +1,98 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/define-props-destructuring
+description: enforce consistent style for props destructuring
+since: v10.1.0
+---
+
+# vue/define-props-destructuring
+
+> enforce consistent style for props destructuring
+
+## :book: Rule Details
+
+This rule enforces a consistent style for handling Vue 3 Composition API props, allowing you to choose between requiring destructuring or prohibiting it.
+
+By default, the rule requires you to use destructuring syntax when using `defineProps` instead of storing props in a variable and warns against combining `withDefaults` with destructuring.
+
+
+
+```vue
+
+```
+
+
+
+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/dot-location.md b/docs/rules/dot-location.md
index 8411d83d8..edb785080 100644
--- a/docs/rules/dot-location.md
+++ b/docs/rules/dot-location.md
@@ -2,24 +2,38 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/dot-location
-description: enforce consistent newlines before and after dots
+description: Enforce consistent newlines before and after dots in ``
+since: v6.0.0
---
+
# vue/dot-location
-> enforce consistent newlines before and after dots
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> Enforce consistent newlines before and after dots 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.
+
+This rule is the same rule as [@stylistic/dot-location] rule but it applies to the expressions in ``.
-This rule is the same rule as core [dot-location] rule but it applies to the expressions in ``.
+This rule extends the rule that [@stylistic/eslint-plugin] has, but if [@stylistic/eslint-plugin] is not installed, this rule extracts and extends the same rule from ESLint core.
+However, if neither is found, the rule cannot be used.
-## :books: Further reading
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
+## :books: Further Reading
+
+- [@stylistic/dot-location]
- [dot-location]
+[@stylistic/dot-location]: https://eslint.style/rules/default/dot-location
[dot-location]: https://eslint.org/docs/rules/dot-location
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v6.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/dot-location.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/dot-location.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/dot-location)
+Taken with ❤️ [from ESLint Stylistic](https://eslint.style/rules/js/dot-location)
diff --git a/docs/rules/dot-notation.md b/docs/rules/dot-notation.md
index 9de04c2cd..101caf5cf 100644
--- a/docs/rules/dot-notation.md
+++ b/docs/rules/dot-notation.md
@@ -2,24 +2,31 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/dot-notation
-description: enforce dot notation whenever possible
+description: Enforce dot notation whenever possible in ``
+since: v7.0.0
---
+
# vue/dot-notation
-> enforce dot notation whenever possible
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> Enforce dot notation whenever possible 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.
This rule is the same rule as core [dot-notation] rule but it applies to the expressions in ``.
-## :books: Further reading
+## :books: Further Reading
- [dot-notation]
[dot-notation]: https://eslint.org/docs/rules/dot-notation
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/dot-notation.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/dot-notation.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/dot-notation)
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/latest/rules/dot-notation)
diff --git a/docs/rules/enforce-style-attribute.md b/docs/rules/enforce-style-attribute.md
new file mode 100644
index 000000000..fcffefbae
--- /dev/null
+++ b/docs/rules/enforce-style-attribute.md
@@ -0,0 +1,89 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/enforce-style-attribute
+description: enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags
+since: v9.20.0
+---
+
+# vue/enforce-style-attribute
+
+> enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags
+
+## :book: Rule Details
+
+This rule allows you to explicitly allow the use of the `scoped` and `module` attributes on your top level style tags.
+
+### `"scoped"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `"module"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+### `"plain"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/enforce-style-attribute": [
+ "error",
+ { "allow": ["scoped", "module", "plain"] }
+ ]
+}
+```
+
+- `"allow"` (`["scoped" | "module" | "plain"]`) Array of attributes to allow on a top level style tag. The option `plain` is used to allow style tags that have neither the `scoped` nor `module` attributes. Default: `["scoped"]`
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.20.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/enforce-style-attribute.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/enforce-style-attribute.js)
diff --git a/docs/rules/eqeqeq.md b/docs/rules/eqeqeq.md
index 2c9489515..392deff7f 100644
--- a/docs/rules/eqeqeq.md
+++ b/docs/rules/eqeqeq.md
@@ -2,24 +2,31 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/eqeqeq
-description: require the use of `===` and `!==`
+description: Require the use of `===` and `!==` in ``
+since: v5.2.0
---
+
# vue/eqeqeq
-> require the use of `===` and `!==`
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> 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.
This rule is the same rule as core [eqeqeq] rule but it applies to the expressions in ``.
-## :books: Further reading
+## :books: Further Reading
- [eqeqeq]
[eqeqeq]: https://eslint.org/docs/rules/eqeqeq
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v5.2.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/eqeqeq.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/eqeqeq.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/eqeqeq)
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/latest/rules/eqeqeq)
diff --git a/docs/rules/experimental-script-setup-vars.md b/docs/rules/experimental-script-setup-vars.md
new file mode 100644
index 000000000..82e7de884
--- /dev/null
+++ b/docs/rules/experimental-script-setup-vars.md
@@ -0,0 +1,49 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/experimental-script-setup-vars
+description: prevent variables defined in `
+```
+
+
+
+After turning on, `props` and `emit` are being marked as defined and `no-undef` rule doesn't report an issue.
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/experimental-script-setup-vars.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/experimental-script-setup-vars.js)
diff --git a/docs/rules/first-attribute-linebreak.md b/docs/rules/first-attribute-linebreak.md
new file mode 100644
index 000000000..6292ef262
--- /dev/null
+++ b/docs/rules/first-attribute-linebreak.md
@@ -0,0 +1,169 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/first-attribute-linebreak
+description: enforce the location of first attribute
+since: v8.0.0
+---
+
+# vue/first-attribute-linebreak
+
+> enforce the location of first attribute
+
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
+
+## :book: Rule Details
+
+This rule aims to enforce a consistent location for the first attribute.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/first-attribute-linebreak": ["error", {
+ "singleline": "ignore",
+ "multiline": "below"
+ }]
+}
+```
+
+- `singleline` ... The location of the first attribute when the attributes on single line. Default is `"ignore"`.
+ - `"below"` ... Requires a newline before the first attribute.
+ - `"beside"` ... Disallows a newline before the first attribute.
+ - `"ignore"` ... Ignores attribute checking.
+- `multiline` ... The location of the first attribute when the attributes span multiple lines. Default is `"below"`.
+ - `"below"` ... Requires a newline before the first attribute.
+ - `"beside"` ... Disallows a newline before the first attribute.
+ - `"ignore"` ... Ignores attribute checking.
+
+### `"singleline": "beside"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `"singleline": "below"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `"multiline": "beside"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `"multiline": "below"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :couple: Related Rules
+
+- [vue/max-attributes-per-line](./max-attributes-per-line.md)
+
+## :books: Further Reading
+
+- [Style guide - Multi attribute elements](https://vuejs.org/style-guide/rules-strongly-recommended.html#multi-attribute-elements)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v8.0.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/first-attribute-linebreak.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/first-attribute-linebreak.js)
diff --git a/docs/rules/func-call-spacing.md b/docs/rules/func-call-spacing.md
index 41af5a45a..f330255d1 100644
--- a/docs/rules/func-call-spacing.md
+++ b/docs/rules/func-call-spacing.md
@@ -2,24 +2,38 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/func-call-spacing
-description: require or disallow spacing between function identifiers and their invocations
+description: Require or disallow spacing between function identifiers and their invocations in ``
+since: v7.0.0
---
+
# vue/func-call-spacing
-> require or disallow spacing between function identifiers and their invocations
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> Require or disallow spacing between function identifiers and their invocations 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.
+
+This rule is the same rule as [@stylistic/function-call-spacing] rule but it applies to the expressions in ``.
-This rule is the same rule as core [func-call-spacing] rule but it applies to the expressions in ``.
+This rule extends the rule that [@stylistic/eslint-plugin] has, but if [@stylistic/eslint-plugin] is not installed, this rule extracts and extends the same rule from ESLint core.
+However, if neither is found, the rule cannot be used.
-## :books: Further reading
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
+## :books: Further Reading
+
+- [@stylistic/function-call-spacing]
- [func-call-spacing]
+[@stylistic/function-call-spacing]: https://eslint.style/rules/default/function-call-spacing
[func-call-spacing]: https://eslint.org/docs/rules/func-call-spacing
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/func-call-spacing.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/func-call-spacing.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/func-call-spacing)
+Taken with ❤️ [from ESLint Stylistic](https://eslint.style/rules/ts/function-call-spacing)
diff --git a/docs/rules/html-button-has-type.md b/docs/rules/html-button-has-type.md
new file mode 100644
index 000000000..e507d42b8
--- /dev/null
+++ b/docs/rules/html-button-has-type.md
@@ -0,0 +1,67 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/html-button-has-type
+description: disallow usage of button without an explicit type attribute
+since: v7.6.0
+---
+
+# vue/html-button-has-type
+
+> disallow usage of button without an explicit type attribute
+
+Forgetting the type attribute on a button defaults it to being a submit type.
+This is nearly never what is intended, especially in your average one-page application.
+
+## :book: Rule Details
+
+This rule aims to warn if no type or an invalid type is used on a button type attribute.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/html-button-has-type": ["error", {
+ "button": true,
+ "submit": true,
+ "reset": true
+ }]
+}
+```
+
+- `button` ... ``
+ - `true` (default) ... allow value `button`.
+ - `false` ... disallow value `button`.
+- `submit` ... ``
+ - `true` (default) ... allow value `submit`.
+ - `false` ... disallow value `submit`.
+- `reset` ... ``
+ - `true` (default) ... allow value `reset`.
+ - `false` ... disallow value `reset`.
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.6.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-button-has-type.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/html-button-has-type.js)
diff --git a/docs/rules/html-closing-bracket-newline.md b/docs/rules/html-closing-bracket-newline.md
index 68d405fef..964317590 100644
--- a/docs/rules/html-closing-bracket-newline.md
+++ b/docs/rules/html-closing-bracket-newline.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/html-closing-bracket-newline
description: require or disallow a line break before tag's closing brackets
+since: v4.1.0
---
+
# vue/html-closing-bracket-newline
+
> require or disallow a line break before tag's closing brackets
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
People have their own preference about the location of closing brackets.
This rule enforces a line break (or no line break) before tag's closing brackets.
@@ -56,19 +59,31 @@ This rule aims to warn the right angle brackets which are at the location other
```json
{
- "vue/html-closing-bracket-newline": ["error", {
- "singleline": "never",
- "multiline": "always"
- }]
+ "vue/html-closing-bracket-newline": [
+ "error",
+ {
+ "singleline": "never",
+ "multiline": "always",
+ "selfClosingTag": {
+ "singleline": "never",
+ "multiline": "always"
+ }
+ }
+ ]
}
```
-- `singleline` ... the configuration for single-line elements. It's a single-line element if the element does not have attributes or the last attribute is on the same line as the opening bracket.
- - `"never"` (default) ... disallow line breaks before the closing bracket.
- - `"always"` ... require one line break before the closing bracket.
-- `multiline` ... the configuration for multiline elements. It's a multiline element if the last attribute is not on the same line of the opening bracket.
- - `"never"` ... disallow line breaks before the closing bracket.
- - `"always"` (default) ... require one line break before the closing bracket.
+- `singleline` (`"never"` by default) ... the configuration for single-line elements. It's a single-line element if the element does not have attributes or the last attribute is on the same line as the opening bracket.
+- `multiline` (`"always"` by default) ... the configuration for multiline elements. It's a multiline element if the last attribute is not on the same line of the opening bracket.
+- `selfClosingTag.singleline` ... the configuration for single-line self closing elements.
+- `selfClosingTag.multiline` ... the configuration for multiline self closing elements.
+
+Every option can be set to one of the following values:
+
+- `"always"` ... require one line break before the closing bracket.
+- `"never"` ... disallow line breaks before the closing bracket.
+
+If `selfClosingTag` is not specified, the `singleline` and `multiline` options are inherited for self-closing tags.
Plus, you can use [`vue/html-indent`](./html-indent.md) rule to enforce indent-level of the closing brackets.
@@ -93,6 +108,29 @@ Plus, you can use [`vue/html-indent`](./html-indent.md) rule to enforce indent-l
+### `"selfClosingTag": { "multiline": "always" }`
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v4.1.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-closing-bracket-newline.js)
diff --git a/docs/rules/html-closing-bracket-spacing.md b/docs/rules/html-closing-bracket-spacing.md
index 32bf93b0d..d1927b4e1 100644
--- a/docs/rules/html-closing-bracket-spacing.md
+++ b/docs/rules/html-closing-bracket-spacing.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/html-closing-bracket-spacing
description: require or disallow a space before tag's closing brackets
+since: v4.1.0
---
+
# vue/html-closing-bracket-spacing
+
> require or disallow a space before tag's closing brackets
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
## :book: Rule Details
@@ -53,14 +56,14 @@ This rule aims to enforce consistent spacing style before closing brackets `>` o
```
- `startTag` (`"always" | "never"`) ... Setting for the `>` of start tags (e.g. `
`). Default is `"never"`.
- - `"always"` ... requires one or more spaces.
- - `"never"` ... disallows spaces.
+ - `"always"` ... requires one or more spaces.
+ - `"never"` ... disallows spaces.
- `endTag` (`"always" | "never"`) ... Setting for the `>` of end tags (e.g. `
`). Default is `"never"`.
- - `"always"` ... requires one or more spaces.
- - `"never"` ... disallows spaces.
+ - `"always"` ... requires one or more spaces.
+ - `"never"` ... disallows spaces.
- `selfClosingTag` (`"always" | "never"`) ... Setting for the `/>` of self-closing tags (e.g. ``). Default is `"always"`.
- - `"always"` ... requires one or more spaces.
- - `"never"` ... disallows spaces.
+ - `"always"` ... requires one or more spaces.
+ - `"never"` ... disallows spaces.
### `"startTag": "always", "endTag": "always", "selfClosingTag": "always"`
@@ -81,11 +84,15 @@ This rule aims to enforce consistent spacing style before closing brackets `>` o
-## :couple: Related rules
+## :couple: Related Rules
- [vue/no-multi-spaces](./no-multi-spaces.md)
- [vue/html-closing-bracket-newline](./html-closing-bracket-newline.md)
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v4.1.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-closing-bracket-spacing.js)
diff --git a/docs/rules/html-comment-content-newline.md b/docs/rules/html-comment-content-newline.md
index 4a94ff7e3..cffd7cdff 100644
--- a/docs/rules/html-comment-content-newline.md
+++ b/docs/rules/html-comment-content-newline.md
@@ -2,12 +2,15 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/html-comment-content-newline
-description: enforce unified line brake in HTML comments
+description: enforce unified line break in HTML comments
+since: v7.0.0
---
+
# vue/html-comment-content-newline
-> enforce unified line brake in HTML comments
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> enforce unified line break in HTML comments
+
+- :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.
## :book: Rule Details
@@ -54,21 +57,23 @@ This rule will enforce consistency of line break after the ``.
- - `"always"` ... require one line break after the ``.
- - `multiline` ... the configuration for multiline comments.
- - `"never"` ... disallow line breaks after the ``.
- - `"always"` (default) ... require one line break after the ``.
- You can also set the same value for both `singleline` and `multiline` by specifies a string.
+ - `singleline` ... the configuration for single-line comments.
+ - `"never"` (default) ... disallow line breaks after the ``.
+ - `"always"` ... require one line break after the ``.
+ - `multiline` ... the configuration for multiline comments.
+ - `"never"` ... disallow line breaks after the ``.
+ - `"always"` (default) ... require one line break after the ``.
+
+ You can also set the same value for both `singleline` and `multiline` by specifies a string.
- This rule can also take a 2nd option, an object with the following key: `"exceptions"`.
- - The `"exceptions"` value is an array of string patterns which are considered exceptions to the rule.
- ```json
- "vue/html-comment-content-newline": ["error", { ... }, { "exceptions": ["*"] }]
- ```
+ - The `"exceptions"` value is an array of string patterns which are considered exceptions to the rule.
+
+ ```json
+ "vue/html-comment-content-newline": ["error", { ... }, { "exceptions": ["*"] }]
+ ```
### `"always"`
@@ -160,7 +165,6 @@ This rule will enforce consistency of line break after the `` makes it easier to read text in
```
- The first is a string which be either `"always"` or `"never"`. The default is `"always"`.
- - `"always"` (default) ... there must be at least one whitespace at after the ``.
- - `"never"` ... there should be no whitespace at after the ``.
+ - `"always"` (default) ... there must be at least one whitespace at after the ``.
+ - `"never"` ... there should be no whitespace at after the ``.
- This rule can also take a 2nd option, an object with the following key: `"exceptions"`.
- - The `"exceptions"` value is an array of string patterns which are considered exceptions to the rule.
+
+ - The `"exceptions"` value is an array of string patterns which are considered exceptions to the rule.
Please note that exceptions are ignored if the first argument is `"never"`.
- ```json
- "vue/html-comment-content-spacing": ["error", "always", { "exceptions": ["*"] }]
- ```
+ ```json
+ "vue/html-comment-content-spacing": ["error", "always", { "exceptions": ["*"] }]
+ ```
### `"always"`
@@ -104,11 +108,15 @@ Whitespace after the `` makes it easier to read text in
-## :couple: Related rules
+## :couple: Related Rules
- [spaced-comment](https://eslint.org/docs/rules/spaced-comment)
- [vue/html-comment-content-newline](./html-comment-content-newline.md)
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-comment-content-spacing.js)
diff --git a/docs/rules/html-comment-indent.md b/docs/rules/html-comment-indent.md
index 6da16a098..2fa8ed7f1 100644
--- a/docs/rules/html-comment-indent.md
+++ b/docs/rules/html-comment-indent.md
@@ -3,11 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/html-comment-indent
description: enforce consistent indentation in HTML comments
+since: v7.0.0
---
+
# vue/html-comment-indent
+
> enforce consistent indentation in HTML comments
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :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.
## :book: Rule Details
@@ -127,6 +130,10 @@ This rule enforces a consistent indentation style in HTML comment (`
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-comment-indent.js)
diff --git a/docs/rules/html-end-tags.md b/docs/rules/html-end-tags.md
index 0cc0dac3f..fa6333b06 100644
--- a/docs/rules/html-end-tags.md
+++ b/docs/rules/html-end-tags.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/html-end-tags
description: enforce end tag style
+since: v3.0.0
---
+
# vue/html-end-tags
+
> enforce end tag style
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
## :book: Rule Details
@@ -37,6 +40,10 @@ This rule aims to disallow lacking end tags.
Nothing.
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v3.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-end-tags.js)
diff --git a/docs/rules/html-indent.md b/docs/rules/html-indent.md
index c3a45f273..2cab727aa 100644
--- a/docs/rules/html-indent.md
+++ b/docs/rules/html-indent.md
@@ -3,19 +3,22 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/html-indent
description: enforce consistent indentation in ``
+since: v3.14.0
---
+
# vue/html-indent
+
> enforce consistent indentation in ``
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
## :book: Rule Details
This rule enforces a consistent indentation style in ``. The default style is 2 spaces.
- This rule checks all tags, also all expressions in directives and mustaches.
-- In the expressions, this rule supports ECMAScript 2020 syntaxes. It ignores unknown AST nodes, but it might be confused by non-standard syntaxes.
+- In the expressions, this rule supports ECMAScript 2022 syntaxes. It ignores unknown AST nodes, but it might be confused by non-standard syntaxes.
@@ -82,7 +85,7 @@ This rule enforces a consistent indentation style in ``. The default s
- `closeBracket.endTag` (`integer`) ... The multiplier of indentation for right brackets of end tags (``). Default is `0`.
- `closeBracket.selfClosingTag` (`integer`) ... The multiplier of indentation for right brackets of start tags (``). Default is `0`.
- `alignAttributesVertically` (`boolean`) ... Condition for whether attributes should be vertically aligned to the first attribute in multiline case or not. Default is `true`
-- `ignores` (`string[]`) ... The selector to ignore nodes. The AST spec is [here](https://github.com/mysticatea/vue-eslint-parser/blob/master/docs/ast.md). You can use [esquery](https://github.com/estools/esquery#readme) to select nodes. Default is an empty array.
+- `ignores` (`string[]`) ... The selector to ignore nodes. The AST spec is [here](https://github.com/vuejs/vue-eslint-parser/blob/master/docs/ast.md). You can use [esquery](https://github.com/estools/esquery#readme) to select nodes. Default is an empty array.
### `2, {"attribute": 1, "closeBracket": 1}`
@@ -190,6 +193,10 @@ This rule enforces a consistent indentation style in ``. The default s
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v3.14.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-indent.js)
diff --git a/docs/rules/html-quotes.md b/docs/rules/html-quotes.md
index 48e40b93f..2bbf38c3d 100644
--- a/docs/rules/html-quotes.md
+++ b/docs/rules/html-quotes.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/html-quotes
description: enforce quotes style of HTML attributes
+since: v3.0.0
---
+
# vue/html-quotes
+
> enforce quotes style of HTML attributes
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
You can choose quotes of HTML attributes from:
@@ -90,9 +93,13 @@ Object option:
-## :books: Further reading
+## :books: Further Reading
+
+- [Style guide - Quoted attribute values](https://vuejs.org/style-guide/rules-strongly-recommended.html#quoted-attribute-values)
+
+## :rocket: Version
-- [Style guide - Quoted attribute values](https://vuejs.org/v2/style-guide/#Quoted-attribute-values-strongly-recommended)
+This rule was introduced in eslint-plugin-vue v3.0.0
## :mag: Implementation
diff --git a/docs/rules/html-self-closing.md b/docs/rules/html-self-closing.md
index d83e15103..316f462c8 100644
--- a/docs/rules/html-self-closing.md
+++ b/docs/rules/html-self-closing.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/html-self-closing
description: enforce self-closing style
+since: v3.11.0
---
+
# vue/html-self-closing
+
> enforce self-closing style
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
## :book: Rule Details
@@ -91,9 +94,13 @@ Every option can be set to one of the following values:
-## :books: Further reading
+## :books: Further Reading
+
+- [Style guide - Self closing components](https://vuejs.org/style-guide/rules-strongly-recommended.html#self-closing-components)
+
+## :rocket: Version
-- [Style guide - Self closing components](https://vuejs.org/v2/style-guide/#Self-closing-components-strongly-recommended)
+This rule was introduced in eslint-plugin-vue v3.11.0
## :mag: Implementation
diff --git a/docs/rules/index.md b/docs/rules/index.md
new file mode 100644
index 000000000..7828b58bb
--- /dev/null
+++ b/docs/rules/index.md
@@ -0,0 +1,647 @@
+---
+sidebarDepth: 0
+pageClass: rule-list
+---
+
+# Available rules
+
+
+
+::: tip Legend
+ :wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fix-problems) can automatically fix some of the reported problems.
+
+ :bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+:::
+
+Mark indicating rule type:
+
+- :warning: Possible Problems: These rules relate to possible logic errors in code.
+- :hammer: Suggestions: These rules suggest alternate ways of doing things.
+- :lipstick: Layout & Formatting: These rules care about how the code looks rather than how it executes.
+
+## Base Rules (Enabling Correct ESLint Parsing)
+
+Rules in this category are enabled for all presets provided by eslint-plugin-vue.
+
+
+
+| Rule ID | Description | | |
+|:--------|:------------|:--:|:--:|
+| [vue/comment-directive] | support comment-directives in `` | | :warning: |
+| [vue/jsx-uses-vars] | prevent variables used in JSX to be marked as unused | | :warning: |
+
+
+
+## Priority A: Essential (Error Prevention)
+
+- :three: Indicates that the rule is for Vue 3 and is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]` presets.
+- :two: Indicates that the rule is for Vue 2 and is included in all of `"plugin:vue/vue2-essential"`,`*.configs["flat/vue2-essential"]`, `"plugin:vue/vue2-strongly-recommended"`,`*.configs["flat/vue2-strongly-recommended"]` and `"plugin:vue/vue2-recommended"`,`*.configs["flat/vue2-recommended"]` presets.
+
+
+
+| Rule ID | Description | | |
+|:--------|:------------|:--:|:--:|
+| [vue/multi-word-component-names] | require component names to be always multi-word | | :three::two::hammer: |
+| [vue/no-arrow-functions-in-watch] | disallow using arrow functions to define watcher | | :three::two::warning: |
+| [vue/no-async-in-computed-properties] | disallow asynchronous actions in computed properties | | :three::two::warning: |
+| [vue/no-child-content] | disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text` | :bulb: | :three::two::warning: |
+| [vue/no-computed-properties-in-data] | disallow accessing computed properties in `data` | | :three::two::warning: |
+| [vue/no-custom-modifiers-on-v-model] | disallow custom modifiers on v-model used on the component | | :two::warning: |
+| [vue/no-deprecated-data-object-declaration] | disallow using deprecated object declaration on data (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
+| [vue/no-deprecated-delete-set] | disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-deprecated-destroyed-lifecycle] | disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
+| [vue/no-deprecated-dollar-listeners-api] | disallow using deprecated `$listeners` (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-deprecated-dollar-scopedslots-api] | disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
+| [vue/no-deprecated-events-api] | disallow using deprecated events api (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-deprecated-filter] | disallow using deprecated filters syntax (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-deprecated-functional-template] | disallow using deprecated the `functional` template (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-deprecated-html-element-is] | disallow using deprecated the `is` attribute on HTML elements (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-deprecated-inline-template] | disallow using deprecated `inline-template` attribute (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-deprecated-model-definition] | disallow deprecated `model` definition (in Vue.js 3.0.0+) | :bulb: | :three::warning: |
+| [vue/no-deprecated-props-default-this] | disallow deprecated `this` access in props default function (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-deprecated-router-link-tag-prop] | disallow using deprecated `tag` property on `RouterLink` (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-deprecated-scope-attribute] | disallow deprecated `scope` attribute (in Vue.js 2.5.0+) | :wrench: | :three::hammer: |
+| [vue/no-deprecated-slot-attribute] | disallow deprecated `slot` attribute (in Vue.js 2.6.0+) | :wrench: | :three::hammer: |
+| [vue/no-deprecated-slot-scope-attribute] | disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+) | :wrench: | :three::hammer: |
+| [vue/no-deprecated-v-bind-sync] | disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
+| [vue/no-deprecated-v-is] | disallow deprecated `v-is` directive (in Vue.js 3.1.0+) | | :three::hammer: |
+| [vue/no-deprecated-v-on-native-modifier] | disallow using deprecated `.native` modifiers (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-deprecated-v-on-number-modifiers] | disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
+| [vue/no-deprecated-vue-config-keycodes] | disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+) | | :three::warning: |
+| [vue/no-dupe-keys] | disallow duplication of field names | | :three::two::warning: |
+| [vue/no-dupe-v-else-if] | disallow duplicate conditions in `v-if` / `v-else-if` chains | | :three::two::warning: |
+| [vue/no-duplicate-attributes] | disallow duplication of attributes | | :three::two::warning: |
+| [vue/no-export-in-script-setup] | disallow `export` in `
```
@@ -126,11 +131,11 @@ export default {
```vue
```
@@ -141,11 +146,11 @@ export default {
```vue
```
@@ -156,10 +161,10 @@ export default {
```vue
```
@@ -304,9 +309,13 @@ export default {
-## :books: Further reading
+## :books: Further Reading
+
+- [Style guide - Single-file component filename casing](https://vuejs.org/style-guide/rules-strongly-recommended.html#single-file-component-filename-casing)
+
+## :rocket: Version
- - [Style guide - Single-file component filename casing](https://vuejs.org/v2/style-guide/#Single-file-component-filename-casing-strongly-recommended)
+This rule was introduced in eslint-plugin-vue v5.2.0
## :mag: Implementation
diff --git a/docs/rules/match-component-import-name.md b/docs/rules/match-component-import-name.md
new file mode 100644
index 000000000..522b90a02
--- /dev/null
+++ b/docs/rules/match-component-import-name.md
@@ -0,0 +1,49 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/match-component-import-name
+description: require the registered component name to match the imported component name
+since: v8.7.0
+---
+
+# vue/match-component-import-name
+
+> require the registered component name to match the imported component name
+
+## :book: Rule Details
+
+By default, this rule will validate that the imported name matches the name of the components object property identifer. Note that "matches" means that the imported name matches either the PascalCase or kebab-case version of the components object property identifer. If you would like to enforce that it must match only one of PascalCase or kebab-case, use this rule in conjunction with the rule [vue/component-definition-name-casing](./component-definition-name-casing.md).
+
+
+
+```vue
+
+```
+
+
+
+## :couple: Related Rules
+
+- [vue/component-definition-name-casing](./component-definition-name-casing.md)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v8.7.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/match-component-import-name.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/match-component-import-name.js)
diff --git a/docs/rules/max-attributes-per-line.md b/docs/rules/max-attributes-per-line.md
index 72eb5212a..141b136e0 100644
--- a/docs/rules/max-attributes-per-line.md
+++ b/docs/rules/max-attributes-per-line.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/max-attributes-per-line
description: enforce the maximum number of attributes per line
+since: v3.12.0
---
+
# vue/max-attributes-per-line
+
> enforce the maximum number of attributes per line
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
Limits the maximum number of attributes/properties per line to improve readability.
@@ -55,18 +58,18 @@ There is a configurable number of attributes that are acceptable in one-line cas
```json
{
"vue/max-attributes-per-line": ["error", {
- "singleline": 1,
+ "singleline": {
+ "max": 1
+ },
"multiline": {
- "max": 1,
- "allowFirstLine": false
+ "max": 1
}
}]
}
```
-- `singleline` (`number`) ... The number of maximum attributes per line when the opening tag is in a single line. Default is `1`.
-- `multiline.max` (`number`) ... The max number of attributes per line when the opening tag is in multiple lines. Default is `1`. This can be `{ multiline: 1 }` instead of `{ multiline: { max: 1 }}` if you don't configure `allowFirstLine` property.
-- `multiline.allowFirstLine` (`boolean`) ... If `true`, it allows attributes on the same line as that tag name. Default is `false`.
+- `singleline.max` (`number`) ... The number of maximum attributes per line when the opening tag is in a single line. Default is `1`. This can be `{ singleline: 1 }` instead of `{ singleline: { max: 1 }}`.
+- `multiline.max` (`number`) ... The max number of attributes per line when the opening tag is in multiple lines. Default is `1`. This can be `{ multiline: 1 }` instead of `{ multiline: { max: 1 }}`.
### `"singleline": 3`
@@ -106,25 +109,17 @@ There is a configurable number of attributes that are acceptable in one-line cas
-### `"multiline": 1, "allowFirstLine": true`
+## :couple: Related Rules
-
+- [vue/first-attribute-linebreak](./first-attribute-linebreak.md)
-```vue
-
-
-
-
-```
+## :books: Further Reading
-
+- [Style guide - Multi attribute elements](https://vuejs.org/style-guide/rules-strongly-recommended.html#multi-attribute-elements)
-## :books: Further reading
+## :rocket: Version
-- [Style guide - Multi attribute elements](https://vuejs.org/v2/style-guide/#Multi-attribute-elements-strongly-recommended)
+This rule was introduced in eslint-plugin-vue v3.12.0
## :mag: Implementation
diff --git a/docs/rules/max-len.md b/docs/rules/max-len.md
index 831090c07..0816000ee 100644
--- a/docs/rules/max-len.md
+++ b/docs/rules/max-len.md
@@ -2,10 +2,13 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/max-len
-description: enforce a maximum line length
+description: enforce a maximum line length in `.vue` files
+since: v6.1.0
---
+
# vue/max-len
-> enforce a maximum line length
+
+> enforce a maximum line length in `.vue` files
## :book: Rule Details
@@ -110,7 +113,6 @@ var foo = ['line', 'length', 'is', '50', '......']
-
### `"template": 120`
@@ -317,12 +319,16 @@ var longRegExpLiteral = /this is a really really really really really long regul
-## :books: Further reading
+## :books: Further Reading
- [max-len]
[max-len]: https://eslint.org/docs/rules/max-len
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v6.1.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/max-len.js)
diff --git a/docs/rules/max-lines-per-block.md b/docs/rules/max-lines-per-block.md
new file mode 100644
index 000000000..a65be3bb9
--- /dev/null
+++ b/docs/rules/max-lines-per-block.md
@@ -0,0 +1,63 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/max-lines-per-block
+description: enforce maximum number of lines in Vue SFC blocks
+since: v9.15.0
+---
+
+# vue/max-lines-per-block
+
+> enforce maximum number of lines in Vue SFC blocks
+
+## :book: Rule Details
+
+This rule enforces a maximum number of lines per block, in order to aid in maintainability and reduce complexity.
+
+## :wrench: Options
+
+This rule takes an object, where you can specify the maximum number of lines in each type of SFC block and customize the line counting behavior.
+The following properties can be specified for the object.
+
+- `script` ... Specify the maximum number of lines in `
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.15.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/max-lines-per-block.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/max-lines-per-block.js)
diff --git a/docs/rules/max-props.md b/docs/rules/max-props.md
new file mode 100644
index 000000000..918c67294
--- /dev/null
+++ b/docs/rules/max-props.md
@@ -0,0 +1,65 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/max-props
+description: enforce maximum number of props in Vue component
+since: v9.28.0
+---
+
+# vue/max-props
+
+> enforce maximum number of props in Vue component
+
+## :book: Rule Details
+
+This rule enforces a maximum number of props in a Vue SFC, in order to aid in maintainability and reduce complexity.
+
+## :wrench: Options
+
+This rule takes an object, where you can specify the maximum number of props allowed in a Vue SFC.
+There is one property that can be specified for the object.
+
+- `maxProps` ... Specify the maximum number of props in the `script` block.
+
+### `{ maxProps: 1 }`
+
+
+
+```vue
+
+
+
+
+
+```
+
+
+
+### `{ maxProps: 5 }`
+
+
+
+```vue
+
+
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.28.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/max-props.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/max-props.js)
diff --git a/docs/rules/max-template-depth.md b/docs/rules/max-template-depth.md
new file mode 100644
index 000000000..42ee4da88
--- /dev/null
+++ b/docs/rules/max-template-depth.md
@@ -0,0 +1,70 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/max-template-depth
+description: enforce maximum depth of template
+since: v9.28.0
+---
+
+# vue/max-template-depth
+
+> enforce maximum depth of template
+
+## :book: Rule Details
+
+This rule enforces a maximum depth of the template in a Vue SFC, in order to aid in maintainability and reduce complexity.
+
+## :wrench: Options
+
+This rule takes an object, where you can specify the maximum depth allowed in a Vue SFC template block.
+There is one property that can be specified for the object.
+
+- `maxDepth` ... Specify the maximum template depth `template` block.
+
+### `{ maxDepth: 3 }`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.28.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/max-template-depth.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/max-template-depth.js)
diff --git a/docs/rules/multi-word-component-names.md b/docs/rules/multi-word-component-names.md
new file mode 100644
index 000000000..08539dddb
--- /dev/null
+++ b/docs/rules/multi-word-component-names.md
@@ -0,0 +1,188 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/multi-word-component-names
+description: require component names to be always multi-word
+since: v7.20.0
+---
+
+# vue/multi-word-component-names
+
+> require component names to be always multi-word
+
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+
+## :book: Rule Details
+
+This rule require component names to be always multi-word, except for root `App`
+components, and built-in components provided by Vue, such as `` or
+``. This prevents conflicts with existing and future HTML elements,
+since all HTML elements are single words.
+
+
+
+```js
+/* ✓ GOOD */
+Vue.component('todo-item', {
+ // ...
+})
+
+/* ✗ BAD */
+Vue.component('Todo', {
+ // ...
+})
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/multi-word-component-names": ["error", {
+ "ignores": []
+ }]
+}
+```
+
+- `ignores` (`string[]`) ... The component names to ignore. Sets the component name to allow.
+
+### `ignores: ["Todo"]`
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+
+
+```
+
+
+
+## :couple: Related Rules
+
+- [vue/no-reserved-component-names](./no-reserved-component-names.md)
+
+## :books: Further Reading
+
+- [Style guide - Multi-word component names](https://vuejs.org/style-guide/rules-essential.html#use-multi-word-component-names)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.20.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/multi-word-component-names.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/multi-word-component-names.js)
diff --git a/docs/rules/multiline-html-element-content-newline.md b/docs/rules/multiline-html-element-content-newline.md
index e2089eaa6..bf654aafe 100644
--- a/docs/rules/multiline-html-element-content-newline.md
+++ b/docs/rules/multiline-html-element-content-newline.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/multiline-html-element-content-newline
description: require a line break before and after the contents of a multiline element
+since: v5.0.0
---
+
# vue/multiline-html-element-content-newline
+
> require a line break before and after the contents of a multiline element
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
## :book: Rule Details
@@ -140,12 +143,16 @@ This rule enforces a line break before and after the contents of a multiline ele
-## :books: Further reading
+## :books: Further Reading
- [no-multiple-empty-lines]
[no-multiple-empty-lines]: https://eslint.org/docs/rules/no-multiple-empty-lines
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v5.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/multiline-html-element-content-newline.js)
diff --git a/docs/rules/multiline-ternary.md b/docs/rules/multiline-ternary.md
new file mode 100644
index 000000000..1fe4d84fb
--- /dev/null
+++ b/docs/rules/multiline-ternary.md
@@ -0,0 +1,71 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/multiline-ternary
+description: Enforce newlines between operands of ternary expressions in ``
+since: v9.7.0
+---
+
+# vue/multiline-ternary
+
+> Enforce newlines between operands of ternary expressions 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.
+
+This rule is the same rule as [@stylistic/multiline-ternary] rule but it applies to the expressions in `` and `
+```
+
+
+
+## :books: Further Reading
+
+- [@stylistic/multiline-ternary]
+- [multiline-ternary]
+
+[@stylistic/multiline-ternary]: https://eslint.style/rules/default/multiline-ternary
+[multiline-ternary]: https://eslint.org/docs/rules/multiline-ternary
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.7.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/multiline-ternary.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/multiline-ternary.js)
+
+Taken with ❤️ [from ESLint Stylistic](https://eslint.style/rules/js/multiline-ternary)
diff --git a/docs/rules/mustache-interpolation-spacing.md b/docs/rules/mustache-interpolation-spacing.md
index 63721f8db..1e66fc942 100644
--- a/docs/rules/mustache-interpolation-spacing.md
+++ b/docs/rules/mustache-interpolation-spacing.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/mustache-interpolation-spacing
description: enforce unified spacing in mustache interpolations
+since: v3.13.0
---
+
# vue/mustache-interpolation-spacing
+
> enforce unified spacing in mustache interpolations
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
## :book: Rule Details
@@ -57,6 +60,10 @@ This rule aims at enforcing unified spacing in mustache interpolations.
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v3.13.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/mustache-interpolation-spacing.js)
diff --git a/docs/rules/name-property-casing.md b/docs/rules/name-property-casing.md
index 852d4e71d..e1840987b 100644
--- a/docs/rules/name-property-casing.md
+++ b/docs/rules/name-property-casing.md
@@ -3,12 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/name-property-casing
description: enforce specific casing for the name property in Vue components
+since: v3.8.0
---
+
# vue/name-property-casing
+
> enforce specific casing for the name property in Vue components
-- :warning: This rule was **deprecated** and replaced by [vue/component-definition-name-casing](component-definition-name-casing.md) rule.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :no_entry: This rule was **removed** in eslint-plugin-vue v9.0.0 and replaced by [vue/component-definition-name-casing](component-definition-name-casing.md) rule.
## :book: Rule Details
@@ -18,10 +20,10 @@ This rule aims at enforcing the style for the `name` property casing for consist
```vue
```
@@ -31,10 +33,10 @@ This rule aims at enforcing the style for the `name` property casing for consist
```vue
```
@@ -57,10 +59,10 @@ This rule aims at enforcing the style for the `name` property casing for consist
```vue
```
@@ -70,18 +72,22 @@ This rule aims at enforcing the style for the `name` property casing for consist
```vue
```
-## :books: Further reading
+## :books: Further Reading
+
+- [Style guide - Component name casing in JS/JSX](https://vuejs.org/style-guide/rules-strongly-recommended.html#component-name-casing-in-js-jsx)
+
+## :rocket: Version
-- [Style guide - Component name casing in JS/JSX](https://vuejs.org/v2/style-guide/#Component-name-casing-in-JS-JSX-strongly-recommended)
+This rule was introduced in eslint-plugin-vue v3.8.0
## :mag: Implementation
diff --git a/docs/rules/new-line-between-multi-line-property.md b/docs/rules/new-line-between-multi-line-property.md
new file mode 100644
index 000000000..1191a1567
--- /dev/null
+++ b/docs/rules/new-line-between-multi-line-property.md
@@ -0,0 +1,102 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/new-line-between-multi-line-property
+description: enforce new lines between multi-line properties in Vue components
+since: v7.3.0
+---
+
+# vue/new-line-between-multi-line-property
+
+> enforce new lines between multi-line properties in Vue components
+
+- :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.
+
+## :book: Rule Details
+
+This rule aims at enforcing new lines between multi-line properties in Vue components to help readability
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/new-line-between-multi-line-property": ["error", {
+ "minLineOfMultilineProperty": 2
+ }]
+}
+```
+
+- `minLineOfMultilineProperty` ... Define the minimum number of rows for a multi-line property. default `2`
+
+## :books: Further Reading
+
+- [Style guide - Empty lines in component/instance options](https://vuejs.org/style-guide/rules-recommended.html#empty-lines-in-component-instance-options)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.3.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/new-line-between-multi-line-property.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/new-line-between-multi-line-property.js)
diff --git a/docs/rules/next-tick-style.md b/docs/rules/next-tick-style.md
new file mode 100644
index 000000000..0e9d7812f
--- /dev/null
+++ b/docs/rules/next-tick-style.md
@@ -0,0 +1,109 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/next-tick-style
+description: enforce Promise or callback style in `nextTick`
+since: v7.5.0
+---
+
+# vue/next-tick-style
+
+> enforce Promise or callback style in `nextTick`
+
+- :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.
+
+## :book: Rule Details
+
+This rule enforces whether the callback version or Promise version (which was introduced in Vue v2.1.0) should be used in `Vue.nextTick` and `this.$nextTick`.
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Default is set to `promise`.
+
+```json
+{
+ "vue/next-tick-style": ["error", "promise" | "callback"]
+}
+```
+
+- `"promise"` (default) ... requires using the promise version.
+- `"callback"` ... requires using the callback version. Use this if you use a Vue version below v2.1.0.
+
+### `"callback"`
+
+
+
+```vue
+
+```
+
+
+
+## :books: Further Reading
+
+- [`Vue.nextTick` API in Vue 2](https://v2.vuejs.org/v2/api/#Vue-nextTick)
+- [`vm.$nextTick` API in Vue 2](https://v2.vuejs.org/v2/api/#vm-nextTick)
+- [Global API Treeshaking](https://v3-migration.vuejs.org/breaking-changes/global-api-treeshaking.html)
+- [Global `nextTick` API in Vue 3](https://vuejs.org/api/general.html#nexttick)
+- [Instance `$nextTick` API in Vue 3](https://vuejs.org/api/component-instance.html#nexttick)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.5.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/next-tick-style.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/next-tick-style.js)
diff --git a/docs/rules/no-arrow-functions-in-watch.md b/docs/rules/no-arrow-functions-in-watch.md
index 4378fe567..fb1f55215 100644
--- a/docs/rules/no-arrow-functions-in-watch.md
+++ b/docs/rules/no-arrow-functions-in-watch.md
@@ -3,15 +3,18 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-arrow-functions-in-watch
description: disallow using arrow functions to define watcher
+since: v7.0.0
---
+
# vue/no-arrow-functions-in-watch
+
> disallow using arrow functions to define watcher
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
## :book: Rule Details
-This rules disallows using arrow functions to defined watcher.The reason is arrow functions bind the parent context, so `this` will not be the Vue instance as you expect.([see here for more details](https://vuejs.org/v2/api/#watch))
+This rule disallows using arrow functions when defining a watcher. Arrow functions bind to their parent context, which means they will not have access to the Vue component instance via `this`. [See here for more details](https://vuejs.org/api/options-state.html#watch).
@@ -40,7 +43,7 @@ export default {
/* ... */
}
],
- 'e.f': function (val, oldVal) { /* ... */ }
+ 'e.f': function (val, oldVal) { /* ... */ },
/* ✗ BAD */
foo: (val, oldVal) => {
@@ -57,6 +60,10 @@ export default {
Nothing.
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-arrow-functions-in-watch.js)
diff --git a/docs/rules/no-async-in-computed-properties.md b/docs/rules/no-async-in-computed-properties.md
index f1ee921d0..814a53238 100644
--- a/docs/rules/no-async-in-computed-properties.md
+++ b/docs/rules/no-async-in-computed-properties.md
@@ -3,18 +3,21 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-async-in-computed-properties
description: disallow asynchronous actions in computed properties
+since: v3.8.0
---
+
# vue/no-async-in-computed-properties
+
> disallow asynchronous actions in computed properties
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
-Computed properties should be synchronous. Asynchronous actions inside them may not work as expected and can lead to an unexpected behaviour, that's why you should avoid them.
+Computed properties and functions should be synchronous. Asynchronous actions inside them may not work as expected and can lead to an unexpected behaviour, that's why you should avoid them.
If you need async computed properties you might want to consider using additional plugin [vue-async-computed]
## :book: Rule Details
-This rule is aimed at preventing asynchronous methods from being called in computed properties.
+This rule is aimed at preventing asynchronous methods from being called in computed properties and functions.
@@ -23,7 +26,7 @@ This rule is aimed at preventing asynchronous methods from being called in compu
export default {
computed: {
/* ✓ GOOD */
- foo () {
+ foo() {
var bar = 0
try {
bar = bar / this.a
@@ -35,22 +38,22 @@ export default {
},
/* ✗ BAD */
- pro () {
+ pro() {
return Promise.all([new Promise((resolve, reject) => {})])
},
foo1: async function () {
return await someFunc()
},
- bar () {
- return fetch(url).then(response => {})
+ bar() {
+ return fetch(url).then((response) => {})
},
- tim () {
- setTimeout(() => { }, 0)
+ tim() {
+ setTimeout(() => {}, 0)
},
- inter () {
- setInterval(() => { }, 0)
+ inter() {
+ setInterval(() => {}, 0)
},
- anim () {
+ anim() {
requestAnimationFrame(() => {})
}
}
@@ -60,14 +63,61 @@ export default {
+
+
+```vue
+
+```
+
+
+
## :wrench: Options
Nothing.
-## :books: Further reading
+## :books: Further Reading
- [vue-async-computed](https://github.com/foxbenjaminfox/vue-async-computed)
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v3.8.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-async-in-computed-properties.js)
diff --git a/docs/rules/no-bare-strings-in-template.md b/docs/rules/no-bare-strings-in-template.md
index 2edbfda6c..23a23c116 100644
--- a/docs/rules/no-bare-strings-in-template.md
+++ b/docs/rules/no-bare-strings-in-template.md
@@ -3,18 +3,20 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-bare-strings-in-template
description: disallow the use of bare strings in ``
+since: v7.0.0
---
+
# vue/no-bare-strings-in-template
+
> disallow the use of bare strings in ``
## :book: Rule Details
-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 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).
-
```vue
@@ -48,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.
:::
@@ -70,11 +72,11 @@ 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.
-## :couple: Related rules
+## :couple: Related Rules
- [vue/no-useless-v-bind]
- [vue/no-useless-mustaches]
@@ -82,6 +84,10 @@ If you want to report these string literals, enable the [vue/no-useless-v-bind]
[vue/no-useless-v-bind]: ./no-useless-v-bind.md
[vue/no-useless-mustaches]: ./no-useless-mustaches.md
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-bare-strings-in-template.js)
diff --git a/docs/rules/no-boolean-default.md b/docs/rules/no-boolean-default.md
index 74e53f92d..78fdbc270 100644
--- a/docs/rules/no-boolean-default.md
+++ b/docs/rules/no-boolean-default.md
@@ -3,19 +3,20 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-boolean-default
description: disallow boolean defaults
+since: v7.0.0
---
+
# vue/no-boolean-default
-> disallow boolean defaults
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> disallow boolean defaults
The rule prevents Boolean props from having a default value.
-
## :book: Rule Details
+
The rule is to enforce the HTML standard of always defaulting boolean attributes to false.
-
+
```vue
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.20.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-computed-properties-in-data.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-computed-properties-in-data.js)
diff --git a/docs/rules/no-confusing-v-for-v-if.md b/docs/rules/no-confusing-v-for-v-if.md
index 4e8fccbd7..f463c883e 100644
--- a/docs/rules/no-confusing-v-for-v-if.md
+++ b/docs/rules/no-confusing-v-for-v-if.md
@@ -3,11 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-confusing-v-for-v-if
description: disallow confusing `v-for` and `v-if` on the same element
+since: v3.0.0
---
+
# vue/no-confusing-v-for-v-if
+
> disallow confusing `v-for` and `v-if` on the same element
-- :warning: This rule was **deprecated** and replaced by [vue/no-use-v-if-with-v-for](no-use-v-if-with-v-for.md) rule.
+- :no_entry: This rule was **removed** in eslint-plugin-vue v9.0.0 and replaced by [vue/no-use-v-if-with-v-for](no-use-v-if-with-v-for.md) rule.
## :book: Rule Details
@@ -48,18 +51,22 @@ In that case, the `v-if` should be written on the wrapper element.
::: warning Note
When they exist on the same node, `v-for` has a higher priority than `v-if`. That means the `v-if` will be run on each iteration of the loop separately.
-[https://vuejs.org/v2/guide/list.html#v-for-with-v-if](https://vuejs.org/v2/guide/list.html#v-for-with-v-if)
+[https://vuejs.org/guide/essentials/list.html#v-for-with-v-if](https://vuejs.org/guide/essentials/list.html#v-for-with-v-if)
:::
## :wrench: Options
Nothing.
-## :books: Further reading
+## :books: Further Reading
+
+- [Style guide - Avoid v-if with v-for](https://vuejs.org/style-guide/rules-essential.html#avoid-v-if-with-v-for)
+- [Guide - Conditional Rendering / v-if with v-for](https://vuejs.org/guide/essentials/conditional.html#v-if-with-v-for)
+- [Guide - List Rendering / v-for with v-if](https://vuejs.org/guide/essentials/list.html#v-for-with-v-if)
+
+## :rocket: Version
-- [Style guide - Avoid v-if with v-for](https://vuejs.org/v2/style-guide/#Avoid-v-if-with-v-for-essential)
-- [Guide - Conditional / v-if with v-for](https://vuejs.org/v2/guide/conditional.html#v-if-with-v-for)
-- [Guide - List / v-for with v-if](https://vuejs.org/v2/guide/list.html#v-for-with-v-if)
+This rule was introduced in eslint-plugin-vue v3.0.0
## :mag: Implementation
diff --git a/docs/rules/no-console.md b/docs/rules/no-console.md
new file mode 100644
index 000000000..64ef0278e
--- /dev/null
+++ b/docs/rules/no-console.md
@@ -0,0 +1,34 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-console
+description: Disallow the use of `console` in ``
+since: v9.15.0
+---
+
+# vue/no-console
+
+> Disallow the use of `console` in ``
+
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+
+## :book: Rule Details
+
+This rule is the same rule as core [no-console] rule but it applies to the expressions in ``.
+
+## :books: Further Reading
+
+- [no-console]
+
+[no-console]: https://eslint.org/docs/latest/rules/no-console
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.15.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-console.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-console.js)
+
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/latest/rules/no-console)
diff --git a/docs/rules/no-constant-condition.md b/docs/rules/no-constant-condition.md
new file mode 100644
index 000000000..36520e25b
--- /dev/null
+++ b/docs/rules/no-constant-condition.md
@@ -0,0 +1,30 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-constant-condition
+description: Disallow constant expressions in conditions in ``
+since: v7.5.0
+---
+
+# vue/no-constant-condition
+
+> Disallow constant expressions in conditions in ``
+
+This rule is the same rule as core [no-constant-condition] rule but it applies to the expressions in ``.
+
+## :books: Further Reading
+
+- [no-constant-condition]
+
+[no-constant-condition]: https://eslint.org/docs/rules/no-constant-condition
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.5.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-constant-condition.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-constant-condition.js)
+
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/latest/rules/no-constant-condition)
diff --git a/docs/rules/no-custom-modifiers-on-v-model.md b/docs/rules/no-custom-modifiers-on-v-model.md
index f204294b0..f657f9cf7 100644
--- a/docs/rules/no-custom-modifiers-on-v-model.md
+++ b/docs/rules/no-custom-modifiers-on-v-model.md
@@ -3,15 +3,18 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-custom-modifiers-on-v-model
description: disallow custom modifiers on v-model used on the component
+since: v7.0.0
---
+
# vue/no-custom-modifiers-on-v-model
+
> disallow custom modifiers on v-model used on the component
-- :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
-This rule checks whether `v-model `used on the component do not have custom modifiers.
+This rule checks whether `v-model` used on the component do not have custom modifiers.
-## Rule Details
+## :book: Rule Details
This rule reports `v-model` directives in the following cases:
@@ -27,25 +30,27 @@ This rule reports `v-model` directives in the following cases:
-
-
```
-### Options
+## :wrench: Options
Nothing.
-## :couple: Related rules
+## :couple: Related Rules
+
+- [vue/valid-v-model]
+
+[vue/valid-v-model]: ./valid-v-model.md
-- [valid-v-model]
+## :rocket: Version
-[valid-v-model]: valid-v-model.md
+This rule was introduced in eslint-plugin-vue v7.0.0
## :mag: Implementation
diff --git a/docs/rules/no-deprecated-data-object-declaration.md b/docs/rules/no-deprecated-data-object-declaration.md
index 1875ff09f..214aacc30 100644
--- a/docs/rules/no-deprecated-data-object-declaration.md
+++ b/docs/rules/no-deprecated-data-object-declaration.md
@@ -3,18 +3,23 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-data-object-declaration
description: disallow using deprecated object declaration on data (in Vue.js 3.0.0+)
+since: v7.0.0
---
+
# vue/no-deprecated-data-object-declaration
+
> disallow using deprecated object declaration on data (in Vue.js 3.0.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+- :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.
## :book: Rule Details
This rule reports use of deprecated object declaration on `data` property (in Vue.js 3.0.0+).
The different from `vue/no-shared-component-data` is the root instance being also disallowed.
+See [Migration Guide - Data Option](https://v3-migration.vuejs.org/breaking-changes/data-option.html) for more details.
+
```js
@@ -27,7 +32,7 @@ createApp({
createApp({
/* ✓ GOOD */
- data () {
+ data() {
return {
foo: null
}
@@ -58,7 +63,7 @@ export default {
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Migration Guide - Removed APIs](https://v3-migration.vuejs.org/breaking-changes/#removed-apis)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.29.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-delete-set.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-delete-set.js)
diff --git a/docs/rules/no-deprecated-destroyed-lifecycle.md b/docs/rules/no-deprecated-destroyed-lifecycle.md
index f7f7cd8dc..9c681e0ea 100644
--- a/docs/rules/no-deprecated-destroyed-lifecycle.md
+++ b/docs/rules/no-deprecated-destroyed-lifecycle.md
@@ -3,30 +3,34 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-destroyed-lifecycle
description: disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+)
+since: v7.0.0
---
+
# vue/no-deprecated-destroyed-lifecycle
+
> disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+- :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.
## :book: Rule Details
This rule reports use of deprecated `destroyed` and `beforeDestroy` lifecycle hooks. (in Vue.js 3.0.0+).
-
+
```vue
```
@@ -37,6 +41,14 @@ export default {
Nothing.
+## :books: Further Reading
+
+- [Migration Guide - VNode Lifecycle Events](https://v3-migration.vuejs.org/breaking-changes/vnode-lifecycle-events.html#migration-strategy)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-destroyed-lifecycle.js)
diff --git a/docs/rules/no-deprecated-dollar-listeners-api.md b/docs/rules/no-deprecated-dollar-listeners-api.md
index 99097e15d..91854806b 100644
--- a/docs/rules/no-deprecated-dollar-listeners-api.md
+++ b/docs/rules/no-deprecated-dollar-listeners-api.md
@@ -3,11 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-dollar-listeners-api
description: disallow using deprecated `$listeners` (in Vue.js 3.0.0+)
+since: v7.0.0
---
+
# vue/no-deprecated-dollar-listeners-api
+
> disallow using deprecated `$listeners` (in Vue.js 3.0.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
## :book: Rule Details
@@ -41,9 +44,14 @@ export default {
Nothing.
-## :books: Further reading
+## :books: Further Reading
- [Vue RFCs - 0031-attr-fallthrough](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0031-attr-fallthrough.md)
+- [Migration Guide - `$listeners` removed](https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
## :mag: Implementation
diff --git a/docs/rules/no-deprecated-dollar-scopedslots-api.md b/docs/rules/no-deprecated-dollar-scopedslots-api.md
index 9b1d577c7..45e67d480 100644
--- a/docs/rules/no-deprecated-dollar-scopedslots-api.md
+++ b/docs/rules/no-deprecated-dollar-scopedslots-api.md
@@ -3,17 +3,22 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-dollar-scopedslots-api
description: disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+)
+since: v7.0.0
---
+
# vue/no-deprecated-dollar-scopedslots-api
+
> disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+- :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.
## :book: Rule Details
This rule reports use of deprecated `$scopedSlots`. (in Vue.js 3.0.0+).
+See [Migration Guide - Slots Unification](https://v3-migration.vuejs.org/breaking-changes/slots-unification.html) for more details.
+
```vue
@@ -37,10 +42,15 @@ export default {
Nothing.
-## :books: Further reading
+## :books: Further Reading
+- [Migration Guide - Slots Unification](https://v3-migration.vuejs.org/breaking-changes/slots-unification.html)
- [Vue RFCs - 0006-slots-unification](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0006-slots-unification.md)
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-dollar-scopedslots-api.js)
diff --git a/docs/rules/no-deprecated-events-api.md b/docs/rules/no-deprecated-events-api.md
index 11fb96a32..a3539d7cf 100644
--- a/docs/rules/no-deprecated-events-api.md
+++ b/docs/rules/no-deprecated-events-api.md
@@ -3,36 +3,49 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-events-api
description: disallow using deprecated events api (in Vue.js 3.0.0+)
+since: v7.0.0
---
+
# vue/no-deprecated-events-api
+
> disallow using deprecated events api (in Vue.js 3.0.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
## :book: Rule Details
This rule reports use of deprecated `$on`, `$off` `$once` api. (in Vue.js 3.0.0+).
+See [Migration Guide - Events API](https://v3-migration.vuejs.org/breaking-changes/events-api.html) for more details.
+
```vue
+```
+
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/no-deprecated-model-definition": ["error", {
+ "allowVue3Compat": true
+ }]
+}
+```
+
+### `"allowVue3Compat": true`
+
+Allow `model` definitions with prop/event names that match the Vue.js 3.0.0+ `v-model` syntax, i.e. `modelValue`/`update:modelValue` or `model-value`/`update:model-value`.
+
+
+
+```vue
+
+```
+
+
+
+## :couple: Related Rules
+
+- [vue/valid-model-definition](./valid-model-definition.md) (for Vue.js 2.x)
+- [vue/no-v-model-argument](./no-v-model-argument.md) (for Vue.js 2.x)
+
+## :books: Further Reading
+
+- [Migration Guide – `v-model`](https://v3-migration.vuejs.org/breaking-changes/v-model.html)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.16.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-model-definition.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-model-definition.js)
diff --git a/docs/rules/no-deprecated-props-default-this.md b/docs/rules/no-deprecated-props-default-this.md
new file mode 100644
index 000000000..af8136fa2
--- /dev/null
+++ b/docs/rules/no-deprecated-props-default-this.md
@@ -0,0 +1,77 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-props-default-this
+description: disallow deprecated `this` access in props default function (in Vue.js 3.0.0+)
+since: v7.0.0
+---
+
+# vue/no-deprecated-props-default-this
+
+> disallow deprecated `this` access in props default function (in Vue.js 3.0.0+)
+
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+
+## :book: Rule Details
+
+This rule reports the use of `this` within the props default value factory functions.
+In Vue.js 3.0.0+, props default value factory functions no longer have access to `this`.
+
+See [Migration Guide - Props Default Function `this` Access](https://v3-migration.vuejs.org/breaking-changes/props-default-this.html) for more details.
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Migration Guide - Props Default Function `this` Access](https://v3-migration.vuejs.org/breaking-changes/props-default-this.html)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-props-default-this.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-props-default-this.js)
diff --git a/docs/rules/no-deprecated-router-link-tag-prop.md b/docs/rules/no-deprecated-router-link-tag-prop.md
new file mode 100644
index 000000000..1ec049fd0
--- /dev/null
+++ b/docs/rules/no-deprecated-router-link-tag-prop.md
@@ -0,0 +1,97 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-router-link-tag-prop
+description: disallow using deprecated `tag` property on `RouterLink` (in Vue.js 3.0.0+)
+since: v7.20.0
+---
+
+# vue/no-deprecated-router-link-tag-prop
+
+> disallow using deprecated `tag` property on `RouterLink` (in Vue.js 3.0.0+)
+
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+
+## :book: Rule Details
+
+This rule reports deprecated the `tag` attribute on `RouterLink` elements (removed in Vue.js v3.0.0+).
+
+
+
+```vue
+
+
+ Home
+ Home
+
+
+
Home
+
+
+
+
Home
+
+
+ Home
+ Home
+
+
+ Home
+ Home
+ Home
+ Home
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/no-deprecated-router-link-tag-prop": ["error", {
+ "components": ['RouterLink']
+ }]
+}
+```
+
+- `components` (`string[]`) ... Component names which will be checked with the `tag` attribute. default `['RouterLink']`.
+
+Note: this rule will check both `kebab-case` and `PascalCase` versions of the
+given component names.
+
+### `{ "components": ['RouterLink', 'NuxtLink'] }`
+
+
+
+```vue
+
+
+ Home
+ Home
+
+ Home
+ Home
+
+ Home
+ Home
+
+ Home
+ Home
+
+```
+
+
+
+## :books: Further Reading
+
+- [Vue RFCs - 0021-router-link-scoped-slot](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0021-router-link-scoped-slot.md)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.20.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-router-link-tag-prop.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-router-link-tag-prop.js)
diff --git a/docs/rules/no-deprecated-scope-attribute.md b/docs/rules/no-deprecated-scope-attribute.md
index d8dcbede3..d19b172a8 100644
--- a/docs/rules/no-deprecated-scope-attribute.md
+++ b/docs/rules/no-deprecated-scope-attribute.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-scope-attribute
description: disallow deprecated `scope` attribute (in Vue.js 2.5.0+)
+since: v6.0.0
---
+
# vue/no-deprecated-scope-attribute
+
> disallow deprecated `scope` attribute (in Vue.js 2.5.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+- :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.
## :book: Rule Details
@@ -38,9 +41,13 @@ This rule reports deprecated `scope` attribute in Vue.js v2.5.0+.
-## :books: Further reading
+## :books: Further Reading
+
+- [API - scope](https://v2.vuejs.org/v2/api/#scope-removed)
+
+## :rocket: Version
-- [API - scope](https://vuejs.org/v2/api/#scope-removed)
+This rule was introduced in eslint-plugin-vue v6.0.0
## :mag: Implementation
diff --git a/docs/rules/no-deprecated-slot-attribute.md b/docs/rules/no-deprecated-slot-attribute.md
index aacfcbda8..64f2c5e00 100644
--- a/docs/rules/no-deprecated-slot-attribute.md
+++ b/docs/rules/no-deprecated-slot-attribute.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-slot-attribute
description: disallow deprecated `slot` attribute (in Vue.js 2.6.0+)
+since: v6.1.0
---
+
# vue/no-deprecated-slot-attribute
+
> disallow deprecated `slot` attribute (in Vue.js 2.6.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+- :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.
## :book: Rule Details
@@ -35,9 +38,56 @@ This rule reports deprecated `slot` attribute in Vue.js v2.6.0+.
-## :books: Further reading
+## :wrench: Options
+
+```json
+{
+ "vue/no-deprecated-slot-attribute": ["error", {
+ "ignore": ["my-component"]
+ }]
+}
+```
+
+- `"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": ["my-component"]`
+
+
+
+```vue
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+```
+
+
+
+## :books: Further Reading
+
+- [API - slot](https://v2.vuejs.org/v2/api/#slot-deprecated)
+
+## :rocket: Version
-- [API - slot](https://vuejs.org/v2/api/#slot-deprecated)
+This rule was introduced in eslint-plugin-vue v6.1.0
## :mag: Implementation
diff --git a/docs/rules/no-deprecated-slot-scope-attribute.md b/docs/rules/no-deprecated-slot-scope-attribute.md
index 321d7a956..86212de0f 100644
--- a/docs/rules/no-deprecated-slot-scope-attribute.md
+++ b/docs/rules/no-deprecated-slot-scope-attribute.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-slot-scope-attribute
description: disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+)
+since: v6.1.0
---
+
# vue/no-deprecated-slot-scope-attribute
+
> disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+- :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.
## :book: Rule Details
@@ -35,9 +38,13 @@ This rule reports deprecated `slot-scope` attribute in Vue.js v2.6.0+.
-## :books: Further reading
+## :books: Further Reading
+
+- [API - slot-scope](https://v2.vuejs.org/v2/api/#slot-scope-deprecated)
+
+## :rocket: Version
-- [API - slot-scope](https://vuejs.org/v2/api/#slot-scope-deprecated)
+This rule was introduced in eslint-plugin-vue v6.1.0
## :mag: Implementation
diff --git a/docs/rules/no-deprecated-v-bind-sync.md b/docs/rules/no-deprecated-v-bind-sync.md
index 75359741a..77a79f16f 100644
--- a/docs/rules/no-deprecated-v-bind-sync.md
+++ b/docs/rules/no-deprecated-v-bind-sync.md
@@ -3,31 +3,35 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-v-bind-sync
description: disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+)
+since: v7.0.0
---
+
# vue/no-deprecated-v-bind-sync
+
> disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+- :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.
## :book: Rule Details
-This rule reports use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+)
+This rule reports use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+).
+
+See [Migration Guide - `v-model`](https://v3-migration.vuejs.org/breaking-changes/v-model.html) for more details.
```vue
-
-
-
+
+
-
-
-
-
+
+
+
+
```
@@ -37,16 +41,21 @@ This rule reports use of deprecated `.sync` modifier on `v-bind` directive (in V
Nothing.
-## :couple: Related rules
+## :couple: Related Rules
-- [valid-v-bind]
+- [vue/valid-v-bind]
-[valid-v-bind]: valid-v-bind.md
+[vue/valid-v-bind]: ./valid-v-bind.md
-## :books: Further reading
+## :books: Further Reading
+- [Migration Guide - `v-model`](https://v3-migration.vuejs.org/breaking-changes/v-model.html)
- [Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0005-replace-v-bind-sync-with-v-model-argument.md)
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-v-bind-sync.js)
diff --git a/docs/rules/no-deprecated-v-is.md b/docs/rules/no-deprecated-v-is.md
new file mode 100644
index 000000000..9fd1590e4
--- /dev/null
+++ b/docs/rules/no-deprecated-v-is.md
@@ -0,0 +1,55 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-v-is
+description: disallow deprecated `v-is` directive (in Vue.js 3.1.0+)
+since: v7.11.0
+---
+
+# vue/no-deprecated-v-is
+
+> disallow deprecated `v-is` directive (in Vue.js 3.1.0+)
+
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+
+## :book: Rule Details
+
+This rule reports deprecated `v-is` directive in Vue.js v3.1.0+.
+
+Use [`is` attribute with `vue:` prefix](https://vuejs.org/api/built-in-special-attributes.html#is) instead.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :couple: Related Rules
+
+- [vue/valid-v-is]
+
+[vue/valid-v-is]: ./valid-v-is.md
+
+## :books: Further Reading
+
+- [Migration Guide - Custom Elements Interop](https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html#vue-prefix-for-in-dom-template-parsing-workarounds)
+- [API - v-is](https://vuejs.org/api/built-in-special-attributes.html#is)
+- [API - v-is (Old)](https://github.com/vuejs/docs-next/blob/008613756c3d781128d96b64a2d27f7598f8f548/src/api/directives.md#v-is)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.11.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-v-is.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-v-is.js)
diff --git a/docs/rules/no-deprecated-v-on-native-modifier.md b/docs/rules/no-deprecated-v-on-native-modifier.md
index 93620cd7b..8b8312235 100644
--- a/docs/rules/no-deprecated-v-on-native-modifier.md
+++ b/docs/rules/no-deprecated-v-on-native-modifier.md
@@ -3,11 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-v-on-native-modifier
description: disallow using deprecated `.native` modifiers (in Vue.js 3.0.0+)
+since: v7.0.0
---
+
# vue/no-deprecated-v-on-native-modifier
+
> disallow using deprecated `.native` modifiers (in Vue.js 3.0.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
## :book: Rule Details
@@ -33,15 +36,20 @@ This rule reports use of deprecated `.native` modifier on `v-on` directive (in V
Nothing.
-## :couple: Related rules
+## :couple: Related Rules
-- [valid-v-on]
+- [vue/valid-v-on]
-[valid-v-on]: valid-v-on.md
+[vue/valid-v-on]: ./valid-v-on.md
-## :books: Further reading
+## :books: Further Reading
- [Vue RFCs - 0031-attr-fallthrough](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0031-attr-fallthrough.md)
+- [Migration Guide - `v-on.native` modifier removed](https://v3-migration.vuejs.org/breaking-changes/v-on-native-modifier-removed.html)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
## :mag: Implementation
diff --git a/docs/rules/no-deprecated-v-on-number-modifiers.md b/docs/rules/no-deprecated-v-on-number-modifiers.md
index 5b720015f..54ea7d431 100644
--- a/docs/rules/no-deprecated-v-on-number-modifiers.md
+++ b/docs/rules/no-deprecated-v-on-number-modifiers.md
@@ -3,16 +3,21 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-v-on-number-modifiers
description: disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+)
+since: v7.0.0
---
+
# vue/no-deprecated-v-on-number-modifiers
+
> disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+- :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.
## :book: Rule Details
-This rule reports use of deprecated `KeyboardEvent.keyCode` modifier on `v-on` directive (in Vue.js 3.0.0+)
+This rule reports use of deprecated `KeyboardEvent.keyCode` modifier on `v-on` directive (in Vue.js 3.0.0+).
+
+See [Migration Guide - KeyCode Modifiers](https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html) for more details.
@@ -36,16 +41,21 @@ This rule reports use of deprecated `KeyboardEvent.keyCode` modifier on `v-on` d
Nothing.
-## :couple: Related rules
+## :couple: Related Rules
-- [valid-v-on]
+- [vue/valid-v-on]
-[valid-v-on]: valid-v-on.md
+[vue/valid-v-on]: ./valid-v-on.md
-## :books: Further reading
+## :books: Further Reading
+- [Migration Guide - KeyCode Modifiers](https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html)
- [Vue RFCs - 0014-drop-keycode-support](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0014-drop-keycode-support.md)
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-v-on-number-modifiers.js)
diff --git a/docs/rules/no-deprecated-vue-config-keycodes.md b/docs/rules/no-deprecated-vue-config-keycodes.md
index 8311e8a27..697604b8c 100644
--- a/docs/rules/no-deprecated-vue-config-keycodes.md
+++ b/docs/rules/no-deprecated-vue-config-keycodes.md
@@ -3,15 +3,20 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-deprecated-vue-config-keycodes
description: disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+)
+since: v7.0.0
---
+
# vue/no-deprecated-vue-config-keycodes
+
> disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+)
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
## :book: Rule Details
-This rule reports use of deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+)
+This rule reports use of deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+).
+
+See [Migration Guide - KeyCode Modifiers](https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html) for more details.
@@ -28,19 +33,25 @@ Vue.config.keyCodes = {
Nothing.
-## :couple: Related rules
+## :couple: Related Rules
- [vue/no-deprecated-v-on-number-modifiers]
[vue/no-deprecated-v-on-number-modifiers]: ./no-deprecated-v-on-number-modifiers.md
-## :books: Further reading
+## :books: Further Reading
+- [Migration Guide - KeyCode Modifiers]
- [Vue RFCs - 0014-drop-keycode-support]
- [API - Global Config - keyCodes]
+[Migration Guide - KeyCode Modifiers]: https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html
[Vue RFCs - 0014-drop-keycode-support]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0014-drop-keycode-support.md
-[API - Global Config - keyCodes]: https://vuejs.org/v2/api/#keyCodes
+[API - Global Config - keyCodes]: https://v2.vuejs.org/v2/api/#keyCodes
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
## :mag: Implementation
diff --git a/docs/rules/no-dupe-keys.md b/docs/rules/no-dupe-keys.md
index b4e2834b8..3372a8e26 100644
--- a/docs/rules/no-dupe-keys.md
+++ b/docs/rules/no-dupe-keys.md
@@ -3,17 +3,21 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-dupe-keys
description: disallow duplication of field names
+since: v3.9.0
---
+
# vue/no-dupe-keys
+
> disallow duplication of field names
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
-This rule prevents to use duplicated names.
+This rule prevents using duplicate key names.
## :book: Rule Details
-This rule is aimed at preventing duplicated property names.
+This rule prevents duplicate `props`/`data`/`methods`/etc. key names defined on a component.
+Even if a key name does not conflict in the `
@@ -62,10 +66,10 @@ export default {
/* ✗ BAD */
export default {
computed: {
- foo () {}
+ foo() {}
},
firebase: {
- foo () {}
+ foo() {}
}
}
@@ -73,6 +77,10 @@ export default {
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v3.9.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-dupe-keys.js)
diff --git a/docs/rules/no-dupe-v-else-if.md b/docs/rules/no-dupe-v-else-if.md
new file mode 100644
index 000000000..d5bf83797
--- /dev/null
+++ b/docs/rules/no-dupe-v-else-if.md
@@ -0,0 +1,105 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-dupe-v-else-if
+description: disallow duplicate conditions in `v-if` / `v-else-if` chains
+since: v7.0.0
+---
+
+# vue/no-dupe-v-else-if
+
+> disallow duplicate conditions in `v-if` / `v-else-if` chains
+
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+
+## :book: Rule Details
+
+This rule disallows duplicate conditions in the same `v-if` / `v-else-if` chain.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+This rule can also detect some cases where the conditions are not identical, but the branch can never execute due to the logic of `||` and `&&` operators.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [no-dupe-else-if]
+
+[no-dupe-else-if]: https://eslint.org/docs/rules/no-dupe-else-if
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-dupe-v-else-if.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-dupe-v-else-if.js)
diff --git a/docs/rules/no-duplicate-attr-inheritance.md b/docs/rules/no-duplicate-attr-inheritance.md
index 959124e48..fe3cd37bf 100644
--- a/docs/rules/no-duplicate-attr-inheritance.md
+++ b/docs/rules/no-duplicate-attr-inheritance.md
@@ -3,16 +3,19 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-duplicate-attr-inheritance
description: enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"`
+since: v7.0.0
---
+
# vue/no-duplicate-attr-inheritance
+
> enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"`
## :book: Rule Details
-This rule aims to prevent duplicated attribute inheritance.
-This rule to warn to apply `inheritAttrs: false` when it detects `v-bind="$attrs"` being used.
+This rule aims to prevent duplicate attribute inheritance.
+This rule suggests applying `inheritAttrs: false` when it detects `v-bind="$attrs"` being used.
-
+
```vue
@@ -23,11 +26,12 @@ export default {
/* ✓ GOOD */
inheritAttrs: false
}
+
```
-
+
```vue
@@ -38,17 +42,50 @@ export default {
/* ✗ BAD */
// inheritAttrs: true (default)
}
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/no-duplicate-attr-inheritance": ["error", {
+ "checkMultiRootNodes": false,
+ }]
+}
+```
+
+- `"checkMultiRootNodes"`: If set to `true`, also suggest applying `inheritAttrs: false` to components with multiple root nodes (where `inheritAttrs: false` is the implicit default, see [attribute inheritance on multiple root nodes](https://vuejs.org/guide/components/attrs.html#attribute-inheritance-on-multiple-root-nodes)), whenever it detects `v-bind="$attrs"` being used. Default is `false`, which will ignore components with multiple root nodes.
+
+### `"checkMultiRootNodes": true`
+
+
+
+```vue
+
+
+
+
+
```
-### Options
+## :books: Further Reading
-Nothing.
+- [API - inheritAttrs](https://vuejs.org/api/options-misc.html#inheritattrs)
+- [Fallthrough Attributes](https://vuejs.org/guide/components/attrs.html#attribute-inheritance-on-multiple-root-nodes)
-## Further Reading
+## :rocket: Version
-- [API - inheritAttrs](https://vuejs.org/v2/api/index.html#inheritAttrs)
+This rule was introduced in eslint-plugin-vue v7.0.0
## :mag: Implementation
diff --git a/docs/rules/no-duplicate-attributes.md b/docs/rules/no-duplicate-attributes.md
index 32db6e44c..9ee2d2be6 100644
--- a/docs/rules/no-duplicate-attributes.md
+++ b/docs/rules/no-duplicate-attributes.md
@@ -3,19 +3,21 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-duplicate-attributes
description: disallow duplication of attributes
+since: v3.0.0
---
+
# vue/no-duplicate-attributes
+
> disallow duplication of attributes
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
-When duplicate arguments exist, only the last one is valid.
-It's possibly mistakes.
+When there are multiple attributes with the same name on a component, only the last one is used and the rest are ignored, so this is usually a mistake.
## :book: Rule Details
This rule reports duplicate attributes.
-`v-bind:foo` directives are handled as the attributes `foo`.
+`v-bind:foo` directives are handled as the attribute `foo`.
@@ -51,8 +53,8 @@ This rule reports duplicate attributes.
- `allowCoexistClass` (`boolean`) ... Enables [`v-bind:class`] directive can coexist with the plain `class` attribute. Default is `true`.
- `allowCoexistStyle` (`boolean`) ... Enables [`v-bind:style`] directive can coexist with the plain `style` attribute. Default is `true`.
-[`v-bind:class`]: https://vuejs.org/v2/guide/class-and-style.html
-[`v-bind:style`]: https://vuejs.org/v2/guide/class-and-style.html
+[`v-bind:class`]: https://vuejs.org/guide/essentials/class-and-style.html
+[`v-bind:style`]: https://vuejs.org/guide/essentials/class-and-style.html
### `"allowCoexistClass": false, "allowCoexistStyle": false`
@@ -68,6 +70,10 @@ This rule reports duplicate attributes.
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v3.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-duplicate-attributes.js)
diff --git a/docs/rules/no-empty-component-block.md b/docs/rules/no-empty-component-block.md
index 25cc0ef1f..344bd0cba 100644
--- a/docs/rules/no-empty-component-block.md
+++ b/docs/rules/no-empty-component-block.md
@@ -3,33 +3,38 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-empty-component-block
description: disallow the `` `
@@ -41,8 +46,7 @@ See: https://vue-loader.vuejs.org/spec.html#src-imports
-
-// ✗ BAD
+
@@ -62,6 +66,10 @@ See: https://vue-loader.vuejs.org/spec.html#src-imports
Nothing.
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-empty-component-block.js)
diff --git a/docs/rules/no-empty-pattern.md b/docs/rules/no-empty-pattern.md
index a86204a92..6434a7bff 100644
--- a/docs/rules/no-empty-pattern.md
+++ b/docs/rules/no-empty-pattern.md
@@ -2,22 +2,29 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/no-empty-pattern
-description: disallow empty destructuring patterns
+description: Disallow empty destructuring patterns in ``
+since: v6.0.0
---
+
# vue/no-empty-pattern
-> disallow empty destructuring patterns
+
+> Disallow empty destructuring patterns in ``
This rule is the same rule as core [no-empty-pattern] rule but it applies to the expressions in ``.
-## :books: Further reading
+## :books: Further Reading
- [no-empty-pattern]
[no-empty-pattern]: https://eslint.org/docs/rules/no-empty-pattern
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v6.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-empty-pattern.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-empty-pattern.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/no-empty-pattern)
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/latest/rules/no-empty-pattern)
diff --git a/docs/rules/no-export-in-script-setup.md b/docs/rules/no-export-in-script-setup.md
new file mode 100644
index 000000000..08166596b
--- /dev/null
+++ b/docs/rules/no-export-in-script-setup.md
@@ -0,0 +1,61 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-export-in-script-setup
+description: disallow `export` in `
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Vue RFCs - 0040-script-setup]
+
+[Vue RFCs - 0040-script-setup]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.13.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-export-in-script-setup.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-export-in-script-setup.js)
diff --git a/docs/rules/no-expose-after-await.md b/docs/rules/no-expose-after-await.md
new file mode 100644
index 000000000..7f8fe9137
--- /dev/null
+++ b/docs/rules/no-expose-after-await.md
@@ -0,0 +1,73 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-expose-after-await
+description: disallow asynchronously registered `expose`
+since: v8.1.0
+---
+
+# vue/no-expose-after-await
+
+> disallow asynchronously registered `expose`
+
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
+
+## :book: Rule Details
+
+This rule reports usages of `expose()` and `defineExpose()` after an `await` expression.
+In the `setup()` function, `expose()` should be registered synchronously.
+In the `
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Vue RFCs - 0042-expose-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0042-expose-api.md)
+- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v8.1.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-expose-after-await.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-expose-after-await.js)
diff --git a/docs/rules/no-extra-parens.md b/docs/rules/no-extra-parens.md
index dc0392ef7..442a965ba 100644
--- a/docs/rules/no-extra-parens.md
+++ b/docs/rules/no-extra-parens.md
@@ -2,19 +2,27 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/no-extra-parens
-description: disallow unnecessary parentheses
+description: Disallow unnecessary parentheses in ``
+since: v7.0.0
---
+
# vue/no-extra-parens
-> disallow unnecessary parentheses
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+> Disallow unnecessary parentheses 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.
+
+This rule is the same rule as [@stylistic/no-extra-parens] rule but it applies to the expressions in ``.
-This rule is the same rule as core [no-extra-parens] rule but it applies to the expressions in ``.
+This rule extends the rule that [@stylistic/eslint-plugin] has, but if [@stylistic/eslint-plugin] is not installed, this rule extracts and extends the same rule from ESLint core.
+However, if neither is found, the rule cannot be used.
+
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
## :book: Rule Details
This rule restricts the use of parentheses to only where they are necessary.
-This rule extends the core [no-extra-parens] rule and applies it to the ``. This rule also checks some Vue.js syntax.
+This rule extends the [@stylistic/no-extra-parens] rule and applies it to the ``. This rule also checks some Vue.js syntax.
@@ -33,15 +41,21 @@ This rule extends the core [no-extra-parens] rule and applies it to the `
-## :books: Further reading
+## :books: Further Reading
+- [@stylistic/no-extra-parens]
- [no-extra-parens]
+[@stylistic/no-extra-parens]: https://eslint.style/rules/default/no-extra-parens
[no-extra-parens]: https://eslint.org/docs/rules/no-extra-parens
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-extra-parens.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-extra-parens.js)
-Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/no-extra-parens)
+Taken with ❤️ [from ESLint Stylistic](https://eslint.style/rules/ts/no-extra-parens)
diff --git a/docs/rules/no-implicit-coercion.md b/docs/rules/no-implicit-coercion.md
new file mode 100644
index 000000000..22698320c
--- /dev/null
+++ b/docs/rules/no-implicit-coercion.md
@@ -0,0 +1,32 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-implicit-coercion
+description: Disallow shorthand type conversions in ``
+since: v9.33.0
+---
+
+# vue/no-implicit-coercion
+
+> 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.
+
+This rule is the same rule as core [no-implicit-coercion] rule but it applies to the expressions in ``.
+
+## :books: Further Reading
+
+- [no-implicit-coercion]
+
+[no-implicit-coercion]: https://eslint.org/docs/rules/no-implicit-coercion
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.33.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-implicit-coercion.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-implicit-coercion.js)
+
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/latest/rules/no-implicit-coercion)
diff --git a/docs/rules/no-import-compiler-macros.md b/docs/rules/no-import-compiler-macros.md
new file mode 100644
index 000000000..7ceadb336
--- /dev/null
+++ b/docs/rules/no-import-compiler-macros.md
@@ -0,0 +1,58 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-import-compiler-macros
+description: disallow importing Vue compiler macros
+since: v10.0.0
+---
+
+# vue/no-import-compiler-macros
+
+> disallow importing Vue compiler macros
+
+- :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.
+
+## :book: Rule Details
+
+This rule disallow importing vue compiler macros.
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [defineProps() & defineEmits()]
+
+[defineProps() & defineEmits()]: https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v10.0.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-import-compiler-macros.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-import-compiler-macros.js)
diff --git a/docs/rules/no-invalid-model-keys.md b/docs/rules/no-invalid-model-keys.md
new file mode 100644
index 000000000..e93c79db4
--- /dev/null
+++ b/docs/rules/no-invalid-model-keys.md
@@ -0,0 +1,121 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-invalid-model-keys
+description: require valid keys in model option
+since: v7.9.0
+---
+
+# vue/no-invalid-model-keys
+
+> require valid keys in model option
+
+- :no_entry: This rule was **removed** in eslint-plugin-vue v10.0.0 and replaced by [vue/valid-model-definition](valid-model-definition.md) rule.
+
+## :book: Rule Details
+
+This rule is aimed at preventing invalid keys in model option.
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.9.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-invalid-model-keys.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-invalid-model-keys.js)
diff --git a/docs/rules/no-irregular-whitespace.md b/docs/rules/no-irregular-whitespace.md
index bb6da716d..615f2294a 100644
--- a/docs/rules/no-irregular-whitespace.md
+++ b/docs/rules/no-irregular-whitespace.md
@@ -2,10 +2,13 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/no-irregular-whitespace
-description: disallow irregular whitespace
+description: disallow irregular whitespace in `.vue` files
+since: v6.1.0
---
+
# vue/no-irregular-whitespace
-> disallow irregular whitespace
+
+> disallow irregular whitespace in `.vue` files
## :book: Rule Details
@@ -157,12 +160,16 @@ var foo = ``
-## :books: Further reading
+## :books: Further Reading
- [no-irregular-whitespace]
[no-irregular-whitespace]: https://eslint.org/docs/rules/no-irregular-whitespace
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v6.1.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-irregular-whitespace.js)
diff --git a/docs/rules/no-lifecycle-after-await.md b/docs/rules/no-lifecycle-after-await.md
index 80f9d1b5e..78d54c526 100644
--- a/docs/rules/no-lifecycle-after-await.md
+++ b/docs/rules/no-lifecycle-after-await.md
@@ -3,11 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-lifecycle-after-await
description: disallow asynchronously registered lifecycle hooks
+since: v7.0.0
---
+
# vue/no-lifecycle-after-await
+
> disallow asynchronously registered lifecycle hooks
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/recommended"]`.
## :book: Rule Details
@@ -39,10 +42,15 @@ export default {
Nothing.
-## :books: Further reading
+## :books: Further Reading
+- [Guide - Composition API - Lifecycle Hooks](https://vuejs.org/api/composition-api-lifecycle.html)
- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-lifecycle-after-await.js)
diff --git a/docs/rules/no-lone-template.md b/docs/rules/no-lone-template.md
new file mode 100644
index 000000000..170b63ce3
--- /dev/null
+++ b/docs/rules/no-lone-template.md
@@ -0,0 +1,89 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-lone-template
+description: disallow unnecessary ``
+since: v7.0.0
+---
+
+# vue/no-lone-template
+
+> disallow unnecessary ``
+
+- :gear: This rule is included in all of `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+
+## :book: Rule Details
+
+This rule aims to eliminate unnecessary and potentially confusing ``.
+In Vue.js 2.x, the `` elements that have no specific directives have no effect.
+In Vue.js 3.x, the `` elements that have no specific directives render the `` elements as is, but in most cases this may not be what you intended.
+
+
+
+```vue
+
+
+ ...
+ ...
+ ...
+ ...
+ ...
+
+
+ ...
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/no-lone-template": ["error", {
+ "ignoreAccessible": false
+ }]
+}
+```
+
+- `ignoreAccessible` ... If `true`, ignore accessible `` elements. default `false`.
+ Note: this option is useless if you are using Vue.js 2.x.
+
+### `"ignoreAccessible": true`
+
+
+
+```vue
+
+
+ ...
+ ...
+
+
+ ...
+
+```
+
+
+
+## :mute: When Not To Use It
+
+If you are using Vue.js 3.x and want to define the `` element intentionally, you will have to turn this rule off or use `"ignoreAccessible"` option.
+
+## :couple: Related Rules
+
+- [vue/no-template-key]
+- [no-lone-blocks]
+
+[no-lone-blocks]: https://eslint.org/docs/rules/no-lone-blocks
+[vue/no-template-key]: ./no-template-key.md
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-lone-template.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-lone-template.js)
diff --git a/docs/rules/no-loss-of-precision.md b/docs/rules/no-loss-of-precision.md
new file mode 100644
index 000000000..c9b88ce63
--- /dev/null
+++ b/docs/rules/no-loss-of-precision.md
@@ -0,0 +1,34 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-loss-of-precision
+description: Disallow literal numbers that lose precision in ``
+since: v8.0.0
+---
+
+# vue/no-loss-of-precision
+
+> Disallow literal numbers that lose precision in ``
+
+This rule is the same rule as core [no-loss-of-precision] rule but it applies to the expressions in ``.
+
+:::warning
+You must be using ESLint v7.1.0 or later to use this rule.
+:::
+
+## :books: Further Reading
+
+- [no-loss-of-precision]
+
+[no-loss-of-precision]: https://eslint.org/docs/rules/no-loss-of-precision
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v8.0.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-loss-of-precision.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-loss-of-precision.js)
+
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/latest/rules/no-loss-of-precision)
diff --git a/docs/rules/no-multi-spaces.md b/docs/rules/no-multi-spaces.md
index 765b81bc0..66c6a81d7 100644
--- a/docs/rules/no-multi-spaces.md
+++ b/docs/rules/no-multi-spaces.md
@@ -3,12 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-multi-spaces
description: disallow multiple spaces
+since: v3.12.0
---
+
# vue/no-multi-spaces
+
> disallow multiple spaces
-- :gear: This rule is included in all of `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+- :gear: This rule is included in all of `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
## :book: Rule Details
@@ -73,6 +76,10 @@ This rule aims at removing multiple spaces in tags, which are not used for inden
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v3.12.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-multi-spaces.js)
diff --git a/docs/rules/no-multiple-objects-in-class.md b/docs/rules/no-multiple-objects-in-class.md
index a7043767e..13f1f591b 100644
--- a/docs/rules/no-multiple-objects-in-class.md
+++ b/docs/rules/no-multiple-objects-in-class.md
@@ -2,14 +2,17 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/no-multiple-objects-in-class
-description: disallow to pass multiple objects into array to class
+description: disallow passing multiple objects in an array to class
+since: v7.0.0
---
+
# vue/no-multiple-objects-in-class
-> disallow to pass multiple objects into array to class
+
+> disallow passing multiple objects in an array to class
## :book: Rule Details
-This rule disallows to pass multiple objects into array to class.
+This rule disallows to pass multiple objects into array to class.
@@ -17,10 +20,10 @@ This rule disallows to pass multiple objects into array to class.
-
+
-
+
```
@@ -31,6 +34,10 @@ This rule disallows to pass multiple objects into array to class.
Nothing.
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-multiple-objects-in-class.js)
diff --git a/docs/rules/no-multiple-slot-args.md b/docs/rules/no-multiple-slot-args.md
index d0d319458..74d6c2931 100644
--- a/docs/rules/no-multiple-slot-args.md
+++ b/docs/rules/no-multiple-slot-args.md
@@ -2,12 +2,15 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/no-multiple-slot-args
-description: disallow to pass multiple arguments to scoped slots
+description: disallow passing multiple arguments to scoped slots
+since: v7.0.0
---
+
# vue/no-multiple-slot-args
-> disallow to pass multiple arguments to scoped slots
-- :gear: This rule is included in `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+> disallow passing multiple arguments to scoped slots
+
+- :gear: This rule is included in all of `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
## :book: Rule Details
@@ -39,10 +42,14 @@ export default {
Nothing.
-## :books: Further reading
+## :books: Further Reading
- [vuejs/vue#9468](https://github.com/vuejs/vue/issues/9468#issuecomment-462210146)
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-multiple-slot-args.js)
diff --git a/docs/rules/no-multiple-template-root.md b/docs/rules/no-multiple-template-root.md
index 3151ffe7c..8a85c9201 100644
--- a/docs/rules/no-multiple-template-root.md
+++ b/docs/rules/no-multiple-template-root.md
@@ -3,11 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-multiple-template-root
description: disallow adding multiple root nodes to the template
+since: v7.0.0
---
+
# vue/no-multiple-template-root
+
> disallow adding multiple root nodes to the template
-- :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
## :book: Rule Details
@@ -39,7 +42,7 @@ This rule checks whether template contains single root element valid for Vue 2.
```vue
-
+
```
@@ -58,7 +61,36 @@ 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
+
+This rule was introduced in eslint-plugin-vue v7.0.0
## :mag: Implementation
diff --git a/docs/rules/no-mutating-props.md b/docs/rules/no-mutating-props.md
index 2d0833333..3ddee4cc2 100644
--- a/docs/rules/no-mutating-props.md
+++ b/docs/rules/no-mutating-props.md
@@ -3,11 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-mutating-props
description: disallow mutation of component props
+since: v7.0.0
---
+
# vue/no-mutating-props
+
> disallow mutation of component props
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
## :book: Rule Details
@@ -20,22 +23,38 @@ This rule reports mutation of component props.
+
+
```
@@ -48,22 +67,38 @@ This rule reports mutation of component props.
+
+
```
@@ -73,12 +108,12 @@ This rule reports mutation of component props.
```vue
```
@@ -86,12 +121,54 @@ This rule reports mutation of component props.
## :wrench: Options
-Nothing.
+```json
+{
+ "vue/no-mutating-props": ["error", {
+ "shallowOnly": false
+ }]
+}
+```
+
+- "shallowOnly" (`boolean`) Enables mutating the value of a prop but leaving the reference the same. Default is `false`.
+
+### "shallowOnly": true
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
+## :books: Further Reading
+
+- [Style guide - Implicit parent-child communication](https://vuejs.org/style-guide/rules-use-with-caution.html#implicit-parent-child-communication)
+- [Vue - Prop Mutation - deprecated](https://v2.vuejs.org/v2/guide/migration.html#Prop-Mutation-deprecated)
-## :books: Further reading
+## :rocket: Version
-- [Vue - Prop Mutation - deprecated](https://vuejs.org/v2/guide/migration.html#Prop-Mutation-deprecated)
-- [Style guide - Implicit parent-child communication](https://vuejs.org/v2/style-guide/#Implicit-parent-child-communication-use-with-caution)
+This rule was introduced in eslint-plugin-vue v7.0.0
## :mag: Implementation
diff --git a/docs/rules/no-parsing-error.md b/docs/rules/no-parsing-error.md
index 6416fb246..a6fcf2908 100644
--- a/docs/rules/no-parsing-error.md
+++ b/docs/rules/no-parsing-error.md
@@ -3,21 +3,24 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-parsing-error
description: disallow parsing errors in ``
+since: v3.0.0
---
+
# vue/no-parsing-error
+
> disallow parsing errors in ``
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
This rule reports syntax errors in ``. For example:
- Syntax errors of scripts in directives.
- Syntax errors of scripts in mustaches.
- Syntax errors of HTML.
- - Invalid end tags.
- - Attributes in end tags.
- - ...
- - See also: [WHATWG HTML spec](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors)
+ - Invalid end tags.
+ - Attributes in end tags.
+ - ...
+ - See also: [WHATWG HTML spec](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors)
## :book: Rule Details
@@ -96,10 +99,14 @@ The error codes which have `x-` prefix are original of this rule because errors
- `x-invalid-end-tag` enables the errors about the end tags of elements which have not opened.
- `x-invalid-namespace` enables the errors about invalid `xmlns` attributes. See also [step 10. of "create an element for a token"](https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token).
-## :books: Further reading
+## :books: Further Reading
- [WHATWG HTML spec](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors)
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v3.0.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-parsing-error.js)
diff --git a/docs/rules/no-potential-component-option-typo.md b/docs/rules/no-potential-component-option-typo.md
index 6474addad..17e21c818 100644
--- a/docs/rules/no-potential-component-option-typo.md
+++ b/docs/rules/no-potential-component-option-typo.md
@@ -3,15 +3,20 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-potential-component-option-typo
description: disallow a potential typo in your component property
+since: v7.0.0
---
+
# vue/no-potential-component-option-typo
+
> disallow a potential typo in your component property
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+
## :book: Rule Details
This rule disallow a potential typo in your component options
-**Here is the config**
+### Here is the config
```json
{
@@ -53,9 +58,9 @@ export default {
-> we use editdistance to compare two string similarity, threshold is an option to control upper bound of editdistance to report
+> we use edit distance to compare two string similarity, threshold is an option to control upper bound of edit distance to report
-**Here is the another example about config option `threshold`**
+### Here is the another example about config option `threshold`
```json
{
@@ -75,15 +80,15 @@ export default {
props: {
},
- /* ✓ GOOD, due to threshold is 5 */
- method: {
+ /* ✗ BAD, due to threshold is 5 */
+ mehtod: {
},
/* ✓ GOOD, due to threshold is 5 */
data: {
},
- /* ✗ BAD, due to we don't choose vue-router preset or add a custom option */
+ /* ✓ GOOD, due to we don't choose vue-router preset or add a custom option */
beforeRouteEnteR() {
}
@@ -97,7 +102,7 @@ export default {
```json
{
- "vue/no-unsed-vars": ["error", {
+ "vue/no-potential-component-option-typo": ["error", {
"presets": ["vue"],
"custom": [],
"threshold": 1
@@ -111,11 +116,15 @@ export default {
## :rocket: Suggestion
-- We provide all the possible component option that editdistance between your vue component option and configuration options is greater than 0 and lessEqual than threshold
+- We provide all the possible component option that edit distance between your vue component option and configuration options is greater than 0 and less equal than threshold
+
+## :books: Further Reading
+
+- [Edit distance](https://en.wikipedia.org/wiki/Edit_distance)
-## :books: Further reading
+## :rocket: Version
-- [Edit_distance](https://en.wikipedia.org/wiki/Edit_distance)
+This rule was introduced in eslint-plugin-vue v7.0.0
## :mag: Implementation
diff --git a/docs/rules/no-ref-as-operand.md b/docs/rules/no-ref-as-operand.md
index 609a07279..af10e0b7d 100644
--- a/docs/rules/no-ref-as-operand.md
+++ b/docs/rules/no-ref-as-operand.md
@@ -3,25 +3,29 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-ref-as-operand
description: disallow use of value wrapped by `ref()` (Composition API) as an operand
+since: v7.0.0
---
+
# vue/no-ref-as-operand
+
> disallow use of value wrapped by `ref()` (Composition API) as an operand
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+- :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.
## :book: Rule Details
This rule reports cases where a ref is used incorrectly as an operand.
You must use `.value` to access the `Ref` value.
-
+
```vue
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/no-required-prop-with-default": ["error", {
+ "autofix": false,
+ }]
+}
+```
+
+- `"autofix"` ... If `true`, enable autofix. (Default: `false`)
+
+## :couple: Related Rules
+
+- [vue/require-default-prop](./require-default-prop.md)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.6.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-required-prop-with-default.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-required-prop-with-default.js)
diff --git a/docs/rules/no-reserved-component-names.md b/docs/rules/no-reserved-component-names.md
index 2d11b2339..c99191872 100644
--- a/docs/rules/no-reserved-component-names.md
+++ b/docs/rules/no-reserved-component-names.md
@@ -3,10 +3,15 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-reserved-component-names
description: disallow the use of reserved names in component definitions
+since: v6.1.0
---
+
# vue/no-reserved-component-names
+
> disallow the use of reserved names in component definitions
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+
## :book: Rule Details
This rule prevents name collisions between Vue components and standard HTML elements and built-in components.
@@ -30,13 +35,15 @@ export default {
{
"vue/no-reserved-component-names": ["error", {
"disallowVueBuiltInComponents": false,
- "disallowVue3BuiltInComponents": false
+ "disallowVue3BuiltInComponents": false,
+ "htmlElementCaseSensitive": false,
}]
}
```
- `disallowVueBuiltInComponents` (`boolean`) ... If `true`, disallow Vue.js 2.x built-in component names. Default is `false`.
- `disallowVue3BuiltInComponents` (`boolean`) ... If `true`, disallow Vue.js 3.x built-in component names. Default is `false`.
+- `htmlElementCaseSensitive` (`boolean`) ... If `true`, component names must exactly match the case of an HTML element to be considered conflicting. Default is `false` (i.e. case-insensitve comparison).
### `"disallowVueBuiltInComponents": true`
@@ -68,13 +75,50 @@ export default {
-## :books: Further reading
+### `"htmlElementCaseSensitive": true`
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :couple: Related Rules
+
+- [vue/multi-word-component-names](./multi-word-component-names.md)
+
+## :books: Further Reading
- [List of html elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)
- [List of SVG elements](https://developer.mozilla.org/en-US/docs/Web/SVG/Element)
- [Kebab case elements](https://stackoverflow.com/questions/22545621/do-custom-elements-require-a-dash-in-their-name/22545622#22545622)
-- [Valid custom element name](https://w3c.github.io/webcomponents/spec/custom/#valid-custom-element-name)
-- [API - Built-In Components](https://vuejs.org/v2/api/index.html#Built-In-Components)
+- [Valid custom element name](https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name)
+- [API - Built-In Components](https://vuejs.org/api/built-in-components.html)
+- [API (for v2) - Built-In Components](https://v2.vuejs.org/v2/api/index.html#Built-In-Components)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v6.1.0
## :mag: Implementation
diff --git a/docs/rules/no-reserved-keys.md b/docs/rules/no-reserved-keys.md
index fee1cf0ee..9244ee0b7 100644
--- a/docs/rules/no-reserved-keys.md
+++ b/docs/rules/no-reserved-keys.md
@@ -3,11 +3,14 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-reserved-keys
description: disallow overwriting reserved keys
+since: v3.9.0
---
+
# vue/no-reserved-keys
+
> disallow overwriting reserved keys
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
## :book: Rule Details
@@ -24,14 +27,14 @@ export default {
},
computed: {
$on: {
- get () {}
+ get() {}
}
},
data: {
_foo: null
},
methods: {
- $nextTick () {}
+ $nextTick() {}
}
}
@@ -62,10 +65,10 @@ export default {
/* ✗ BAD */
export default {
computed: {
- foo () {}
+ foo() {}
},
firebase: {
- foo2 () {}
+ foo2() {}
}
}
@@ -73,10 +76,14 @@ export default {
-## :books: Further reading
+## :books: Further Reading
- [List of reserved keys](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/utils/vue-reserved.json)
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v3.9.0
+
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-reserved-keys.js)
diff --git a/docs/rules/no-reserved-props.md b/docs/rules/no-reserved-props.md
new file mode 100644
index 000000000..982ddb947
--- /dev/null
+++ b/docs/rules/no-reserved-props.md
@@ -0,0 +1,57 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-reserved-props
+description: disallow reserved names in props
+since: v8.0.0
+---
+
+# vue/no-reserved-props
+
+> disallow reserved names in props
+
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `*.configs["flat/essential"]`, `"plugin:vue/vue2-essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/vue2-strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/vue2-recommended"` and `*.configs["flat/vue2-recommended"]`.
+
+## :book: Rule Details
+
+This rule disallow reserved names to be used in props.
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/no-reserved-props": ["error", {
+ "vueVersion": 3, // or 2
+ }]
+}
+```
+
+- `vueVersion` (`2 | 3`) ... Specify the version of Vue you are using. Default is `3`.
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v8.0.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-reserved-props.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-reserved-props.js)
diff --git a/docs/rules/no-restricted-block.md b/docs/rules/no-restricted-block.md
new file mode 100644
index 000000000..0a96a64a4
--- /dev/null
+++ b/docs/rules/no-restricted-block.md
@@ -0,0 +1,92 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-block
+description: disallow specific block
+since: v7.4.0
+---
+
+# vue/no-restricted-block
+
+> disallow specific block
+
+## :book: Rule Details
+
+This rule allows you to specify block names that you don't want to use in your application.
+
+## :wrench: Options
+
+This rule takes a list of strings, where each string is a block name or pattern to be restricted:
+
+```json
+{
+ "vue/no-restricted-block": ["error", "style", "foo", "bar"]
+}
+```
+
+
+
+```vue
+
+
+ Custom block
+
+
+ Custom block
+
+
+```
+
+
+
+Alternatively, the rule also accepts objects.
+
+```json
+{
+ "vue/no-restricted-block": ["error",
+ {
+ "element": "style",
+ "message": "Do not use
+ *
+ *
+ * @type {Record}
+ */
+const DEFAULT_LANGUAGES = {
+ template: ['html'],
+ style: ['css'],
+ script: ['js', 'javascript']
+}
+
+/**
+ * @param {NonNullable} lang
+ */
+function getAllowsLangPhrase(lang) {
+ const langs = [...lang].map((s) => `"${s}"`)
+ switch (langs.length) {
+ case 1: {
+ return langs[0]
+ }
+ default: {
+ return `${langs.slice(0, -1).join(', ')}, and ${langs[langs.length - 1]}`
+ }
+ }
+}
+
+/**
+ * Normalizes a given option.
+ * @param {string} blockName The block name.
+ * @param {UserBlockOptions} option An option to parse.
+ * @returns {BlockOptions} Normalized option.
+ */
+function normalizeOption(blockName, option) {
+ /** @type {Set} */
+ let lang
+
+ if (Array.isArray(option.lang)) {
+ lang = new Set(option.lang)
+ } else if (typeof option.lang === 'string') {
+ lang = new Set([option.lang])
+ } else {
+ lang = new Set()
+ }
+
+ let hasDefault = false
+ for (const def of DEFAULT_LANGUAGES[blockName] || []) {
+ if (lang.has(def)) {
+ lang.delete(def)
+ hasDefault = true
+ }
+ }
+ if (lang.size === 0) {
+ return {
+ lang,
+ allowNoLang: true
+ }
+ }
+ return {
+ lang,
+ allowNoLang: hasDefault || Boolean(option.allowNoLang)
+ }
+}
+/**
+ * Normalizes a given options.
+ * @param { UserOptions } options An option to parse.
+ * @returns {Options} Normalized option.
+ */
+function normalizeOptions(options) {
+ if (!options) {
+ return {}
+ }
+
+ /** @type {Options} */
+ const normalized = {}
+
+ for (const blockName of Object.keys(options)) {
+ const value = options[blockName]
+ if (value) {
+ normalized[blockName] = normalizeOption(blockName, value)
+ }
+ }
+
+ return normalized
+}
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'disallow use other than available `lang`',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/block-lang.html'
+ },
+ schema: [
+ {
+ type: 'object',
+ patternProperties: {
+ '^(?:\\S+)$': {
+ oneOf: [
+ {
+ type: 'object',
+ properties: {
+ lang: {
+ oneOf: [
+ { type: 'string' },
+ {
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ uniqueItems: true,
+ additionalItems: false
+ }
+ ]
+ },
+ allowNoLang: { type: 'boolean' }
+ },
+ additionalProperties: false
+ }
+ ]
+ }
+ },
+ minProperties: 1,
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ expected:
+ "Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'.",
+ missing: "The 'lang' attribute of '<{{tag}}>' is missing.",
+ unexpected: "Do not specify the 'lang' attribute of '<{{tag}}>'.",
+ useOrNot:
+ "Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'. Or, not specifying the `lang` attribute is allowed.",
+ unexpectedDefault:
+ "Do not explicitly specify the default language for the 'lang' attribute of '<{{tag}}>'."
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const options = normalizeOptions(
+ context.options[0] || {
+ script: { allowNoLang: true },
+ template: { allowNoLang: true },
+ style: { allowNoLang: true }
+ }
+ )
+ if (Object.keys(options).length === 0) {
+ return {}
+ }
+
+ /**
+ * @param {VElement} element
+ * @returns {void}
+ */
+ function verify(element) {
+ const tag = element.name
+ const option = options[tag]
+ if (!option) {
+ return
+ }
+ const lang = utils.getAttribute(element, 'lang')
+ if (lang == null || lang.value == null) {
+ if (!option.allowNoLang) {
+ context.report({
+ node: element.startTag,
+ messageId: 'missing',
+ data: {
+ tag
+ }
+ })
+ }
+ return
+ }
+ if (!option.lang.has(lang.value.value)) {
+ let messageId
+ if (!option.allowNoLang) {
+ messageId = 'expected'
+ } else if (option.lang.size === 0) {
+ messageId = (DEFAULT_LANGUAGES[tag] || []).includes(lang.value.value)
+ ? 'unexpectedDefault'
+ : 'unexpected'
+ } else {
+ messageId = 'useOrNot'
+ }
+ context.report({
+ node: lang,
+ messageId,
+ data: {
+ tag,
+ allows: getAllowsLangPhrase(option.lang)
+ }
+ })
+ }
+ }
+
+ return utils.defineDocumentVisitor(context, {
+ 'VDocumentFragment > VElement': verify
+ })
+ }
+}
diff --git a/lib/rules/block-order.js b/lib/rules/block-order.js
new file mode 100644
index 000000000..d4fdb62ea
--- /dev/null
+++ b/lib/rules/block-order.js
@@ -0,0 +1,185 @@
+/**
+ * @author Yosuke Ota
+ * issue https://github.com/vuejs/eslint-plugin-vue/issues/140
+ */
+'use strict'
+
+const utils = require('../utils')
+const { parseSelector } = require('../utils/selector')
+
+/**
+ * @typedef {import('../utils/selector').VElementSelector} VElementSelector
+ */
+
+const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style'])
+
+/**
+ * @param {VElement} element
+ * @return {string}
+ */
+function getAttributeString(element) {
+ return element.startTag.attributes
+ .map((attribute) => {
+ if (attribute.value && attribute.value.type !== 'VLiteral') {
+ return ''
+ }
+
+ return `${attribute.key.name}${
+ attribute.value && attribute.value.value
+ ? `=${attribute.value.value}`
+ : ''
+ }`
+ })
+ .join(' ')
+}
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'enforce order of component top-level elements',
+ categories: ['vue3-recommended', 'vue2-recommended'],
+ url: 'https://eslint.vuejs.org/rules/block-order.html'
+ },
+ fixable: 'code',
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ order: {
+ type: 'array',
+ items: {
+ oneOf: [
+ { type: 'string' },
+ { type: 'array', items: { type: 'string' }, uniqueItems: true }
+ ]
+ },
+ uniqueItems: true,
+ additionalItems: false
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ unexpected:
+ "'<{{elementName}}{{elementAttributes}}>' should be above '<{{firstUnorderedName}}{{firstUnorderedAttributes}}>' on line {{line}}."
+ }
+ },
+ /**
+ * @param {RuleContext} context - The rule context.
+ * @returns {RuleListener} AST event handlers.
+ */
+ create(context) {
+ /**
+ * @typedef {object} OrderElement
+ * @property {string} selectorText
+ * @property {VElementSelector} selector
+ * @property {number} index
+ */
+ /** @type {OrderElement[]} */
+ const orders = []
+ /** @type {(string|string[])[]} */
+ const orderOptions =
+ (context.options[0] && context.options[0].order) || DEFAULT_ORDER
+ for (const [index, selectorOrSelectors] of orderOptions.entries()) {
+ if (Array.isArray(selectorOrSelectors)) {
+ for (const selector of selectorOrSelectors) {
+ orders.push({
+ selectorText: selector,
+ selector: parseSelector(selector, context),
+ index
+ })
+ }
+ } else {
+ orders.push({
+ selectorText: selectorOrSelectors,
+ selector: parseSelector(selectorOrSelectors, context),
+ index
+ })
+ }
+ }
+
+ /**
+ * @param {VElement} element
+ */
+ function getOrderElement(element) {
+ return orders.find((o) => o.selector.test(element))
+ }
+ const sourceCode = context.getSourceCode()
+ const documentFragment =
+ sourceCode.parserServices.getDocumentFragment &&
+ sourceCode.parserServices.getDocumentFragment()
+
+ function getTopLevelHTMLElements() {
+ if (documentFragment) {
+ return documentFragment.children.filter(utils.isVElement)
+ }
+ return []
+ }
+
+ return {
+ Program(node) {
+ if (utils.hasInvalidEOF(node)) {
+ return
+ }
+ const elements = getTopLevelHTMLElements()
+
+ const elementsWithOrder = elements.flatMap((element) => {
+ const order = getOrderElement(element)
+ return order ? [{ order, element }] : []
+ })
+ const sourceCode = context.getSourceCode()
+ for (const [index, elementWithOrders] of elementsWithOrder.entries()) {
+ const { order: expected, element } = elementWithOrders
+ const firstUnordered = elementsWithOrder
+ .slice(0, index)
+ .filter(({ order }) => expected.index < order.index)
+ .sort((e1, e2) => e1.order.index - e2.order.index)[0]
+ if (firstUnordered) {
+ const firstUnorderedAttributes = getAttributeString(
+ firstUnordered.element
+ )
+ const elementAttributes = getAttributeString(element)
+
+ context.report({
+ node: element,
+ loc: element.loc,
+ messageId: 'unexpected',
+ data: {
+ elementName: element.name,
+ elementAttributes: elementAttributes
+ ? ` ${elementAttributes}`
+ : '',
+ firstUnorderedName: firstUnordered.element.name,
+ firstUnorderedAttributes: firstUnorderedAttributes
+ ? ` ${firstUnorderedAttributes}`
+ : '',
+ line: firstUnordered.element.loc.start.line
+ },
+ *fix(fixer) {
+ // insert element before firstUnordered
+ const fixedElements = elements.flatMap((it) => {
+ if (it === firstUnordered.element) {
+ return [element, it]
+ } else if (it === element) {
+ return []
+ }
+ return [it]
+ })
+ for (let i = elements.length - 1; i >= 0; i--) {
+ if (elements[i] !== fixedElements[i]) {
+ yield fixer.replaceTextRange(
+ elements[i].range,
+ sourceCode.text.slice(...fixedElements[i].range)
+ )
+ }
+ }
+ }
+ })
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/rules/block-spacing.js b/lib/rules/block-spacing.js
index d12ad7da8..37c526e91 100644
--- a/lib/rules/block-spacing.js
+++ b/lib/rules/block-spacing.js
@@ -3,9 +3,9 @@
*/
'use strict'
-const { wrapCoreRule } = require('../utils')
+const { wrapStylisticOrCoreRule } = require('../utils')
-// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
-module.exports = wrapCoreRule(require('eslint/lib/rules/block-spacing'), {
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapStylisticOrCoreRule('block-spacing', {
skipDynamicArguments: true
})
diff --git a/lib/rules/block-tag-newline.js b/lib/rules/block-tag-newline.js
new file mode 100644
index 000000000..22fb12870
--- /dev/null
+++ b/lib/rules/block-tag-newline.js
@@ -0,0 +1,364 @@
+/**
+ * @fileoverview Enforce line breaks style after opening and before closing block-level tags.
+ * @author Yosuke Ota
+ */
+'use strict'
+const utils = require('../utils')
+
+/**
+ * @typedef { 'always' | 'never' | 'consistent' | 'ignore' } OptionType
+ * @typedef { { singleline?: OptionType, multiline?: OptionType, maxEmptyLines?: number } } ContentsOptions
+ * @typedef { ContentsOptions & { blocks?: { [element: string]: ContentsOptions } } } Options
+ * @typedef { Required } ArgsOptions
+ */
+
+/**
+ * @param {string} text Source code as a string.
+ * @returns {number}
+ */
+function getLinebreakCount(text) {
+ return text.split(/\r\n|[\r\n\u2028\u2029]/gu).length - 1
+}
+
+/**
+ * @param {number} lineBreaks
+ */
+function getPhrase(lineBreaks) {
+ switch (lineBreaks) {
+ case 1: {
+ return '1 line break'
+ }
+ default: {
+ return `${lineBreaks} line breaks`
+ }
+ }
+}
+
+const ENUM_OPTIONS = { enum: ['always', 'never', 'consistent', 'ignore'] }
+module.exports = {
+ meta: {
+ type: 'layout',
+ docs: {
+ description:
+ 'enforce line breaks after opening and before closing block-level tags',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/block-tag-newline.html'
+ },
+ fixable: 'whitespace',
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ singleline: ENUM_OPTIONS,
+ multiline: ENUM_OPTIONS,
+ maxEmptyLines: { type: 'number', minimum: 0 },
+ blocks: {
+ type: 'object',
+ patternProperties: {
+ '^(?:\\S+)$': {
+ type: 'object',
+ properties: {
+ singleline: ENUM_OPTIONS,
+ multiline: ENUM_OPTIONS,
+ maxEmptyLines: { type: 'number', minimum: 0 }
+ },
+ additionalProperties: false
+ }
+ },
+ additionalProperties: false
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ unexpectedOpeningLinebreak:
+ "There should be no line break after '<{{tag}}>'.",
+ expectedOpeningLinebreak:
+ "Expected {{expected}} after '<{{tag}}>', but {{actual}} found.",
+ expectedClosingLinebreak:
+ "Expected {{expected}} before '{{tag}}>', but {{actual}} found.",
+ missingOpeningLinebreak: "A line break is required after '<{{tag}}>'.",
+ missingClosingLinebreak: "A line break is required before '{{tag}}>'."
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const sourceCode = context.getSourceCode()
+ const df =
+ sourceCode.parserServices.getDocumentFragment &&
+ sourceCode.parserServices.getDocumentFragment()
+ if (!df) {
+ return {}
+ }
+
+ /**
+ * @param {VStartTag} startTag
+ * @param {string} beforeText
+ * @param {number} beforeLinebreakCount
+ * @param {'always' | 'never'} beforeOption
+ * @param {number} maxEmptyLines
+ * @returns {void}
+ */
+ function verifyBeforeSpaces(
+ startTag,
+ beforeText,
+ beforeLinebreakCount,
+ beforeOption,
+ maxEmptyLines
+ ) {
+ if (beforeOption === 'always') {
+ if (beforeLinebreakCount === 0) {
+ context.report({
+ loc: {
+ start: startTag.loc.end,
+ end: startTag.loc.end
+ },
+ messageId: 'missingOpeningLinebreak',
+ data: { tag: startTag.parent.name },
+ fix(fixer) {
+ return fixer.insertTextAfter(startTag, '\n')
+ }
+ })
+ } else if (maxEmptyLines < beforeLinebreakCount - 1) {
+ context.report({
+ loc: {
+ start: startTag.loc.end,
+ end: sourceCode.getLocFromIndex(
+ startTag.range[1] + beforeText.length
+ )
+ },
+ messageId: 'expectedOpeningLinebreak',
+ data: {
+ tag: startTag.parent.name,
+ expected: getPhrase(maxEmptyLines + 1),
+ actual: getPhrase(beforeLinebreakCount)
+ },
+ fix(fixer) {
+ return fixer.replaceTextRange(
+ [startTag.range[1], startTag.range[1] + beforeText.length],
+ '\n'.repeat(maxEmptyLines + 1)
+ )
+ }
+ })
+ }
+ } else {
+ if (beforeLinebreakCount > 0) {
+ context.report({
+ loc: {
+ start: startTag.loc.end,
+ end: sourceCode.getLocFromIndex(
+ startTag.range[1] + beforeText.length
+ )
+ },
+ messageId: 'unexpectedOpeningLinebreak',
+ data: { tag: startTag.parent.name },
+ fix(fixer) {
+ return fixer.removeRange([
+ startTag.range[1],
+ startTag.range[1] + beforeText.length
+ ])
+ }
+ })
+ }
+ }
+ }
+ /**
+ * @param {VEndTag} endTag
+ * @param {string} afterText
+ * @param {number} afterLinebreakCount
+ * @param {'always' | 'never'} afterOption
+ * @param {number} maxEmptyLines
+ * @returns {void}
+ */
+ function verifyAfterSpaces(
+ endTag,
+ afterText,
+ afterLinebreakCount,
+ afterOption,
+ maxEmptyLines
+ ) {
+ if (afterOption === 'always') {
+ if (afterLinebreakCount === 0) {
+ context.report({
+ loc: {
+ start: endTag.loc.start,
+ end: endTag.loc.start
+ },
+ messageId: 'missingClosingLinebreak',
+ data: { tag: endTag.parent.name },
+ fix(fixer) {
+ return fixer.insertTextBefore(endTag, '\n')
+ }
+ })
+ } else if (maxEmptyLines < afterLinebreakCount - 1) {
+ context.report({
+ loc: {
+ start: sourceCode.getLocFromIndex(
+ endTag.range[0] - afterText.length
+ ),
+ end: endTag.loc.start
+ },
+ messageId: 'expectedClosingLinebreak',
+ data: {
+ tag: endTag.parent.name,
+ expected: getPhrase(maxEmptyLines + 1),
+ actual: getPhrase(afterLinebreakCount)
+ },
+ fix(fixer) {
+ return fixer.replaceTextRange(
+ [endTag.range[0] - afterText.length, endTag.range[0]],
+ '\n'.repeat(maxEmptyLines + 1)
+ )
+ }
+ })
+ }
+ } else {
+ if (afterLinebreakCount > 0) {
+ context.report({
+ loc: {
+ start: sourceCode.getLocFromIndex(
+ endTag.range[0] - afterText.length
+ ),
+ end: endTag.loc.start
+ },
+ messageId: 'unexpectedOpeningLinebreak',
+ data: { tag: endTag.parent.name },
+ fix(fixer) {
+ return fixer.removeRange([
+ endTag.range[0] - afterText.length,
+ endTag.range[0]
+ ])
+ }
+ })
+ }
+ }
+ }
+ /**
+ * @param {VElement} element
+ * @param {ArgsOptions} options
+ * @returns {void}
+ */
+ function verifyElement(element, options) {
+ const { startTag, endTag } = element
+ if (startTag.selfClosing || endTag == null) {
+ return
+ }
+ const text = sourceCode.text.slice(startTag.range[1], endTag.range[0])
+
+ const trimText = text.trim()
+ if (!trimText) {
+ return
+ }
+
+ const option =
+ options.multiline !== options.singleline &&
+ /[\n\r\u2028\u2029]/u.test(text.trim())
+ ? options.multiline
+ : options.singleline
+ if (option === 'ignore') {
+ return
+ }
+ const beforeText = /** @type {RegExpExecArray} */ (/^\s*/u.exec(text))[0]
+ const afterText = /** @type {RegExpExecArray} */ (/\s*$/u.exec(text))[0]
+ const beforeLinebreakCount = getLinebreakCount(beforeText)
+ const afterLinebreakCount = getLinebreakCount(afterText)
+
+ /** @type {'always' | 'never'} */
+ let beforeOption
+ /** @type {'always' | 'never'} */
+ let afterOption
+ if (option === 'always' || option === 'never') {
+ beforeOption = option
+ afterOption = option
+ } else {
+ // consistent
+ if (beforeLinebreakCount > 0 === afterLinebreakCount > 0) {
+ return
+ }
+ beforeOption = 'always'
+ afterOption = 'always'
+ }
+
+ verifyBeforeSpaces(
+ startTag,
+ beforeText,
+ beforeLinebreakCount,
+ beforeOption,
+ options.maxEmptyLines
+ )
+
+ verifyAfterSpaces(
+ endTag,
+ afterText,
+ afterLinebreakCount,
+ afterOption,
+ options.maxEmptyLines
+ )
+ }
+
+ /**
+ * Normalizes a given option value.
+ * @param { Options | undefined } option An option value to parse.
+ * @returns { (element: VElement) => void } Verify function.
+ */
+ function normalizeOptionValue(option) {
+ if (!option) {
+ return normalizeOptionValue({})
+ }
+
+ /** @type {ContentsOptions} */
+ const contentsOptions = option
+ /** @type {ArgsOptions} */
+ const options = {
+ singleline: contentsOptions.singleline || 'consistent',
+ multiline: contentsOptions.multiline || 'always',
+ maxEmptyLines: contentsOptions.maxEmptyLines || 0
+ }
+ const { blocks } = option
+ if (!blocks) {
+ return (element) => verifyElement(element, options)
+ }
+
+ return (element) => {
+ const { name } = element
+ const elementsOptions = blocks[name]
+ if (elementsOptions) {
+ normalizeOptionValue({
+ singleline: elementsOptions.singleline || options.singleline,
+ multiline: elementsOptions.multiline || options.multiline,
+ maxEmptyLines:
+ elementsOptions.maxEmptyLines == null
+ ? options.maxEmptyLines
+ : elementsOptions.maxEmptyLines
+ })(element)
+ } else {
+ verifyElement(element, options)
+ }
+ }
+ }
+
+ const documentFragment = df
+
+ const verify = normalizeOptionValue(context.options[0])
+
+ return utils.defineTemplateBodyVisitor(
+ context,
+ {},
+ {
+ /** @param {Program} node */
+ Program(node) {
+ if (utils.hasInvalidEOF(node)) {
+ return
+ }
+
+ for (const element of documentFragment.children) {
+ if (utils.isVElement(element)) {
+ verify(element)
+ }
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/lib/rules/brace-style.js b/lib/rules/brace-style.js
index 66680948d..507f5e101 100644
--- a/lib/rules/brace-style.js
+++ b/lib/rules/brace-style.js
@@ -3,9 +3,9 @@
*/
'use strict'
-const { wrapCoreRule } = require('../utils')
+const { wrapStylisticOrCoreRule } = require('../utils')
-// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
-module.exports = wrapCoreRule(require('eslint/lib/rules/brace-style'), {
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapStylisticOrCoreRule('brace-style', {
skipDynamicArguments: true
})
diff --git a/lib/rules/camelcase.js b/lib/rules/camelcase.js
index 4aebfabab..8aa43e19b 100644
--- a/lib/rules/camelcase.js
+++ b/lib/rules/camelcase.js
@@ -5,5 +5,5 @@
const { wrapCoreRule } = require('../utils')
-// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
-module.exports = wrapCoreRule(require('eslint/lib/rules/camelcase'))
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapCoreRule('camelcase')
diff --git a/lib/rules/comma-dangle.js b/lib/rules/comma-dangle.js
index aa6c83c5e..760611cfa 100644
--- a/lib/rules/comma-dangle.js
+++ b/lib/rules/comma-dangle.js
@@ -3,7 +3,7 @@
*/
'use strict'
-const { wrapCoreRule } = require('../utils')
+const { wrapStylisticOrCoreRule } = require('../utils')
-// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
-module.exports = wrapCoreRule(require('eslint/lib/rules/comma-dangle'))
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapStylisticOrCoreRule('comma-dangle')
diff --git a/lib/rules/comma-spacing.js b/lib/rules/comma-spacing.js
index 2a68be8f1..4be3bc85a 100644
--- a/lib/rules/comma-spacing.js
+++ b/lib/rules/comma-spacing.js
@@ -3,10 +3,11 @@
*/
'use strict'
-const { wrapCoreRule } = require('../utils')
+const { wrapStylisticOrCoreRule } = require('../utils')
-// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
-module.exports = wrapCoreRule(require('eslint/lib/rules/comma-spacing'), {
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapStylisticOrCoreRule('comma-spacing', {
skipDynamicArguments: true,
- skipDynamicArgumentsReport: true
+ skipDynamicArgumentsReport: true,
+ applyDocument: true
})
diff --git a/lib/rules/comma-style.js b/lib/rules/comma-style.js
index e7313b8ac..815bd23be 100644
--- a/lib/rules/comma-style.js
+++ b/lib/rules/comma-style.js
@@ -3,16 +3,16 @@
*/
'use strict'
-const { wrapCoreRule } = require('../utils')
+const { wrapStylisticOrCoreRule } = require('../utils')
-// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
-module.exports = wrapCoreRule(require('eslint/lib/rules/comma-style'), {
- create(_context, { coreHandlers }) {
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapStylisticOrCoreRule('comma-style', {
+ create(_context, { baseHandlers }) {
return {
VSlotScopeExpression(node) {
- if (coreHandlers.FunctionExpression) {
+ if (baseHandlers.FunctionExpression) {
// @ts-expect-error -- Process params of VSlotScopeExpression as FunctionExpression.
- coreHandlers.FunctionExpression(node)
+ baseHandlers.FunctionExpression(node)
}
}
}
diff --git a/lib/rules/comment-directive.js b/lib/rules/comment-directive.js
index 9ff9d2fdf..655e222bd 100644
--- a/lib/rules/comment-directive.js
+++ b/lib/rules/comment-directive.js
@@ -1,14 +1,10 @@
/**
* @author Toru Nagashima
*/
-/* eslint-disable eslint-plugin/report-message-format, consistent-docs-description */
+/* eslint-disable eslint-plugin/report-message-format */
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
/**
@@ -17,9 +13,6 @@ const utils = require('../utils')
* @property {number} RuleAndLocation.index
* @property {string} [RuleAndLocation.key]
*/
-// -----------------------------------------------------------------------------
-// Helpers
-// -----------------------------------------------------------------------------
const COMMENT_DIRECTIVE_B = /^\s*(eslint-(?:en|dis)able)(?:\s+|$)/
const COMMENT_DIRECTIVE_L = /^\s*(eslint-disable(?:-next)?-line)(?:\s+|$)/
@@ -51,7 +44,7 @@ function parse(pattern, comment) {
/** @type {RuleAndLocation[]} */
const rules = []
- const rulesRe = /([^,\s]+)[,\s]*/g
+ const rulesRe = /([^\s,]+)[\s,]*/g
let startIndex = match[0].length
rulesRe.lastIndex = startIndex
@@ -77,16 +70,16 @@ function parse(pattern, comment) {
* @returns {void}
*/
function enable(context, loc, group, rule) {
- if (!rule) {
+ if (rule) {
context.report({
loc,
- messageId: group === 'block' ? 'enableBlock' : 'enableLine'
+ messageId: group === 'block' ? 'enableBlockRule' : 'enableLineRule',
+ data: { rule }
})
} else {
context.report({
loc,
- messageId: group === 'block' ? 'enableBlockRule' : 'enableLineRule',
- data: { rule }
+ messageId: group === 'block' ? 'enableBlock' : 'enableLine'
})
}
}
@@ -101,17 +94,17 @@ function enable(context, loc, group, rule) {
* @returns {void}
*/
function disable(context, loc, group, rule, key) {
- if (!rule) {
+ if (rule) {
context.report({
loc,
- messageId: group === 'block' ? 'disableBlock' : 'disableLine',
- data: { key }
+ messageId: group === 'block' ? 'disableBlockRule' : 'disableLineRule',
+ data: { rule, key }
})
} else {
context.report({
loc,
- messageId: group === 'block' ? 'disableBlockRule' : 'disableLineRule',
- data: { rule, key }
+ messageId: group === 'block' ? 'disableBlock' : 'disableLine',
+ data: { key }
})
}
}
@@ -126,35 +119,35 @@ function disable(context, loc, group, rule, key) {
*/
function processBlock(context, comment, reportUnusedDisableDirectives) {
const parsed = parse(COMMENT_DIRECTIVE_B, comment.value)
- if (parsed != null) {
- if (parsed.type === 'eslint-disable') {
- if (parsed.rules.length) {
- const rules = reportUnusedDisableDirectives
- ? reportUnusedRules(context, comment, parsed.type, parsed.rules)
- : parsed.rules
- for (const rule of rules) {
- disable(
- context,
- comment.loc.start,
- 'block',
- rule.ruleId,
- rule.key || '*'
- )
- }
- } else {
- const key = reportUnusedDisableDirectives
- ? reportUnused(context, comment, parsed.type)
- : ''
- disable(context, comment.loc.start, 'block', null, key)
+ if (parsed === null) return
+
+ if (parsed.type === 'eslint-disable') {
+ if (parsed.rules.length > 0) {
+ const rules = reportUnusedDisableDirectives
+ ? reportUnusedRules(context, comment, parsed.type, parsed.rules)
+ : parsed.rules
+ for (const rule of rules) {
+ disable(
+ context,
+ comment.loc.start,
+ 'block',
+ rule.ruleId,
+ rule.key || '*'
+ )
}
} else {
- if (parsed.rules.length) {
- for (const rule of parsed.rules) {
- enable(context, comment.loc.start, 'block', rule.ruleId)
- }
- } else {
- enable(context, comment.loc.start, 'block', null)
+ const key = reportUnusedDisableDirectives
+ ? reportUnused(context, comment, parsed.type)
+ : ''
+ disable(context, comment.loc.start, 'block', null, key)
+ }
+ } else {
+ if (parsed.rules.length > 0) {
+ for (const rule of parsed.rules) {
+ enable(context, comment.loc.start, 'block', rule.ruleId)
}
+ } else {
+ enable(context, comment.loc.start, 'block', null)
}
}
}
@@ -173,7 +166,7 @@ function processLine(context, comment, reportUnusedDisableDirectives) {
const line =
comment.loc.start.line + (parsed.type === 'eslint-disable-line' ? 0 : 1)
const column = -1
- if (parsed.rules.length) {
+ if (parsed.rules.length > 0) {
const rules = reportUnusedDisableDirectives
? reportUnusedRules(context, comment, parsed.type, parsed.rules)
: parsed.rules
@@ -277,15 +270,11 @@ function extractTopLevelDocumentFragmentComments(documentFragment) {
)
}
-// -----------------------------------------------------------------------------
-// Rule Definition
-// -----------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
- description: 'support comment-directives in ``',
+ description: 'support comment-directives in ``', // eslint-disable-line eslint-plugin/require-meta-docs-description
categories: ['base'],
url: 'https://eslint.vuejs.org/rules/comment-directive.html'
},
@@ -324,9 +313,10 @@ module.exports = {
const options = context.options[0] || {}
/** @type {boolean} */
const reportUnusedDisableDirectives = options.reportUnusedDisableDirectives
+ const sourceCode = context.getSourceCode()
const documentFragment =
- context.parserServices.getDocumentFragment &&
- context.parserServices.getDocumentFragment()
+ sourceCode.parserServices.getDocumentFragment &&
+ sourceCode.parserServices.getDocumentFragment()
return {
Program(node) {
diff --git a/lib/rules/component-api-style.js b/lib/rules/component-api-style.js
new file mode 100644
index 000000000..550eebfed
--- /dev/null
+++ b/lib/rules/component-api-style.js
@@ -0,0 +1,308 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @typedef { 'script-setup' | 'composition' | 'composition-vue2' | 'options' } PreferOption
+ *
+ * @typedef {PreferOption[]} UserPreferOption
+ *
+ * @typedef {object} NormalizeOptions
+ * @property {object} allowsSFC
+ * @property {boolean} [allowsSFC.scriptSetup]
+ * @property {boolean} [allowsSFC.composition]
+ * @property {boolean} [allowsSFC.compositionVue2]
+ * @property {boolean} [allowsSFC.options]
+ * @property {object} allowsOther
+ * @property {boolean} [allowsOther.composition]
+ * @property {boolean} [allowsOther.compositionVue2]
+ * @property {boolean} [allowsOther.options]
+ */
+
+/** @type {PreferOption[]} */
+const STYLE_OPTIONS = [
+ 'script-setup',
+ 'composition',
+ 'composition-vue2',
+ 'options'
+]
+
+/**
+ * Normalize options.
+ * @param {any[]} options The options user configured.
+ * @returns {NormalizeOptions} The normalized options.
+ */
+function parseOptions(options) {
+ /** @type {NormalizeOptions} */
+ const opts = { allowsSFC: {}, allowsOther: {} }
+
+ /** @type {UserPreferOption} */
+ const preferOptions = options[0] || ['script-setup', 'composition']
+ for (const prefer of preferOptions) {
+ switch (prefer) {
+ case 'script-setup': {
+ opts.allowsSFC.scriptSetup = true
+ break
+ }
+ case 'composition': {
+ opts.allowsSFC.composition = true
+ opts.allowsOther.composition = true
+ break
+ }
+ case 'composition-vue2': {
+ opts.allowsSFC.compositionVue2 = true
+ opts.allowsOther.compositionVue2 = true
+ break
+ }
+ case 'options': {
+ opts.allowsSFC.options = true
+ opts.allowsOther.options = true
+ break
+ }
+ }
+ }
+
+ if (
+ !opts.allowsOther.composition &&
+ !opts.allowsOther.compositionVue2 &&
+ !opts.allowsOther.options
+ ) {
+ opts.allowsOther.composition = true
+ opts.allowsOther.compositionVue2 = true
+ opts.allowsOther.options = true
+ }
+
+ return opts
+}
+
+const OPTIONS_API_OPTIONS = new Set([
+ 'mixins',
+ 'extends',
+ // state
+ 'data',
+ 'computed',
+ 'methods',
+ 'watch',
+ 'provide',
+ 'inject',
+ // lifecycle
+ 'beforeCreate',
+ 'created',
+ 'beforeMount',
+ 'mounted',
+ 'beforeUpdate',
+ 'updated',
+ 'activated',
+ 'deactivated',
+ 'beforeDestroy',
+ 'beforeUnmount',
+ 'destroyed',
+ 'unmounted',
+ 'render',
+ 'renderTracked',
+ 'renderTriggered',
+ 'errorCaptured',
+ // public API
+ 'expose'
+])
+const COMPOSITION_API_OPTIONS = new Set(['setup'])
+
+const COMPOSITION_API_VUE2_OPTIONS = new Set([
+ 'setup',
+ 'render', // https://github.com/vuejs/composition-api#template-refs
+ 'renderTracked', // https://github.com/vuejs/composition-api#missing-apis
+ 'renderTriggered' // https://github.com/vuejs/composition-api#missing-apis
+])
+
+const LIFECYCLE_HOOK_OPTIONS = new Set([
+ 'beforeCreate',
+ 'created',
+ 'beforeMount',
+ 'mounted',
+ 'beforeUpdate',
+ 'updated',
+ 'activated',
+ 'deactivated',
+ 'beforeDestroy',
+ 'beforeUnmount',
+ 'destroyed',
+ 'unmounted',
+ 'renderTracked',
+ 'renderTriggered',
+ 'errorCaptured'
+])
+
+/**
+ * @typedef { 'script-setup' | 'composition' | 'options' } ApiStyle
+ */
+
+/**
+ * @param {object} allowsOpt
+ * @param {boolean} [allowsOpt.scriptSetup]
+ * @param {boolean} [allowsOpt.composition]
+ * @param {boolean} [allowsOpt.compositionVue2]
+ * @param {boolean} [allowsOpt.options]
+ */
+function buildAllowedPhrase(allowsOpt) {
+ const phrases = []
+ if (allowsOpt.scriptSetup) {
+ phrases.push('`\n`
+ ),
+ fixer.removeRange(removeRange)
+ ]
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/define-slots.js b/lib/rules/syntaxes/define-slots.js
new file mode 100644
index 000000000..450ec3e89
--- /dev/null
+++ b/lib/rules/syntaxes/define-slots.js
@@ -0,0 +1,22 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../../utils/index')
+
+module.exports = {
+ supported: '>=3.3.0',
+ /** @param {RuleContext} context @returns {RuleListener} */
+ createScriptVisitor(context) {
+ return utils.defineScriptSetupVisitor(context, {
+ onDefineSlotsEnter(node) {
+ context.report({
+ node,
+ messageId: 'forbiddenDefineSlots'
+ })
+ }
+ })
+ }
+}
diff --git a/lib/rules/syntaxes/dynamic-directive-arguments.js b/lib/rules/syntaxes/dynamic-directive-arguments.js
index 595f70fb9..e670fa3fc 100644
--- a/lib/rules/syntaxes/dynamic-directive-arguments.js
+++ b/lib/rules/syntaxes/dynamic-directive-arguments.js
@@ -4,7 +4,7 @@
*/
'use strict'
module.exports = {
- supported: '2.6.0',
+ supported: '>=2.6.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
@@ -20,7 +20,8 @@ module.exports = {
}
return {
- 'VAttribute[directive=true] > VDirectiveKey > VExpressionContainer': reportDynamicArgument
+ 'VAttribute[directive=true] > VDirectiveKey > VExpressionContainer':
+ reportDynamicArgument
}
}
}
diff --git a/lib/rules/syntaxes/is-attribute-with-vue-prefix.js b/lib/rules/syntaxes/is-attribute-with-vue-prefix.js
new file mode 100644
index 000000000..9fd2afdd0
--- /dev/null
+++ b/lib/rules/syntaxes/is-attribute-with-vue-prefix.js
@@ -0,0 +1,25 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+module.exports = {
+ supported: '>=3.1.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createTemplateBodyVisitor(context) {
+ return {
+ /** @param {VAttribute} node */
+ "VAttribute[directive=false][key.name='is']"(node) {
+ if (!node.value) {
+ return
+ }
+ if (node.value.value.startsWith('vue:')) {
+ context.report({
+ node: node.value,
+ messageId: 'forbiddenIsAttributeWithVuePrefix'
+ })
+ }
+ }
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/scope-attribute.js b/lib/rules/syntaxes/scope-attribute.js
index c1673c3cc..af7dbcd2f 100644
--- a/lib/rules/syntaxes/scope-attribute.js
+++ b/lib/rules/syntaxes/scope-attribute.js
@@ -5,6 +5,7 @@
'use strict'
module.exports = {
deprecated: '2.5.0',
+ supported: '<3.0.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
@@ -22,7 +23,8 @@ module.exports = {
}
return {
- "VAttribute[directive=true] > VDirectiveKey[name.name='scope']": reportScope
+ "VAttribute[directive=true] > VDirectiveKey[name.name='scope']":
+ reportScope
}
}
}
diff --git a/lib/rules/syntaxes/script-setup.js b/lib/rules/syntaxes/script-setup.js
new file mode 100644
index 000000000..7c0538b3d
--- /dev/null
+++ b/lib/rules/syntaxes/script-setup.js
@@ -0,0 +1,28 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../../utils')
+
+module.exports = {
+ supported: '>=2.7.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createScriptVisitor(context) {
+ const scriptSetup = utils.getScriptSetupElement(context)
+ if (!scriptSetup) {
+ return {}
+ }
+ const reportNode =
+ utils.getAttribute(scriptSetup, 'setup') || scriptSetup.startTag
+ return {
+ Program() {
+ context.report({
+ node: reportNode,
+ messageId: 'forbiddenScriptSetup'
+ })
+ }
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js
index bf84361fb..27087cb37 100644
--- a/lib/rules/syntaxes/slot-attribute.js
+++ b/lib/rules/syntaxes/slot-attribute.js
@@ -3,11 +3,23 @@
* See LICENSE file in root directory for full license.
*/
'use strict'
+
+const canConvertToVSlot = require('./utils/can-convert-to-v-slot')
+const casing = require('../../utils/casing')
+
module.exports = {
deprecated: '2.6.0',
+ supported: '<3.0.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
+ const options = context.options[0] || {}
+ /** @type {Set} */
+ const ignore = new Set(options.ignore)
+
const sourceCode = context.getSourceCode()
+ const tokenStore =
+ sourceCode.parserServices.getTemplateBodyTokenStore &&
+ sourceCode.parserServices.getTemplateBodyTokenStore()
/**
* Checks whether the given node can convert to the `v-slot`.
@@ -15,15 +27,15 @@ module.exports = {
* @returns {boolean} `true` if the given node can convert to the `v-slot`
*/
function canConvertFromSlotToVSlot(slotAttr) {
- if (slotAttr.parent.parent.name !== 'template') {
+ if (!canConvertToVSlot(slotAttr.parent.parent, sourceCode, tokenStore)) {
return false
}
if (!slotAttr.value) {
return true
}
const slotName = slotAttr.value.value
- // If non-Latin characters are included it can not be converted.
- return !/[^a-z]/i.test(slotName)
+ // If other than alphanumeric, underscore and hyphen characters are included it can not be converted.
+ return !/[^\w\-]/u.test(slotName)
}
/**
@@ -32,7 +44,7 @@ module.exports = {
* @returns {boolean} `true` if the given node can convert to the `v-slot`
*/
function canConvertFromVBindSlotToVSlot(slotAttr) {
- if (slotAttr.parent.parent.name !== 'template') {
+ if (!canConvertToVSlot(slotAttr.parent.parent, sourceCode, tokenStore)) {
return false
}
@@ -44,10 +56,8 @@ module.exports = {
// parse error or empty expression
return false
}
- const slotName = sourceCode.getText(slotAttr.value.expression).trim()
- // If non-Latin characters are included it can not be converted.
- // It does not check the space only because `a>b?c:d` should be rejected.
- return !/[^a-z]/i.test(slotName)
+
+ return slotAttr.value.expression.type === 'Identifier'
}
/**
@@ -56,33 +66,54 @@ module.exports = {
* @param {VAttribute|VDirective} slotAttr node of `slot`
* @param {string | null} slotName name of `slot`
* @param {boolean} vBind `true` if `slotAttr` is `v-bind:slot`
- * @returns {Fix[]} fix data
+ * @returns {IterableIterator} fix data
*/
- function fixSlotToVSlot(fixer, slotAttr, slotName, vBind) {
- const element = slotAttr.parent
- const scopeAttr = element.attributes.find(
+ function* fixSlotToVSlot(fixer, slotAttr, slotName, vBind) {
+ const startTag = slotAttr.parent
+ const scopeAttr = startTag.attributes.find(
(attr) =>
attr.directive === true &&
attr.key.name &&
(attr.key.name.name === 'slot-scope' ||
attr.key.name.name === 'scope')
)
- const nameArgument = slotName
- ? vBind
- ? `:[${slotName}]`
- : `:${slotName}`
- : ''
+ let nameArgument = ''
+ if (slotName) {
+ nameArgument = vBind ? `:[${slotName}]` : `:${slotName}`
+ }
const scopeValue =
scopeAttr && scopeAttr.value
? `=${sourceCode.getText(scopeAttr.value)}`
: ''
const replaceText = `v-slot${nameArgument}${scopeValue}`
- const fixers = [fixer.replaceText(slotAttr || scopeAttr, replaceText)]
- if (slotAttr && scopeAttr) {
- fixers.push(fixer.remove(scopeAttr))
+
+ const element = startTag.parent
+ if (element.name === 'template') {
+ yield fixer.replaceText(slotAttr || scopeAttr, replaceText)
+ if (slotAttr && scopeAttr) {
+ yield fixer.remove(scopeAttr)
+ }
+ } else {
+ yield fixer.remove(slotAttr || scopeAttr)
+ if (slotAttr && scopeAttr) {
+ yield fixer.remove(scopeAttr)
+ }
+
+ const vFor = startTag.attributes.find(
+ (attr) => attr.directive && attr.key.name.name === 'for'
+ )
+ const vForText = vFor ? `${sourceCode.getText(vFor)} ` : ''
+ if (vFor) {
+ yield fixer.remove(vFor)
+ }
+
+ yield fixer.insertTextBefore(
+ element,
+ `\n`
+ )
+ yield fixer.insertTextAfter(element, `\n`)
}
- return fixers
}
/**
* Reports `slot` node
@@ -90,16 +121,25 @@ module.exports = {
* @returns {void}
*/
function reportSlot(slotAttr) {
+ const componentName = slotAttr.parent.parent.rawName
+ if (
+ ignore.has(componentName) ||
+ ignore.has(casing.pascalCase(componentName)) ||
+ ignore.has(casing.kebabCase(componentName))
+ ) {
+ return
+ }
+
context.report({
node: slotAttr.key,
messageId: 'forbiddenSlotAttribute',
// fix to use `v-slot`
- fix(fixer) {
+ *fix(fixer) {
if (!canConvertFromSlotToVSlot(slotAttr)) {
- return null
+ return
}
const slotName = slotAttr.value && slotAttr.value.value
- return fixSlotToVSlot(fixer, slotAttr, slotName, false)
+ yield* fixSlotToVSlot(fixer, slotAttr, slotName, false)
}
})
}
@@ -113,22 +153,23 @@ module.exports = {
node: slotAttr.key,
messageId: 'forbiddenSlotAttribute',
// fix to use `v-slot`
- fix(fixer) {
+ *fix(fixer) {
if (!canConvertFromVBindSlotToVSlot(slotAttr)) {
- return null
+ return
}
const slotName =
slotAttr.value &&
slotAttr.value.expression &&
sourceCode.getText(slotAttr.value.expression).trim()
- return fixSlotToVSlot(fixer, slotAttr, slotName, true)
+ yield* fixSlotToVSlot(fixer, slotAttr, slotName, true)
}
})
}
return {
"VAttribute[directive=false][key.name='slot']": reportSlot,
- "VAttribute[directive=true][key.name.name='bind'][key.argument.name='slot']": reportVBindSlot
+ "VAttribute[directive=true][key.name.name='bind'][key.argument.name='slot']":
+ reportVBindSlot
}
}
}
diff --git a/lib/rules/syntaxes/slot-scope-attribute.js b/lib/rules/syntaxes/slot-scope-attribute.js
index e05be5c3f..5f8070498 100644
--- a/lib/rules/syntaxes/slot-scope-attribute.js
+++ b/lib/rules/syntaxes/slot-scope-attribute.js
@@ -3,9 +3,12 @@
* See LICENSE file in root directory for full license.
*/
'use strict'
+
+const canConvertToVSlotForElement = require('./utils/can-convert-to-v-slot')
+
module.exports = {
deprecated: '2.6.0',
- supported: '2.5.0',
+ supported: '>=2.5.0 <3.0.0',
/**
* @param {RuleContext} context
* @param {object} option
@@ -14,6 +17,9 @@ module.exports = {
*/
createTemplateBodyVisitor(context, { fixToUpgrade } = {}) {
const sourceCode = context.getSourceCode()
+ const tokenStore =
+ sourceCode.parserServices.getTemplateBodyTokenStore &&
+ sourceCode.parserServices.getTemplateBodyTokenStore()
/**
* Checks whether the given node can convert to the `v-slot`.
@@ -21,7 +27,9 @@ module.exports = {
* @returns {boolean} `true` if the given node can convert to the `v-slot`
*/
function canConvertToVSlot(startTag) {
- if (startTag.parent.name !== 'template') {
+ if (
+ !canConvertToVSlotForElement(startTag.parent, sourceCode, tokenStore)
+ ) {
return false
}
@@ -54,16 +62,26 @@ module.exports = {
* Convert to `v-slot`.
* @param {RuleFixer} fixer fixer
* @param {VDirective} scopeAttr node of `slot-scope`
- * @returns {Fix} fix data
+ * @returns {Fix[]} fix data
*/
function fixSlotScopeToVSlot(fixer, scopeAttr) {
+ const element = scopeAttr.parent.parent
const scopeValue =
scopeAttr && scopeAttr.value
? `=${sourceCode.getText(scopeAttr.value)}`
: ''
const replaceText = `v-slot${scopeValue}`
- return fixer.replaceText(scopeAttr, replaceText)
+ if (element.name === 'template') {
+ return [fixer.replaceText(scopeAttr, replaceText)]
+ } else {
+ const tokenBefore = tokenStore.getTokenBefore(scopeAttr)
+ return [
+ fixer.removeRange([tokenBefore.range[1], scopeAttr.range[1]]),
+ fixer.insertTextBefore(element, `\n`),
+ fixer.insertTextAfter(element, `\n`)
+ ]
+ }
}
/**
* Reports `slot-scope` node
@@ -74,16 +92,17 @@ module.exports = {
context.report({
node: scopeAttr.key,
messageId: 'forbiddenSlotScopeAttribute',
- fix: fixToUpgrade
- ? // fix to use `v-slot`
- (fixer) => {
- const startTag = scopeAttr.parent
- if (!canConvertToVSlot(startTag)) {
- return null
- }
- return fixSlotScopeToVSlot(fixer, scopeAttr)
- }
- : null
+ fix(fixer) {
+ if (!fixToUpgrade) {
+ return null
+ }
+ // fix to use `v-slot`
+ const startTag = scopeAttr.parent
+ if (!canConvertToVSlot(startTag)) {
+ return null
+ }
+ return fixSlotScopeToVSlot(fixer, scopeAttr)
+ }
})
}
diff --git a/lib/rules/syntaxes/style-css-vars-injection.js b/lib/rules/syntaxes/style-css-vars-injection.js
new file mode 100644
index 000000000..03608b5e1
--- /dev/null
+++ b/lib/rules/syntaxes/style-css-vars-injection.js
@@ -0,0 +1,28 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { getStyleVariablesContext } = require('../../utils/style-variables')
+
+module.exports = {
+ supported: '>=3.0.3 || >=2.7.0 <3.0.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createScriptVisitor(context) {
+ const styleVars = getStyleVariablesContext(context)
+ if (!styleVars) {
+ return {}
+ }
+ return {
+ Program() {
+ for (const vBind of styleVars.vBinds) {
+ context.report({
+ node: vBind,
+ messageId: 'forbiddenStyleCssVarsInjection'
+ })
+ }
+ }
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/utils/can-convert-to-v-slot.js b/lib/rules/syntaxes/utils/can-convert-to-v-slot.js
new file mode 100644
index 000000000..e0e51053d
--- /dev/null
+++ b/lib/rules/syntaxes/utils/can-convert-to-v-slot.js
@@ -0,0 +1,223 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../../../utils')
+/**
+ * @typedef {object} SlotVForVariables
+ * @property {VForExpression} expr
+ * @property {VVariable[]} variables
+ */
+/**
+ * @typedef {object} SlotContext
+ * @property {VElement} element
+ * @property {VAttribute | VDirective | null} slot
+ * @property {VDirective | null} vFor
+ * @property {SlotVForVariables | null} slotVForVars
+ * @property {string} normalizedName
+ */
+/**
+ * Checks whether the given element can use v-slot.
+ * @param {VElement} element
+ * @param {SourceCode} sourceCode
+ * @param {ParserServices.TokenStore} tokenStore
+ */
+module.exports = function canConvertToVSlot(element, sourceCode, tokenStore) {
+ const ownerElement = element.parent
+ if (
+ ownerElement.type === 'VDocumentFragment' ||
+ !utils.isCustomComponent(ownerElement) ||
+ ownerElement.name === 'component'
+ ) {
+ return false
+ }
+ const slot = getSlotContext(element, sourceCode)
+ if (slot.vFor && !slot.slotVForVars) {
+ // E.g.,
+ return false
+ }
+ if (hasSameSlotDirective(ownerElement, slot, sourceCode, tokenStore)) {
+ return false
+ }
+ return true
+}
+/**
+ * @param {VElement} element
+ * @param {SourceCode} sourceCode
+ * @returns {SlotContext}
+ */
+function getSlotContext(element, sourceCode) {
+ const slot =
+ utils.getAttribute(element, 'slot') ||
+ utils.getDirective(element, 'bind', 'slot')
+ const vFor = utils.getDirective(element, 'for')
+ const slotVForVars = getSlotVForVariableIfUsingIterationVars(slot, vFor)
+
+ return {
+ element,
+ slot,
+ vFor,
+ slotVForVars,
+ normalizedName: getNormalizedName(slot, sourceCode)
+ }
+}
+
+/**
+ * Gets the `v-for` directive and variable that provide the variables used by the given `slot` attribute.
+ * @param {VAttribute | VDirective | null} slot The current `slot` attribute node.
+ * @param {VDirective | null} [vFor] The current `v-for` directive node.
+ * @returns { SlotVForVariables | null } The SlotVForVariables.
+ */
+function getSlotVForVariableIfUsingIterationVars(slot, vFor) {
+ if (!slot || !slot.directive) {
+ return null
+ }
+ const expr =
+ vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression)
+ const variables =
+ expr && getUsingIterationVars(slot.value, slot.parent.parent)
+ return expr && variables && variables.length > 0 ? { expr, variables } : null
+}
+
+/**
+ * Gets iterative variables if a given expression node is using iterative variables that the element defined.
+ * @param {VExpressionContainer|null} expression The expression node to check.
+ * @param {VElement} element The element node which has the expression.
+ * @returns {VVariable[]} The expression node is using iteration variables.
+ */
+function getUsingIterationVars(expression, element) {
+ const vars = []
+ if (expression && expression.type === 'VExpressionContainer') {
+ for (const { variable } of expression.references) {
+ if (
+ variable != null &&
+ variable.kind === 'v-for' &&
+ variable.id.range[0] > element.startTag.range[0] &&
+ variable.id.range[1] < element.startTag.range[1]
+ ) {
+ vars.push(variable)
+ }
+ }
+ }
+ return vars
+}
+
+/**
+ * Get the normalized name of a given `slot` attribute node.
+ * @param {VAttribute | VDirective | null} slotAttr node of `slot`
+ * @param {SourceCode} sourceCode The source code.
+ * @returns {string} The normalized name.
+ */
+function getNormalizedName(slotAttr, sourceCode) {
+ if (!slotAttr) {
+ return 'default'
+ }
+ if (!slotAttr.directive) {
+ return slotAttr.value ? slotAttr.value.value : 'default'
+ }
+ return slotAttr.value ? `[${sourceCode.getText(slotAttr.value)}]` : '[null]'
+}
+
+/**
+ * Checks whether parent element has the same slot as the given slot.
+ * @param {VElement} ownerElement The parent element.
+ * @param {SlotContext} targetSlot The SlotContext with a slot to check if they are the same.
+ * @param {SourceCode} sourceCode
+ * @param {ParserServices.TokenStore} tokenStore
+ */
+function hasSameSlotDirective(
+ ownerElement,
+ targetSlot,
+ sourceCode,
+ tokenStore
+) {
+ for (const group of utils.iterateChildElementsChains(ownerElement)) {
+ if (group.includes(targetSlot.element)) {
+ continue
+ }
+ for (const childElement of group) {
+ const slot = getSlotContext(childElement, sourceCode)
+ if (!targetSlot.slotVForVars || !slot.slotVForVars) {
+ if (
+ !targetSlot.slotVForVars &&
+ !slot.slotVForVars &&
+ targetSlot.normalizedName === slot.normalizedName
+ ) {
+ return true
+ }
+ continue
+ }
+ if (
+ equalSlotVForVariables(
+ targetSlot.slotVForVars,
+ slot.slotVForVars,
+ tokenStore
+ )
+ ) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+/**
+ * Determines whether the two given `v-slot` variables are considered to be equal.
+ * @param {SlotVForVariables} a First element.
+ * @param {SlotVForVariables} b Second element.
+ * @param {ParserServices.TokenStore} tokenStore The token store.
+ * @returns {boolean} `true` if the elements are considered to be equal.
+ */
+function equalSlotVForVariables(a, b, tokenStore) {
+ if (a.variables.length !== b.variables.length) {
+ return false
+ }
+ if (!equal(a.expr.right, b.expr.right)) {
+ return false
+ }
+
+ const checkedVarNames = new Set()
+ const len = Math.min(a.expr.left.length, b.expr.left.length)
+ for (let index = 0; index < len; index++) {
+ const aPtn = a.expr.left[index]
+ const bPtn = b.expr.left[index]
+
+ const aVar = a.variables.find(
+ (v) => aPtn.range[0] <= v.id.range[0] && v.id.range[1] <= aPtn.range[1]
+ )
+ const bVar = b.variables.find(
+ (v) => bPtn.range[0] <= v.id.range[0] && v.id.range[1] <= bPtn.range[1]
+ )
+ if (aVar && bVar) {
+ if (aVar.id.name !== bVar.id.name) {
+ return false
+ }
+ if (!equal(aPtn, bPtn)) {
+ return false
+ }
+ checkedVarNames.add(aVar.id.name)
+ } else if (aVar || bVar) {
+ return false
+ }
+ }
+ return a.variables.every(
+ (v) =>
+ checkedVarNames.has(v.id.name) ||
+ b.variables.some((bv) => v.id.name === bv.id.name)
+ )
+
+ /**
+ * Determines whether the two given nodes are considered to be equal.
+ * @param {ASTNode} a First node.
+ * @param {ASTNode} b Second node.
+ * @returns {boolean} `true` if the nodes are considered to be equal.
+ */
+ function equal(a, b) {
+ if (a.type !== b.type) {
+ return false
+ }
+ return utils.equalTokens(a, b, tokenStore)
+ }
+}
diff --git a/lib/rules/syntaxes/v-bind-attr-modifier.js b/lib/rules/syntaxes/v-bind-attr-modifier.js
new file mode 100644
index 000000000..82a0922aa
--- /dev/null
+++ b/lib/rules/syntaxes/v-bind-attr-modifier.js
@@ -0,0 +1,32 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+module.exports = {
+ supported: '>=3.2.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createTemplateBodyVisitor(context) {
+ /**
+ * Reports `v-bind.attr` node
+ * @param { VIdentifier } mod node of `v-bind.attr`
+ * @returns {void}
+ */
+ function report(mod) {
+ context.report({
+ node: mod,
+ messageId: 'forbiddenVBindAttrModifier'
+ })
+ }
+
+ return {
+ "VAttribute[directive=true][key.name.name='bind']"(node) {
+ const attrMod = node.key.modifiers.find((m) => m.name === 'attr')
+ if (attrMod) {
+ report(attrMod)
+ }
+ }
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
index 4038c81a5..645ed375d 100644
--- a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
+++ b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
@@ -3,15 +3,9 @@
* See LICENSE file in root directory for full license.
*/
'use strict'
-const semver = require('semver')
-const unsupported = new semver.Range('<=2.5 || >=2.6.0')
module.exports = {
- // >=2.6.0-beta.1 <=2.6.0-beta.3
- /** @param {semver.Range} versionRange */
- supported: (versionRange) => {
- return !versionRange.intersects(unsupported)
- },
+ supported: '>=3.2.0 || >=2.6.0-beta.1 <=2.6.0-beta.3',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
@@ -33,7 +27,8 @@ module.exports = {
}
return {
- "VAttribute[directive=true] > VDirectiveKey[name.name='bind'][name.rawName='.']": reportPropModifierShorthand
+ "VAttribute[directive=true] > VDirectiveKey[name.name='bind'][name.rawName='.']":
+ reportPropModifierShorthand
}
}
}
diff --git a/lib/rules/syntaxes/v-bind-same-name-shorthand.js b/lib/rules/syntaxes/v-bind-same-name-shorthand.js
new file mode 100644
index 000000000..d9e7a388c
--- /dev/null
+++ b/lib/rules/syntaxes/v-bind-same-name-shorthand.js
@@ -0,0 +1,34 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../../utils')
+
+module.exports = {
+ supported: '>=3.4.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createTemplateBodyVisitor(context) {
+ /**
+ * Verify the directive node
+ * @param {VDirective} node The directive node to check
+ * @returns {void}
+ */
+ function checkDirective(node) {
+ if (utils.isVBindSameNameShorthand(node)) {
+ context.report({
+ node,
+ messageId: 'forbiddenVBindSameNameShorthand',
+ // fix to use `:x="x"` (downgrade)
+ fix: (fixer) =>
+ fixer.insertTextAfter(node, `="${node.value.expression.name}"`)
+ })
+ }
+ }
+
+ return {
+ "VAttribute[directive=true][key.name.name='bind']": checkDirective
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/v-is.js b/lib/rules/syntaxes/v-is.js
new file mode 100644
index 000000000..7fb1f862e
--- /dev/null
+++ b/lib/rules/syntaxes/v-is.js
@@ -0,0 +1,27 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+module.exports = {
+ deprecated: '3.1.0',
+ supported: '>=3.0.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createTemplateBodyVisitor(context) {
+ /**
+ * Reports `v-is` node
+ * @param {VDirective} vIsAttr node of `v-is`
+ * @returns {void}
+ */
+ function reportVIs(vIsAttr) {
+ context.report({
+ node: vIsAttr.key,
+ messageId: 'forbiddenVIs'
+ })
+ }
+
+ return {
+ "VAttribute[directive=true][key.name.name='is']": reportVIs
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/v-memo.js b/lib/rules/syntaxes/v-memo.js
new file mode 100644
index 000000000..958b51cf3
--- /dev/null
+++ b/lib/rules/syntaxes/v-memo.js
@@ -0,0 +1,26 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+module.exports = {
+ supported: '>=3.2.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createTemplateBodyVisitor(context) {
+ /**
+ * Reports `v-is` node
+ * @param {VDirective} vMemoAttr node of `v-is`
+ * @returns {void}
+ */
+ function reportVMemo(vMemoAttr) {
+ context.report({
+ node: vMemoAttr.key,
+ messageId: 'forbiddenVMemo'
+ })
+ }
+
+ return {
+ "VAttribute[directive=true][key.name.name='memo']": reportVMemo
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/v-model-argument.js b/lib/rules/syntaxes/v-model-argument.js
index bba028e12..d1bd59738 100644
--- a/lib/rules/syntaxes/v-model-argument.js
+++ b/lib/rules/syntaxes/v-model-argument.js
@@ -5,7 +5,7 @@
'use strict'
module.exports = {
- supported: '3.0.0',
+ supported: '>=3.0.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
return {
diff --git a/lib/rules/syntaxes/v-model-custom-modifiers.js b/lib/rules/syntaxes/v-model-custom-modifiers.js
index 5630f9ad4..4b17b47d6 100644
--- a/lib/rules/syntaxes/v-model-custom-modifiers.js
+++ b/lib/rules/syntaxes/v-model-custom-modifiers.js
@@ -4,14 +4,10 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
const BUILTIN_MODIFIERS = new Set(['lazy', 'number', 'trim'])
module.exports = {
- supported: '3.0.0',
+ supported: '>=3.0.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
return {
diff --git a/lib/rules/syntaxes/v-slot.js b/lib/rules/syntaxes/v-slot.js
index 9d563a2dd..a6a4b4ebd 100644
--- a/lib/rules/syntaxes/v-slot.js
+++ b/lib/rules/syntaxes/v-slot.js
@@ -3,23 +3,22 @@
* See LICENSE file in root directory for full license.
*/
'use strict'
+
+/**
+ * Checks whether the given node can convert to the `slot`.
+ * @param {VDirective} vSlotAttr node of `v-slot`
+ * @returns {boolean} `true` if the given node can convert to the `slot`
+ */
+function canConvertToSlot(vSlotAttr) {
+ return vSlotAttr.parent.parent.name === 'template'
+}
+
module.exports = {
- supported: '2.6.0',
+ supported: '>=2.6.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
const sourceCode = context.getSourceCode()
- /**
- * Checks whether the given node can convert to the `slot`.
- * @param {VDirective} vSlotAttr node of `v-slot`
- * @returns {boolean} `true` if the given node can convert to the `slot`
- */
- function canConvertToSlot(vSlotAttr) {
- if (vSlotAttr.parent.parent.name !== 'template') {
- return false
- }
- return true
- }
/**
* Convert to `slot` and `slot-scope`.
* @param {RuleFixer} fixer fixer
@@ -28,7 +27,7 @@ module.exports = {
*/
function fixVSlotToSlot(fixer, vSlotAttr) {
const key = vSlotAttr.key
- if (key.modifiers.length) {
+ if (key.modifiers.length > 0) {
// unknown modifiers
return null
}
@@ -54,7 +53,7 @@ module.exports = {
if (scopedValueNode) {
attrs.push(`slot-scope=${sourceCode.getText(scopedValueNode)}`)
}
- if (!attrs.length) {
+ if (attrs.length === 0) {
attrs.push('slot') // useless
}
return fixer.replaceText(vSlotAttr, attrs.join(' '))
@@ -69,7 +68,7 @@ module.exports = {
node: vSlotAttr.key,
messageId: 'forbiddenVSlot',
// fix to use `slot` (downgrade)
- fix: (fixer) => {
+ fix(fixer) {
if (!canConvertToSlot(vSlotAttr)) {
return null
}
diff --git a/lib/rules/template-curly-spacing.js b/lib/rules/template-curly-spacing.js
index 777c5d820..e215108e9 100644
--- a/lib/rules/template-curly-spacing.js
+++ b/lib/rules/template-curly-spacing.js
@@ -3,10 +3,10 @@
*/
'use strict'
-const { wrapCoreRule } = require('../utils')
+const { wrapStylisticOrCoreRule } = require('../utils')
-// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
-module.exports = wrapCoreRule(
- require('eslint/lib/rules/template-curly-spacing'),
- { skipDynamicArguments: true }
-)
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapStylisticOrCoreRule('template-curly-spacing', {
+ skipDynamicArguments: true,
+ applyDocument: true
+})
diff --git a/lib/rules/this-in-template.js b/lib/rules/this-in-template.js
index 0efa03035..a664a8edf 100644
--- a/lib/rules/this-in-template.js
+++ b/lib/rules/this-in-template.js
@@ -4,31 +4,27 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
const RESERVED_NAMES = new Set(require('../utils/js-reserved.json'))
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow usage of `this` in template',
- categories: ['vue3-recommended', 'recommended'],
+ categories: ['vue3-recommended', 'vue2-recommended'],
url: 'https://eslint.vuejs.org/rules/this-in-template.html'
},
- fixable: null,
+ fixable: 'code',
schema: [
{
enum: ['always', 'never']
}
- ]
+ ],
+ messages: {
+ unexpected: "Unexpected usage of 'this'.",
+ expected: "Expected 'this'."
+ }
},
/**
@@ -38,7 +34,7 @@ module.exports = {
* @returns {Object} AST event handlers.
*/
create(context) {
- const options = context.options[0] !== 'always' ? 'never' : 'always'
+ const options = context.options[0] === 'always' ? 'always' : 'never'
/**
* @typedef {object} ScopeStack
* @property {ScopeStack | null} parent
@@ -54,7 +50,7 @@ module.exports = {
scopeStack = {
parent: scopeStack,
nodes: scopeStack
- ? scopeStack.nodes.slice() // make copy
+ ? [...scopeStack.nodes] // make copy
: []
}
if (node.variables) {
@@ -83,7 +79,7 @@ module.exports = {
!propertyName ||
scopeStack.nodes.some((el) => el.name === propertyName) ||
RESERVED_NAMES.has(propertyName) || // this.class | this['class']
- /^[0-9].*$|[^a-zA-Z0-9_]/.test(propertyName) // this['0aaaa'] | this['foo-bar bas']
+ /^\d.*$|[^\w$]/.test(propertyName) // this['0aaaa'] | this['foo-bar bas']
) {
return
}
@@ -91,7 +87,11 @@ module.exports = {
context.report({
node,
loc: node.loc,
- message: "Unexpected usage of 'this'."
+ fix(fixer) {
+ // node.parent should be some code like `this.test`, `this?.['result']`
+ return fixer.replaceText(node.parent, propertyName)
+ },
+ messageId: 'unexpected'
})
}
}
@@ -116,7 +116,10 @@ module.exports = {
context.report({
node: reference.id,
loc: reference.id.loc,
- message: "Expected 'this'."
+ messageId: 'expected',
+ fix(fixer) {
+ return fixer.insertTextBefore(reference.id, 'this.')
+ }
})
}
}
diff --git a/lib/rules/use-v-on-exact.js b/lib/rules/use-v-on-exact.js
index 0a1790658..f048b70ef 100644
--- a/lib/rules/use-v-on-exact.js
+++ b/lib/rules/use-v-on-exact.js
@@ -4,10 +4,6 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
/**
* @typedef { {name: string, node: VDirectiveKey, modifiers: string[] } } EventDirective
*/
@@ -25,10 +21,6 @@ const GLOBAL_MODIFIERS = new Set([
'native'
])
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
/**
* Finds and returns all keys for event directives
*
@@ -82,17 +74,18 @@ function hasSystemModifier(modifiers) {
* with keys represinting each event name
*
* @param {EventDirective[]} events
- * @returns { { [key: string]: EventDirective[] } } { click: [], keypress: [] }
+ * @returns { { [key: string]: EventDirective[] } } { click: [], keypress: [] }
*/
function groupEvents(events) {
- return events.reduce((acc, event) => {
- if (acc[event.name]) {
- acc[event.name].push(event)
- } else {
- acc[event.name] = [event]
+ /** @type { { [key: string]: EventDirective[] } } */
+ const grouped = {}
+ for (const event of events) {
+ if (!grouped[event.name]) {
+ grouped[event.name] = []
}
- return acc
- }, /** @type { { [key: string]: EventDirective[] } }*/ ({}))
+ grouped[event.name].push(event)
+ }
+ return grouped
}
/**
@@ -141,9 +134,9 @@ function hasConflictedModifiers(baseEvent, event) {
const baseEventSystemModifiers = getSystemModifiersString(baseEvent.modifiers)
return (
- baseEvent.modifiers.length >= 1 &&
+ baseEvent.modifiers.length > 0 &&
baseEventSystemModifiers !== eventSystemModifiers &&
- baseEventSystemModifiers.indexOf(eventSystemModifiers) > -1
+ baseEventSystemModifiers.includes(eventSystemModifiers)
)
}
@@ -154,30 +147,31 @@ function hasConflictedModifiers(baseEvent, event) {
* @returns {EventDirective[]} conflicted events, without duplicates
*/
function findConflictedEvents(events) {
- return events.reduce((acc, event) => {
- return [
- ...acc,
+ /** @type {EventDirective[]} */
+ const conflictedEvents = []
+ for (const event of events) {
+ conflictedEvents.push(
...events
- .filter((evt) => !acc.find((e) => evt === e)) // No duplicates
+ .filter((evt) => !conflictedEvents.includes(evt)) // No duplicates
.filter(hasConflictedModifiers.bind(null, event))
- ]
- }, /** @type {EventDirective[]} */ ([]))
+ )
+ }
+ return conflictedEvents
}
-// ------------------------------------------------------------------------------
-// Rule details
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce usage of `exact` modifier on `v-on`',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/use-v-on-exact.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ considerExact: "Consider to use '.exact' modifier."
+ }
},
/**
@@ -204,24 +198,24 @@ module.exports = {
const grouppedEvents = groupEvents(events)
- Object.keys(grouppedEvents).forEach((eventName) => {
+ for (const eventName of Object.keys(grouppedEvents)) {
const eventsInGroup = grouppedEvents[eventName]
const hasEventWithKeyModifier = eventsInGroup.some((event) =>
hasSystemModifier(event.modifiers)
)
- if (!hasEventWithKeyModifier) return
+ if (!hasEventWithKeyModifier) continue
const conflictedEvents = findConflictedEvents(eventsInGroup)
- conflictedEvents.forEach((e) => {
+ for (const e of conflictedEvents) {
context.report({
node: e.node,
loc: e.node.loc,
- message: "Consider to use '.exact' modifier."
+ messageId: 'considerExact'
})
- })
- })
+ }
+ }
}
})
}
diff --git a/lib/rules/v-bind-style.js b/lib/rules/v-bind-style.js
index e6878a349..752a8fdf0 100644
--- a/lib/rules/v-bind-style.js
+++ b/lib/rules/v-bind-style.js
@@ -5,71 +5,166 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
+const casing = require('../utils/casing')
+
+/**
+ * @typedef { VDirectiveKey & { name: VIdentifier & { name: 'bind' }, argument: VExpressionContainer | VIdentifier } } VBindDirectiveKey
+ * @typedef { VDirective & { key: VBindDirectiveKey } } VBindDirective
+ */
+
+/**
+ * @param {string} name
+ * @returns {string}
+ */
+function kebabCaseToCamelCase(name) {
+ return casing.isKebabCase(name) ? casing.camelCase(name) : name
+}
+
+/**
+ * @param {VBindDirective} node
+ * @returns {boolean}
+ */
+function isSameName(node) {
+ const attrName =
+ node.key.argument.type === 'VIdentifier' ? node.key.argument.rawName : null
+ const valueName =
+ node.value?.expression?.type === 'Identifier'
+ ? node.value.expression.name
+ : null
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+ if (!attrName || !valueName) return false
+
+ return kebabCaseToCamelCase(attrName) === kebabCaseToCamelCase(valueName)
+}
+
+/**
+ * @param {VBindDirectiveKey} key
+ * @returns {number}
+ */
+function getCutStart(key) {
+ const modifiers = key.modifiers
+ return modifiers.length > 0
+ ? modifiers[modifiers.length - 1].range[1]
+ : key.argument.range[1]
+}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce `v-bind` directive style',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
+ categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/v-bind-style.html'
},
fixable: 'code',
- schema: [{ enum: ['shorthand', 'longform'] }]
+ schema: [
+ { enum: ['shorthand', 'longform'] },
+ {
+ type: 'object',
+ properties: {
+ sameNameShorthand: { enum: ['always', 'never', 'ignore'] }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ expectedLonghand: "Expected 'v-bind' before ':'.",
+ unexpectedLonghand: "Unexpected 'v-bind' before ':'.",
+ expectedLonghandForProp: "Expected 'v-bind:' instead of '.'.",
+ expectedShorthand: 'Expected same-name shorthand.',
+ unexpectedShorthand: 'Unexpected same-name shorthand.'
+ }
},
/** @param {RuleContext} context */
create(context) {
const preferShorthand = context.options[0] !== 'longform'
+ /** @type {"always" | "never" | "ignore"} */
+ const sameNameShorthand = context.options[1]?.sameNameShorthand || 'ignore'
- return utils.defineTemplateBodyVisitor(context, {
- /** @param {VDirective} node */
- "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]"(
- node
- ) {
- const shorthandProp = node.key.name.rawName === '.'
- const shorthand = node.key.name.rawName === ':' || shorthandProp
- if (shorthand === preferShorthand) {
- return
- }
+ /** @param {VBindDirective} node */
+ function checkAttributeStyle(node) {
+ const shorthandProp = node.key.name.rawName === '.'
+ const shorthand = node.key.name.rawName === ':' || shorthandProp
+ if (shorthand === preferShorthand) {
+ return
+ }
+
+ let messageId = 'expectedLonghand'
+ if (preferShorthand) {
+ messageId = 'unexpectedLonghand'
+ } else if (shorthandProp) {
+ messageId = 'expectedLonghandForProp'
+ }
+
+ context.report({
+ node,
+ loc: node.loc,
+ messageId,
+ *fix(fixer) {
+ if (preferShorthand) {
+ yield fixer.remove(node.key.name)
+ } else {
+ yield fixer.insertTextBefore(node, 'v-bind')
- context.report({
- node,
- loc: node.loc,
- message: preferShorthand
- ? "Unexpected 'v-bind' before ':'."
- : shorthandProp
- ? "Expected 'v-bind:' instead of '.'."
- : /* otherwise */ "Expected 'v-bind' before ':'.",
- *fix(fixer) {
- if (preferShorthand) {
- yield fixer.remove(node.key.name)
- } else {
- yield fixer.insertTextBefore(node, 'v-bind')
-
- if (shorthandProp) {
- // Replace `.` by `:`.
- yield fixer.replaceText(node.key.name, ':')
-
- // Insert `.prop` modifier if it doesn't exist.
- const modifier = node.key.modifiers[0]
- const isAutoGeneratedPropModifier =
- modifier.name === 'prop' && modifier.rawName === ''
- if (isAutoGeneratedPropModifier) {
- yield fixer.insertTextBefore(modifier, '.prop')
- }
+ if (shorthandProp) {
+ // Replace `.` by `:`.
+ yield fixer.replaceText(node.key.name, ':')
+
+ // Insert `.prop` modifier if it doesn't exist.
+ const modifier = node.key.modifiers[0]
+ const isAutoGeneratedPropModifier =
+ modifier.name === 'prop' && modifier.rawName === ''
+ if (isAutoGeneratedPropModifier) {
+ yield fixer.insertTextBefore(modifier, '.prop')
}
}
}
- })
+ }
+ })
+ }
+
+ /** @param {VBindDirective} node */
+ function checkAttributeSameName(node) {
+ if (sameNameShorthand === 'ignore' || !isSameName(node)) return
+
+ const preferShorthand = sameNameShorthand === 'always'
+ const isShorthand = utils.isVBindSameNameShorthand(node)
+ if (isShorthand === preferShorthand) {
+ return
+ }
+
+ const messageId = preferShorthand
+ ? 'expectedShorthand'
+ : 'unexpectedShorthand'
+
+ context.report({
+ node,
+ loc: node.loc,
+ messageId,
+ *fix(fixer) {
+ if (preferShorthand) {
+ /** @type {Range} */
+ const valueRange = [getCutStart(node.key), node.range[1]]
+
+ yield fixer.removeRange(valueRange)
+ } else if (node.key.argument.type === 'VIdentifier') {
+ yield fixer.insertTextAfter(
+ node,
+ `="${kebabCaseToCamelCase(node.key.argument.rawName)}"`
+ )
+ }
+ }
+ })
+ }
+
+ return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VBindDirective} node */
+ "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]"(
+ node
+ ) {
+ checkAttributeSameName(node)
+ checkAttributeStyle(node)
}
})
}
diff --git a/lib/rules/v-for-delimiter-style.js b/lib/rules/v-for-delimiter-style.js
new file mode 100644
index 000000000..2b20cfd0a
--- /dev/null
+++ b/lib/rules/v-for-delimiter-style.js
@@ -0,0 +1,67 @@
+/**
+ * @fileoverview enforce `v-for` directive's delimiter style
+ * @author Flo Edelmann
+ * @copyright 2020 Flo Edelmann. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'layout',
+ docs: {
+ description: "enforce `v-for` directive's delimiter style",
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/v-for-delimiter-style.html'
+ },
+ fixable: 'code',
+ schema: [{ enum: ['in', 'of'] }],
+ messages: {
+ expected:
+ "Expected '{{preferredDelimiter}}' instead of '{{usedDelimiter}}' in 'v-for'."
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const preferredDelimiter =
+ /** @type {string|undefined} */ (context.options[0]) || 'in'
+
+ return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VForExpression} node */
+ VForExpression(node) {
+ const sourceCode = context.getSourceCode()
+ const tokenStore =
+ sourceCode.parserServices.getTemplateBodyTokenStore &&
+ sourceCode.parserServices.getTemplateBodyTokenStore()
+
+ const delimiterToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(
+ node.left.length > 0
+ ? node.left[node.left.length - 1]
+ : tokenStore.getFirstToken(node),
+ (token) => token.type !== 'Punctuator'
+ )
+ )
+
+ if (delimiterToken.value === preferredDelimiter) {
+ return
+ }
+
+ context.report({
+ node,
+ loc: node.loc,
+ messageId: 'expected',
+ data: {
+ preferredDelimiter,
+ usedDelimiter: delimiterToken.value
+ },
+ *fix(fixer) {
+ yield fixer.replaceText(delimiterToken, preferredDelimiter)
+ }
+ })
+ }
+ })
+ }
+}
diff --git a/lib/rules/v-if-else-key.js b/lib/rules/v-if-else-key.js
new file mode 100644
index 000000000..d8e913670
--- /dev/null
+++ b/lib/rules/v-if-else-key.js
@@ -0,0 +1,317 @@
+/**
+ * @author Felipe Melendez
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// =============================================================================
+// Requirements
+// =============================================================================
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+// =============================================================================
+// Rule Helpers
+// =============================================================================
+
+/**
+ * A conditional family is made up of a group of repeated components that are conditionally rendered
+ * using v-if, v-else-if, and v-else.
+ *
+ * @typedef {Object} ConditionalFamily
+ * @property {VElement} if - The node associated with the 'v-if' directive.
+ * @property {VElement[]} elseIf - An array of nodes associated with 'v-else-if' directives.
+ * @property {VElement | null} else - The node associated with the 'v-else' directive, or null if there isn't one.
+ */
+
+/**
+ * Checks if a given node has sibling nodes of the same type that are also conditionally rendered.
+ * This is used to determine if multiple instances of the same component are being conditionally
+ * rendered within the same parent scope.
+ *
+ * @param {VElement} node - The Vue component node to check for conditional rendering siblings.
+ * @param {string} componentName - The name of the component to check for sibling instances.
+ * @returns {boolean} True if there are sibling nodes of the same type and conditionally rendered, false otherwise.
+ */
+const hasConditionalRenderedSiblings = (node, componentName) => {
+ if (!node.parent || node.parent.type !== 'VElement') {
+ return false
+ }
+ return node.parent.children.some(
+ (sibling) =>
+ sibling !== node &&
+ sibling.type === 'VElement' &&
+ sibling.rawName === componentName &&
+ hasConditionalDirective(sibling)
+ )
+}
+
+/**
+ * Checks for the presence of a 'key' attribute in the given node. If the 'key' attribute is missing
+ * and the node is part of a conditional family a report is generated.
+ * The fix proposed adds a unique key based on the component's name and count,
+ * following the format '${kebabCase(componentName)}-${componentCount}', e.g., 'some-component-2'.
+ *
+ * @param {VElement} node - The Vue component node to check for a 'key' attribute.
+ * @param {RuleContext} context - The rule's context object, used for reporting.
+ * @param {string} componentName - Name of the component.
+ * @param {string} uniqueKey - A unique key for the repeated component, used for the fix.
+ * @param {Map} conditionalFamilies - Map of conditionally rendered components and their respective conditional directives.
+ */
+const checkForKey = (
+ node,
+ context,
+ componentName,
+ uniqueKey,
+ conditionalFamilies
+) => {
+ if (
+ !node.parent ||
+ node.parent.type !== 'VElement' ||
+ !hasConditionalRenderedSiblings(node, componentName)
+ ) {
+ return
+ }
+
+ const conditionalFamily = conditionalFamilies.get(node.parent)
+
+ if (!conditionalFamily || utils.hasAttribute(node, 'key')) {
+ return
+ }
+
+ const needsKey =
+ conditionalFamily.if === node ||
+ conditionalFamily.else === node ||
+ conditionalFamily.elseIf.includes(node)
+
+ if (needsKey) {
+ context.report({
+ node: node.startTag,
+ loc: node.startTag.loc,
+ messageId: 'requireKey',
+ data: { componentName },
+ fix(fixer) {
+ const afterComponentNamePosition =
+ node.startTag.range[0] + componentName.length + 1
+ return fixer.insertTextBeforeRange(
+ [afterComponentNamePosition, afterComponentNamePosition],
+ ` key="${uniqueKey}"`
+ )
+ }
+ })
+ }
+}
+
+/**
+ * Checks for the presence of conditional directives in the given node.
+ *
+ * @param {VElement} node - The node to check for conditional directives.
+ * @returns {boolean} Returns true if a conditional directive is found in the node or its parents,
+ * false otherwise.
+ */
+const hasConditionalDirective = (node) =>
+ utils.hasDirective(node, 'if') ||
+ utils.hasDirective(node, 'else-if') ||
+ utils.hasDirective(node, 'else')
+
+// =============================================================================
+// Rule Definition
+// =============================================================================
+
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'require key attribute for conditionally rendered repeated components',
+ categories: null,
+ url: 'https://eslint.vuejs.org/rules/v-if-else-key.html'
+ },
+ // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
+ fixable: 'code',
+ schema: [],
+ messages: {
+ requireKey:
+ "Conditionally rendered repeated component '{{componentName}}' expected to have a 'key' attribute."
+ }
+ },
+ /**
+ * Creates and returns a rule object which checks usage of repeated components. If a component
+ * is used more than once, it checks for the presence of a key.
+ *
+ * @param {RuleContext} context - The context object.
+ * @returns {Object} A dictionary of functions to be called on traversal of the template body by
+ * the eslint parser.
+ */
+ create(context) {
+ /**
+ * Map to store conditionally rendered components and their respective conditional directives.
+ *
+ * @type {Map}
+ */
+ const conditionalFamilies = new Map()
+
+ /**
+ * Array of Maps to keep track of components and their usage counts along with the first
+ * node instance. Each Map represents a different scope level, and maps a component name to
+ * an object containing the count and a reference to the first node.
+ */
+ /** @type {Map[]} */
+ const componentUsageStack = [new Map()]
+
+ /**
+ * Checks if a given node represents a custom component without any conditional directives.
+ *
+ * @param {VElement} node - The AST node to check.
+ * @returns {boolean} True if the node represents a custom component without any conditional directives, false otherwise.
+ */
+ const isCustomComponentWithoutCondition = (node) =>
+ node.type === 'VElement' &&
+ utils.isCustomComponent(node) &&
+ !hasConditionalDirective(node)
+
+ /** Set of built-in Vue components that are exempt from the rule. */
+ /** @type {Set} */
+ const exemptTags = new Set(['component', 'slot', 'template'])
+
+ /** Set to keep track of nodes we've pushed to the stack. */
+ /** @type {Set} */
+ const pushedNodes = new Set()
+
+ /**
+ * Creates and returns an object representing a conditional family.
+ *
+ * @param {VElement} ifNode - The VElement associated with the 'v-if' directive.
+ * @returns {ConditionalFamily}
+ */
+ const createConditionalFamily = (ifNode) => ({
+ if: ifNode,
+ elseIf: [],
+ else: null
+ })
+
+ return utils.defineTemplateBodyVisitor(context, {
+ /**
+ * Callback to be executed when a Vue element is traversed. This function checks if the
+ * element is a component, increments the usage count of the component in the
+ * current scope, and checks for the key directive if the component is repeated.
+ *
+ * @param {VElement} node - The traversed Vue element.
+ */
+ VElement(node) {
+ if (exemptTags.has(node.rawName)) {
+ return
+ }
+
+ const condition =
+ utils.getDirective(node, 'if') ||
+ utils.getDirective(node, 'else-if') ||
+ utils.getDirective(node, 'else')
+
+ if (condition) {
+ const conditionType = condition.key.name.name
+
+ if (node.parent && node.parent.type === 'VElement') {
+ let conditionalFamily = conditionalFamilies.get(node.parent)
+
+ if (!conditionalFamily) {
+ conditionalFamily = createConditionalFamily(node)
+ conditionalFamilies.set(node.parent, conditionalFamily)
+ }
+
+ if (conditionalFamily) {
+ switch (conditionType) {
+ case 'if': {
+ conditionalFamily = createConditionalFamily(node)
+ conditionalFamilies.set(node.parent, conditionalFamily)
+ break
+ }
+ case 'else-if': {
+ conditionalFamily.elseIf.push(node)
+ break
+ }
+ case 'else': {
+ conditionalFamily.else = node
+ break
+ }
+ }
+ }
+ }
+ }
+
+ if (isCustomComponentWithoutCondition(node)) {
+ componentUsageStack.push(new Map())
+ return
+ }
+
+ if (!utils.isCustomComponent(node)) {
+ return
+ }
+
+ const componentName = node.rawName
+ const currentScope = componentUsageStack[componentUsageStack.length - 1]
+ const usageInfo = currentScope.get(componentName) || {
+ count: 0,
+ firstNode: null
+ }
+
+ if (hasConditionalDirective(node)) {
+ // Store the first node if this is the first occurrence
+ if (usageInfo.count === 0) {
+ usageInfo.firstNode = node
+ }
+
+ if (usageInfo.count > 0) {
+ const uniqueKey = `${casing.kebabCase(componentName)}-${
+ usageInfo.count + 1
+ }`
+ checkForKey(
+ node,
+ context,
+ componentName,
+ uniqueKey,
+ conditionalFamilies
+ )
+
+ // If this is the second occurrence, also apply a fix to the first occurrence
+ if (usageInfo.count === 1) {
+ const uniqueKeyForFirstInstance = `${casing.kebabCase(
+ componentName
+ )}-1`
+ checkForKey(
+ usageInfo.firstNode,
+ context,
+ componentName,
+ uniqueKeyForFirstInstance,
+ conditionalFamilies
+ )
+ }
+ }
+ usageInfo.count += 1
+ currentScope.set(componentName, usageInfo)
+ }
+ componentUsageStack.push(new Map())
+ pushedNodes.add(node)
+ },
+
+ 'VElement:exit'(node) {
+ if (exemptTags.has(node.rawName)) {
+ return
+ }
+ if (isCustomComponentWithoutCondition(node)) {
+ componentUsageStack.pop()
+ return
+ }
+ if (!utils.isCustomComponent(node)) {
+ return
+ }
+ if (pushedNodes.has(node)) {
+ componentUsageStack.pop()
+ pushedNodes.delete(node)
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/v-on-event-hyphenation.js b/lib/rules/v-on-event-hyphenation.js
new file mode 100644
index 000000000..c9fac76e8
--- /dev/null
+++ b/lib/rules/v-on-event-hyphenation.js
@@ -0,0 +1,138 @@
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+const { toRegExp } = require('../utils/regexp')
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description:
+ 'enforce v-on event naming style on custom components in template',
+ categories: ['vue3-strongly-recommended'],
+ url: 'https://eslint.vuejs.org/rules/v-on-event-hyphenation.html',
+ defaultOptions: {
+ vue3: ['always', { autofix: true }]
+ }
+ },
+ fixable: 'code',
+ schema: [
+ {
+ enum: ['always', 'never']
+ },
+ {
+ type: 'object',
+ properties: {
+ autofix: { type: 'boolean' },
+ ignore: {
+ type: 'array',
+ items: {
+ allOf: [
+ { type: 'string' },
+ { not: { type: 'string', pattern: ':exit$' } },
+ { not: { type: 'string', pattern: String.raw`^\s*$` } }
+ ]
+ },
+ uniqueItems: true,
+ additionalItems: false
+ },
+ ignoreTags: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true,
+ additionalItems: false
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ // eslint-disable-next-line eslint-plugin/report-message-format
+ mustBeHyphenated: "v-on event '{{text}}' must be hyphenated.",
+ // eslint-disable-next-line eslint-plugin/report-message-format
+ cannotBeHyphenated: "v-on event '{{text}}' can't be hyphenated."
+ }
+ },
+
+ /** @param {RuleContext} context */
+ create(context) {
+ const sourceCode = context.getSourceCode()
+ const option = context.options[0]
+ const optionsPayload = context.options[1]
+ const useHyphenated = option !== 'never'
+ /** @type {string[]} */
+ const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || []
+ /** @type {RegExp[]} */
+ const ignoredTagsRegexps = (
+ (optionsPayload && optionsPayload.ignoreTags) ||
+ []
+ ).map(toRegExp)
+ const autofix = Boolean(optionsPayload && optionsPayload.autofix)
+
+ const caseConverter = casing.getConverter(
+ useHyphenated ? 'kebab-case' : 'camelCase'
+ )
+
+ /**
+ * @param {VDirective} node
+ * @param {VIdentifier} argument
+ * @param {string} name
+ */
+ function reportIssue(node, argument, name) {
+ const text = sourceCode.getText(node.key)
+
+ context.report({
+ node: node.key,
+ loc: node.loc,
+ messageId: useHyphenated ? 'mustBeHyphenated' : 'cannotBeHyphenated',
+ data: {
+ text
+ },
+ fix:
+ autofix &&
+ // It cannot be converted in snake_case.
+ !name.includes('_')
+ ? (fixer) => fixer.replaceText(argument, caseConverter(name))
+ : null
+ })
+ }
+
+ /**
+ * @param {string} value
+ */
+ function isIgnoredAttribute(value) {
+ const isIgnored = ignoredAttributes.some((attr) => value.includes(attr))
+
+ if (isIgnored) {
+ return true
+ }
+
+ 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)
+ ) {
+ return
+ }
+ if (!node.key.argument || node.key.argument.type !== 'VIdentifier') {
+ return
+ }
+ const name = node.key.argument.rawName
+ if (!name || isIgnoredAttribute(name)) return
+
+ reportIssue(node, node.key.argument, name)
+ }
+ })
+ }
+}
diff --git a/lib/rules/v-on-function-call.js b/lib/rules/v-on-function-call.js
deleted file mode 100644
index 04eb79792..000000000
--- a/lib/rules/v-on-function-call.js
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- * @author Niklas Higi
- */
-'use strict'
-
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
-const utils = require('../utils')
-
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
-/**
- * Check whether the given token is a quote.
- * @param {Token} token The token to check.
- * @returns {boolean} `true` if the token is a quote.
- */
-function isQuote(token) {
- return (
- token != null &&
- token.type === 'Punctuator' &&
- (token.value === '"' || token.value === "'")
- )
-}
-
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
-module.exports = {
- meta: {
- type: 'suggestion',
- docs: {
- description:
- 'enforce or forbid parentheses after method calls without arguments in `v-on` directives',
- categories: undefined,
- url: 'https://eslint.vuejs.org/rules/v-on-function-call.html'
- },
- fixable: 'code',
- schema: [
- { enum: ['always', 'never'] },
- {
- type: 'object',
- properties: {
- ignoreIncludesComment: {
- type: 'boolean'
- }
- },
- additionalProperties: false
- }
- ]
- },
- /** @param {RuleContext} context */
- create(context) {
- const always = context.options[0] === 'always'
-
- /**
- * @param {VOnExpression} node
- * @returns {CallExpression | null}
- */
- function getInvalidNeverCallExpression(node) {
- /** @type {ExpressionStatement} */
- let exprStatement
- let body = node.body
- while (true) {
- const statements = body.filter((st) => st.type !== 'EmptyStatement')
- if (statements.length !== 1) {
- return null
- }
- const statement = statements[0]
- if (statement.type === 'ExpressionStatement') {
- exprStatement = statement
- break
- }
- if (statement.type === 'BlockStatement') {
- body = statement.body
- continue
- }
- return null
- }
- const expression = exprStatement.expression
- if (expression.type !== 'CallExpression' || expression.arguments.length) {
- return null
- }
- const callee = expression.callee
- if (callee.type !== 'Identifier') {
- return null
- }
- return expression
- }
-
- return utils.defineTemplateBodyVisitor(context, {
- ...(always
- ? {
- /** @param {Identifier} node */
- "VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer > Identifier"(
- node
- ) {
- context.report({
- node,
- message:
- "Method calls inside of 'v-on' directives must have parentheses."
- })
- }
- }
- : {
- /** @param {VOnExpression} node */
- "VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression"(
- node
- ) {
- const expression = getInvalidNeverCallExpression(node)
- if (!expression) {
- return
- }
- const option = context.options[1] || {}
- const ignoreIncludesComment = !!option.ignoreIncludesComment
-
- const tokenStore = context.parserServices.getTemplateBodyTokenStore()
- const tokens = tokenStore.getTokens(node.parent, {
- includeComments: true
- })
- /** @type {Token | undefined} */
- let leftQuote
- /** @type {Token | undefined} */
- let rightQuote
- if (isQuote(tokens[0])) {
- leftQuote = tokens.shift()
- rightQuote = tokens.pop()
- }
-
- const hasComment = tokens.some(
- (token) => token.type === 'Block' || token.type === 'Line'
- )
-
- if (ignoreIncludesComment && hasComment) {
- return
- }
-
- context.report({
- node: expression,
- message:
- "Method calls without arguments inside of 'v-on' directives must not have parentheses.",
- fix: hasComment
- ? null /* The comment is included and cannot be fixed. */
- : (fixer) => {
- /** @type {Range} */
- const range =
- leftQuote && rightQuote
- ? [leftQuote.range[1], rightQuote.range[0]]
- : [
- tokens[0].range[0],
- tokens[tokens.length - 1].range[1]
- ]
-
- return fixer.replaceTextRange(
- range,
- context.getSourceCode().getText(expression.callee)
- )
- }
- })
- }
- })
- })
- }
-}
diff --git a/lib/rules/v-on-handler-style.js b/lib/rules/v-on-handler-style.js
new file mode 100644
index 000000000..10ff9b6b1
--- /dev/null
+++ b/lib/rules/v-on-handler-style.js
@@ -0,0 +1,587 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @typedef {import('eslint').ReportDescriptorFix} ReportDescriptorFix
+ * @typedef {'method' | 'inline' | 'inline-function'} HandlerKind
+ * @typedef {object} ObjectOption
+ * @property {boolean} [ignoreIncludesComment]
+ */
+
+/**
+ * @param {RuleContext} context
+ */
+function parseOptions(context) {
+ /** @type {[HandlerKind | HandlerKind[] | undefined, ObjectOption | undefined]} */
+ const options = /** @type {any} */ (context.options)
+ /** @type {HandlerKind[]} */
+ const allows = []
+ if (options[0]) {
+ if (Array.isArray(options[0])) {
+ allows.push(...options[0])
+ } else {
+ allows.push(options[0])
+ }
+ } else {
+ allows.push('method', 'inline-function')
+ }
+
+ const option = options[1] || {}
+ const ignoreIncludesComment = !!option.ignoreIncludesComment
+
+ return { allows, ignoreIncludesComment }
+}
+
+/**
+ * Check whether the given token is a quote.
+ * @param {Token} token The token to check.
+ * @returns {boolean} `true` if the token is a quote.
+ */
+function isQuote(token) {
+ return (
+ token != null &&
+ token.type === 'Punctuator' &&
+ (token.value === '"' || token.value === "'")
+ )
+}
+/**
+ * Check whether the given node is an identifier call expression. e.g. `foo()`
+ * @param {Expression} node The node to check.
+ * @returns {node is CallExpression & {callee: Identifier}}
+ */
+function isIdentifierCallExpression(node) {
+ if (node.type !== 'CallExpression') {
+ return false
+ }
+ if (node.optional) {
+ // optional chaining
+ return false
+ }
+ const callee = node.callee
+ return callee.type === 'Identifier'
+}
+
+/**
+ * Returns a call expression node if the given VOnExpression or BlockStatement consists
+ * of only a single identifier call expression.
+ * e.g.
+ * @click="foo()"
+ * @click="{ foo() }"
+ * @click="foo();;"
+ * @param {VOnExpression | BlockStatement} node
+ * @returns {CallExpression & {callee: Identifier} | null}
+ */
+function getIdentifierCallExpression(node) {
+ /** @type {ExpressionStatement} */
+ let exprStatement
+ let body = node.body
+ while (true) {
+ const statements = body.filter((st) => st.type !== 'EmptyStatement')
+ if (statements.length !== 1) {
+ return null
+ }
+ const statement = statements[0]
+ if (statement.type === 'ExpressionStatement') {
+ exprStatement = statement
+ break
+ }
+ if (statement.type === 'BlockStatement') {
+ body = statement.body
+ continue
+ }
+ return null
+ }
+ const expression = exprStatement.expression
+ if (!isIdentifierCallExpression(expression)) {
+ return null
+ }
+ return expression
+}
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'enforce writing style for handlers in `v-on` directives',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/v-on-handler-style.html'
+ },
+ fixable: 'code',
+ schema: [
+ {
+ oneOf: [
+ { enum: ['inline', 'inline-function'] },
+ {
+ type: 'array',
+ items: [
+ { const: 'method' },
+ { enum: ['inline', 'inline-function'] }
+ ],
+ uniqueItems: true,
+ additionalItems: false,
+ minItems: 2,
+ maxItems: 2
+ }
+ ]
+ },
+ {
+ type: 'object',
+ properties: {
+ ignoreIncludesComment: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ preferMethodOverInline:
+ 'Prefer method handler over inline handler in v-on.',
+ preferMethodOverInlineWithoutIdCall:
+ 'Prefer method handler over inline handler in v-on. Note that you may need to create a new method.',
+ preferMethodOverInlineFunction:
+ 'Prefer method handler over inline function in v-on.',
+ preferMethodOverInlineFunctionWithoutIdCall:
+ 'Prefer method handler over inline function in v-on. Note that you may need to create a new method.',
+ preferInlineOverMethod:
+ 'Prefer inline handler over method handler in v-on.',
+ preferInlineOverInlineFunction:
+ 'Prefer inline handler over inline function in v-on.',
+ preferInlineOverInlineFunctionWithMultipleParams:
+ 'Prefer inline handler over inline function in v-on. Note that the custom event must be changed to a single payload.',
+ preferInlineFunctionOverMethod:
+ 'Prefer inline function over method handler in v-on.',
+ preferInlineFunctionOverInline:
+ 'Prefer inline function over inline handler in v-on.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const { allows, ignoreIncludesComment } = parseOptions(context)
+
+ /** @type {Set} */
+ const upperElements = new Set()
+ /** @type {Map} */
+ const methodParamCountMap = new Map()
+ /** @type {Identifier[]} */
+ const $eventIdentifiers = []
+
+ /**
+ * Verify for inline handler.
+ * @param {VOnExpression} node
+ * @param {HandlerKind} kind
+ * @returns {boolean} Returns `true` if reported.
+ */
+ function verifyForInlineHandler(node, kind) {
+ switch (kind) {
+ case 'method': {
+ return verifyCanUseMethodHandlerForInlineHandler(node)
+ }
+ case 'inline-function': {
+ reportCanUseInlineFunctionForInlineHandler(node)
+ return true
+ }
+ }
+ return false
+ }
+ /**
+ * Report for method handler.
+ * @param {Identifier} node
+ * @param {HandlerKind} kind
+ * @returns {boolean} Returns `true` if reported.
+ */
+ function reportForMethodHandler(node, kind) {
+ switch (kind) {
+ case 'inline':
+ case 'inline-function': {
+ context.report({
+ node,
+ messageId:
+ kind === 'inline'
+ ? 'preferInlineOverMethod'
+ : 'preferInlineFunctionOverMethod'
+ })
+ return true
+ }
+ }
+ // This path is currently not taken.
+ return false
+ }
+ /**
+ * Verify for inline function handler.
+ * @param {ArrowFunctionExpression | FunctionExpression} node
+ * @param {HandlerKind} kind
+ * @returns {boolean} Returns `true` if reported.
+ */
+ function verifyForInlineFunction(node, kind) {
+ switch (kind) {
+ case 'method': {
+ return verifyCanUseMethodHandlerForInlineFunction(node)
+ }
+ case 'inline': {
+ reportCanUseInlineHandlerForInlineFunction(node)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Get token information for the given VExpressionContainer node.
+ * @param {VExpressionContainer} node
+ */
+ function getVExpressionContainerTokenInfo(node) {
+ const sourceCode = context.getSourceCode()
+ const tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore()
+ const tokens = tokenStore.getTokens(node, {
+ includeComments: true
+ })
+ const firstToken = tokens[0]
+ const lastToken = tokens[tokens.length - 1]
+
+ const hasQuote = isQuote(firstToken)
+ /** @type {Range} */
+ const rangeWithoutQuotes = hasQuote
+ ? [firstToken.range[1], lastToken.range[0]]
+ : [firstToken.range[0], lastToken.range[1]]
+
+ return {
+ rangeWithoutQuotes,
+ get hasComment() {
+ return tokens.some(
+ (token) => token.type === 'Block' || token.type === 'Line'
+ )
+ },
+ hasQuote
+ }
+ }
+
+ /**
+ * Checks whether the given node refers to a variable of the element.
+ * @param {Expression | VOnExpression} node
+ */
+ function hasReferenceUpperElementVariable(node) {
+ for (const element of upperElements) {
+ for (const vv of element.variables) {
+ for (const reference of vv.references) {
+ const { range } = reference.id
+ if (node.range[0] <= range[0] && range[1] <= node.range[1]) {
+ return true
+ }
+ }
+ }
+ }
+ return false
+ }
+ /**
+ * Check if `v-on:click="foo()"` can be converted to `v-on:click="foo"` and report if it can.
+ * @param {VOnExpression} node
+ * @returns {boolean} Returns `true` if reported.
+ */
+ function verifyCanUseMethodHandlerForInlineHandler(node) {
+ const { rangeWithoutQuotes, hasComment } =
+ getVExpressionContainerTokenInfo(node.parent)
+ if (ignoreIncludesComment && hasComment) {
+ return false
+ }
+
+ const idCallExpr = getIdentifierCallExpression(node)
+ if (
+ (!idCallExpr || idCallExpr.arguments.length > 0) &&
+ hasReferenceUpperElementVariable(node)
+ ) {
+ // It cannot be converted to method because it refers to the variable of the element.
+ // e.g.
+ return false
+ }
+
+ context.report({
+ node,
+ messageId: idCallExpr
+ ? 'preferMethodOverInline'
+ : 'preferMethodOverInlineWithoutIdCall',
+ fix: (fixer) => {
+ if (
+ hasComment /* The statement contains comment and cannot be fixed. */ ||
+ !idCallExpr /* The statement is not a simple identifier call and cannot be fixed. */ ||
+ idCallExpr.arguments.length > 0
+ ) {
+ return null
+ }
+ const paramCount = methodParamCountMap.get(idCallExpr.callee.name)
+ if (paramCount != null && paramCount > 0) {
+ // The behavior of target method can change given the arguments.
+ return null
+ }
+ return fixer.replaceTextRange(
+ rangeWithoutQuotes,
+ context.getSourceCode().getText(idCallExpr.callee)
+ )
+ }
+ })
+ return true
+ }
+ /**
+ * Check if `v-on:click="() => foo()"` can be converted to `v-on:click="foo"` and report if it can.
+ * @param {ArrowFunctionExpression | FunctionExpression} node
+ * @returns {boolean} Returns `true` if reported.
+ */
+ function verifyCanUseMethodHandlerForInlineFunction(node) {
+ const { rangeWithoutQuotes, hasComment } =
+ getVExpressionContainerTokenInfo(
+ /** @type {VExpressionContainer} */ (node.parent)
+ )
+ if (ignoreIncludesComment && hasComment) {
+ return false
+ }
+
+ /** @type {CallExpression & {callee: Identifier} | null} */
+ let idCallExpr = null
+ if (node.body.type === 'BlockStatement') {
+ idCallExpr = getIdentifierCallExpression(node.body)
+ } else if (isIdentifierCallExpression(node.body)) {
+ idCallExpr = node.body
+ }
+ if (
+ (!idCallExpr || !isSameParamsAndArgs(idCallExpr)) &&
+ hasReferenceUpperElementVariable(node)
+ ) {
+ // It cannot be converted to method because it refers to the variable of the element.
+ // e.g.
+ return false
+ }
+
+ context.report({
+ node,
+ messageId: idCallExpr
+ ? 'preferMethodOverInlineFunction'
+ : 'preferMethodOverInlineFunctionWithoutIdCall',
+ fix: (fixer) => {
+ if (
+ hasComment /* The function contains comment and cannot be fixed. */ ||
+ !idCallExpr /* The function is not a simple identifier call and cannot be fixed. */
+ ) {
+ return null
+ }
+ if (!isSameParamsAndArgs(idCallExpr)) {
+ // It is not a call with the arguments given as is.
+ return null
+ }
+ const paramCount = methodParamCountMap.get(idCallExpr.callee.name)
+ if (
+ paramCount != null &&
+ paramCount !== idCallExpr.arguments.length
+ ) {
+ // The behavior of target method can change given the arguments.
+ return null
+ }
+ return fixer.replaceTextRange(
+ rangeWithoutQuotes,
+ context.getSourceCode().getText(idCallExpr.callee)
+ )
+ }
+ })
+ return true
+
+ /**
+ * Checks whether parameters are passed as arguments as-is.
+ * @param {CallExpression} expression
+ */
+ function isSameParamsAndArgs(expression) {
+ return (
+ node.params.length === expression.arguments.length &&
+ node.params.every((param, index) => {
+ if (param.type !== 'Identifier') {
+ return false
+ }
+ const arg = expression.arguments[index]
+ if (!arg || arg.type !== 'Identifier') {
+ return false
+ }
+ return param.name === arg.name
+ })
+ )
+ }
+ }
+ /**
+ * Report `v-on:click="foo()"` can be converted to `v-on:click="()=>foo()"`.
+ * @param {VOnExpression} node
+ * @returns {void}
+ */
+ function reportCanUseInlineFunctionForInlineHandler(node) {
+ context.report({
+ node,
+ messageId: 'preferInlineFunctionOverInline',
+ *fix(fixer) {
+ const has$Event = $eventIdentifiers.some(
+ ({ range }) =>
+ node.range[0] <= range[0] && range[1] <= node.range[1]
+ )
+ if (has$Event) {
+ /* The statements contains $event and cannot be fixed. */
+ return
+ }
+ const { rangeWithoutQuotes, hasQuote } =
+ getVExpressionContainerTokenInfo(node.parent)
+ if (!hasQuote) {
+ /* The statements is not enclosed in quotes and cannot be fixed. */
+ return
+ }
+ yield fixer.insertTextBeforeRange(rangeWithoutQuotes, '() => ')
+ const sourceCode = context.getSourceCode()
+ const tokenStore =
+ sourceCode.parserServices.getTemplateBodyTokenStore()
+ const firstToken = tokenStore.getFirstToken(node)
+ const lastToken = tokenStore.getLastToken(node)
+ if (firstToken.value === '{' && lastToken.value === '}') return
+ if (
+ lastToken.value !== ';' &&
+ node.body.length === 1 &&
+ node.body[0].type === 'ExpressionStatement'
+ ) {
+ // it is a single expression
+ return
+ }
+ yield fixer.insertTextBefore(firstToken, '{')
+ yield fixer.insertTextAfter(lastToken, '}')
+ }
+ })
+ }
+ /**
+ * Report `v-on:click="() => foo()"` can be converted to `v-on:click="foo()"`.
+ * @param {ArrowFunctionExpression | FunctionExpression} node
+ * @returns {void}
+ */
+ function reportCanUseInlineHandlerForInlineFunction(node) {
+ // If a function has one parameter, you can turn it into an inline handler using $event.
+ // If a function has two or more parameters, it cannot be easily converted to an inline handler.
+ // However, users can use inline handlers by changing the payload of the component's custom event.
+ // So we report it regardless of the number of parameters.
+
+ context.report({
+ node,
+ messageId:
+ node.params.length > 1
+ ? 'preferInlineOverInlineFunctionWithMultipleParams'
+ : 'preferInlineOverInlineFunction',
+ fix:
+ node.params.length > 0
+ ? null /* The function has parameters and cannot be fixed. */
+ : (fixer) => {
+ let text = context.getSourceCode().getText(node.body)
+ if (node.body.type === 'BlockStatement') {
+ text = text.slice(1, -1) // strip braces
+ }
+ return fixer.replaceText(node, text)
+ }
+ })
+ }
+
+ return utils.defineTemplateBodyVisitor(
+ context,
+ {
+ VElement(node) {
+ upperElements.add(node)
+ },
+ 'VElement:exit'(node) {
+ upperElements.delete(node)
+ },
+ /** @param {VExpressionContainer} node */
+ "VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer.value:exit"(
+ node
+ ) {
+ const expression = node.expression
+ if (!expression) {
+ return
+ }
+ switch (expression.type) {
+ case 'VOnExpression': {
+ // e.g. v-on:click="foo()"
+ if (allows[0] === 'inline') {
+ return
+ }
+ for (const allow of allows) {
+ if (verifyForInlineHandler(expression, allow)) {
+ return
+ }
+ }
+ break
+ }
+ case 'Identifier': {
+ // e.g. v-on:click="foo"
+ if (allows[0] === 'method') {
+ return
+ }
+ for (const allow of allows) {
+ if (reportForMethodHandler(expression, allow)) {
+ return
+ }
+ }
+ break
+ }
+ case 'ArrowFunctionExpression':
+ case 'FunctionExpression': {
+ // e.g. v-on:click="()=>foo()"
+ if (allows[0] === 'inline-function') {
+ return
+ }
+ for (const allow of allows) {
+ if (verifyForInlineFunction(expression, allow)) {
+ return
+ }
+ }
+ break
+ }
+ default: {
+ return
+ }
+ }
+ },
+ ...(allows.includes('inline-function')
+ ? // Collect $event identifiers to check for side effects
+ // when converting from `v-on:click="foo($event)"` to `v-on:click="()=>foo($event)"` .
+ {
+ 'Identifier[name="$event"]'(node) {
+ $eventIdentifiers.push(node)
+ }
+ }
+ : {})
+ },
+ allows.includes('method')
+ ? // Collect method definition with params information to check for side effects.
+ // when converting from `v-on:click="foo()"` to `v-on:click="foo"`, or
+ // converting from `v-on:click="() => foo()"` to `v-on:click="foo"`.
+ utils.defineVueVisitor(context, {
+ onVueObjectEnter(node) {
+ for (const method of utils.iterateProperties(
+ node,
+ new Set(['methods'])
+ )) {
+ if (method.type !== 'object') {
+ // This branch is usually not passed.
+ continue
+ }
+ const value = method.property.value
+ if (
+ value.type === 'FunctionExpression' ||
+ value.type === 'ArrowFunctionExpression'
+ ) {
+ methodParamCountMap.set(
+ method.name,
+ value.params.some((p) => p.type === 'RestElement')
+ ? Number.POSITIVE_INFINITY
+ : value.params.length
+ )
+ }
+ }
+ }
+ })
+ : {}
+ )
+ }
+}
diff --git a/lib/rules/v-on-style.js b/lib/rules/v-on-style.js
index 4649aacf9..eb5950026 100644
--- a/lib/rules/v-on-style.js
+++ b/lib/rules/v-on-style.js
@@ -5,26 +5,22 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce `v-on` directive style',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
+ categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/v-on-style.html'
},
fixable: 'code',
- schema: [{ enum: ['shorthand', 'longform'] }]
+ schema: [{ enum: ['shorthand', 'longform'] }],
+ messages: {
+ expectedShorthand: "Expected '@' instead of 'v-on:'.",
+ expectedLonghand: "Expected 'v-on:' instead of '@'."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -44,9 +40,7 @@ module.exports = {
context.report({
node,
loc: node.loc,
- message: preferShorthand
- ? "Expected '@' instead of 'v-on:'."
- : "Expected 'v-on:' instead of '@'.",
+ messageId: preferShorthand ? 'expectedShorthand' : 'expectedLonghand',
fix: (fixer) =>
preferShorthand
? fixer.replaceTextRange([pos, pos + 5], '@')
diff --git a/lib/rules/v-slot-style.js b/lib/rules/v-slot-style.js
index f0cbe3610..964550b98 100644
--- a/lib/rules/v-slot-style.js
+++ b/lib/rules/v-slot-style.js
@@ -28,7 +28,10 @@ function normalizeOptions(options) {
}
if (typeof options === 'string') {
- normalized.atComponent = normalized.default = normalized.named = /** @type {"shorthand" | "longform"} */ (options)
+ normalized.atComponent =
+ normalized.default =
+ normalized.named =
+ /** @type {"shorthand" | "longform"} */ (options)
} else if (options != null) {
/** @type {(keyof Options)[]} */
const keys = ['atComponent', 'default', 'named']
@@ -83,13 +86,13 @@ module.exports = {
type: 'suggestion',
docs: {
description: 'enforce `v-slot` directive style',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
+ categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/v-slot-style.html'
},
fixable: 'code',
schema: [
{
- anyOf: [
+ oneOf: [
{ enum: ['shorthand', 'longform'] },
{
type: 'object',
@@ -138,14 +141,18 @@ module.exports = {
fix(fixer) {
switch (expected) {
- case 'shorthand':
+ case 'shorthand': {
return fixer.replaceTextRange(range, `#${argumentText}`)
- case 'longform':
+ }
+ case 'longform': {
return fixer.replaceTextRange(range, `v-slot:${argumentText}`)
- case 'v-slot':
+ }
+ case 'v-slot': {
return fixer.replaceTextRange(range, 'v-slot')
- default:
+ }
+ default: {
return null
+ }
}
}
})
diff --git a/lib/rules/valid-attribute-name.js b/lib/rules/valid-attribute-name.js
new file mode 100644
index 000000000..51d52a1b6
--- /dev/null
+++ b/lib/rules/valid-attribute-name.js
@@ -0,0 +1,69 @@
+/**
+ * @author Doug Wade
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const xnv = require('xml-name-validator')
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'require valid attribute names',
+ categories: ['vue3-essential', 'vue2-essential'],
+ url: 'https://eslint.vuejs.org/rules/valid-attribute-name.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ attribute: 'Attribute name {{name}} is not valid.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /**
+ * @param {string | VIdentifier} key
+ * @return {string}
+ */
+ const getName = (key) => (typeof key === 'string' ? key : key.name)
+
+ return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective | VAttribute} node */
+ VAttribute(node) {
+ if (utils.isCustomComponent(node.parent.parent)) {
+ return
+ }
+
+ const name = getName(node.key.name)
+
+ if (
+ node.directive &&
+ name === 'bind' &&
+ node.key.argument &&
+ node.key.argument.type === 'VIdentifier' &&
+ !xnv.name(node.key.argument.name)
+ ) {
+ context.report({
+ node,
+ messageId: 'attribute',
+ data: {
+ name: node.key.argument.name
+ }
+ })
+ }
+
+ if (!node.directive && !xnv.name(name)) {
+ context.report({
+ node,
+ messageId: 'attribute',
+ data: {
+ name
+ }
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/valid-define-emits.js b/lib/rules/valid-define-emits.js
new file mode 100644
index 000000000..8d2306c23
--- /dev/null
+++ b/lib/rules/valid-define-emits.js
@@ -0,0 +1,144 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { findVariable } = require('@eslint-community/eslint-utils')
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce valid `defineEmits` compiler macro',
+ categories: ['vue3-essential', 'vue2-essential'],
+ url: 'https://eslint.vuejs.org/rules/valid-define-emits.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ hasTypeAndArg: '`defineEmits` has both a type-only emit and an argument.',
+ referencingLocally:
+ '`defineEmits` is referencing locally declared variables.',
+ multiple: '`defineEmits` has been called multiple times.',
+ notDefined: 'Custom events are not defined.',
+ definedInBoth:
+ 'Custom events are defined in both `defineEmits` and `export default {}`.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const scriptSetup = utils.getScriptSetupElement(context)
+ if (!scriptSetup) {
+ return {}
+ }
+
+ /** @type {Set} */
+ const emitsDefExpressions = new Set()
+ let hasDefaultExport = false
+ /** @type {CallExpression[]} */
+ const defineEmitsNodes = []
+ /** @type {CallExpression | null} */
+ let emptyDefineEmits = null
+
+ return utils.compositingVisitors(
+ utils.defineScriptSetupVisitor(context, {
+ onDefineEmitsEnter(node) {
+ defineEmitsNodes.push(node)
+
+ const typeArguments =
+ 'typeArguments' in node ? node.typeArguments : node.typeParameters
+ if (node.arguments.length > 0) {
+ if (typeArguments && typeArguments.params.length > 0) {
+ // `defineEmits` has both a literal type and an argument.
+ context.report({
+ node,
+ messageId: 'hasTypeAndArg'
+ })
+ return
+ }
+
+ emitsDefExpressions.add(node.arguments[0])
+ } else {
+ if (!typeArguments || typeArguments.params.length === 0) {
+ emptyDefineEmits = node
+ }
+ }
+ },
+ Identifier(node) {
+ for (const defineEmits of emitsDefExpressions) {
+ if (utils.inRange(defineEmits.range, node)) {
+ const variable = findVariable(utils.getScope(context, node), node)
+ if (
+ variable &&
+ variable.references.some((ref) => ref.identifier === node) &&
+ variable.defs.length > 0 &&
+ variable.defs.every(
+ (def) =>
+ def.type !== 'ImportBinding' &&
+ utils.inRange(scriptSetup.range, def.name) &&
+ !utils.inRange(defineEmits.range, def.name)
+ )
+ ) {
+ if (utils.withinTypeNode(node)) {
+ continue
+ }
+ //`defineEmits` is referencing locally declared variables.
+ context.report({
+ node,
+ messageId: 'referencingLocally'
+ })
+ }
+ }
+ }
+ }
+ }),
+ utils.defineVueVisitor(context, {
+ onVueObjectEnter(node, { type }) {
+ if (type !== 'export' || utils.inRange(scriptSetup.range, node)) {
+ return
+ }
+
+ hasDefaultExport = Boolean(utils.findProperty(node, 'emits'))
+ }
+ }),
+ {
+ 'Program:exit'() {
+ if (defineEmitsNodes.length === 0) {
+ return
+ }
+ if (defineEmitsNodes.length > 1) {
+ // `defineEmits` has been called multiple times.
+ for (const node of defineEmitsNodes) {
+ context.report({
+ node,
+ messageId: 'multiple'
+ })
+ }
+ return
+ }
+ if (emptyDefineEmits) {
+ if (!hasDefaultExport) {
+ // Custom events are not defined.
+ context.report({
+ node: emptyDefineEmits,
+ messageId: 'notDefined'
+ })
+ }
+ } else {
+ if (hasDefaultExport) {
+ // Custom events are defined in both `defineEmits` and `export default {}`.
+ for (const node of defineEmitsNodes) {
+ context.report({
+ node,
+ messageId: 'definedInBoth'
+ })
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/lib/rules/valid-define-options.js b/lib/rules/valid-define-options.js
new file mode 100644
index 000000000..12771b8fe
--- /dev/null
+++ b/lib/rules/valid-define-options.js
@@ -0,0 +1,127 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { findVariable } = require('@eslint-community/eslint-utils')
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce valid `defineOptions` compiler macro',
+ categories: ['vue3-essential'],
+ url: 'https://eslint.vuejs.org/rules/valid-define-options.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ referencingLocally:
+ '`defineOptions` is referencing locally declared variables.',
+ multiple: '`defineOptions` has been called multiple times.',
+ notDefined: 'Options are not defined.',
+ disallowProp:
+ '`defineOptions()` cannot be used to declare `{{propName}}`. Use `{{insteadMacro}}()` instead.',
+ typeArgs: '`defineOptions()` cannot accept type arguments.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const scriptSetup = utils.getScriptSetupElement(context)
+ if (!scriptSetup) {
+ return {}
+ }
+
+ /** @type {Set} */
+ const optionsDefExpressions = new Set()
+ /** @type {CallExpression[]} */
+ const defineOptionsNodes = []
+
+ return utils.compositingVisitors(
+ utils.defineScriptSetupVisitor(context, {
+ onDefineOptionsEnter(node) {
+ defineOptionsNodes.push(node)
+
+ if (node.arguments.length > 0) {
+ const define = node.arguments[0]
+ if (define.type === 'ObjectExpression') {
+ for (const [propName, insteadMacro] of [
+ ['props', 'defineProps'],
+ ['emits', 'defineEmits'],
+ ['expose', 'defineExpose'],
+ ['slots', 'defineSlots']
+ ]) {
+ const prop = utils.findProperty(define, propName)
+ if (prop) {
+ context.report({
+ node,
+ messageId: 'disallowProp',
+ data: { propName, insteadMacro }
+ })
+ }
+ }
+ }
+
+ optionsDefExpressions.add(node.arguments[0])
+ } else {
+ context.report({
+ node,
+ messageId: 'notDefined'
+ })
+ }
+
+ const typeArguments =
+ 'typeArguments' in node ? node.typeArguments : node.typeParameters
+ if (typeArguments) {
+ context.report({
+ node: typeArguments,
+ messageId: 'typeArgs'
+ })
+ }
+ },
+ Identifier(node) {
+ for (const defineOptions of optionsDefExpressions) {
+ if (utils.inRange(defineOptions.range, node)) {
+ const variable = findVariable(utils.getScope(context, node), node)
+ if (
+ variable &&
+ variable.references.some((ref) => ref.identifier === node) &&
+ variable.defs.length > 0 &&
+ variable.defs.every(
+ (def) =>
+ def.type !== 'ImportBinding' &&
+ utils.inRange(scriptSetup.range, def.name) &&
+ !utils.inRange(defineOptions.range, def.name)
+ )
+ ) {
+ if (utils.withinTypeNode(node)) {
+ continue
+ }
+ //`defineOptions` is referencing locally declared variables.
+ context.report({
+ node,
+ messageId: 'referencingLocally'
+ })
+ }
+ }
+ }
+ }
+ }),
+ {
+ 'Program:exit'() {
+ if (defineOptionsNodes.length > 1) {
+ // `defineOptions` has been called multiple times.
+ for (const node of defineOptionsNodes) {
+ context.report({
+ node,
+ messageId: 'multiple'
+ })
+ }
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/lib/rules/valid-define-props.js b/lib/rules/valid-define-props.js
new file mode 100644
index 000000000..2ba81b188
--- /dev/null
+++ b/lib/rules/valid-define-props.js
@@ -0,0 +1,145 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { findVariable } = require('@eslint-community/eslint-utils')
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce valid `defineProps` compiler macro',
+ categories: ['vue3-essential', 'vue2-essential'],
+ url: 'https://eslint.vuejs.org/rules/valid-define-props.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ hasTypeAndArg:
+ '`defineProps` has both a type-only props and an argument.',
+ referencingLocally:
+ '`defineProps` is referencing locally declared variables.',
+ multiple: '`defineProps` has been called multiple times.',
+ notDefined: 'Props are not defined.',
+ definedInBoth:
+ 'Props are defined in both `defineProps` and `export default {}`.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const scriptSetup = utils.getScriptSetupElement(context)
+ if (!scriptSetup) {
+ return {}
+ }
+
+ /** @type {Set} */
+ const propsDefExpressions = new Set()
+ let hasDefaultExport = false
+ /** @type {CallExpression[]} */
+ const definePropsNodes = []
+ /** @type {CallExpression | null} */
+ let emptyDefineProps = null
+
+ return utils.compositingVisitors(
+ utils.defineScriptSetupVisitor(context, {
+ onDefinePropsEnter(node) {
+ definePropsNodes.push(node)
+
+ const typeArguments =
+ 'typeArguments' in node ? node.typeArguments : node.typeParameters
+ if (node.arguments.length > 0) {
+ if (typeArguments && typeArguments.params.length > 0) {
+ // `defineProps` has both a literal type and an argument.
+ context.report({
+ node,
+ messageId: 'hasTypeAndArg'
+ })
+ return
+ }
+
+ propsDefExpressions.add(node.arguments[0])
+ } else {
+ if (!typeArguments || typeArguments.params.length === 0) {
+ emptyDefineProps = node
+ }
+ }
+ },
+ Identifier(node) {
+ for (const defineProps of propsDefExpressions) {
+ if (utils.inRange(defineProps.range, node)) {
+ const variable = findVariable(utils.getScope(context, node), node)
+ if (
+ variable &&
+ variable.references.some((ref) => ref.identifier === node) &&
+ variable.defs.length > 0 &&
+ variable.defs.every(
+ (def) =>
+ def.type !== 'ImportBinding' &&
+ utils.inRange(scriptSetup.range, def.name) &&
+ !utils.inRange(defineProps.range, def.name)
+ )
+ ) {
+ if (utils.withinTypeNode(node)) {
+ continue
+ }
+ //`defineProps` is referencing locally declared variables.
+ context.report({
+ node,
+ messageId: 'referencingLocally'
+ })
+ }
+ }
+ }
+ }
+ }),
+ utils.defineVueVisitor(context, {
+ onVueObjectEnter(node, { type }) {
+ if (type !== 'export' || utils.inRange(scriptSetup.range, node)) {
+ return
+ }
+
+ hasDefaultExport = Boolean(utils.findProperty(node, 'props'))
+ }
+ }),
+ {
+ 'Program:exit'() {
+ if (definePropsNodes.length === 0) {
+ return
+ }
+ if (definePropsNodes.length > 1) {
+ // `defineProps` has been called multiple times.
+ for (const node of definePropsNodes) {
+ context.report({
+ node,
+ messageId: 'multiple'
+ })
+ }
+ return
+ }
+ if (emptyDefineProps) {
+ if (!hasDefaultExport) {
+ // Props are not defined.
+ context.report({
+ node: emptyDefineProps,
+ messageId: 'notDefined'
+ })
+ }
+ } else {
+ if (hasDefaultExport) {
+ // Props are defined in both `defineProps` and `export default {}`.
+ for (const node of definePropsNodes) {
+ context.report({
+ node,
+ messageId: 'definedInBoth'
+ })
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/lib/rules/valid-model-definition.js b/lib/rules/valid-model-definition.js
new file mode 100644
index 000000000..6d82fca32
--- /dev/null
+++ b/lib/rules/valid-model-definition.js
@@ -0,0 +1,54 @@
+/**
+ * @fileoverview Requires valid keys in model option.
+ * @author Alex Sokolov
+ */
+'use strict'
+
+const utils = require('../utils')
+
+const VALID_MODEL_KEYS = new Set(['prop', 'event'])
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'require valid keys in model option',
+ categories: ['vue2-essential'],
+ url: 'https://eslint.vuejs.org/rules/valid-model-definition.html'
+ },
+ fixable: null,
+ deprecated: true,
+ schema: [],
+ messages: {
+ invalidKey: "Invalid key '{{name}}' in model option."
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ return utils.executeOnVue(context, (obj) => {
+ const modelProperty = utils.findProperty(obj, 'model')
+ if (!modelProperty || modelProperty.value.type !== 'ObjectExpression') {
+ return
+ }
+
+ for (const p of modelProperty.value.properties) {
+ if (p.type !== 'Property') {
+ continue
+ }
+ const name = utils.getStaticPropertyName(p)
+ if (!name) {
+ continue
+ }
+ if (!VALID_MODEL_KEYS.has(name)) {
+ context.report({
+ node: p,
+ messageId: 'invalidKey',
+ data: {
+ name
+ }
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/valid-next-tick.js b/lib/rules/valid-next-tick.js
new file mode 100644
index 000000000..c384f6ac0
--- /dev/null
+++ b/lib/rules/valid-next-tick.js
@@ -0,0 +1,211 @@
+/**
+ * @fileoverview enforce valid `nextTick` function calls
+ * @author Flo Edelmann
+ * @copyright 2021 Flo Edelmann. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const { findVariable } = require('@eslint-community/eslint-utils')
+
+/**
+ * @param {Identifier} identifier
+ * @param {RuleContext} context
+ * @returns {ASTNode|undefined}
+ */
+function getVueNextTickNode(identifier, context) {
+ // Instance API: this.$nextTick()
+ if (
+ identifier.name === '$nextTick' &&
+ identifier.parent.type === 'MemberExpression' &&
+ utils.isThis(identifier.parent.object, context)
+ ) {
+ return identifier.parent
+ }
+
+ // Vue 2 Global API: Vue.nextTick()
+ if (
+ identifier.name === 'nextTick' &&
+ identifier.parent.type === 'MemberExpression' &&
+ identifier.parent.object.type === 'Identifier' &&
+ identifier.parent.object.name === 'Vue'
+ ) {
+ return identifier.parent
+ }
+
+ // Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
+ const variable = findVariable(utils.getScope(context, identifier), identifier)
+
+ if (variable != null && variable.defs.length === 1) {
+ const def = variable.defs[0]
+ if (
+ def.type === 'ImportBinding' &&
+ def.node.type === 'ImportSpecifier' &&
+ def.node.imported.type === 'Identifier' &&
+ def.node.imported.name === 'nextTick' &&
+ def.node.parent.type === 'ImportDeclaration' &&
+ def.node.parent.source.value === 'vue'
+ ) {
+ return identifier
+ }
+ }
+
+ return undefined
+}
+
+/**
+ * @param {CallExpression} callExpression
+ * @returns {boolean}
+ */
+function isAwaitedPromise(callExpression) {
+ if (callExpression.parent.type === 'AwaitExpression') {
+ // cases like `await nextTick()`
+ return true
+ }
+
+ if (callExpression.parent.type === 'ReturnStatement') {
+ // cases like `return nextTick()`
+ return true
+ }
+ if (
+ callExpression.parent.type === 'ArrowFunctionExpression' &&
+ callExpression.parent.body === callExpression
+ ) {
+ // cases like `() => nextTick()`
+ return true
+ }
+
+ if (
+ callExpression.parent.type === 'MemberExpression' &&
+ callExpression.parent.property.type === 'Identifier' &&
+ callExpression.parent.property.name === 'then'
+ ) {
+ // cases like `nextTick().then()`
+ return true
+ }
+
+ if (
+ callExpression.parent.type === 'VariableDeclarator' ||
+ callExpression.parent.type === 'AssignmentExpression'
+ ) {
+ // cases like `let foo = nextTick()` or `foo = nextTick()`
+ return true
+ }
+
+ if (
+ callExpression.parent.type === 'ArrayExpression' &&
+ callExpression.parent.parent.type === 'CallExpression' &&
+ callExpression.parent.parent.callee.type === 'MemberExpression' &&
+ callExpression.parent.parent.callee.object.type === 'Identifier' &&
+ callExpression.parent.parent.callee.object.name === 'Promise' &&
+ callExpression.parent.parent.callee.property.type === 'Identifier'
+ ) {
+ // cases like `Promise.all([nextTick()])`
+ return true
+ }
+
+ return false
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce valid `nextTick` function calls',
+ categories: ['vue3-essential', 'vue2-essential'],
+ url: 'https://eslint.vuejs.org/rules/valid-next-tick.html'
+ },
+ fixable: 'code',
+ hasSuggestions: true,
+ schema: [],
+ messages: {
+ shouldBeFunction: '`nextTick` is a function.',
+ missingCallbackOrAwait:
+ 'Await the Promise returned by `nextTick` or pass a callback function.',
+ addAwait: 'Add missing `await` statement.',
+ tooManyParameters: '`nextTick` expects zero or one parameters.',
+ eitherAwaitOrCallback:
+ 'Either await the Promise or pass a callback function to `nextTick`.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ return utils.defineVueVisitor(context, {
+ /** @param {Identifier} node */
+ Identifier(node) {
+ const nextTickNode = getVueNextTickNode(node, context)
+ if (!nextTickNode || !nextTickNode.parent) {
+ return
+ }
+
+ let parentNode = nextTickNode.parent
+
+ // skip conditional expressions like `foo ? nextTick : bar`
+ if (parentNode.type === 'ConditionalExpression') {
+ parentNode = parentNode.parent
+ }
+
+ if (
+ parentNode.type === 'CallExpression' &&
+ parentNode.callee !== nextTickNode
+ ) {
+ // cases like `foo.then(nextTick)` are allowed
+ return
+ }
+
+ if (
+ parentNode.type === 'VariableDeclarator' ||
+ parentNode.type === 'AssignmentExpression'
+ ) {
+ // cases like `let foo = nextTick` or `foo = nextTick` are allowed
+ return
+ }
+
+ if (parentNode.type !== 'CallExpression') {
+ context.report({
+ node,
+ messageId: 'shouldBeFunction',
+ fix(fixer) {
+ return fixer.insertTextAfter(node, '()')
+ }
+ })
+ return
+ }
+
+ if (parentNode.arguments.length === 0) {
+ if (!isAwaitedPromise(parentNode)) {
+ context.report({
+ node,
+ messageId: 'missingCallbackOrAwait',
+ suggest: [
+ {
+ messageId: 'addAwait',
+ fix(fixer) {
+ return fixer.insertTextBefore(parentNode, 'await ')
+ }
+ }
+ ]
+ })
+ }
+ return
+ }
+
+ if (parentNode.arguments.length > 1) {
+ context.report({
+ node,
+ messageId: 'tooManyParameters'
+ })
+ return
+ }
+
+ if (isAwaitedPromise(parentNode)) {
+ context.report({
+ node,
+ messageId: 'eitherAwaitOrCallback'
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/valid-template-root.js b/lib/rules/valid-template-root.js
index 703ab8b8e..c604a43a8 100644
--- a/lib/rules/valid-template-root.js
+++ b/lib/rules/valid-template-root.js
@@ -5,26 +5,23 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid template root',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-template-root.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ emptySrc:
+ "The template root with 'src' attribute is required to be empty.",
+ noChild: 'The template requires child element.'
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -47,20 +44,19 @@ module.exports = {
}
}
- if (hasSrc && rootElements.length) {
+ if (hasSrc && rootElements.length > 0) {
for (const element of rootElements) {
context.report({
node: element,
loc: element.loc,
- message:
- "The template root with 'src' attribute is required to be empty."
+ messageId: 'emptySrc'
})
}
} else if (rootElements.length === 0 && !hasSrc) {
context.report({
node: element,
loc: element.loc,
- message: 'The template requires child element.'
+ messageId: 'noChild'
})
}
}
diff --git a/lib/rules/valid-v-bind-sync.js b/lib/rules/valid-v-bind-sync.js
index 3799d0895..ba04d6117 100644
--- a/lib/rules/valid-v-bind-sync.js
+++ b/lib/rules/valid-v-bind-sync.js
@@ -4,27 +4,15 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
/**
* Check whether the given node is valid or not.
* @param {VElement} node The element node to check.
* @returns {boolean} `true` if the node is valid.
*/
function isValidElement(node) {
- if (
- (!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
- utils.isHtmlWellKnownElementName(node.rawName) ||
- utils.isSvgWellKnownElementName(node.rawName)
- ) {
+ if (!utils.isCustomComponent(node)) {
// non Vue-component
return false
}
@@ -32,7 +20,22 @@ function isValidElement(node) {
}
/**
- * Check whether the given node can be LHS.
+ * Check whether the given node is a MemberExpression containing an optional chaining.
+ * e.g.
+ * - `a?.b`
+ * - `a?.b.c`
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} `true` if the node is a MemberExpression containing an optional chaining.
+ */
+function isOptionalChainingMemberExpression(node) {
+ return (
+ node.type === 'ChainExpression' &&
+ node.expression.type === 'MemberExpression'
+ )
+}
+
+/**
+ * Check whether the given node can be LHS (left-hand side).
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node can be LHS.
*/
@@ -40,25 +43,53 @@ function isLhs(node) {
return node.type === 'Identifier' || node.type === 'MemberExpression'
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+/**
+ * Check whether the given node is a MemberExpression of a possibly null object.
+ * e.g.
+ * - `(a?.b).c`
+ * - `(null).foo`
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} `true` if the node is a MemberExpression of a possibly null object.
+ */
+function maybeNullObjectMemberExpression(node) {
+ if (node.type !== 'MemberExpression') {
+ return false
+ }
+ const { object } = node
+ if (object.type === 'ChainExpression') {
+ // `(a?.b).c`
+ return true
+ }
+ if (object.type === 'Literal' && object.value === null && !object.bigint) {
+ // `(null).foo`
+ return true
+ }
+ if (object.type === 'MemberExpression') {
+ return maybeNullObjectMemberExpression(object)
+ }
+ return false
+}
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `.sync` modifier on `v-bind` directives',
- categories: ['essential'],
+ categories: ['vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-bind-sync.html'
},
fixable: null,
+ deprecated: true,
schema: [],
messages: {
unexpectedInvalidElement:
"'.sync' modifiers aren't supported on <{{name}}> non Vue-components.",
+ unexpectedOptionalChaining:
+ "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers.",
unexpectedNonLhsExpression:
"'.sync' modifiers require the attribute value which is valid as LHS.",
+ unexpectedNullObject:
+ "'.sync' modifier has potential null object property access.",
unexpectedUpdateIterationVariable:
"'.sync' modifiers cannot update the iteration variable '{{varName}}' itself."
}
@@ -77,22 +108,35 @@ module.exports = {
if (!isValidElement(element)) {
context.report({
node,
- loc: node.loc,
messageId: 'unexpectedInvalidElement',
data: { name }
})
}
- if (!node.value || !node.value.expression) {
+ if (!node.value) {
return
}
- if (!isLhs(node.value.expression)) {
+ const expression = node.value.expression
+ if (!expression) {
+ // Parsing error
+ return
+ }
+ if (isOptionalChainingMemberExpression(expression)) {
context.report({
- node,
- loc: node.loc,
+ node: expression,
+ messageId: 'unexpectedOptionalChaining'
+ })
+ } else if (!isLhs(expression)) {
+ context.report({
+ node: expression,
messageId: 'unexpectedNonLhsExpression'
})
+ } else if (maybeNullObjectMemberExpression(expression)) {
+ context.report({
+ node: expression,
+ messageId: 'unexpectedNullObject'
+ })
}
for (const reference of node.value.references) {
@@ -103,8 +147,7 @@ module.exports = {
const variable = reference.variable
if (variable) {
context.report({
- node,
- loc: node.loc,
+ node: expression,
messageId: 'unexpectedUpdateIterationVariable',
data: { varName: id.name }
})
diff --git a/lib/rules/valid-v-bind.js b/lib/rules/valid-v-bind.js
index 385272f71..0f44dad26 100644
--- a/lib/rules/valid-v-bind.js
+++ b/lib/rules/valid-v-bind.js
@@ -5,32 +5,25 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
-const VALID_MODIFIERS = new Set(['prop', 'camel', 'sync'])
-
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+const VALID_MODIFIERS = new Set(['prop', 'camel', 'sync', 'attr'])
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-bind` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-bind.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ unsupportedModifier:
+ "'v-bind' directives don't support the modifier '{{name}}'.",
+ expectedValue: "'v-bind' directives require an attribute value."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -40,10 +33,8 @@ module.exports = {
for (const modifier of node.key.modifiers) {
if (!VALID_MODIFIERS.has(modifier.name)) {
context.report({
- node,
- loc: node.key.loc,
- message:
- "'v-bind' directives don't support the modifier '{{name}}'.",
+ node: modifier,
+ messageId: 'unsupportedModifier',
data: { name: modifier.name }
})
}
@@ -52,8 +43,7 @@ module.exports = {
if (!node.value || utils.isEmptyValueDirective(node, context)) {
context.report({
node,
- loc: node.loc,
- message: "'v-bind' directives require an attribute value."
+ messageId: 'expectedValue'
})
}
}
diff --git a/lib/rules/valid-v-cloak.js b/lib/rules/valid-v-cloak.js
index 4fd51d781..5ff956818 100644
--- a/lib/rules/valid-v-cloak.js
+++ b/lib/rules/valid-v-cloak.js
@@ -5,26 +5,23 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-cloak` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-cloak.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ unexpectedArgument: "'v-cloak' directives require no argument.",
+ unexpectedModifier: "'v-cloak' directives require no modifier.",
+ unexpectedValue: "'v-cloak' directives require no attribute value."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -33,23 +30,24 @@ module.exports = {
"VAttribute[directive=true][key.name.name='cloak']"(node) {
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-cloak' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
- loc: node.loc,
- message: "'v-cloak' directives require no modifier."
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
})
}
if (node.value) {
context.report({
- node,
- loc: node.loc,
- message: "'v-cloak' directives require no attribute value."
+ node: node.value,
+ messageId: 'unexpectedValue'
})
}
}
diff --git a/lib/rules/valid-v-else-if.js b/lib/rules/valid-v-else-if.js
index aafe97b1c..b94437466 100644
--- a/lib/rules/valid-v-else-if.js
+++ b/lib/rules/valid-v-else-if.js
@@ -5,26 +5,29 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-else-if` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-else-if.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ missingVIf:
+ "'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive.",
+ withVIf:
+ "'v-else-if' and 'v-if' directives can't exist on the same element.",
+ withVElse:
+ "'v-else-if' and 'v-else' directives can't exist on the same element.",
+ unexpectedArgument: "'v-else-if' directives require no argument.",
+ unexpectedModifier: "'v-else-if' directives require no modifier.",
+ expectedValue: "'v-else-if' directives require that attribute value."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -36,46 +39,41 @@ module.exports = {
if (!utils.prevElementHasIf(element)) {
context.report({
node,
- loc: node.loc,
- message:
- "'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."
+ messageId: 'missingVIf'
})
}
if (utils.hasDirective(element, 'if')) {
context.report({
node,
- loc: node.loc,
- message:
- "'v-else-if' and 'v-if' directives can't exist on the same element."
+ messageId: 'withVIf'
})
}
if (utils.hasDirective(element, 'else')) {
context.report({
node,
- loc: node.loc,
- message:
- "'v-else-if' and 'v-else' directives can't exist on the same element."
+ messageId: 'withVElse'
})
}
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-else-if' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
- loc: node.loc,
- message: "'v-else-if' directives require no modifier."
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
})
}
if (!node.value || utils.isEmptyValueDirective(node, context)) {
context.report({
node,
- loc: node.loc,
- message: "'v-else-if' directives require that attribute value."
+ messageId: 'expectedValue'
})
}
}
diff --git a/lib/rules/valid-v-else.js b/lib/rules/valid-v-else.js
index 7a8542787..f35ac661d 100644
--- a/lib/rules/valid-v-else.js
+++ b/lib/rules/valid-v-else.js
@@ -5,26 +5,29 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-else` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-else.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ missingVIf:
+ "'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive.",
+ withVIf:
+ "'v-else' and 'v-if' directives can't exist on the same element. You may want 'v-else-if' directives.",
+ withVElseIf:
+ "'v-else' and 'v-else-if' directives can't exist on the same element.",
+ unexpectedArgument: "'v-else' directives require no argument.",
+ unexpectedModifier: "'v-else' directives require no modifier.",
+ unexpectedValue: "'v-else' directives require no attribute value."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -36,46 +39,41 @@ module.exports = {
if (!utils.prevElementHasIf(element)) {
context.report({
node,
- loc: node.loc,
- message:
- "'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."
+ messageId: 'missingVIf'
})
}
if (utils.hasDirective(element, 'if')) {
context.report({
node,
- loc: node.loc,
- message:
- "'v-else' and 'v-if' directives can't exist on the same element. You may want 'v-else-if' directives."
+ messageId: 'withVIf'
})
}
if (utils.hasDirective(element, 'else-if')) {
context.report({
node,
- loc: node.loc,
- message:
- "'v-else' and 'v-else-if' directives can't exist on the same element."
+ messageId: 'withVElseIf'
})
}
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-else' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
- loc: node.loc,
- message: "'v-else' directives require no modifier."
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
})
}
if (node.value) {
context.report({
- node,
- loc: node.loc,
- message: "'v-else' directives require no attribute value."
+ node: node.value,
+ messageId: 'unexpectedValue'
})
}
}
diff --git a/lib/rules/valid-v-for.js b/lib/rules/valid-v-for.js
index 81fcc8a0d..05183dd7e 100644
--- a/lib/rules/valid-v-for.js
+++ b/lib/rules/valid-v-for.js
@@ -5,16 +5,8 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
/**
* Check whether the given attribute is using the variables which are defined by `v-for` directives.
* @param {VDirective} vFor The attribute node of `v-for` to check.
@@ -70,7 +62,9 @@ function checkChildKey(context, vFor, child) {
* @param {VElement} element The element node to check.
*/
function checkKey(context, vFor, element) {
- if (element.name === 'template') {
+ const vBindKey = utils.getDirective(element, 'bind', 'key')
+
+ if (vBindKey == null && element.name === 'template') {
for (const child of element.children) {
if (child.type === 'VElement') {
checkChildKey(context, vFor, child)
@@ -79,39 +73,43 @@ function checkKey(context, vFor, element) {
return
}
- const vBindKey = utils.getDirective(element, 'bind', 'key')
-
if (utils.isCustomComponent(element) && vBindKey == null) {
context.report({
node: element.startTag,
- loc: element.startTag.loc,
- message: "Custom elements in iteration require 'v-bind:key' directives."
+ messageId: 'requireKey'
})
}
if (vBindKey != null && !isUsingIterationVar(vFor, vBindKey)) {
context.report({
node: vBindKey,
- loc: vBindKey.loc,
- message:
- "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."
+ messageId: 'keyUseFVorVars'
})
}
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-for` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-for.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ requireKey:
+ "Custom elements in iteration require 'v-bind:key' directives.",
+ keyUseFVorVars:
+ "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive.",
+ unexpectedArgument: "'v-for' directives require no argument.",
+ unexpectedModifier: "'v-for' directives require no modifier.",
+ expectedValue: "'v-for' directives require that attribute value.",
+ unexpectedExpression:
+ "'v-for' directives require the special syntax ' in '.",
+ invalidEmptyAlias: "Invalid alias ''.",
+ invalidAlias: "Invalid alias '{{text}}'."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -126,23 +124,24 @@ module.exports = {
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-for' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
- loc: node.loc,
- message: "'v-for' directives require no modifier."
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
})
}
if (!node.value || utils.isEmptyValueDirective(node, context)) {
context.report({
node,
- loc: node.loc,
- message: "'v-for' directives require that attribute value."
+ messageId: 'expectedValue'
})
return
}
@@ -154,9 +153,7 @@ module.exports = {
if (expr.type !== 'VForExpression') {
context.report({
node: node.value,
- loc: node.value.loc,
- message:
- "'v-for' directives require the special syntax ' in '."
+ messageId: 'unexpectedExpression'
})
return
}
@@ -169,22 +166,20 @@ module.exports = {
if (value === null) {
context.report({
node: expr,
- message: "Invalid alias ''."
+ messageId: 'invalidEmptyAlias'
})
}
if (key !== undefined && (!key || key.type !== 'Identifier')) {
context.report({
node: key || expr,
- loc: key && key.loc,
- message: "Invalid alias '{{text}}'.",
+ messageId: 'invalidAlias',
data: { text: key ? sourceCode.getText(key) : '' }
})
}
if (index !== undefined && (!index || index.type !== 'Identifier')) {
context.report({
node: index || expr,
- loc: index && index.loc,
- message: "Invalid alias '{{text}}'.",
+ messageId: 'invalidAlias',
data: { text: index ? sourceCode.getText(index) : '' }
})
}
diff --git a/lib/rules/valid-v-html.js b/lib/rules/valid-v-html.js
index d88997682..1df254033 100644
--- a/lib/rules/valid-v-html.js
+++ b/lib/rules/valid-v-html.js
@@ -5,26 +5,23 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-html` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-html.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ unexpectedArgument: "'v-html' directives require no argument.",
+ unexpectedModifier: "'v-html' directives require no modifier.",
+ expectedValue: "'v-html' directives require that attribute value."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -33,23 +30,24 @@ module.exports = {
"VAttribute[directive=true][key.name.name='html']"(node) {
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-html' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
- loc: node.loc,
- message: "'v-html' directives require no modifier."
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
})
}
if (!node.value || utils.isEmptyValueDirective(node, context)) {
context.report({
node,
- loc: node.loc,
- message: "'v-html' directives require that attribute value."
+ messageId: 'expectedValue'
})
}
}
diff --git a/lib/rules/valid-v-if.js b/lib/rules/valid-v-if.js
index b61435ab0..2e8651b44 100644
--- a/lib/rules/valid-v-if.js
+++ b/lib/rules/valid-v-if.js
@@ -5,26 +5,27 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-if` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-if.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ withVElse:
+ "'v-if' and 'v-else' directives can't exist on the same element. You may want 'v-else-if' directives.",
+ withVElseIf:
+ "'v-if' and 'v-else-if' directives can't exist on the same element.",
+ unexpectedArgument: "'v-if' directives require no argument.",
+ unexpectedModifier: "'v-if' directives require no modifier.",
+ expectedValue: "'v-if' directives require that attribute value."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -36,38 +37,35 @@ module.exports = {
if (utils.hasDirective(element, 'else')) {
context.report({
node,
- loc: node.loc,
- message:
- "'v-if' and 'v-else' directives can't exist on the same element. You may want 'v-else-if' directives."
+ messageId: 'withVElse'
})
}
if (utils.hasDirective(element, 'else-if')) {
context.report({
node,
- loc: node.loc,
- message:
- "'v-if' and 'v-else-if' directives can't exist on the same element."
+ messageId: 'withVElseIf'
})
}
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-if' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
- loc: node.loc,
- message: "'v-if' directives require no modifier."
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
})
}
if (!node.value || utils.isEmptyValueDirective(node, context)) {
context.report({
node,
- loc: node.loc,
- message: "'v-if' directives require that attribute value."
+ messageId: 'expectedValue'
})
}
}
diff --git a/lib/rules/valid-v-is.js b/lib/rules/valid-v-is.js
new file mode 100644
index 000000000..2f97d1fd9
--- /dev/null
+++ b/lib/rules/valid-v-is.js
@@ -0,0 +1,83 @@
+/**
+ * @fileoverview enforce valid `v-is` directives
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * Check whether the given node is valid or not.
+ * @param {VElement} node The element node to check.
+ * @returns {boolean} `true` if the node is valid.
+ */
+function isValidElement(node) {
+ if (
+ utils.isHtmlElementNode(node) &&
+ !utils.isHtmlWellKnownElementName(node.rawName)
+ ) {
+ // Vue-component
+ return false
+ }
+ return true
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce valid `v-is` directives',
+ categories: ['vue3-essential'],
+ url: 'https://eslint.vuejs.org/rules/valid-v-is.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ unexpectedArgument: "'v-is' directives require no argument.",
+ unexpectedModifier: "'v-is' directives require no modifier.",
+ expectedValue: "'v-is' directives require that attribute value.",
+ ownerMustBeHTMLElement:
+ "'v-is' directive must be owned by a native HTML element, but '{{name}}' is not."
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ return utils.defineTemplateBodyVisitor(context, {
+ "VAttribute[directive=true][key.name.name='is']"(node) {
+ if (node.key.argument) {
+ context.report({
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
+ })
+ }
+ if (node.key.modifiers.length > 0) {
+ context.report({
+ node,
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
+ })
+ }
+ if (!node.value || utils.isEmptyValueDirective(node, context)) {
+ context.report({
+ node,
+ messageId: 'expectedValue'
+ })
+ }
+
+ const element = node.parent.parent
+
+ if (!isValidElement(element)) {
+ const name = element.name
+ context.report({
+ node,
+ messageId: 'ownerMustBeHTMLElement',
+ data: { name }
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/valid-v-memo.js b/lib/rules/valid-v-memo.js
new file mode 100644
index 000000000..66ba9a75d
--- /dev/null
+++ b/lib/rules/valid-v-memo.js
@@ -0,0 +1,119 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce valid `v-memo` directives',
+ categories: ['vue3-essential'],
+ url: 'https://eslint.vuejs.org/rules/valid-v-memo.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ unexpectedArgument: "'v-memo' directives require no argument.",
+ unexpectedModifier: "'v-memo' directives require no modifier.",
+ expectedValue: "'v-memo' directives require that attribute value.",
+ expectedArray:
+ "'v-memo' directives require the attribute value to be an array.",
+ insideVFor: "'v-memo' directive does not work inside 'v-for'."
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /** @type {VElement | null} */
+ let vForElement = null
+ return utils.defineTemplateBodyVisitor(context, {
+ VElement(node) {
+ if (!vForElement && utils.hasDirective(node, 'for')) {
+ vForElement = node
+ }
+ },
+ 'VElement:exit'(node) {
+ if (vForElement === node) {
+ vForElement = null
+ }
+ },
+ /** @param {VDirective} node */
+ "VAttribute[directive=true][key.name.name='memo']"(node) {
+ if (vForElement && vForElement !== node.parent.parent) {
+ context.report({
+ node: node.key,
+ messageId: 'insideVFor'
+ })
+ }
+ if (node.key.argument) {
+ context.report({
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
+ })
+ }
+ if (node.key.modifiers.length > 0) {
+ context.report({
+ node,
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
+ })
+ }
+ if (!node.value || utils.isEmptyValueDirective(node, context)) {
+ context.report({
+ node,
+ messageId: 'expectedValue'
+ })
+ return
+ }
+ if (!node.value.expression) {
+ return
+ }
+ const expressions = [node.value.expression]
+ let expression
+ while ((expression = expressions.pop())) {
+ switch (expression.type) {
+ case 'ObjectExpression':
+ case 'ClassExpression':
+ case 'ArrowFunctionExpression':
+ case 'FunctionExpression':
+ case 'Literal':
+ case 'TemplateLiteral':
+ case 'UnaryExpression':
+ case 'BinaryExpression':
+ case 'UpdateExpression': {
+ context.report({
+ node: expression,
+ messageId: 'expectedArray'
+ })
+ break
+ }
+ case 'AssignmentExpression': {
+ expressions.push(expression.right)
+ break
+ }
+ case 'TSAsExpression': {
+ expressions.push(expression.expression)
+ break
+ }
+ case 'SequenceExpression': {
+ expressions.push(
+ expression.expressions[expression.expressions.length - 1]
+ )
+ break
+ }
+ case 'ConditionalExpression': {
+ expressions.push(expression.consequent, expression.alternate)
+ break
+ }
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/valid-v-model.js b/lib/rules/valid-v-model.js
index 451787367..ca2830dc6 100644
--- a/lib/rules/valid-v-model.js
+++ b/lib/rules/valid-v-model.js
@@ -5,16 +5,8 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
const VALID_MODIFIERS = new Set(['lazy', 'number', 'trim'])
/**
@@ -37,14 +29,60 @@ function isValidElement(node) {
}
/**
- * Check whether the given node can be LHS.
+ * Check whether the given node is a MemberExpression containing an optional chaining.
+ * e.g.
+ * - `a?.b`
+ * - `a?.b.c`
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} `true` if the node is a MemberExpression containing an optional chaining.
+ */
+function isOptionalChainingMemberExpression(node) {
+ return (
+ node.type === 'ChainExpression' &&
+ node.expression.type === 'MemberExpression'
+ )
+}
+
+/**
+ * Check whether the given node can be LHS (left-hand side).
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node can be LHS.
*/
function isLhs(node) {
+ if (node.type === 'TSAsExpression' || node.type === 'TSNonNullExpression') {
+ return isLhs(node.expression)
+ }
+
return node.type === 'Identifier' || node.type === 'MemberExpression'
}
+/**
+ * Check whether the given node is a MemberExpression of a possibly null object.
+ * e.g.
+ * - `(a?.b).c`
+ * - `(null).foo`
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} `true` if the node is a MemberExpression of a possibly null object.
+ */
+function maybeNullObjectMemberExpression(node) {
+ if (node.type !== 'MemberExpression') {
+ return false
+ }
+ const { object } = node
+ if (object.type === 'ChainExpression') {
+ // `(a?.b).c`
+ return true
+ }
+ if (object.type === 'Literal' && object.value === null && !object.bigint) {
+ // `(null).foo`
+ return true
+ }
+ if (object.type === 'MemberExpression') {
+ return maybeNullObjectMemberExpression(object)
+ }
+ return false
+}
+
/**
* Get the variable by names.
* @param {string} name The variable name to find.
@@ -72,20 +110,35 @@ function getVariable(name, leafNode) {
return null
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
+/** @type {RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-model` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-model.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ unexpectedInvalidElement:
+ "'v-model' directives aren't supported on <{{name}}> elements.",
+ unexpectedInputFile:
+ "'v-model' directives don't support 'file' input type.",
+ unexpectedArgument: "'v-model' directives require no argument.",
+ unexpectedModifier:
+ "'v-model' directives don't support the modifier '{{name}}'.",
+ missingValue: "'v-model' directives require that attribute value.",
+ unexpectedOptionalChaining:
+ "Optional chaining cannot appear in 'v-model' directives.",
+ unexpectedNonLhsExpression:
+ "'v-model' directives require the attribute value which is valid as LHS.",
+ unexpectedNullObject:
+ "'v-model' directive has potential null object property access.",
+ unexpectedUpdateIterationVariable:
+ "'v-model' directives cannot update the iteration variable '{{varName}}' itself."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -98,9 +151,7 @@ module.exports = {
if (!isValidElement(element)) {
context.report({
node,
- loc: node.loc,
- message:
- "'v-model' directives aren't supported on <{{name}}> elements.",
+ messageId: 'unexpectedInvalidElement',
data: { name }
})
}
@@ -108,27 +159,23 @@ module.exports = {
if (name === 'input' && utils.hasAttribute(element, 'type', 'file')) {
context.report({
node,
- loc: node.loc,
- message: "'v-model' directives don't support 'file' input type."
+ messageId: 'unexpectedInputFile'
})
}
if (!utils.isCustomComponent(element)) {
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-model' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
for (const modifier of node.key.modifiers) {
if (!VALID_MODIFIERS.has(modifier.name)) {
context.report({
- node,
- loc: node.loc,
- message:
- "'v-model' directives don't support the modifier '{{name}}'.",
+ node: modifier,
+ messageId: 'unexpectedModifier',
data: { name: modifier.name }
})
}
@@ -138,21 +185,29 @@ module.exports = {
if (!node.value || utils.isEmptyValueDirective(node, context)) {
context.report({
node,
- loc: node.loc,
- message: "'v-model' directives require that attribute value."
+ messageId: 'missingValue'
})
return
}
- if (!node.value.expression) {
+ const expression = node.value.expression
+ if (!expression) {
// Parsing error
return
}
- if (!isLhs(node.value.expression)) {
+ if (isOptionalChainingMemberExpression(expression)) {
context.report({
- node,
- loc: node.loc,
- message:
- "'v-model' directives require the attribute value which is valid as LHS."
+ node: expression,
+ messageId: 'unexpectedOptionalChaining'
+ })
+ } else if (!isLhs(expression)) {
+ context.report({
+ node: expression,
+ messageId: 'unexpectedNonLhsExpression'
+ })
+ } else if (maybeNullObjectMemberExpression(expression)) {
+ context.report({
+ node: expression,
+ messageId: 'unexpectedNullObject'
})
}
@@ -165,10 +220,9 @@ module.exports = {
const variable = getVariable(id.name, element)
if (variable != null) {
context.report({
- node,
- loc: node.loc,
- message:
- "'v-model' directives cannot update the iteration variable '{{varName}}' itself.",
+ node: expression,
+ messageId: 'unexpectedUpdateIterationVariable',
+
data: { varName: id.name }
})
}
diff --git a/lib/rules/valid-v-on.js b/lib/rules/valid-v-on.js
index 4d55d7f19..82612d496 100644
--- a/lib/rules/valid-v-on.js
+++ b/lib/rules/valid-v-on.js
@@ -5,17 +5,9 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
const keyAliases = require('../utils/key-aliases.json')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
const VALID_MODIFIERS = new Set([
'stop',
'prevent',
@@ -56,9 +48,9 @@ function isValidModifier(modifierNode, customModifiers) {
// built-in aliases
VALID_MODIFIERS.has(modifier) ||
// keyCode
- Number.isInteger(parseInt(modifier, 10)) ||
+ Number.isInteger(Number.parseInt(modifier, 10)) ||
// keyAlias (an Unicode character)
- Array.from(modifier).length === 1 ||
+ [...modifier].length === 1 ||
// keyAlias (special keys)
KEY_ALIASES.has(modifier) ||
// custom modifiers
@@ -66,16 +58,12 @@ function isValidModifier(modifierNode, customModifiers) {
)
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-on` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-on.html'
},
fixable: null,
@@ -89,7 +77,15 @@ module.exports = {
},
additionalProperties: false
}
- ]
+ ],
+ messages: {
+ unsupportedModifier:
+ "'v-on' directives don't support the modifier '{{modifier}}'.",
+ avoidKeyword:
+ 'Avoid using JavaScript keyword as "v-on" value: {{value}}.',
+ expectedValueOrVerb:
+ "'v-on' directives require a value or verb modifier (like 'stop' or 'prevent')."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -104,10 +100,8 @@ module.exports = {
for (const modifier of node.key.modifiers) {
if (!isValidModifier(modifier, customModifiers)) {
context.report({
- node,
- loc: node.loc,
- message:
- "'v-on' directives don't support the modifier '{{modifier}}'.",
+ node: modifier,
+ messageId: 'unsupportedModifier',
data: { modifier: modifier.name }
})
}
@@ -131,19 +125,15 @@ module.exports = {
}
if (/^\w+$/.test(innerText)) {
context.report({
- node,
- loc: node.loc,
- message:
- 'Avoid using JavaScript keyword as "v-on" value: {{value}}.',
+ node: node.value,
+ messageId: 'avoidKeyword',
data: { value: valueText }
})
}
} else {
context.report({
node,
- loc: node.loc,
- message:
- "'v-on' directives require a value or verb modifier (like 'stop' or 'prevent')."
+ messageId: 'expectedValueOrVerb'
})
}
}
diff --git a/lib/rules/valid-v-once.js b/lib/rules/valid-v-once.js
index 83ff3ddcf..9d3d8bf8f 100644
--- a/lib/rules/valid-v-once.js
+++ b/lib/rules/valid-v-once.js
@@ -5,26 +5,23 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-once` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-once.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ unexpectedArgument: "'v-once' directives require no argument.",
+ unexpectedModifier: "'v-once' directives require no modifier.",
+ unexpectedValue: "'v-once' directives require no attribute value."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -33,23 +30,24 @@ module.exports = {
"VAttribute[directive=true][key.name.name='once']"(node) {
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-once' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
- loc: node.loc,
- message: "'v-once' directives require no modifier."
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
})
}
if (node.value) {
context.report({
- node,
- loc: node.loc,
- message: "'v-once' directives require no attribute value."
+ node: node.value,
+ messageId: 'unexpectedValue'
})
}
}
diff --git a/lib/rules/valid-v-pre.js b/lib/rules/valid-v-pre.js
index 90174db12..0505b9cb5 100644
--- a/lib/rules/valid-v-pre.js
+++ b/lib/rules/valid-v-pre.js
@@ -5,26 +5,23 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-pre` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-pre.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ unexpectedArgument: "'v-pre' directives require no argument.",
+ unexpectedModifier: "'v-pre' directives require no modifier.",
+ unexpectedValue: "'v-pre' directives require no attribute value."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -33,23 +30,24 @@ module.exports = {
"VAttribute[directive=true][key.name.name='pre']"(node) {
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-pre' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
- loc: node.loc,
- message: "'v-pre' directives require no modifier."
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
})
}
if (node.value) {
context.report({
- node,
- loc: node.loc,
- message: "'v-pre' directives require no attribute value."
+ node: node.value,
+ messageId: 'unexpectedValue'
})
}
}
diff --git a/lib/rules/valid-v-show.js b/lib/rules/valid-v-show.js
index d5378a63a..1da617806 100644
--- a/lib/rules/valid-v-show.js
+++ b/lib/rules/valid-v-show.js
@@ -5,26 +5,25 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-show` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-show.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ unexpectedArgument: "'v-show' directives require no argument.",
+ unexpectedModifier: "'v-show' directives require no modifier.",
+ expectedValue: "'v-show' directives require that attribute value.",
+ unexpectedTemplate:
+ "'v-show' directives cannot be put on tags."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -33,23 +32,30 @@ module.exports = {
"VAttribute[directive=true][key.name.name='show']"(node) {
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-show' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
- loc: node.loc,
- message: "'v-show' directives require no modifier."
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
})
}
if (!node.value || utils.isEmptyValueDirective(node, context)) {
context.report({
node,
- loc: node.loc,
- message: "'v-show' directives require that attribute value."
+ messageId: 'expectedValue'
+ })
+ }
+ if (node.parent.parent.name === 'template') {
+ context.report({
+ node,
+ messageId: 'unexpectedTemplate'
})
}
}
diff --git a/lib/rules/valid-v-slot.js b/lib/rules/valid-v-slot.js
index a94d8c9dc..d5c0efd1e 100644
--- a/lib/rules/valid-v-slot.js
+++ b/lib/rules/valid-v-slot.js
@@ -6,6 +6,10 @@
const utils = require('../utils')
+/**
+ * @typedef { { expr: VForExpression, variables: VVariable[] } } VSlotVForVariables
+ */
+
/**
* Get all `v-slot` directives on a given element.
* @param {VElement} node The VElement node to check.
@@ -24,93 +28,162 @@ function getSlotDirectivesOnElement(node) {
* by `v-if`/`v-else-if`/`v-else`.
*/
function getSlotDirectivesOnChildren(node) {
- return node.children
- .reduce(
- ({ groups, vIf }, childNode) => {
- if (childNode.type === 'VElement') {
- let connected
- if (utils.hasDirective(childNode, 'if')) {
- connected = false
- vIf = true
- } else if (utils.hasDirective(childNode, 'else-if')) {
- connected = vIf
- vIf = true
- } else if (utils.hasDirective(childNode, 'else')) {
- connected = vIf
- vIf = false
- } else {
- connected = false
- vIf = false
- }
+ /** @type {VDirective[][]} */
+ const groups = []
+ for (const group of utils.iterateChildElementsChains(node)) {
+ const slotDirs = group
+ .map((childElement) =>
+ childElement.name === 'template'
+ ? utils.getDirective(childElement, 'slot')
+ : null
+ )
+ .filter(utils.isDef)
+ if (slotDirs.length > 0) {
+ groups.push(slotDirs)
+ }
+ }
- if (connected) {
- groups[groups.length - 1].push(childNode)
- } else {
- groups.push([childNode])
- }
- } else if (
- childNode.type !== 'VText' ||
- childNode.value.trim() !== ''
- ) {
- vIf = false
- }
- return { groups, vIf }
- },
- {
- /** @type {VElement[][]} */
- groups: [],
- vIf: false
- }
- )
- .groups.map((group) =>
- group
- .map((childElement) =>
- childElement.name === 'template'
- ? utils.getDirective(childElement, 'slot')
- : null
- )
- .filter(utils.isDef)
- )
- .filter((group) => group.length >= 1)
+ return groups
}
/**
- * Get the normalized name of a given `v-slot` directive node.
+ * Get the normalized name of a given `v-slot` directive node with modifiers after `v-slot:` directive.
* @param {VDirective} node The `v-slot` directive node.
* @param {SourceCode} sourceCode The source code.
* @returns {string} The normalized name.
*/
function getNormalizedName(node, sourceCode) {
- return node.key.argument == null
- ? 'default'
- : sourceCode.getText(node.key.argument)
+ if (node.key.argument == null) {
+ return 'default'
+ }
+ return node.key.modifiers.length === 0
+ ? sourceCode.getText(node.key.argument)
+ : sourceCode.text.slice(node.key.argument.range[0], node.key.range[1])
}
/**
* Get all `v-slot` directives which are distributed to the same slot as a given `v-slot` directive node.
* @param {VDirective[][]} vSlotGroups The result of `getAllNamedSlotElements()`.
* @param {VDirective} currentVSlot The current `v-slot` directive node.
+ * @param {VSlotVForVariables | null} currentVSlotVForVars The current `v-for` variables.
* @param {SourceCode} sourceCode The source code.
+ * @param {ParserServices.TokenStore} tokenStore The token store.
* @returns {VDirective[][]} The array of the group of `v-slot` directives.
*/
-function filterSameSlot(vSlotGroups, currentVSlot, sourceCode) {
+function filterSameSlot(
+ vSlotGroups,
+ currentVSlot,
+ currentVSlotVForVars,
+ sourceCode,
+ tokenStore
+) {
const currentName = getNormalizedName(currentVSlot, sourceCode)
return vSlotGroups
.map((vSlots) =>
- vSlots.filter(
- (vSlot) => getNormalizedName(vSlot, sourceCode) === currentName
- )
+ vSlots.filter((vSlot) => {
+ if (getNormalizedName(vSlot, sourceCode) !== currentName) {
+ return false
+ }
+ const vForExpr = getVSlotVForVariableIfUsingIterationVars(
+ vSlot,
+ utils.getDirective(vSlot.parent.parent, 'for')
+ )
+ if (!currentVSlotVForVars || !vForExpr) {
+ return !currentVSlotVForVars && !vForExpr
+ }
+ if (
+ !equalVSlotVForVariables(currentVSlotVForVars, vForExpr, tokenStore)
+ ) {
+ return false
+ }
+ //
+ return true
+ })
)
- .filter((slots) => slots.length >= 1)
+ .filter((slots) => slots.length > 0)
}
/**
- * Check whether a given argument node is using an iteration variable that the element defined.
+ * Determines whether the two given `v-slot` variables are considered to be equal.
+ * @param {VSlotVForVariables} a First element.
+ * @param {VSlotVForVariables} b Second element.
+ * @param {ParserServices.TokenStore} tokenStore The token store.
+ * @returns {boolean} `true` if the elements are considered to be equal.
+ */
+function equalVSlotVForVariables(a, b, tokenStore) {
+ if (a.variables.length !== b.variables.length) {
+ return false
+ }
+ if (!equal(a.expr.right, b.expr.right)) {
+ return false
+ }
+
+ const checkedVarNames = new Set()
+ const len = Math.min(a.expr.left.length, b.expr.left.length)
+ for (let index = 0; index < len; index++) {
+ const aPtn = a.expr.left[index]
+ const bPtn = b.expr.left[index]
+
+ const aVar = a.variables.find(
+ (v) => aPtn.range[0] <= v.id.range[0] && v.id.range[1] <= aPtn.range[1]
+ )
+ const bVar = b.variables.find(
+ (v) => bPtn.range[0] <= v.id.range[0] && v.id.range[1] <= bPtn.range[1]
+ )
+ if (aVar && bVar) {
+ if (aVar.id.name !== bVar.id.name) {
+ return false
+ }
+ if (!equal(aPtn, bPtn)) {
+ return false
+ }
+ checkedVarNames.add(aVar.id.name)
+ } else if (aVar || bVar) {
+ return false
+ }
+ }
+ return a.variables.every(
+ (v) =>
+ checkedVarNames.has(v.id.name) ||
+ b.variables.some((bv) => v.id.name === bv.id.name)
+ )
+
+ /**
+ * Determines whether the two given nodes are considered to be equal.
+ * @param {ASTNode} a First node.
+ * @param {ASTNode} b Second node.
+ * @returns {boolean} `true` if the nodes are considered to be equal.
+ */
+ function equal(a, b) {
+ if (a.type !== b.type) {
+ return false
+ }
+ return utils.equalTokens(a, b, tokenStore)
+ }
+}
+
+/**
+ * Gets the `v-for` directive and variable that provide the variables used by the given` v-slot` directive.
+ * @param {VDirective} vSlot The current `v-slot` directive node.
+ * @param {VDirective | null} [vFor] The current `v-for` directive node.
+ * @returns { VSlotVForVariables | null } The VSlotVForVariable.
+ */
+function getVSlotVForVariableIfUsingIterationVars(vSlot, vFor) {
+ const expr =
+ vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression)
+ const variables =
+ expr && getUsingIterationVars(vSlot.key.argument, vSlot.parent.parent)
+ return expr && variables && variables.length > 0 ? { expr, variables } : null
+}
+
+/**
+ * Gets iterative variables if a given argument node is using iterative variables that the element defined.
* @param {VExpressionContainer|VIdentifier|null} argument The argument node to check.
* @param {VElement} element The element node which has the argument.
- * @returns {boolean} `true` if the argument node is using the iteration variable.
+ * @returns {VVariable[]} The argument node is using iteration variables.
*/
-function isUsingIterationVar(argument, element) {
+function getUsingIterationVars(argument, element) {
+ const vars = []
if (argument && argument.type === 'VExpressionContainer') {
for (const { variable } of argument.references) {
if (
@@ -119,11 +192,11 @@ function isUsingIterationVar(argument, element) {
variable.id.range[0] > element.startTag.range[0] &&
variable.id.range[1] < element.startTag.range[1]
) {
- return true
+ vars.push(variable)
}
}
}
- return false
+ return vars
}
/**
@@ -150,16 +223,39 @@ function isUsingScopeVar(vSlot) {
return false
}
+/**
+ * If `allowModifiers` option is set to `true`, check whether a given argument node has invalid modifiers like `v-slot.foo`.
+ * Otherwise, check whether a given argument node has at least one modifier.
+ * @param {VDirective} vSlot The `v-slot` directive to check.
+ * @param {boolean} allowModifiers `allowModifiers` option in context.
+ * @return {boolean} `true` if that argument node has invalid modifiers like `v-slot.foo`.
+ */
+function hasInvalidModifiers(vSlot, allowModifiers) {
+ return allowModifiers
+ ? vSlot.key.argument == null && vSlot.key.modifiers.length > 0
+ : vSlot.key.modifiers.length > 0
+}
+
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-slot` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-slot.html'
},
fixable: null,
- schema: [],
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ allowModifiers: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
+ }
+ ],
messages: {
ownerMustBeCustomElement:
"'v-slot' directive must be owned by a custom element, but '{{name}}' is not.",
@@ -181,6 +277,11 @@ module.exports = {
/** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
+ const tokenStore =
+ sourceCode.parserServices.getTemplateBodyTokenStore &&
+ sourceCode.parserServices.getTemplateBodyTokenStore()
+ const options = context.options[0] || {}
+ const allowModifiers = options.allowModifiers === true
return utils.defineTemplateBodyVisitor(context, {
/** @param {VDirective} node */
@@ -213,7 +314,7 @@ module.exports = {
messageId: 'namedSlotMustBeOnTemplate'
})
}
- if (ownerElement === element && vSlotGroupsOnChildren.length >= 1) {
+ if (ownerElement === element && vSlotGroupsOnChildren.length > 0) {
context.report({
node,
messageId: 'defaultSlotMustBeOnTemplate'
@@ -229,12 +330,18 @@ module.exports = {
})
}
if (ownerElement === parentElement) {
+ const vFor = utils.getDirective(element, 'for')
+ const vSlotVForVar = getVSlotVForVariableIfUsingIterationVars(
+ node,
+ vFor
+ )
const vSlotGroupsOfSameSlot = filterSameSlot(
vSlotGroupsOnChildren,
node,
- sourceCode
+ vSlotVForVar,
+ sourceCode,
+ tokenStore
)
- const vFor = utils.getDirective(element, 'for')
if (
vSlotGroupsOfSameSlot.length >= 2 &&
!vSlotGroupsOfSameSlot[0].includes(node)
@@ -246,7 +353,7 @@ module.exports = {
messageId: 'disallowDuplicateSlotsOnChildren'
})
}
- if (vFor && !isUsingIterationVar(node.key.argument, element)) {
+ if (vFor && !vSlotVForVar) {
// E.g.,
context.report({
node,
@@ -264,9 +371,14 @@ module.exports = {
}
// Verify modifiers.
- if (node.key.modifiers.length >= 1) {
+ if (hasInvalidModifiers(node, allowModifiers)) {
+ // E.g.,
context.report({
node,
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
messageId: 'disallowAnyModifier'
})
}
diff --git a/lib/rules/valid-v-text.js b/lib/rules/valid-v-text.js
index 1e90ec688..b41e7f2b3 100644
--- a/lib/rules/valid-v-text.js
+++ b/lib/rules/valid-v-text.js
@@ -5,26 +5,23 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-text` directives',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/valid-v-text.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ unexpectedArgument: "'v-text' directives require no argument.",
+ unexpectedModifier: "'v-text' directives require no modifier.",
+ expectedValue: "'v-text' directives require that attribute value."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -33,23 +30,24 @@ module.exports = {
"VAttribute[directive=true][key.name.name='text']"(node) {
if (node.key.argument) {
context.report({
- node,
- loc: node.loc,
- message: "'v-text' directives require no argument."
+ node: node.key.argument,
+ messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
- loc: node.loc,
- message: "'v-text' directives require no modifier."
+ loc: {
+ start: node.key.modifiers[0].loc.start,
+ end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
+ },
+ messageId: 'unexpectedModifier'
})
}
if (!node.value || utils.isEmptyValueDirective(node, context)) {
context.report({
node,
- loc: node.loc,
- message: "'v-text' directives require that attribute value."
+ messageId: 'expectedValue'
})
}
}
diff --git a/lib/utils/casing.js b/lib/utils/casing.js
index f6dea6d6e..a47e978cd 100644
--- a/lib/utils/casing.js
+++ b/lib/utils/casing.js
@@ -1,7 +1,3 @@
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
/**
* Capitalize a string.
* @param {string} str
@@ -41,15 +37,12 @@ function kebabCase(str) {
* @param {string} str
*/
function isKebabCase(str) {
- if (
- hasUpper(str) ||
- hasSymbols(str) ||
- /^-/u.exec(str) || // starts with hyphen is not kebab-case
- /_|--|\s/u.exec(str)
- ) {
- return false
- }
- return true
+ return (
+ !hasUpper(str) &&
+ !hasSymbols(str) &&
+ !str.startsWith('-') && // starts with hyphen is not kebab-case
+ !/_|--|\s/u.test(str)
+ )
}
/**
@@ -69,10 +62,7 @@ function snakeCase(str) {
* @param {string} str
*/
function isSnakeCase(str) {
- if (hasUpper(str) || hasSymbols(str) || /-|__|\s/u.exec(str)) {
- return false
- }
- return true
+ return !hasUpper(str) && !hasSymbols(str) && !/-|__|\s/u.test(str)
}
/**
@@ -92,14 +82,7 @@ function camelCase(str) {
* @param {string} str
*/
function isCamelCase(str) {
- if (
- hasSymbols(str) ||
- /^[A-Z]/u.exec(str) ||
- /-|_|\s/u.exec(str) // kebab or snake or space
- ) {
- return false
- }
- return true
+ return !hasSymbols(str) && !/^[A-Z]/u.test(str) && !/-|_|\s/u.test(str)
}
/**
@@ -116,14 +99,7 @@ function pascalCase(str) {
* @param {string} str
*/
function isPascalCase(str) {
- if (
- hasSymbols(str) ||
- /^[a-z]/u.exec(str) ||
- /-|_|\s/u.exec(str) // kebab or snake or space
- ) {
- return false
- }
- return true
+ return !hasSymbols(str) && !/^[a-z]/u.test(str) && !/-|_|\s/u.test(str)
}
const convertersMap = {
@@ -197,5 +173,7 @@ module.exports = {
isCamelCase,
isPascalCase,
isKebabCase,
- isSnakeCase
+ isSnakeCase,
+
+ capitalize
}
diff --git a/lib/utils/comments.js b/lib/utils/comments.js
new file mode 100644
index 000000000..d285e7cac
--- /dev/null
+++ b/lib/utils/comments.js
@@ -0,0 +1,21 @@
+/**
+ * @param {Comment} node
+ * @returns {boolean}
+ */
+const isJSDocComment = (node) =>
+ node.type === 'Block' &&
+ node.value.charAt(0) === '*' &&
+ node.value.charAt(1) !== '*'
+
+/**
+ * @param {Comment} node
+ * @returns {boolean}
+ */
+const isBlockComment = (node) =>
+ node.type === 'Block' &&
+ (node.value.charAt(0) !== '*' || node.value.charAt(1) === '*')
+
+module.exports = {
+ isJSDocComment,
+ isBlockComment
+}
diff --git a/lib/utils/deprecated-html-elements.json b/lib/utils/deprecated-html-elements.json
index daf23f512..63a3f7162 100644
--- a/lib/utils/deprecated-html-elements.json
+++ b/lib/utils/deprecated-html-elements.json
@@ -1 +1,31 @@
-["acronym","applet","basefont","bgsound","big","blink","center","command","content","dir","element","font","frame","frameset","image","isindex","keygen","listing","marquee","menuitem","multicol","nextid","nobr","noembed","noframes","plaintext","shadow","spacer","strike","tt","xmp"]
\ No newline at end of file
+[
+ "acronym",
+ "applet",
+ "basefont",
+ "bgsound",
+ "big",
+ "blink",
+ "center",
+ "dir",
+ "font",
+ "frame",
+ "frameset",
+ "isindex",
+ "keygen",
+ "listing",
+ "marquee",
+ "menuitem",
+ "multicol",
+ "nextid",
+ "nobr",
+ "noembed",
+ "noframes",
+ "param",
+ "plaintext",
+ "rb",
+ "rtc",
+ "spacer",
+ "strike",
+ "tt",
+ "xmp"
+]
diff --git a/lib/utils/html-comments.js b/lib/utils/html-comments.js
index aced46e71..53f7df5ae 100644
--- a/lib/utils/html-comments.js
+++ b/lib/utils/html-comments.js
@@ -10,19 +10,11 @@
* @typedef { Token & { type: 'HTMLCommentCloseDecoration' } } HTMLCommentCloseDecoration
* @typedef { { open: HTMLCommentOpen, openDecoration: HTMLCommentOpenDecoration | null, value: HTMLCommentValue | null, closeDecoration: HTMLCommentCloseDecoration | null, close: HTMLCommentClose } } ParsedHTMLComment
*/
-// -----------------------------------------------------------------------------
-// Requirements
-// -----------------------------------------------------------------------------
-
const utils = require('./')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
const COMMENT_DIRECTIVE = /^\s*eslint-(?:en|dis)able/
const IE_CONDITIONAL_IF = /^\[if\s+/
-const IE_CONDITIONAL_ENDIF = /\[endif\]$/
+const IE_CONDITIONAL_ENDIF = /\[endif]$/
/** @type { 'HTMLCommentOpen' } */
const TYPE_HTML_COMMENT_OPEN = 'HTMLCommentOpen'
@@ -110,8 +102,8 @@ function defineParser(sourceCode, config) {
/**
* Parse HTMLComment.
- * @param {ASTToken} node a comment token
- * @returns {HTMLComment | null} the result of HTMLComment tokens.
+ * @param {Token} node a comment token
+ * @returns {ParsedHTMLComment | null} the result of HTMLComment tokens.
*/
return function parseHTMLComment(node) {
if (node.type !== 'HTMLComment') {
diff --git a/lib/utils/html-elements.json b/lib/utils/html-elements.json
index 721f7876d..ff9cdf313 100644
--- a/lib/utils/html-elements.json
+++ b/lib/utils/html-elements.json
@@ -1 +1,116 @@
-["html","body","base","head","link","meta","style","title","address","article","aside","footer","header","h1","h2","h3","h4","h5","h6","hgroup","nav","section","div","dd","dl","dt","figcaption","figure","hr","img","li","main","ol","p","pre","ul","a","b","abbr","bdi","bdo","br","cite","code","data","dfn","em","i","kbd","mark","q","rp","rt","rtc","ruby","s","samp","small","span","strong","sub","sup","time","u","var","wbr","area","audio","map","track","video","embed","object","param","source","canvas","script","noscript","del","ins","caption","col","colgroup","table","thead","tbody","tfoot","td","th","tr","button","datalist","fieldset","form","input","label","legend","meter","optgroup","option","output","progress","select","textarea","details","dialog","menu","menuitem","summary","content","element","shadow","template","slot","blockquote","iframe","noframes","picture"]
+[
+ "a",
+ "abbr",
+ "address",
+ "area",
+ "article",
+ "aside",
+ "audio",
+ "b",
+ "base",
+ "bdi",
+ "bdo",
+ "blockquote",
+ "body",
+ "br",
+ "button",
+ "canvas",
+ "caption",
+ "cite",
+ "code",
+ "col",
+ "colgroup",
+ "data",
+ "datalist",
+ "dd",
+ "del",
+ "details",
+ "dfn",
+ "dialog",
+ "div",
+ "dl",
+ "dt",
+ "em",
+ "embed",
+ "fencedframe",
+ "fieldset",
+ "figcaption",
+ "figure",
+ "footer",
+ "form",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ "head",
+ "header",
+ "hgroup",
+ "hr",
+ "html",
+ "i",
+ "iframe",
+ "img",
+ "input",
+ "ins",
+ "kbd",
+ "label",
+ "legend",
+ "li",
+ "link",
+ "main",
+ "map",
+ "mark",
+ "menu",
+ "meta",
+ "meter",
+ "nav",
+ "noscript",
+ "object",
+ "ol",
+ "optgroup",
+ "option",
+ "output",
+ "p",
+ "picture",
+ "pre",
+ "progress",
+ "q",
+ "rp",
+ "rt",
+ "ruby",
+ "s",
+ "samp",
+ "script",
+ "search",
+ "section",
+ "select",
+ "selectedcontent",
+ "slot",
+ "small",
+ "source",
+ "span",
+ "strong",
+ "style",
+ "sub",
+ "summary",
+ "sup",
+ "table",
+ "tbody",
+ "td",
+ "template",
+ "textarea",
+ "tfoot",
+ "th",
+ "thead",
+ "time",
+ "title",
+ "tr",
+ "track",
+ "u",
+ "ul",
+ "var",
+ "video",
+ "wbr"
+]
diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js
index 9e6bfdeff..d2879b120 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -4,112 +4,45 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
-/** @type {Set} */
-const KNOWN_NODES = new Set([
- 'ArrayExpression',
- 'ArrayPattern',
- 'ArrowFunctionExpression',
- 'AssignmentExpression',
- 'AssignmentPattern',
- 'AwaitExpression',
- 'BinaryExpression',
- 'BlockStatement',
- 'BreakStatement',
- 'CallExpression',
- 'CatchClause',
- 'ChainExpression',
- 'ClassBody',
- 'ClassDeclaration',
- 'ClassExpression',
- 'ConditionalExpression',
- 'ContinueStatement',
- 'DebuggerStatement',
- 'DoWhileStatement',
- 'EmptyStatement',
- 'ExportAllDeclaration',
- 'ExportDefaultDeclaration',
- 'ExportNamedDeclaration',
- 'ExportSpecifier',
- 'ExpressionStatement',
- 'ForInStatement',
- 'ForOfStatement',
- 'ForStatement',
- 'FunctionDeclaration',
- 'FunctionExpression',
- 'Identifier',
- 'IfStatement',
- 'ImportDeclaration',
- 'ImportDefaultSpecifier',
- 'ImportExpression',
- 'ImportNamespaceSpecifier',
- 'ImportSpecifier',
- 'LabeledStatement',
- 'Literal',
- 'LogicalExpression',
- 'MemberExpression',
- 'MetaProperty',
- 'MethodDefinition',
- 'NewExpression',
- 'ObjectExpression',
- 'ObjectPattern',
- 'Program',
- 'Property',
- 'RestElement',
- 'ReturnStatement',
- 'SequenceExpression',
- 'SpreadElement',
- 'Super',
- 'SwitchCase',
- 'SwitchStatement',
- 'TaggedTemplateExpression',
- 'TemplateElement',
- 'TemplateLiteral',
- 'ThisExpression',
- 'ThrowStatement',
- 'TryStatement',
- 'UnaryExpression',
- 'UpdateExpression',
- 'VariableDeclaration',
- 'VariableDeclarator',
- 'WhileStatement',
- 'WithStatement',
- 'YieldExpression',
- 'VAttribute',
- 'VDirectiveKey',
- 'VDocumentFragment',
- 'VElement',
- 'VEndTag',
- 'VExpressionContainer',
- 'VFilter',
- 'VFilterSequenceExpression',
- 'VForExpression',
- 'VIdentifier',
- 'VLiteral',
- 'VOnExpression',
- 'VSlotScopeExpression',
- 'VStartTag',
- 'VText'
-])
-const NON_STANDARD_KNOWN_NODES = new Set([
- 'ExperimentalRestProperty',
- 'ExperimentalSpreadProperty'
-])
-const LT_CHAR = /[\r\n\u2028\u2029]/
-const LINES = /[^\r\n\u2028\u2029]+(?:$|\r\n|[\r\n\u2028\u2029])/g
+const {
+ isArrowToken,
+ isOpeningParenToken,
+ isClosingParenToken,
+ isNotOpeningParenToken,
+ isNotClosingParenToken,
+ isOpeningBraceToken,
+ isClosingBraceToken,
+ isNotOpeningBraceToken,
+ isOpeningBracketToken,
+ isClosingBracketToken,
+ isSemicolonToken,
+ isNotSemicolonToken
+} = require('@eslint-community/eslint-utils')
+const {
+ isComment,
+ isNotComment,
+ isWildcard,
+ isExtendsKeyword,
+ isNotWhitespace,
+ isNotEmptyTextNode,
+ isPipeOperator,
+ last
+} = require('./indent-utils')
+const { defineVisitor: tsDefineVisitor } = require('./indent-ts')
+
+/**
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/node').HasLocation} HasLocation
+ * @typedef { { type: string } & HasLocation } MaybeNode
+ */
+
+const LT_CHAR = /[\n\r\u2028\u2029]/
+const LINES = /[^\n\r\u2028\u2029]+(?:$|\r\n|[\n\r\u2028\u2029])/g
const BLOCK_COMMENT_PREFIX = /^\s*\*/
const ITERATION_OPTS = Object.freeze({
includeComments: true,
filter: isNotWhitespace
})
-const PREFORMATTED_ELEMENT_NAMES = ['pre', 'textarea']
+const PREFORMATTED_ELEMENT_NAMES = new Set(['pre', 'textarea'])
/**
* @typedef {object} IndentOptions
@@ -170,10 +103,10 @@ function parseOptions(type, options, defaultOptions) {
ret.indentSize = 1
}
- if (Number.isSafeInteger(options.baseIndent)) {
+ if (options.baseIndent != null && Number.isSafeInteger(options.baseIndent)) {
ret.baseIndent = options.baseIndent
}
- if (Number.isSafeInteger(options.attribute)) {
+ if (options.attribute != null && Number.isSafeInteger(options.attribute)) {
ret.attribute = options.attribute
}
if (Number.isSafeInteger(options.closeBracket)) {
@@ -193,7 +126,7 @@ function parseOptions(type, options, defaultOptions) {
options.closeBracket
)
}
- if (Number.isSafeInteger(options.switchCase)) {
+ if (options.switchCase != null && Number.isSafeInteger(options.switchCase)) {
ret.switchCase = options.switchCase
}
@@ -207,191 +140,11 @@ function parseOptions(type, options, defaultOptions) {
return ret
}
-/**
- * Check whether the given token is an arrow.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is an arrow.
- */
-function isArrow(token) {
- return token != null && token.type === 'Punctuator' && token.value === '=>'
-}
-
-/**
- * Check whether the given token is a left parenthesis.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a left parenthesis.
- */
-function isLeftParen(token) {
- return token != null && token.type === 'Punctuator' && token.value === '('
-}
-
-/**
- * Check whether the given token is a left parenthesis.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `false` if the token is a left parenthesis.
- */
-function isNotLeftParen(token) {
- return token != null && (token.type !== 'Punctuator' || token.value !== '(')
-}
-
-/**
- * Check whether the given token is a right parenthesis.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a right parenthesis.
- */
-function isRightParen(token) {
- return token != null && token.type === 'Punctuator' && token.value === ')'
-}
-
-/**
- * Check whether the given token is a right parenthesis.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `false` if the token is a right parenthesis.
- */
-function isNotRightParen(token) {
- return token != null && (token.type !== 'Punctuator' || token.value !== ')')
-}
-
-/**
- * Check whether the given token is a left brace.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a left brace.
- */
-function isLeftBrace(token) {
- return token != null && token.type === 'Punctuator' && token.value === '{'
-}
-
-/**
- * Check whether the given token is a right brace.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a right brace.
- */
-function isRightBrace(token) {
- return token != null && token.type === 'Punctuator' && token.value === '}'
-}
-
-/**
- * Check whether the given token is a left bracket.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a left bracket.
- */
-function isLeftBracket(token) {
- return token != null && token.type === 'Punctuator' && token.value === '['
-}
-
-/**
- * Check whether the given token is a right bracket.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a right bracket.
- */
-function isRightBracket(token) {
- return token != null && token.type === 'Punctuator' && token.value === ']'
-}
-
-/**
- * Check whether the given token is a semicolon.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a semicolon.
- */
-function isSemicolon(token) {
- return token != null && token.type === 'Punctuator' && token.value === ';'
-}
-
-/**
- * Check whether the given token is a comma.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a comma.
- */
-function isComma(token) {
- return token != null && token.type === 'Punctuator' && token.value === ','
-}
-/**
- * Check whether the given token is a wildcard.
- * @param {Token} token The token to check.
- * @returns {boolean} `true` if the token is a wildcard.
- */
-function isWildcard(token) {
- return token != null && token.type === 'Punctuator' && token.value === '*'
-}
-
-/**
- * Check whether the given token is a whitespace.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a whitespace.
- */
-function isNotWhitespace(token) {
- return token != null && token.type !== 'HTMLWhitespace'
-}
-
-/**
- * Check whether the given token is a comment.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a comment.
- */
-function isComment(token) {
- return (
- token != null &&
- (token.type === 'Block' ||
- token.type === 'Line' ||
- token.type === 'Shebang' ||
- (typeof token.type ===
- 'string' /* Although acorn supports new tokens, espree may not yet support new tokens.*/ &&
- token.type.endsWith('Comment')))
- )
-}
-
-/**
- * Check whether the given token is a comment.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `false` if the token is a comment.
- */
-function isNotComment(token) {
- return (
- token != null &&
- token.type !== 'Block' &&
- token.type !== 'Line' &&
- token.type !== 'Shebang' &&
- !(
- typeof token.type ===
- 'string' /* Although acorn supports new tokens, espree may not yet support new tokens.*/ &&
- token.type.endsWith('Comment')
- )
- )
-}
-
-/**
- * Check whether the given node is not an empty text node.
- * @param {ASTNode} node The node to check.
- * @returns {boolean} `false` if the token is empty text node.
- */
-function isNotEmptyTextNode(node) {
- return !(node.type === 'VText' && node.value.trim() === '')
-}
-
-/**
- * Check whether the given token is a pipe operator.
- * @param {Token|undefined|null} token The token to check.
- * @returns {boolean} `true` if the token is a pipe operator.
- */
-function isPipeOperator(token) {
- return token != null && token.type === 'Punctuator' && token.value === '|'
-}
-
-/**
- * Get the last element.
- * @template T
- * @param {T[]} xs The array to get the last element.
- * @returns {T | undefined} The last element or undefined.
- */
-function last(xs) {
- return xs.length === 0 ? undefined : xs[xs.length - 1]
-}
-
/**
* Check whether the node is at the beginning of line.
- * @param {ASTNode|null} node The node to check.
+ * @param {MaybeNode|null} node The node to check.
* @param {number} index The index of the node in the nodes.
- * @param {(ASTNode|null)[]} nodes The array of nodes.
+ * @param {(MaybeNode|null)[]} nodes The array of nodes.
* @returns {boolean} `true` if the node is at the beginning of line.
*/
function isBeginningOfLine(node, index, nodes) {
@@ -423,6 +176,15 @@ function isClosingToken(token) {
)
}
+/**
+ * Checks whether a given token is a optional token.
+ * @param {Token} token The token to check.
+ * @returns {boolean} `true` if the token is a optional token.
+ */
+function isOptionalToken(token) {
+ return token.type === 'Punctuator' && token.value === '?.'
+}
+
/**
* Creates AST event handlers for html-indent.
*
@@ -444,6 +206,10 @@ module.exports.defineVisitor = function create(
defaultOptions
)
const sourceCode = context.getSourceCode()
+ /**
+ * @typedef { { baseToken: Token | null, offset: number, baseline: boolean, expectedIndent: number | undefined } } OffsetData
+ */
+ /** @type {Map} */
const offsets = new Map()
const ignoreTokens = new Set()
@@ -455,11 +221,12 @@ module.exports.defineVisitor = function create(
* @returns {void}
*/
function setOffset(token, offset, baseToken) {
- if (!token) {
+ if (!token || token === baseToken) {
return
}
if (Array.isArray(token)) {
for (const t of token) {
+ if (!t || t === baseToken) continue
offsets.set(t, {
baseToken,
offset,
@@ -477,6 +244,33 @@ module.exports.defineVisitor = function create(
}
}
+ /**
+ * Copy offset to the given tokens from srcToken.
+ * @param {Token} token The token to set.
+ * @param {Token} srcToken The token of the source offset.
+ * @returns {void}
+ */
+ function copyOffset(token, srcToken) {
+ if (!token) {
+ return
+ }
+ const offsetData = offsets.get(srcToken)
+ if (!offsetData) {
+ return
+ }
+
+ setOffset(
+ token,
+ offsetData.offset,
+ /** @type {Token} */ (offsetData.baseToken)
+ )
+ if (offsetData.baseline) {
+ setBaseline(token)
+ }
+ const o = /** @type {OffsetData} */ (offsets.get(token))
+ o.expectedIndent = offsetData.expectedIndent
+ }
+
/**
* Set baseline flag to the given token.
* @param {Token} token The token to set.
@@ -500,7 +294,7 @@ module.exports.defineVisitor = function create(
tokenStore.getTokenAfter(node)
/** @type {SourceCode.CursorWithSkipOptions} */
- const option = {
+ const cursorOptions = {
includeComments: true,
filter: (token) =>
token != null &&
@@ -510,11 +304,11 @@ module.exports.defineVisitor = function create(
token.type === 'HTMLEndTagOpen' ||
token.type === 'HTMLComment')
}
- for (const token of tokenStore.getTokensBetween(
- node.startTag,
- endToken,
- option
- )) {
+ const contentTokens = endToken
+ ? tokenStore.getTokensBetween(node.startTag, endToken, cursorOptions)
+ : tokenStore.getTokensAfter(node.startTag, cursorOptions)
+
+ for (const token of contentTokens) {
ignoreTokens.add(token)
}
ignoreTokens.add(endToken)
@@ -523,12 +317,12 @@ module.exports.defineVisitor = function create(
/**
* Get the first and last tokens of the given node.
* If the node is parenthesized, this gets the outermost parentheses.
- * @param {ASTNode} node The node to get.
+ * @param {MaybeNode} node The node to get.
* @param {number} [borderOffset] The least offset of the first token. Defailt is 0. This value is used to prevent false positive in the following case: `(a) => {}` The parentheses are enclosing the whole parameter part rather than the first parameter, but this offset parameter is needed to distinguish.
* @returns {{firstToken:Token,lastToken:Token}} The gotten tokens.
*/
function getFirstAndLastTokens(node, borderOffset = 0) {
- borderOffset |= 0
+ borderOffset = Math.trunc(borderOffset)
let firstToken = tokenStore.getFirstToken(node)
let lastToken = tokenStore.getLastToken(node)
@@ -538,8 +332,8 @@ module.exports.defineVisitor = function create(
while (
(t = tokenStore.getTokenBefore(firstToken)) != null &&
(u = tokenStore.getTokenAfter(lastToken)) != null &&
- isLeftParen(t) &&
- isRightParen(u) &&
+ isOpeningParenToken(t) &&
+ isClosingParenToken(u) &&
t.range[0] >= borderOffset
) {
firstToken = t
@@ -553,9 +347,9 @@ module.exports.defineVisitor = function create(
* Process the given node list.
* The first node is offsetted from the given left token.
* Rest nodes are adjusted to the first node.
- * @param {(ASTNode|null)[]} nodeList The node to process.
- * @param {ASTNode|Token|null} left The left parenthesis token.
- * @param {ASTNode|Token|null} right The right parenthesis token.
+ * @param {(MaybeNode|null)[]} nodeList The node to process.
+ * @param {MaybeNode|Token|null} left The left parenthesis token.
+ * @param {MaybeNode|Token|null} right The right parenthesis token.
* @param {number} offset The offset to set.
* @param {boolean} [alignVertically=true] The flag to align vertically. If `false`, this doesn't align vertically even if the first node is not at beginning of line.
* @returns {void}
@@ -565,21 +359,20 @@ module.exports.defineVisitor = function create(
const leftToken = left && tokenStore.getFirstToken(left)
const rightToken = right && tokenStore.getFirstToken(right)
- if (nodeList.length >= 1) {
+ if (nodeList.length > 0) {
let baseToken = null
let lastToken = left
const alignTokensBeforeBaseToken = []
const alignTokens = []
- for (let i = 0; i < nodeList.length; ++i) {
- const node = nodeList[i]
+ for (const node of nodeList) {
if (node == null) {
// Holes of an array.
continue
}
const elementTokens = getFirstAndLastTokens(
node,
- lastToken != null ? lastToken.range[1] : 0
+ lastToken == null ? 0 : lastToken.range[1]
)
// Collect comma/comment tokens between the last token of the previous node and the first token of this node.
@@ -661,28 +454,35 @@ module.exports.defineVisitor = function create(
*/
function processMaybeBlock(node, baseToken) {
const firstToken = getFirstAndLastTokens(node).firstToken
- setOffset(firstToken, isLeftBrace(firstToken) ? 0 : 1, baseToken)
+ setOffset(firstToken, isOpeningBraceToken(firstToken) ? 0 : 1, baseToken)
}
/**
- * Collect prefix tokens of the given property.
- * The prefix includes `async`, `get`, `set`, `static`, and `*`.
- * @param {Property|MethodDefinition} node The property node to collect prefix tokens.
+ * Process semicolons of the given statement node.
+ * @param {MaybeNode} node The statement node to process.
+ * @returns {void}
*/
- function getPrefixTokens(node) {
- const prefixes = []
-
- /** @type {Token|null} */
- let token = tokenStore.getFirstToken(node)
- while (token != null && token.range[1] <= node.key.range[0]) {
- prefixes.push(token)
- token = tokenStore.getTokenAfter(token)
- }
- while (isLeftParen(last(prefixes)) || isLeftBracket(last(prefixes))) {
- prefixes.pop()
+ function processSemicolons(node) {
+ const firstToken = tokenStore.getFirstToken(node)
+ const lastToken = tokenStore.getLastToken(node)
+ if (isSemicolonToken(lastToken) && firstToken !== lastToken) {
+ setOffset(lastToken, 0, firstToken)
}
- return prefixes
+ // Set to the semicolon of the previous token for semicolon-free style.
+ // E.g.,
+ // foo
+ // ;[1,2,3].forEach(f)
+ const info = offsets.get(firstToken)
+ const prevToken = tokenStore.getTokenBefore(firstToken)
+ if (
+ info != null &&
+ prevToken &&
+ isSemicolonToken(prevToken) &&
+ prevToken.loc.end.line === firstToken.loc.start.line
+ ) {
+ offsets.set(prevToken, info)
+ }
}
/**
@@ -694,7 +494,7 @@ module.exports.defineVisitor = function create(
const type = node.type
while (node.parent && node.parent.type === type) {
const prevToken = tokenStore.getTokenBefore(node)
- if (isLeftParen(prevToken)) {
+ if (isOpeningParenToken(prevToken)) {
// The chaining is broken by parentheses.
break
}
@@ -732,17 +532,16 @@ module.exports.defineVisitor = function create(
return false
}
const prevToken = tokenStore.getTokenBefore(belongingNode)
- if (isLeftParen(prevToken)) {
+ if (isOpeningParenToken(prevToken)) {
// It is not the first token because it is enclosed in parentheses.
return false
}
return true
}
if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
- const openParen = /** @type {Token} */ (tokenStore.getTokenAfter(
- parent.callee,
- isNotRightParen
- ))
+ const openParen = /** @type {Token} */ (
+ tokenStore.getTokenAfter(parent.callee, isNotClosingParenToken)
+ )
return parent.arguments.some(
(param) =>
getFirstAndLastTokens(param, openParen.range[1]).firstToken
@@ -779,15 +578,15 @@ module.exports.defineVisitor = function create(
function processTopLevelNode(node, expectedIndent) {
const token = tokenStore.getFirstToken(node)
const offsetInfo = offsets.get(token)
- if (offsetInfo != null) {
- offsetInfo.expectedIndent = expectedIndent
- } else {
+ if (offsetInfo == null) {
offsets.set(token, {
baseToken: null,
offset: 0,
baseline: false,
expectedIndent
})
+ } else {
+ offsetInfo.expectedIndent = expectedIndent
}
}
@@ -836,14 +635,11 @@ module.exports.defineVisitor = function create(
function getExpectedIndents(tokens) {
const expectedIndents = []
- for (let i = 0; i < tokens.length; ++i) {
- const token = tokens[i]
+ for (const [i, token] of tokens.entries()) {
const offsetInfo = offsets.get(token)
if (offsetInfo != null) {
- if (offsetInfo.expectedIndent != null) {
- expectedIndents.push(offsetInfo.expectedIndent)
- } else {
+ if (offsetInfo.expectedIndent == null) {
const baseOffsetInfo = offsets.get(offsetInfo.baseToken)
if (
baseOffsetInfo != null &&
@@ -858,16 +654,18 @@ module.exports.defineVisitor = function create(
break
}
}
+ } else {
+ expectedIndents.push(offsetInfo.expectedIndent)
}
}
}
- if (!expectedIndents.length) {
+ if (expectedIndents.length === 0) {
return null
}
return {
expectedIndent: expectedIndents[0],
- expectedBaseIndent: expectedIndents.reduce((a, b) => Math.min(a, b))
+ expectedBaseIndent: Math.min(...expectedIndents)
}
}
@@ -927,7 +725,7 @@ module.exports.defineVisitor = function create(
* Validate the given token with the pre-calculated expected indentation.
* @param {Token} token The token to validate.
* @param {number} expectedIndent The expected indentation.
- * @param {number[]} [optionalExpectedIndents] The optional expected indentation.
+ * @param {[number, number?]} [optionalExpectedIndents] The optional expected indentation.
* @returns {void}
*/
function validateCore(token, expectedIndent, optionalExpectedIndents) {
@@ -944,8 +742,8 @@ module.exports.defineVisitor = function create(
const actualIndent = token.loc.start.column
const unit = options.indentChar === '\t' ? 'tab' : 'space'
- for (let i = 0; i < indentText.length; ++i) {
- if (indentText[i] !== options.indentChar) {
+ for (const [i, char] of [...indentText].entries()) {
+ if (char !== options.indentChar) {
context.report({
loc: {
start: { line, column: i },
@@ -955,7 +753,7 @@ module.exports.defineVisitor = function create(
'Expected {{expected}} character, but found {{actual}} character.',
data: {
expected: JSON.stringify(options.indentChar),
- actual: JSON.stringify(indentText[i])
+ actual: JSON.stringify(char)
},
fix: defineFix(token, actualIndent, expectedIndent)
})
@@ -991,8 +789,8 @@ module.exports.defineVisitor = function create(
* Get the expected indent of comments.
* @param {Token} nextToken The next token of comments.
* @param {number} nextExpectedIndent The expected indent of the next token.
- * @param {number} lastExpectedIndent The expected indent of the last token.
- * @returns {number[]}
+ * @param {number|undefined} lastExpectedIndent The expected indent of the last token.
+ * @returns {[number, number?]}
*/
function getCommentExpectedIndents(
nextToken,
@@ -1064,22 +862,20 @@ module.exports.defineVisitor = function create(
if (offsetInfo != null) {
if (offsetInfo.baseline) {
// This is a baseline token, so the expected indent is the column of this token.
- if (options.indentChar === ' ') {
- offsetInfo.expectedIndent = Math.max(
- 0,
- token.loc.start.column + expectedBaseIndent - actualIndent
- )
- } else {
- // In hard-tabs mode, it cannot align tokens strictly, so use one additional offset.
- // But the additional offset isn't needed if it's at the beginning of the line.
- offsetInfo.expectedIndent =
- expectedBaseIndent + (token === tokens[0] ? 0 : 1)
- }
+ offsetInfo.expectedIndent =
+ options.indentChar === ' '
+ ? Math.max(
+ 0,
+ token.loc.start.column + expectedBaseIndent - actualIndent
+ )
+ : // In hard-tabs mode, it cannot align tokens strictly, so use one additional offset.
+ // But the additional offset isn't needed if it's at the beginning of the line.
+ expectedBaseIndent + (token === tokens[0] ? 0 : 1)
baseline.add(token)
} else if (baseline.has(offsetInfo.baseToken)) {
// The base token is a baseline token on this line, so inherit it.
- offsetInfo.expectedIndent = offsets.get(
- offsetInfo.baseToken
+ offsetInfo.expectedIndent = /** @type {OffsetData} */ (
+ offsets.get(offsetInfo.baseToken)
).expectedIndent
baseline.add(token)
} else {
@@ -1124,8 +920,14 @@ module.exports.defineVisitor = function create(
// Main
// ------------------------------------------------------------------------------
- return processIgnores({
- /** @param {VAttribute} node */
+ /** @type {Set} */
+ const knownNodes = new Set()
+ /** @type {TemplateListener} */
+ const visitor = {
+ // ----------------------------------------------------------------------
+ // Vue NODES
+ // ----------------------------------------------------------------------
+ /** @param {VAttribute | VDirective} node */
VAttribute(node) {
const keyToken = tokenStore.getFirstToken(node)
const eqToken = tokenStore.getTokenAfter(node.key)
@@ -1141,7 +943,12 @@ module.exports.defineVisitor = function create(
},
/** @param {VElement} node */
VElement(node) {
- if (!PREFORMATTED_ELEMENT_NAMES.includes(node.name)) {
+ if (PREFORMATTED_ELEMENT_NAMES.has(node.name)) {
+ const startTagToken = tokenStore.getFirstToken(node)
+ const endTagToken = node.endTag && tokenStore.getFirstToken(node.endTag)
+ setOffset(endTagToken, 0, startTagToken)
+ setPreformattedTokens(node)
+ } else {
const isTopLevel = node.parent.type !== 'VElement'
const offset = isTopLevel ? options.baseIndent : 1
processNodeList(
@@ -1151,11 +958,6 @@ module.exports.defineVisitor = function create(
offset,
false
)
- } else {
- const startTagToken = tokenStore.getFirstToken(node)
- const endTagToken = node.endTag && tokenStore.getFirstToken(node.endTag)
- setOffset(endTagToken, 0, startTagToken)
- setPreformattedTokens(node)
}
},
/** @param {VEndTag} node */
@@ -1186,7 +988,7 @@ module.exports.defineVisitor = function create(
VFilter(node) {
const idToken = tokenStore.getFirstToken(node)
const lastToken = tokenStore.getLastToken(node)
- if (isRightParen(lastToken)) {
+ if (isClosingParenToken(lastToken)) {
const leftParenToken = tokenStore.getTokenAfter(node.callee)
setOffset(leftParenToken, 1, idToken)
processNodeList(node.arguments, leftParenToken, lastToken, 1)
@@ -1215,14 +1017,16 @@ module.exports.defineVisitor = function create(
VForExpression(node) {
const firstToken = tokenStore.getFirstToken(node)
const lastOfLeft = last(node.left) || firstToken
- const inToken = /** @type {Token} */ (tokenStore.getTokenAfter(
- lastOfLeft,
- isNotRightParen
- ))
+ const inToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(lastOfLeft, isNotClosingParenToken)
+ )
const rightToken = tokenStore.getFirstToken(node.right)
- if (isLeftParen(firstToken)) {
- const rightToken = tokenStore.getTokenAfter(lastOfLeft, isRightParen)
+ if (isOpeningParenToken(firstToken)) {
+ const rightToken = tokenStore.getTokenAfter(
+ lastOfLeft,
+ isClosingParenToken
+ )
processNodeList(node.left, firstToken, rightToken, 1)
}
setOffset(inToken, 1, firstToken)
@@ -1246,9 +1050,9 @@ module.exports.defineVisitor = function create(
)
if (closeToken != null && closeToken.type.endsWith('TagClose')) {
const offset =
- closeToken.type !== 'HTMLSelfClosingTagClose'
- ? options.closeBracket.startTag
- : options.closeBracket.selfClosingTag
+ closeToken.type === 'HTMLSelfClosingTagClose'
+ ? options.closeBracket.selfClosingTag
+ : options.closeBracket.startTag
setOffset(closeToken, offset, openToken)
}
},
@@ -1261,29 +1065,42 @@ module.exports.defineVisitor = function create(
offsets.set(token, Object.assign({}, firstTokenInfo))
}
},
+ // ----------------------------------------------------------------------
+ // SINGLE TOKEN NODES
+ // ----------------------------------------------------------------------
+ VIdentifier() {},
+ VLiteral() {},
+ // ----------------------------------------------------------------------
+ // WRAPPER NODES
+ // ----------------------------------------------------------------------
+ VDirectiveKey() {},
+ VSlotScopeExpression() {},
+ // ----------------------------------------------------------------------
+ // ES NODES
+ // ----------------------------------------------------------------------
/** @param {ArrayExpression | ArrayPattern} node */
'ArrayExpression, ArrayPattern'(node) {
- processNodeList(
- node.elements,
- tokenStore.getFirstToken(node),
- tokenStore.getLastToken(node),
- 1
+ const firstToken = tokenStore.getFirstToken(node)
+ const rightToken = tokenStore.getTokenAfter(
+ node.elements[node.elements.length - 1] || firstToken,
+ isClosingBracketToken
)
+ processNodeList(node.elements, firstToken, rightToken, 1)
},
/** @param {ArrowFunctionExpression} node */
ArrowFunctionExpression(node) {
const firstToken = tokenStore.getFirstToken(node)
const secondToken = tokenStore.getTokenAfter(firstToken)
const leftToken = node.async ? secondToken : firstToken
- const arrowToken = tokenStore.getTokenBefore(node.body, isArrow)
+ const arrowToken = tokenStore.getTokenBefore(node.body, isArrowToken)
if (node.async) {
setOffset(secondToken, 1, firstToken)
}
- if (isLeftParen(leftToken)) {
+ if (isOpeningParenToken(leftToken)) {
const rightToken = tokenStore.getTokenAfter(
last(node.params) || leftToken,
- isRightParen
+ isClosingParenToken
)
processNodeList(node.params, leftToken, rightToken, 1)
}
@@ -1296,10 +1113,9 @@ module.exports.defineVisitor = function create(
node
) {
const leftToken = getChainHeadToken(node)
- const opToken = /** @type {Token} */ (tokenStore.getTokenAfter(
- node.left,
- isNotRightParen
- ))
+ const opToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(node.left, isNotClosingParenToken)
+ )
const rightToken = tokenStore.getTokenAfter(opToken)
const prevToken = tokenStore.getTokenBefore(leftToken)
const shouldIndent =
@@ -1325,6 +1141,16 @@ module.exports.defineVisitor = function create(
1
)
},
+ StaticBlock(node) {
+ const firstToken = tokenStore.getFirstToken(node)
+ let next = tokenStore.getTokenAfter(firstToken)
+ while (next && isNotOpeningBraceToken(next)) {
+ setOffset(next, 0, firstToken)
+ next = tokenStore.getTokenAfter(next)
+ }
+ setOffset(next, 0, firstToken)
+ processNodeList(node.body, next, tokenStore.getLastToken(node), 1)
+ },
/** @param {BreakStatement | ContinueStatement | ReturnStatement | ThrowStatement} node */
'BreakStatement, ContinueStatement, ReturnStatement, ThrowStatement'(node) {
if (
@@ -1342,9 +1168,28 @@ module.exports.defineVisitor = function create(
},
/** @param {CallExpression} node */
CallExpression(node) {
+ const typeArguments =
+ 'typeArguments' in node ? node.typeArguments : node.typeParameters
const firstToken = tokenStore.getFirstToken(node)
const rightToken = tokenStore.getLastToken(node)
- const leftToken = tokenStore.getTokenAfter(node.callee, isLeftParen)
+ const leftToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(
+ typeArguments || node.callee,
+ isOpeningParenToken
+ )
+ )
+
+ if (typeArguments) {
+ setOffset(tokenStore.getFirstToken(typeArguments), 1, firstToken)
+ }
+
+ for (const optionalToken of tokenStore.getTokensBetween(
+ tokenStore.getLastToken(typeArguments || node.callee),
+ leftToken,
+ isOptionalToken
+ )) {
+ setOffset(optionalToken, 1, firstToken)
+ }
setOffset(leftToken, 1, firstToken)
processNodeList(node.arguments, leftToken, rightToken, 1)
@@ -1353,7 +1198,10 @@ module.exports.defineVisitor = function create(
ImportExpression(node) {
const firstToken = tokenStore.getFirstToken(node)
const rightToken = tokenStore.getLastToken(node)
- const leftToken = tokenStore.getTokenAfter(firstToken, isLeftParen)
+ const leftToken = tokenStore.getTokenAfter(
+ firstToken,
+ isOpeningParenToken
+ )
setOffset(leftToken, 1, firstToken)
processNodeList([node.source], leftToken, rightToken, 1)
@@ -1381,7 +1229,9 @@ module.exports.defineVisitor = function create(
setOffset(tokenStore.getFirstToken(node.id), 1, firstToken)
}
if (node.superClass != null) {
- const extendsToken = tokenStore.getTokenAfter(node.id || firstToken)
+ const extendsToken = /** @type {Token} */ (
+ tokenStore.getTokenBefore(node.superClass, isExtendsKeyword)
+ )
const superClassToken = tokenStore.getTokenAfter(extendsToken)
setOffset(extendsToken, 1, firstToken)
setOffset(superClassToken, 1, extendsToken)
@@ -1392,15 +1242,13 @@ module.exports.defineVisitor = function create(
ConditionalExpression(node) {
const prevToken = tokenStore.getTokenBefore(node)
const firstToken = tokenStore.getFirstToken(node)
- const questionToken = /** @type {Token} */ (tokenStore.getTokenAfter(
- node.test,
- isNotRightParen
- ))
+ const questionToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(node.test, isNotClosingParenToken)
+ )
const consequentToken = tokenStore.getTokenAfter(questionToken)
- const colonToken = /** @type {Token} */ (tokenStore.getTokenAfter(
- node.consequent,
- isNotRightParen
- ))
+ const colonToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(node.consequent, isNotClosingParenToken)
+ )
const alternateToken = tokenStore.getTokenAfter(colonToken)
const isFlat =
prevToken &&
@@ -1421,14 +1269,13 @@ module.exports.defineVisitor = function create(
/** @param {DoWhileStatement} node */
DoWhileStatement(node) {
const doToken = tokenStore.getFirstToken(node)
- const whileToken = /** @type {Token} */ (tokenStore.getTokenAfter(
- node.body,
- isNotRightParen
- ))
+ const whileToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(node.body, isNotClosingParenToken)
+ )
const leftToken = tokenStore.getTokenAfter(whileToken)
const testToken = tokenStore.getTokenAfter(leftToken)
const lastToken = tokenStore.getLastToken(node)
- const rightToken = isSemicolon(lastToken)
+ const rightToken = isSemicolonToken(lastToken)
? tokenStore.getTokenBefore(lastToken)
: lastToken
@@ -1440,32 +1287,51 @@ module.exports.defineVisitor = function create(
},
/** @param {ExportAllDeclaration} node */
ExportAllDeclaration(node) {
- const tokens = tokenStore.getTokens(node)
- const firstToken = /** @type {Token} */ (tokens.shift())
- if (isSemicolon(last(tokens))) {
- tokens.pop()
- }
- if (!node.exported) {
- setOffset(tokens, 1, firstToken)
- } else {
+ const exportToken = tokenStore.getFirstToken(node)
+ const tokens = [
+ ...tokenStore.getTokensBetween(exportToken, node.source),
+ tokenStore.getFirstToken(node.source)
+ ]
+ if (node.exported) {
// export * as foo from "mod"
const starToken = /** @type {Token} */ (tokens.find(isWildcard))
const asToken = tokenStore.getTokenAfter(starToken)
const exportedToken = tokenStore.getTokenAfter(asToken)
const afterTokens = tokens.slice(tokens.indexOf(exportedToken) + 1)
- setOffset(starToken, 1, firstToken)
+ setOffset(starToken, 1, exportToken)
setOffset(asToken, 1, starToken)
setOffset(exportedToken, 1, starToken)
- setOffset(afterTokens, 1, firstToken)
+ setOffset(afterTokens, 1, exportToken)
+ } else {
+ setOffset(tokens, 1, exportToken)
+ }
+
+ // assertions
+ const lastToken = /** @type {Token} */ (
+ tokenStore.getLastToken(node, isNotSemicolonToken)
+ )
+ const assertionTokens = tokenStore.getTokensBetween(
+ node.source,
+ lastToken
+ )
+ if (assertionTokens.length > 0) {
+ const assertToken = /** @type {Token} */ (assertionTokens.shift())
+ setOffset(assertToken, 0, exportToken)
+ const assertionOpen = assertionTokens.shift()
+ if (assertionOpen) {
+ setOffset(assertionOpen, 1, assertToken)
+ processNodeList(assertionTokens, assertionOpen, lastToken, 1)
+ }
}
},
/** @param {ExportDefaultDeclaration} node */
ExportDefaultDeclaration(node) {
const exportToken = tokenStore.getFirstToken(node)
const defaultToken = tokenStore.getFirstToken(node, 1)
- const declarationToken = getFirstAndLastTokens(node.declaration)
- .firstToken
+ const declarationToken = getFirstAndLastTokens(
+ node.declaration
+ ).firstToken
setOffset([defaultToken, declarationToken], 1, exportToken)
},
/** @param {ExportNamedDeclaration} node */
@@ -1479,32 +1345,66 @@ module.exports.defineVisitor = function create(
const firstSpecifier = node.specifiers[0]
if (!firstSpecifier || firstSpecifier.type === 'ExportSpecifier') {
// export {foo, bar}; or export {foo, bar} from "mod";
- const leftParenToken = tokenStore.getFirstToken(node, 1)
- const rightParenToken = /** @type {Token} */ (tokenStore.getLastToken(
- node,
- isRightBrace
- ))
- setOffset(leftParenToken, 0, exportToken)
- processNodeList(node.specifiers, leftParenToken, rightParenToken, 1)
-
- const maybeFromToken = tokenStore.getTokenAfter(rightParenToken)
- if (
- maybeFromToken != null &&
- sourceCode.getText(maybeFromToken) === 'from'
- ) {
- const fromToken = maybeFromToken
- const nameToken = tokenStore.getTokenAfter(fromToken)
- setOffset([fromToken, nameToken], 1, exportToken)
+ const leftBraceTokens = firstSpecifier
+ ? tokenStore.getTokensBetween(exportToken, firstSpecifier)
+ : [tokenStore.getTokenAfter(exportToken)]
+ const rightBraceToken = /** @type {Token} */ (
+ node.source
+ ? tokenStore.getTokenBefore(node.source, isClosingBraceToken)
+ : tokenStore.getLastToken(node, isClosingBraceToken)
+ )
+ setOffset(leftBraceTokens, 0, exportToken)
+ processNodeList(
+ node.specifiers,
+ /** @type {Token} */ (last(leftBraceTokens)),
+ rightBraceToken,
+ 1
+ )
+
+ if (node.source) {
+ const tokens = tokenStore.getTokensBetween(
+ rightBraceToken,
+ node.source
+ )
+ setOffset(
+ [...tokens, sourceCode.getFirstToken(node.source)],
+ 1,
+ exportToken
+ )
+
+ // assertions
+ const lastToken = /** @type {Token} */ (
+ tokenStore.getLastToken(node, isNotSemicolonToken)
+ )
+ const assertionTokens = tokenStore.getTokensBetween(
+ node.source,
+ lastToken
+ )
+ if (assertionTokens.length > 0) {
+ const assertToken = /** @type {Token} */ (assertionTokens.shift())
+ setOffset(assertToken, 0, exportToken)
+ const assertionOpen = assertionTokens.shift()
+ if (assertionOpen) {
+ setOffset(assertionOpen, 1, assertToken)
+ processNodeList(assertionTokens, assertionOpen, lastToken, 1)
+ }
+ }
}
} else {
- // maybe babel-eslint
+ // maybe babel parser
}
}
},
- /** @param {ExportSpecifier} node */
- ExportSpecifier(node) {
+ /** @param {ExportSpecifier | ImportSpecifier} node */
+ 'ExportSpecifier, ImportSpecifier'(node) {
const tokens = tokenStore.getTokens(node)
- const firstToken = /** @type {Token} */ (tokens.shift())
+ let firstToken = /** @type {Token} */ (tokens.shift())
+ if (firstToken.value === 'type') {
+ const typeToken = firstToken
+ firstToken = /** @type {Token} */ (tokens.shift())
+ setOffset(firstToken, 0, typeToken)
+ }
+
setOffset(tokens, 1, firstToken)
},
/** @param {ForInStatement | ForOfStatement} node */
@@ -1517,14 +1417,13 @@ module.exports.defineVisitor = function create(
null
const leftParenToken = tokenStore.getTokenAfter(awaitToken || forToken)
const leftToken = tokenStore.getTokenAfter(leftParenToken)
- const inToken = /** @type {Token} */ (tokenStore.getTokenAfter(
- leftToken,
- isNotRightParen
- ))
+ const inToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(leftToken, isNotClosingParenToken)
+ )
const rightToken = tokenStore.getTokenAfter(inToken)
const rightParenToken = tokenStore.getTokenBefore(
node.body,
- isNotLeftParen
+ isNotOpeningParenToken
)
if (awaitToken != null) {
@@ -1543,7 +1442,7 @@ module.exports.defineVisitor = function create(
const leftParenToken = tokenStore.getTokenAfter(forToken)
const rightParenToken = tokenStore.getTokenBefore(
node.body,
- isNotLeftParen
+ isNotOpeningParenToken
)
setOffset(leftParenToken, 1, forToken)
@@ -1558,48 +1457,51 @@ module.exports.defineVisitor = function create(
/** @param {FunctionDeclaration | FunctionExpression} node */
'FunctionDeclaration, FunctionExpression'(node) {
const firstToken = tokenStore.getFirstToken(node)
- if (isLeftParen(firstToken)) {
+ let leftParenToken, bodyBaseToken
+ if (isOpeningParenToken(firstToken)) {
// Methods.
- const leftToken = firstToken
- const rightToken = tokenStore.getTokenAfter(
- last(node.params) || leftToken,
- isRightParen
- )
- const bodyToken = tokenStore.getFirstToken(node.body)
-
- processNodeList(node.params, leftToken, rightToken, 1)
- setOffset(bodyToken, 0, tokenStore.getFirstToken(node.parent))
+ leftParenToken = firstToken
+ bodyBaseToken = tokenStore.getFirstToken(node.parent)
} else {
// Normal functions.
- const functionToken = node.async
- ? tokenStore.getTokenAfter(firstToken)
- : firstToken
- const starToken = node.generator
- ? tokenStore.getTokenAfter(functionToken)
- : null
- const idToken = node.id && tokenStore.getFirstToken(node.id)
- const leftToken = tokenStore.getTokenAfter(
- idToken || starToken || functionToken
- )
- const rightToken = tokenStore.getTokenAfter(
- last(node.params) || leftToken,
- isRightParen
- )
- const bodyToken = tokenStore.getFirstToken(node.body)
-
- if (node.async) {
- setOffset(functionToken, 0, firstToken)
- }
- if (node.generator) {
- setOffset(starToken, 1, firstToken)
- }
- if (node.id != null) {
- setOffset(idToken, 1, firstToken)
+ let nextToken = tokenStore.getTokenAfter(firstToken)
+ let nextTokenOffset = 0
+ while (
+ nextToken &&
+ !isOpeningParenToken(nextToken) &&
+ nextToken.value !== '<'
+ ) {
+ if (
+ nextToken.value === '*' ||
+ (node.id && nextToken.range[0] === node.id.range[0])
+ ) {
+ nextTokenOffset = 1
+ }
+ setOffset(nextToken, nextTokenOffset, firstToken)
+ nextToken = tokenStore.getTokenAfter(nextToken)
}
- setOffset(leftToken, 1, firstToken)
- processNodeList(node.params, leftToken, rightToken, 1)
- setOffset(bodyToken, 0, firstToken)
+
+ leftParenToken = nextToken
+ bodyBaseToken = firstToken
}
+
+ if (
+ !isOpeningParenToken(leftParenToken) &&
+ /** @type {any} */ (node).typeParameters
+ ) {
+ leftParenToken = tokenStore.getTokenAfter(
+ /** @type {any} */ (node).typeParameters
+ )
+ }
+ const rightParenToken = tokenStore.getTokenAfter(
+ node.params[node.params.length - 1] || leftParenToken,
+ isClosingParenToken
+ )
+ setOffset(leftParenToken, 1, bodyBaseToken)
+ processNodeList(node.params, leftParenToken, rightParenToken, 1)
+
+ const bodyToken = tokenStore.getFirstToken(node.body)
+ setOffset(bodyToken, 0, bodyBaseToken)
},
/** @param {IfStatement} node */
IfStatement(node) {
@@ -1607,7 +1509,7 @@ module.exports.defineVisitor = function create(
const ifLeftParenToken = tokenStore.getTokenAfter(ifToken)
const ifRightParenToken = tokenStore.getTokenBefore(
node.consequent,
- isRightParen
+ isClosingParenToken
)
setOffset(ifLeftParenToken, 1, ifToken)
@@ -1615,10 +1517,9 @@ module.exports.defineVisitor = function create(
processMaybeBlock(node.consequent, ifToken)
if (node.alternate != null) {
- const elseToken = /** @type {Token} */ (tokenStore.getTokenAfter(
- node.consequent,
- isNotRightParen
- ))
+ const elseToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(node.consequent, isNotClosingParenToken)
+ )
setOffset(elseToken, 0, ifToken)
processMaybeBlock(node.alternate, elseToken)
@@ -1626,99 +1527,91 @@ module.exports.defineVisitor = function create(
},
/** @param {ImportDeclaration} node */
ImportDeclaration(node) {
- const firstSpecifier = node.specifiers[0]
- const secondSpecifier = node.specifiers[1]
const importToken = tokenStore.getFirstToken(node)
- const hasSemi = tokenStore.getLastToken(node).value === ';'
- /** @type {Token[]} */
- const tokens = [] // tokens to one indent
-
- if (!firstSpecifier) {
- // There are 2 patterns:
- // import "foo"
- // import {} from "foo"
- const secondToken = tokenStore.getFirstToken(node, 1)
- if (isLeftBrace(secondToken)) {
- setOffset(
- [secondToken, tokenStore.getTokenAfter(secondToken)],
- 0,
- importToken
- )
- tokens.push(
- tokenStore.getLastToken(node, hasSemi ? 2 : 1), // from
- tokenStore.getLastToken(node, hasSemi ? 1 : 0) // "foo"
- )
+ const tokens = tokenStore.getTokensBetween(importToken, node.source)
+ const fromIndex = tokens.map((t) => t.value).lastIndexOf('from')
+ const { fromToken, beforeTokens, afterTokens } =
+ fromIndex === -1
+ ? {
+ fromToken: null,
+ beforeTokens: [...tokens, tokenStore.getFirstToken(node.source)],
+ afterTokens: []
+ }
+ : {
+ fromToken: tokens[fromIndex],
+ beforeTokens: tokens.slice(0, fromIndex),
+ afterTokens: [
+ ...tokens.slice(fromIndex + 1),
+ tokenStore.getFirstToken(node.source)
+ ]
+ }
+
+ /** @type {ImportSpecifier[]} */
+ const namedSpecifiers = []
+ for (const specifier of node.specifiers) {
+ if (specifier.type === 'ImportSpecifier') {
+ namedSpecifiers.push(specifier)
} else {
- tokens.push(tokenStore.getLastToken(node, hasSemi ? 1 : 0))
+ const removeTokens = tokenStore.getTokens(specifier)
+ removeTokens.shift()
+ for (const token of removeTokens) {
+ const i = beforeTokens.indexOf(token)
+ if (i !== -1) {
+ beforeTokens.splice(i, 1)
+ }
+ }
}
- } else if (firstSpecifier.type === 'ImportDefaultSpecifier') {
- if (
- secondSpecifier &&
- secondSpecifier.type === 'ImportNamespaceSpecifier'
- ) {
- // There is a pattern:
- // import Foo, * as foo from "foo"
- tokens.push(
- tokenStore.getFirstToken(firstSpecifier), // Foo
- tokenStore.getTokenAfter(firstSpecifier), // comma
- tokenStore.getFirstToken(secondSpecifier), // *
- tokenStore.getLastToken(node, hasSemi ? 2 : 1), // from
- tokenStore.getLastToken(node, hasSemi ? 1 : 0) // "foo"
+ }
+ if (namedSpecifiers.length > 0) {
+ const leftBrace = tokenStore.getTokenBefore(namedSpecifiers[0])
+ const rightBrace = /** @type {Token} */ (
+ tokenStore.getTokenAfter(
+ namedSpecifiers[namedSpecifiers.length - 1],
+ isClosingBraceToken
)
- } else {
- // There are 3 patterns:
- // import Foo from "foo"
- // import Foo, {} from "foo"
- // import Foo, {a} from "foo"
- const idToken = tokenStore.getFirstToken(firstSpecifier)
- const nextToken = tokenStore.getTokenAfter(firstSpecifier)
- if (isComma(nextToken)) {
- const leftBrace = tokenStore.getTokenAfter(nextToken)
- const rightBrace = tokenStore.getLastToken(node, hasSemi ? 3 : 2)
- setOffset([idToken, nextToken], 1, importToken)
- setOffset(leftBrace, 0, idToken)
- processNodeList(node.specifiers.slice(1), leftBrace, rightBrace, 1)
- tokens.push(
- tokenStore.getLastToken(node, hasSemi ? 2 : 1), // from
- tokenStore.getLastToken(node, hasSemi ? 1 : 0) // "foo"
- )
- } else {
- tokens.push(
- idToken,
- nextToken, // from
- tokenStore.getTokenAfter(nextToken) // "foo"
- )
+ )
+ processNodeList(namedSpecifiers, leftBrace, rightBrace, 1)
+ for (const token of [
+ ...tokenStore.getTokensBetween(leftBrace, rightBrace),
+ rightBrace
+ ]) {
+ const i = beforeTokens.indexOf(token)
+ if (i !== -1) {
+ beforeTokens.splice(i, 1)
}
}
- } else if (firstSpecifier.type === 'ImportNamespaceSpecifier') {
- // There is a pattern:
- // import * as foo from "foo"
- tokens.push(
- tokenStore.getFirstToken(firstSpecifier), // *
- tokenStore.getLastToken(node, hasSemi ? 2 : 1), // from
- tokenStore.getLastToken(node, hasSemi ? 1 : 0) // "foo"
+ }
+
+ if (
+ beforeTokens.every(
+ (t) => isOpeningBraceToken(t) || isClosingBraceToken(t)
)
+ ) {
+ setOffset(beforeTokens, 0, importToken)
} else {
- // There is a pattern:
- // import {a} from "foo"
- const leftBrace = tokenStore.getFirstToken(node, 1)
- const rightBrace = tokenStore.getLastToken(node, hasSemi ? 3 : 2)
- setOffset(leftBrace, 0, importToken)
- processNodeList(node.specifiers, leftBrace, rightBrace, 1)
- tokens.push(
- tokenStore.getLastToken(node, hasSemi ? 2 : 1), // from
- tokenStore.getLastToken(node, hasSemi ? 1 : 0) // "foo"
- )
+ setOffset(beforeTokens, 1, importToken)
+ }
+ if (fromToken) {
+ setOffset(fromToken, 1, importToken)
+ setOffset(afterTokens, 0, fromToken)
}
- setOffset(tokens, 1, importToken)
- },
- /** @param {ImportSpecifier} node */
- ImportSpecifier(node) {
- if (node.local.range[0] !== node.imported.range[0]) {
- const tokens = tokenStore.getTokens(node)
- const firstToken = /** @type {Token} */ (tokens.shift())
- setOffset(tokens, 1, firstToken)
+ // assertions
+ const lastToken = /** @type {Token} */ (
+ tokenStore.getLastToken(node, isNotSemicolonToken)
+ )
+ const assertionTokens = tokenStore.getTokensBetween(
+ node.source,
+ lastToken
+ )
+ if (assertionTokens.length > 0) {
+ const assertToken = /** @type {Token} */ (assertionTokens.shift())
+ setOffset(assertToken, 0, importToken)
+ const assertionOpen = assertionTokens.shift()
+ if (assertionOpen) {
+ setOffset(assertionOpen, 1, assertToken)
+ processNodeList(assertionTokens, assertionOpen, lastToken, 1)
+ }
}
},
/** @param {ImportNamespaceSpecifier} node */
@@ -1739,16 +1632,23 @@ module.exports.defineVisitor = function create(
'MemberExpression, MetaProperty'(node) {
const objectToken = tokenStore.getFirstToken(node)
if (node.type === 'MemberExpression' && node.computed) {
- const leftBracketToken = /** @type {Token} */ (tokenStore.getTokenBefore(
- node.property,
- isLeftBracket
- ))
+ const leftBracketToken = /** @type {Token} */ (
+ tokenStore.getTokenBefore(node.property, isOpeningBracketToken)
+ )
const propertyToken = tokenStore.getTokenAfter(leftBracketToken)
const rightBracketToken = tokenStore.getTokenAfter(
node.property,
- isRightBracket
+ isClosingBracketToken
)
+ for (const optionalToken of tokenStore.getTokensBetween(
+ tokenStore.getLastToken(node.object),
+ leftBracketToken,
+ isOptionalToken
+ )) {
+ setOffset(optionalToken, 1, objectToken)
+ }
+
setOffset(leftBracketToken, 1, objectToken)
setOffset(propertyToken, 1, leftBracketToken)
setOffset(rightBracketToken, 0, leftBracketToken)
@@ -1759,62 +1659,60 @@ module.exports.defineVisitor = function create(
setOffset([dotToken, propertyToken], 1, objectToken)
}
},
- /** @param {MethodDefinition | Property} node */
- 'MethodDefinition, Property'(node) {
- const isMethod = node.type === 'MethodDefinition' || node.method === true
- const prefixTokens = getPrefixTokens(node)
- const hasPrefix = prefixTokens.length >= 1
-
- for (let i = 1; i < prefixTokens.length; ++i) {
- setOffset(prefixTokens[i], 0, prefixTokens[i - 1])
+ /** @param {MethodDefinition | Property | PropertyDefinition} node */
+ 'MethodDefinition, Property, PropertyDefinition'(node) {
+ const firstToken = tokenStore.getFirstToken(node)
+ const keyTokens = getFirstAndLastTokens(node.key)
+ const prefixTokens = tokenStore.getTokensBetween(
+ firstToken,
+ keyTokens.firstToken
+ )
+ if (node.computed) {
+ prefixTokens.pop() // pop [
}
+ setOffset(prefixTokens, 0, firstToken)
- /** @type {Token} */
let lastKeyToken
if (node.computed) {
- const keyLeftToken = /** @type {Token} */ (tokenStore.getFirstToken(
- node,
- isLeftBracket
+ const leftBracketToken = tokenStore.getTokenBefore(keyTokens.firstToken)
+ const rightBracketToken = (lastKeyToken = tokenStore.getTokenAfter(
+ keyTokens.lastToken
))
- const keyToken = tokenStore.getTokenAfter(keyLeftToken)
- const keyRightToken = (lastKeyToken = /** @type {Token} */ (tokenStore.getTokenAfter(
- node.key,
- isRightBracket
- )))
-
- if (hasPrefix) {
- setOffset(keyLeftToken, 0, /** @type {Token} */ (last(prefixTokens)))
- }
- setOffset(keyToken, 1, keyLeftToken)
- setOffset(keyRightToken, 0, keyLeftToken)
+ setOffset(leftBracketToken, 0, firstToken)
+ processNodeList([node.key], leftBracketToken, rightBracketToken, 1)
} else {
- const idToken = (lastKeyToken = tokenStore.getFirstToken(node.key))
-
- if (hasPrefix) {
- setOffset(idToken, 0, /** @type {Token} */ (last(prefixTokens)))
- }
+ setOffset(keyTokens.firstToken, 0, firstToken)
+ lastKeyToken = keyTokens.lastToken
}
- if (isMethod) {
- const leftParenToken = tokenStore.getTokenAfter(lastKeyToken)
-
- setOffset(leftParenToken, 1, lastKeyToken)
- } else if (node.type === 'Property' && !node.shorthand) {
- const colonToken = tokenStore.getTokenAfter(lastKeyToken)
- const valueToken = tokenStore.getTokenAfter(colonToken)
-
- setOffset([colonToken, valueToken], 1, lastKeyToken)
+ if (node.value != null) {
+ const initToken = tokenStore.getFirstToken(node.value)
+ setOffset(
+ [...tokenStore.getTokensBetween(lastKeyToken, initToken), initToken],
+ 1,
+ lastKeyToken
+ )
}
},
/** @param {NewExpression} node */
NewExpression(node) {
+ const typeArguments =
+ 'typeArguments' in node ? node.typeArguments : node.typeParameters
const newToken = tokenStore.getFirstToken(node)
const calleeToken = tokenStore.getTokenAfter(newToken)
const rightToken = tokenStore.getLastToken(node)
- const leftToken = isRightParen(rightToken)
- ? tokenStore.getFirstTokenBetween(node.callee, rightToken, isLeftParen)
+ const leftToken = isClosingParenToken(rightToken)
+ ? tokenStore.getFirstTokenBetween(
+ typeArguments || node.callee,
+ rightToken,
+ isOpeningParenToken
+ )
: null
+ if (typeArguments) {
+ setOffset(tokenStore.getFirstToken(typeArguments), 1, calleeToken)
+ }
+
setOffset(calleeToken, 1, newToken)
if (leftToken != null) {
setOffset(leftToken, 1, calleeToken)
@@ -1823,12 +1721,12 @@ module.exports.defineVisitor = function create(
},
/** @param {ObjectExpression | ObjectPattern} node */
'ObjectExpression, ObjectPattern'(node) {
- processNodeList(
- node.properties,
- tokenStore.getFirstToken(node),
- tokenStore.getLastToken(node),
- 1
+ const firstToken = tokenStore.getFirstToken(node)
+ const rightToken = tokenStore.getTokenAfter(
+ node.properties[node.properties.length - 1] || firstToken,
+ isClosingBraceToken
)
+ processNodeList(node.properties, firstToken, rightToken, 1)
},
/** @param {SequenceExpression} node */
SequenceExpression(node) {
@@ -1838,15 +1736,18 @@ module.exports.defineVisitor = function create(
SwitchCase(node) {
const caseToken = tokenStore.getFirstToken(node)
- if (node.test != null) {
- const testToken = tokenStore.getTokenAfter(caseToken)
- const colonToken = tokenStore.getTokenAfter(node.test, isNotRightParen)
-
- setOffset([testToken, colonToken], 1, caseToken)
- } else {
+ if (node.test == null) {
const colonToken = tokenStore.getTokenAfter(caseToken)
setOffset(colonToken, 1, caseToken)
+ } else {
+ const testToken = tokenStore.getTokenAfter(caseToken)
+ const colonToken = tokenStore.getTokenAfter(
+ node.test,
+ isNotClosingParenToken
+ )
+
+ setOffset([testToken, colonToken], 1, caseToken)
}
if (
@@ -1854,7 +1755,7 @@ module.exports.defineVisitor = function create(
node.consequent[0].type === 'BlockStatement'
) {
setOffset(tokenStore.getFirstToken(node.consequent[0]), 0, caseToken)
- } else if (node.consequent.length >= 1) {
+ } else if (node.consequent.length > 0) {
setOffset(tokenStore.getFirstToken(node.consequent[0]), 1, caseToken)
processNodeList(node.consequent, null, null, 0)
}
@@ -1864,10 +1765,9 @@ module.exports.defineVisitor = function create(
const switchToken = tokenStore.getFirstToken(node)
const leftParenToken = tokenStore.getTokenAfter(switchToken)
const discriminantToken = tokenStore.getTokenAfter(leftParenToken)
- const leftBraceToken = /** @type {Token} */ (tokenStore.getTokenAfter(
- node.discriminant,
- isLeftBrace
- ))
+ const leftBraceToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(node.discriminant, isOpeningBraceToken)
+ )
const rightParenToken = tokenStore.getTokenBefore(leftBraceToken)
const rightBraceToken = tokenStore.getLastToken(node)
@@ -1952,7 +1852,10 @@ module.exports.defineVisitor = function create(
'WhileStatement, WithStatement'(node) {
const firstToken = tokenStore.getFirstToken(node)
const leftParenToken = tokenStore.getTokenAfter(firstToken)
- const rightParenToken = tokenStore.getTokenBefore(node.body, isRightParen)
+ const rightParenToken = tokenStore.getTokenBefore(
+ node.body,
+ isClosingParenToken
+ )
setOffset(leftParenToken, 1, firstToken)
setOffset(rightParenToken, 0, leftParenToken)
@@ -1969,38 +1872,45 @@ module.exports.defineVisitor = function create(
}
}
},
+ // ----------------------------------------------------------------------
+ // SINGLE TOKEN NODES
+ // ----------------------------------------------------------------------
+ DebuggerStatement() {},
+ Identifier() {},
+ ImportDefaultSpecifier() {},
+ Literal() {},
+ PrivateIdentifier() {},
+ Super() {},
+ TemplateElement() {},
+ ThisExpression() {},
+ // ----------------------------------------------------------------------
+ // WRAPPER NODES
+ // ----------------------------------------------------------------------
+ ExpressionStatement() {},
+ ChainExpression() {},
+ EmptyStatement() {},
+ // ----------------------------------------------------------------------
+ // COMMONS
+ // ----------------------------------------------------------------------
/** @param {Statement} node */
// Process semicolons.
- ':statement'(node) {
- const firstToken = tokenStore.getFirstToken(node)
- const lastToken = tokenStore.getLastToken(node)
- if (isSemicolon(lastToken) && firstToken !== lastToken) {
- setOffset(lastToken, 0, firstToken)
- }
-
- // Set to the semicolon of the previous token for semicolon-free style.
- // E.g.,
- // foo
- // ;[1,2,3].forEach(f)
- const info = offsets.get(firstToken)
- const prevToken = tokenStore.getTokenBefore(firstToken)
- if (
- info != null &&
- isSemicolon(prevToken) &&
- prevToken.loc.end.line === firstToken.loc.start.line
- ) {
- offsets.set(prevToken, info)
- }
+ ':statement, PropertyDefinition'(node) {
+ processSemicolons(node)
},
/** @param {Expression | MetaProperty | TemplateLiteral} node */
// Process parentheses.
// `:expression` does not match with MetaProperty and TemplateLiteral as a bug: https://github.com/estools/esquery/pull/59
- ':expression, MetaProperty, TemplateLiteral'(node) {
+ ':expression'(node) {
let leftToken = tokenStore.getTokenBefore(node)
let rightToken = tokenStore.getTokenAfter(node)
let firstToken = tokenStore.getFirstToken(node)
- while (isLeftParen(leftToken) && isRightParen(rightToken)) {
+ while (
+ leftToken &&
+ rightToken &&
+ isOpeningParenToken(leftToken) &&
+ isClosingParenToken(rightToken)
+ ) {
setOffset(firstToken, 1, leftToken)
setOffset(rightToken, 0, leftToken)
@@ -2009,13 +1919,22 @@ module.exports.defineVisitor = function create(
rightToken = tokenStore.getTokenAfter(rightToken)
}
},
+
+ .../** @type {TemplateListener} */ (
+ tsDefineVisitor({
+ processNodeList,
+ tokenStore,
+ setOffset,
+ copyOffset,
+ processSemicolons,
+ getFirstAndLastTokens
+ })
+ ),
+
/** @param {ASTNode} node */
// Ignore tokens of unknown nodes.
'*:exit'(node) {
- if (
- !KNOWN_NODES.has(node.type) &&
- !NON_STANDARD_KNOWN_NODES.has(node.type)
- ) {
+ if (!knownNodes.has(node.type)) {
ignore(node)
}
},
@@ -2050,9 +1969,10 @@ module.exports.defineVisitor = function create(
// Validate indentation of tokens.
for (const token of tokenStore.getTokens(node, ITERATION_OPTS)) {
+ const tokenStartLine = token.loc.start.line
if (
tokensOnSameLine.length === 0 ||
- tokensOnSameLine[0].loc.start.line === token.loc.start.line
+ tokensOnSameLine[0].loc.start.line === tokenStartLine
) {
// This is on the same line (or the first token).
tokensOnSameLine.push(token)
@@ -2062,7 +1982,7 @@ module.exports.defineVisitor = function create(
comments.push(tokensOnSameLine[0])
isBesideMultilineToken =
/** @type {Token} */ (last(tokensOnSameLine)).loc.end.line ===
- token.loc.start.line
+ tokenStartLine
tokensOnSameLine = [token]
} else {
// New line is detected, so validate the tokens.
@@ -2072,14 +1992,25 @@ module.exports.defineVisitor = function create(
}
isBesideMultilineToken =
/** @type {Token} */ (last(tokensOnSameLine)).loc.end.line ===
- token.loc.start.line
+ tokenStartLine
tokensOnSameLine = [token]
comments = []
}
}
- if (tokensOnSameLine.length >= 1 && tokensOnSameLine.some(isNotComment)) {
+ if (tokensOnSameLine.some(isNotComment)) {
validate(tokensOnSameLine, comments, lastValidatedToken)
}
}
- })
+ }
+
+ for (const key of Object.keys(visitor)) {
+ for (const nodeName of key
+ .split(/\s*,\s*/gu)
+ .map((s) => s.trim())
+ .filter((s) => /[a-z]+/i.test(s))) {
+ knownNodes.add(nodeName)
+ }
+ }
+
+ return processIgnores(visitor)
}
diff --git a/lib/utils/indent-ts.js b/lib/utils/indent-ts.js
new file mode 100644
index 000000000..314858c9a
--- /dev/null
+++ b/lib/utils/indent-ts.js
@@ -0,0 +1,1449 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const {
+ isClosingParenToken,
+ isOpeningParenToken,
+ isOpeningBraceToken,
+ isNotClosingParenToken,
+ isClosingBracketToken,
+ isOpeningBracketToken
+} = require('@eslint-community/eslint-utils')
+const { isTypeNode } = require('./ts-utils')
+
+/**
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/indent-helper').TSNodeListener} TSNodeListener
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/node').HasLocation} HasLocation
+ * @typedef { { type: string } & HasLocation } MaybeNode
+ */
+/**
+ * @typedef {import('@typescript-eslint/types').TSESTree.Node} TSESTreeNode
+ * @typedef {import('@typescript-eslint/types').TSESTree.ClassExpression} ClassExpression
+ * @typedef {import('@typescript-eslint/types').TSESTree.ClassDeclaration} ClassDeclaration
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeAliasDeclaration} TSTypeAliasDeclaration
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSCallSignatureDeclaration} TSCallSignatureDeclaration
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSConstructSignatureDeclaration} TSConstructSignatureDeclaration
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSImportEqualsDeclaration} TSImportEqualsDeclaration
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSAbstractMethodDefinition} TSAbstractMethodDefinition
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSAbstractPropertyDefinition} TSAbstractPropertyDefinition
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSAbstractAccessorProperty} TSAbstractAccessorProperty
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSEnumMember} TSEnumMember
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSPropertySignature} TSPropertySignature
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSIndexSignature} TSIndexSignature
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSMethodSignature} TSMethodSignature
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeParameterInstantiation} TSTypeParameterInstantiation
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeParameterDeclaration} TSTypeParameterDeclaration
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSConstructorType} TSConstructorType
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSFunctionType} TSFunctionType
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSUnionType} TSUnionType
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSIntersectionType} TSIntersectionType
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSInterfaceHeritage} TSInterfaceHeritage
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSClassImplements} TSClassImplements
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSInterfaceBody} TSInterfaceBody
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSModuleBlock} TSModuleBlock
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSDeclareFunction} TSDeclareFunction
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSEmptyBodyFunctionExpression} TSEmptyBodyFunctionExpression
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeOperator} TSTypeOperator
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeQuery} TSTypeQuery
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSInferType} TSInferType
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSOptionalType} TSOptionalType
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSNonNullExpression} TSNonNullExpression
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSAsExpression} TSAsExpression
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSSatisfiesExpression} TSSatisfiesExpression
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeReference} TSTypeReference
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSInstantiationExpression} TSInstantiationExpression
+ * @typedef {import('@typescript-eslint/types').TSESTree.JSXChild} JSXChild
+ * @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TypeNode
+ *
+ */
+/**
+ * Deprecated in @typescript-eslint/parser v5
+ * @typedef {import('@typescript-eslint/types').TSESTree.PropertyDefinition} ClassProperty
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSAbstractPropertyDefinition} TSAbstractClassProperty
+ */
+
+module.exports = {
+ defineVisitor
+}
+
+/**
+ * Process the given node list.
+ * The first node is offsetted from the given left token.
+ * Rest nodes are adjusted to the first node.
+ * @callback ProcessNodeList
+ * @param {(MaybeNode|null)[]} nodeList The node to process.
+ * @param {MaybeNode|Token|null} left The left parenthesis token.
+ * @param {MaybeNode|Token|null} right The right parenthesis token.
+ * @param {number} offset The offset to set.
+ * @param {boolean} [alignVertically=true] The flag to align vertically. If `false`, this doesn't align vertically even if the first node is not at beginning of line.
+ * @returns {void}
+ */
+/**
+ * Set offset to the given tokens.
+ * @callback SetOffset
+ * @param {Token|Token[]|null|(Token|null)[]} token The token to set.
+ * @param {number} offset The offset of the tokens.
+ * @param {Token} baseToken The token of the base offset.
+ * @returns {void}
+ */
+/**
+ *
+ * Copy offset to the given tokens from srcToken.
+ * @callback CopyOffset
+ * @param {Token} token The token to set.
+ * @param {Token} srcToken The token of the source offset.
+ * @returns {void}
+ */
+/**
+ * Process semicolons of the given statement node.
+ * @callback ProcessSemicolons
+ * @param {MaybeNode} node The statement node to process.
+ * @returns {void}
+ */
+/**
+ * Get the first and last tokens of the given node.
+ * If the node is parenthesized, this gets the outermost parentheses.
+ * @callback GetFirstAndLastTokens
+ * @param {MaybeNode} node The node to get.
+ * @param {number} [borderOffset] The least offset of the first token. Defailt is 0. This value is used to prevent false positive in the following case: `(a) => {}` The parentheses are enclosing the whole parameter part rather than the first parameter, but this offset parameter is needed to distinguish.
+ * @returns {{firstToken:Token,lastToken:Token}} The gotten tokens.
+ */
+/**
+ * @typedef {object} DefineVisitorParam
+ * @property {ProcessNodeList} processNodeList
+ * @property {ParserServices.TokenStore | SourceCode} tokenStore
+ * @property {SetOffset} setOffset
+ * @property {CopyOffset} copyOffset
+ * @property {ProcessSemicolons} processSemicolons
+ * @property {GetFirstAndLastTokens} getFirstAndLastTokens
+ */
+
+/**
+ * @param {DefineVisitorParam} param
+ * @returns {TSNodeListener}
+ */
+function defineVisitor({
+ processNodeList,
+ tokenStore,
+ setOffset,
+ copyOffset,
+ processSemicolons,
+ getFirstAndLastTokens
+}) {
+ /**
+ * Check whether a given token is the first token of:
+ *
+ * - A parameter of TSTypeParameterInstantiation
+ * - An element of TSTupleType
+ *
+ * @param {Token} token The token to check.
+ * @param {TSUnionType | TSIntersectionType} belongingNode The node that the token is belonging to.
+ * @returns {boolean} `true` if the token is the first token of an element.
+ */
+ function isBeginningOfElement(token, belongingNode) {
+ /** @type {TSESTreeNode | null} */
+ let node = belongingNode
+
+ while (node != null && node.parent != null) {
+ /** @type {TSESTreeNode} */
+ const parent = node.parent
+ if (parent.type === 'TSTypeParameterInstantiation') {
+ return (
+ parent.params.length >= 2 &&
+ parent.params.some(
+ (param) =>
+ getFirstAndLastTokens(param).firstToken.range[0] ===
+ token.range[0]
+ )
+ )
+ }
+ if (parent.type === 'TSTupleType') {
+ return parent.elementTypes.some(
+ (element) =>
+ element != null &&
+ getFirstAndLastTokens(element).firstToken.range[0] ===
+ token.range[0]
+ )
+ }
+
+ node = parent
+ }
+
+ return false
+ }
+
+ return {
+ // Support TypeScript
+ /** @param {ClassDeclaration | ClassExpression} node */
+ ['ClassDeclaration[implements], ClassDeclaration[typeParameters], ClassDeclaration[superTypeParameters],' +
+ 'ClassExpression[implements], ClassExpression[typeParameters], ClassExpression[superTypeParameters]'](
+ node
+ ) {
+ if (node.typeParameters != null) {
+ setOffset(
+ tokenStore.getFirstToken(node.typeParameters),
+ 1,
+ tokenStore.getFirstToken(node.id || node)
+ )
+ }
+ if (node.superTypeParameters != null && node.superClass != null) {
+ setOffset(
+ tokenStore.getFirstToken(node.superTypeParameters),
+ 1,
+ tokenStore.getFirstToken(node.superClass)
+ )
+ }
+ if (node.implements != null && node.implements.length > 0) {
+ const classToken = tokenStore.getFirstToken(node)
+ const implementsToken = tokenStore.getTokenBefore(node.implements[0])
+ setOffset(implementsToken, 1, classToken)
+ processNodeList(node.implements, implementsToken, null, 1)
+ }
+ },
+ // Process semicolons.
+ /**
+ * @param {TSTypeAliasDeclaration
+ * | TSCallSignatureDeclaration
+ * | TSConstructSignatureDeclaration
+ * | TSImportEqualsDeclaration
+ * | TSAbstractMethodDefinition
+ * | TSAbstractPropertyDefinition
+ * | TSAbstractAccessorProperty
+ * | TSEnumMember
+ * | TSPropertySignature
+ * | TSIndexSignature
+ * | TSMethodSignature
+ * | ClassProperty
+ * | TSAbstractClassProperty} node
+ */
+ ['TSTypeAliasDeclaration, TSCallSignatureDeclaration, TSConstructSignatureDeclaration, TSImportEqualsDeclaration,' +
+ 'TSAbstractMethodDefinition, TSAbstractPropertyDefinition, TSAbstractAccessorProperty, TSEnumMember,' +
+ 'TSPropertySignature, TSIndexSignature, TSMethodSignature,' +
+ // Deprecated in @typescript-eslint/parser v5
+ 'ClassProperty, TSAbstractClassProperty'](node) {
+ processSemicolons(node)
+ },
+ /**
+ * @param {ASTNode} node
+ */
+ '*[type=/^TS/]'(node) {
+ if (!isTypeNode(node)) {
+ return
+ }
+ const typeNode = node
+ if (/** @type {any} */ (typeNode.parent).type === 'TSParenthesizedType') {
+ return
+ }
+ // Process parentheses.
+ let leftToken = tokenStore.getTokenBefore(node)
+ let rightToken = tokenStore.getTokenAfter(node)
+ let firstToken = tokenStore.getFirstToken(node)
+
+ while (
+ leftToken &&
+ rightToken &&
+ isOpeningParenToken(leftToken) &&
+ isClosingParenToken(rightToken)
+ ) {
+ setOffset(firstToken, 1, leftToken)
+ setOffset(rightToken, 0, leftToken)
+
+ firstToken = leftToken
+ leftToken = tokenStore.getTokenBefore(leftToken)
+ rightToken = tokenStore.getTokenAfter(rightToken)
+ }
+ },
+ /**
+ * Process type annotation
+ *
+ * e.g.
+ * ```
+ * const foo: Type
+ * // ^^^^^^
+ * type foo = () => string
+ * // ^^^^^^^^^
+ * ```
+ */
+ TSTypeAnnotation(node) {
+ const [colonOrArrowToken, secondToken] = tokenStore.getFirstTokens(node, {
+ count: 2,
+ includeComments: false
+ })
+ const baseToken = tokenStore.getFirstToken(
+ /** @type {HasLocation} */ (node.parent)
+ )
+ setOffset([colonOrArrowToken, secondToken], 1, baseToken)
+
+ // a ?: T
+ const before = tokenStore.getTokenBefore(colonOrArrowToken)
+ if (before && before.value === '?') {
+ setOffset(before, 1, baseToken)
+ }
+ },
+ /**
+ * Process as expression or satisfies expression
+ *
+ * e.g.
+ * ```
+ * var foo = bar as boolean
+ * // ^^^^^^^^^^^^^^
+ * ```
+ *
+ * e.g.
+ * ```
+ * var foo = bar satisfies Bar
+ * // ^^^^^^^^^^^^^^^^^
+ * ```
+ *
+ * @param {TSAsExpression | TSSatisfiesExpression} node
+ */
+ 'TSAsExpression, TSSatisfiesExpression'(node) {
+ const expressionTokens = getFirstAndLastTokens(node.expression)
+ const asOrSatisfiesToken = tokenStore.getTokenAfter(
+ expressionTokens.lastToken
+ )
+ setOffset(
+ [
+ asOrSatisfiesToken,
+ getFirstAndLastTokens(node.typeAnnotation).firstToken
+ ],
+ 1,
+ expressionTokens.firstToken
+ )
+ },
+ /**
+ * Process type reference and instantiation expression
+ *
+ * e.g.
+ * ```
+ * const foo: Type
'.",
+ line: 1,
+ column: 11
+ },
+ {
+ message: "A line break is required after '',
+ output: '\n\n\n',
+ errors: [
+ {
+ message: "A line break is required before ''.",
+ line: 2,
+ column: 8
+ },
+ {
+ message: "A line break is required before ''.",
+ line: 4,
+ column: 6
+ }
+ ]
+ },
+ {
+ code: '
\n
\n',
+ output:
+ '\n
\n
\n\n',
+ errors: [
+ {
+ message: "A line break is required after ''.",
+ line: 1,
+ column: 11
+ },
+ {
+ message: "A line break is required before ''.",
+ line: 2,
+ column: 7
+ },
+ {
+ message: "A line break is required after ''.",
+ line: 4,
+ column: 6
+ }
+ ]
+ },
+ {
+ code: '\n
\n
\n\n',
+ output: '
\n
\n',
+ options: [{ singleline: 'never', multiline: 'never' }],
+ errors: [
+ {
+ message: "There should be no line break after ''.",
+ line: 1,
+ column: 11
+ },
+ {
+ message: "There should be no line break after ''.",
+ line: 3,
+ column: 7
+ },
+ {
+ message: "There should be no line break after '',
+ output:
+ '\n
+
+
+ `,
+ options: [['composition']]
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message:
+ 'Options API is not allowed in your project. `data` option is part of the Options API. Use `
+ `,
+ options: [['options']],
+ errors: [
+ {
+ message:
+ '`
+ `,
+ options: [['options']],
+ errors: [
+ {
+ message:
+ 'Composition API is not allowed in your project. `setup` function is part of the Composition API. Use Options API instead.',
+ line: 5,
+ column: 9
+ }
+ ]
+ },
+ {
+ filename: 'test.js',
+ code: `
+ import { defineComponent } from 'vue'
+ defineComponent({
+ data () {
+ return {
+ msg: 'Hello World!',
+ // ...
+ }
+ },
+ // ...
+ })
+ `,
+ errors: [
+ {
+ message:
+ 'Options API is not allowed in your project. `data` option is part of the Options API. Use Composition API instead.',
+ line: 4,
+ column: 9
+ }
+ ]
+ },
+ {
+ filename: 'test.js',
+ code: `
+ import { ref, defineComponent } from 'vue'
+ defineComponent({
+ setup() {
+ const msg = ref('Hello World!')
+ // ...
+ return {
+ msg,
+ // ...
+ }
+ }
+ })
+ `,
+ options: [['options']],
+ errors: [
+ {
+ message:
+ 'Composition API is not allowed in your project. `setup` function is part of the Composition API. Use Options API instead.',
+ line: 4,
+ column: 9
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [['script-setup']],
+ errors: [
+ {
+ message:
+ 'Composition API is not allowed in your project. Use `
+ `,
+ options: [['script-setup']],
+ errors: [
+ {
+ message:
+ 'Options API is not allowed in your project. Use `
+ `,
+ options: [['composition']],
+ errors: [
+ {
+ message:
+ '`
+ `,
+ options: [['composition']],
+ errors: [
+ {
+ message:
+ 'Options API is not allowed in your project. `data` option is part of the Options API. Use Composition API instead.',
+ line: 4,
+ column: 9
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [['composition']],
+ errors: [
+ {
+ message:
+ 'Options API is not allowed in your project. `mixins` option is part of the Options API. Use Composition API instead.',
+ line: 4,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `extends` option is part of the Options API. Use Composition API instead.',
+ line: 5,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `data` option is part of the Options API. Use Composition API instead.',
+ line: 7,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `computed` option is part of the Options API. Use Composition API instead.',
+ line: 8,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `methods` option is part of the Options API. Use Composition API instead.',
+ line: 9,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `watch` option is part of the Options API. Use Composition API instead.',
+ line: 10,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `provide` option is part of the Options API. Use Composition API instead.',
+ line: 11,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `inject` option is part of the Options API. Use Composition API instead.',
+ line: 12,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `beforeCreate` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 14,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `created` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 15,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `beforeMount` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 16,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `mounted` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 17,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `beforeUpdate` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 18,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `updated` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 19,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `activated` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 20,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `deactivated` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 21,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `beforeDestroy` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 22,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `beforeUnmount` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 23,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `destroyed` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 24,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `unmounted` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 25,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `render` function is part of the Options API. Use Composition API instead.',
+ line: 26,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `renderTracked` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 27,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `renderTriggered` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 28,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `errorCaptured` lifecycle hook is part of the Options API. Use Composition API instead.',
+ line: 29,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `expose` option is part of the Options API. Use Composition API instead.',
+ line: 31,
+ column: 9
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [['composition-vue2']],
+ errors: [
+ {
+ message:
+ '`
+ `,
+ options: [['composition-vue2']],
+ errors: [
+ {
+ message:
+ 'Options API is not allowed in your project. `data` option is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 4,
+ column: 9
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [['composition-vue2']],
+ errors: [
+ {
+ message:
+ 'Options API is not allowed in your project. `mixins` option is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 4,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `extends` option is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 5,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `data` option is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 7,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `computed` option is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 8,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `methods` option is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 9,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `watch` option is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 10,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `provide` option is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 11,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `inject` option is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 12,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `beforeCreate` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 14,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `created` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 15,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `beforeMount` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 16,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `mounted` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 17,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `beforeUpdate` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 18,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `updated` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 19,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `activated` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 20,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `deactivated` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 21,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `beforeDestroy` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 22,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `beforeUnmount` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 23,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `destroyed` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 24,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `unmounted` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 25,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `errorCaptured` lifecycle hook is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 29,
+ column: 9
+ },
+ {
+ message:
+ 'Options API is not allowed in your project. `expose` option is part of the Options API. Use Composition API (Vue 2) instead.',
+ line: 31,
+ column: 9
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [['script-setup', 'composition', 'composition-vue2']],
+ errors: [
+ {
+ message:
+ 'Options API is not allowed in your project. `data` option is part of the Options API. Use ``,
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ...languageOptions
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: ``,
+ options: ['PascalCase'],
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ...languageOptions
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: ``,
+ options: ['kebab-case'],
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ...languageOptions
+ }
}
],
@@ -169,7 +187,7 @@ ruleTester.run('component-definition-name-casing', rule, {
name: 'FooBar'
}
`,
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo-bar" is not PascalCase.',
@@ -186,7 +204,7 @@ ruleTester.run('component-definition-name-casing', rule, {
}
`,
output: null,
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo bar" is not PascalCase.',
@@ -203,7 +221,7 @@ ruleTester.run('component-definition-name-casing', rule, {
}
`,
output: null,
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo!bar" is not PascalCase.',
@@ -220,7 +238,7 @@ ruleTester.run('component-definition-name-casing', rule, {
})
`,
output: null,
- parserOptions: { ecmaVersion: 6 },
+ languageOptions: { ecmaVersion: 6 },
errors: [
{
message: 'Property name "foo!bar" is not PascalCase.',
@@ -241,7 +259,7 @@ ruleTester.run('component-definition-name-casing', rule, {
name: 'FooBar'
}
`,
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo_bar" is not PascalCase.',
@@ -263,7 +281,7 @@ ruleTester.run('component-definition-name-casing', rule, {
}
`,
options: ['PascalCase'],
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo_bar" is not PascalCase.',
@@ -285,7 +303,7 @@ ruleTester.run('component-definition-name-casing', rule, {
}
`,
options: ['kebab-case'],
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo_bar" is not kebab-case.',
@@ -298,7 +316,7 @@ ruleTester.run('component-definition-name-casing', rule, {
filename: 'test.vue',
code: `Vue.component('foo-bar', component)`,
output: `Vue.component('FooBar', component)`,
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo-bar" is not PascalCase.',
@@ -311,7 +329,7 @@ ruleTester.run('component-definition-name-casing', rule, {
filename: 'test.vue',
code: `app.component('foo-bar', component)`,
output: `app.component('FooBar', component)`,
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo-bar" is not PascalCase.',
@@ -324,8 +342,10 @@ ruleTester.run('component-definition-name-casing', rule, {
filename: 'test.vue',
code: `(Vue as VueConstructor).component('foo-bar', component)`,
output: `(Vue as VueConstructor).component('FooBar', component)`,
- parserOptions,
- parser: require.resolve('@typescript-eslint/parser'),
+ languageOptions: {
+ parser: require('@typescript-eslint/parser'),
+ ...languageOptions
+ },
errors: [
{
message: 'Property name "foo-bar" is not PascalCase.',
@@ -338,7 +358,7 @@ ruleTester.run('component-definition-name-casing', rule, {
filename: 'test.vue',
code: `Vue.component('foo-bar', {})`,
output: `Vue.component('FooBar', {})`,
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo-bar" is not PascalCase.',
@@ -351,7 +371,7 @@ ruleTester.run('component-definition-name-casing', rule, {
filename: 'test.vue',
code: `app.component('foo-bar', {})`,
output: `app.component('FooBar', {})`,
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo-bar" is not PascalCase.',
@@ -365,7 +385,7 @@ ruleTester.run('component-definition-name-casing', rule, {
code: `Vue.component('foo_bar', {})`,
output: `Vue.component('FooBar', {})`,
options: ['PascalCase'],
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo_bar" is not PascalCase.',
@@ -379,7 +399,7 @@ ruleTester.run('component-definition-name-casing', rule, {
code: `Vue.component('foo_bar', {})`,
output: `Vue.component('foo-bar', {})`,
options: ['kebab-case'],
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo_bar" is not kebab-case.',
@@ -393,7 +413,7 @@ ruleTester.run('component-definition-name-casing', rule, {
code: `Vue.component(\`foo_bar\`, {})`,
output: `Vue.component(\`foo-bar\`, {})`,
options: ['kebab-case'],
- parserOptions,
+ languageOptions,
errors: [
{
message: 'Property name "foo_bar" is not kebab-case.',
@@ -401,6 +421,38 @@ ruleTester.run('component-definition-name-casing', rule, {
line: 1
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: ``,
+ output: ``,
+ options: ['PascalCase'],
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ...languageOptions
+ },
+ errors: [
+ {
+ message: 'Property name "foo-bar" is not PascalCase.',
+ line: 1
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: ``,
+ output: ``,
+ options: ['kebab-case'],
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ...languageOptions
+ },
+ errors: [
+ {
+ message: 'Property name "FooBar" is not kebab-case.',
+ line: 1
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/component-name-in-template-casing.js b/tests/lib/rules/component-name-in-template-casing.js
index 31c4ae832..44a23d532 100644
--- a/tests/lib/rules/component-name-in-template-casing.js
+++ b/tests/lib/rules/component-name-in-template-casing.js
@@ -3,20 +3,13 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const rule = require('../../../lib/rules/component-name-in-template-casing')
-const RuleTester = require('eslint').RuleTester
-
-// ------------------------------------------------------------------------------
-// Tests
-// ------------------------------------------------------------------------------
+const semver = require('semver')
+const RuleTester = require('../../eslint-compat').RuleTester
const tester = new RuleTester({
- parser: require.resolve('vue-eslint-parser'),
- parserOptions: {
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
ecmaVersion: 2018,
sourceType: 'module'
}
@@ -26,6 +19,7 @@ tester.run('component-name-in-template-casing', rule, {
valid: [
// default
{
+ filename: 'test.vue',
code: `
@@ -40,8 +34,7 @@ tester.run('component-name-in-template-casing', rule, {
}
}
- `,
- filename: 'test.vue'
+ `
},
// element types test
@@ -69,6 +62,10 @@ tester.run('component-name-in-template-casing', rule, {
code: '