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 2c468cf31..187237ed8 100644
--- a/docs/rules/block-spacing.md
+++ b/docs/rules/block-spacing.md
@@ -2,22 +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 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.
-This rule is the same rule as core [block-spacing] rule but it applies to the expressions in ``.
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
-## :books: Further reading
+## :books: Further Reading
+- [@stylistic/block-spacing]
- [block-spacing]
+[@stylistic/block-spacing]: https://eslint.style/rules/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 Stylistic](https://eslint.style/rules/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 4e11a9f26..2667e1829 100644
--- a/docs/rules/brace-style.md
+++ b/docs/rules/brace-style.md
@@ -2,22 +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 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.
-This rule is the same rule as core [brace-style] rule but it applies to the expressions in ``.
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
-## :books: Further reading
+## :books: Further Reading
+- [@stylistic/brace-style]
- [brace-style]
+[@stylistic/brace-style]: https://eslint.style/rules/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 Stylistic](https://eslint.style/rules/brace-style)
diff --git a/docs/rules/camelcase.md b/docs/rules/camelcase.md
index c54c44457..6ed97876a 100644
--- a/docs/rules/camelcase.md
+++ b/docs/rules/camelcase.md
@@ -2,20 +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/latest/rules/camelcase)
diff --git a/docs/rules/comma-dangle.md b/docs/rules/comma-dangle.md
index 0ccdf1518..7fd33c445 100644
--- a/docs/rules/comma-dangle.md
+++ b/docs/rules/comma-dangle.md
@@ -2,22 +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 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.
-This rule is the same rule as core [comma-dangle] rule but it applies to the expressions in ``.
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
-## :books: Further reading
+## :books: Further Reading
+- [@stylistic/comma-dangle]
- [comma-dangle]
+[@stylistic/comma-dangle]: https://eslint.style/rules/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 Stylistic](https://eslint.style/rules/comma-dangle)
diff --git a/docs/rules/comma-spacing.md b/docs/rules/comma-spacing.md
index d596ee4ea..8f31fa50a 100644
--- a/docs/rules/comma-spacing.md
+++ b/docs/rules/comma-spacing.md
@@ -2,22 +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 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.
-This rule is the same rule as core [comma-spacing] rule but it applies to the expressions in ``.
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
-## :books: Further reading
+## :books: Further Reading
+- [@stylistic/comma-spacing]
- [comma-spacing]
+[@stylistic/comma-spacing]: https://eslint.style/rules/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 Stylistic](https://eslint.style/rules/comma-spacing)
diff --git a/docs/rules/comma-style.md b/docs/rules/comma-style.md
index 6e4ea7422..c4c6eea96 100644
--- a/docs/rules/comma-style.md
+++ b/docs/rules/comma-style.md
@@ -2,22 +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 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.
-This rule is the same rule as core [comma-style] rule but it applies to the expressions in ``.
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
-## :books: Further reading
+## :books: Further Reading
+- [@stylistic/comma-style]
- [comma-style]
+[@stylistic/comma-style]: https://eslint.style/rules/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 Stylistic](https://eslint.style/rules/comma-style)
diff --git a/docs/rules/comment-directive.md b/docs/rules/comment-directive.md
index 33ad1fa9d..499cf9579 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:
@@ -21,8 +24,6 @@ It supports usage of the following comments:
We can't write HTML comments in tags.
:::
-This rule doesn't throw any warning.
-
## :book: Rule Details
ESLint doesn't provide any API to enhance `eslint-disable` functionality and ESLint rules cannot affect other rules. But ESLint provides [processors API](https://eslint.org/docs/developer-guide/working-with-plugins#processors-in-plugins).
@@ -42,7 +43,7 @@ This rule sends all `eslint-disable`-like comments as errors to the post-process
The `eslint-disable`-like comments can be used in the `` and in the block level.
-
+
```vue
@@ -50,27 +51,28 @@ The `eslint-disable`-like comments can be used in the `` and in the bl
-
-
+
+
```
The `eslint-disable` comments has no effect after one block.
-
+
```vue
-
-
-
-
-
-
+
+
+
+
```
@@ -88,9 +90,49 @@ The `eslint-disable`-like comments can include descriptions to explain why the c
-## :books: Further reading
+## :wrench: Options
+
+```json
+{
+ "vue/comment-directive": ["error", {
+ "reportUnusedDisableDirectives": false
+ }]
+}
+```
+
+- `reportUnusedDisableDirectives` ... If `true`, to report unused `eslint-disable` HTML comments. default `false`
+
+### `{ "reportUnusedDisableDirectives": true }`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+::: warning Note
+Unused reports cannot be suppressed with `eslint-disable` HTML comments.
+:::
+
+## :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
-- [Disabling rules with inline comments](https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments)
+This rule was introduced in eslint-plugin-vue v4.1.0
## :mag: Implementation
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 3d157dc00..a5037cdee 100644
--- a/docs/rules/component-tags-order.md
+++ b/docs/rules/component-tags-order.md
@@ -3,31 +3,34 @@ 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 `
+
+```
+
+
+
+
```vue
@@ -51,7 +65,7 @@ This rule warns about the order of the `
+...
+
+```
+
+
+
### `{ "order": ["docs", "template", "script", "style"] }`
-
+
```vue
- documents
+ documentation
...
@@ -76,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
new file mode 100644
index 000000000..a57e0eb80
--- /dev/null
+++ b/docs/rules/custom-event-name-casing.md
@@ -0,0 +1,187 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/custom-event-name-casing
+description: enforce specific casing for custom event name
+since: v7.0.0
+---
+
+# vue/custom-event-name-casing
+
+> enforce specific casing for custom event name
+
+Define a style for custom event name casing for consistency purposes.
+
+## :book: Rule Details
+
+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
+
+
+
+
+
+
+
+
+```
+
+
+
+### `"camelCase"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+### `"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
+
+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/custom-event-name-casing.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/custom-event-name-casing.js)
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 56a9c3342..101b58776 100644
--- a/docs/rules/dot-location.md
+++ b/docs/rules/dot-location.md
@@ -2,22 +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 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.
-This rule is the same rule as core [dot-location] rule but it applies to the expressions in ``.
+[@stylistic/eslint-plugin]: https://eslint.style/packages/default
-## :books: Further reading
+## :books: Further Reading
+- [@stylistic/dot-location]
- [dot-location]
+[@stylistic/dot-location]: https://eslint.style/rules/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 Stylistic](https://eslint.style/rules/dot-location)
diff --git a/docs/rules/dot-notation.md b/docs/rules/dot-notation.md
new file mode 100644
index 000000000..101caf5cf
--- /dev/null
+++ b/docs/rules/dot-notation.md
@@ -0,0 +1,32 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/dot-notation
+description: Enforce dot notation whenever possible in ``
+since: v7.0.0
+---
+
+# vue/dot-notation
+
+> 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
+
+- [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/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 06a0a0b67..fb0133251 100644
--- a/docs/rules/eqeqeq.md
+++ b/docs/rules/eqeqeq.md
@@ -2,22 +2,32 @@
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.
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
This rule is the same rule as core [eqeqeq] rule but it applies to the expressions in ``.
-## :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/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
new file mode 100644
index 000000000..89365a8c9
--- /dev/null
+++ b/docs/rules/func-call-spacing.md
@@ -0,0 +1,39 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/func-call-spacing
+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 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 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
+
+## :books: Further Reading
+
+- [@stylistic/function-call-spacing]
+- [func-call-spacing]
+
+[@stylistic/function-call-spacing]: https://eslint.style/rules/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 Stylistic](https://eslint.style/rules/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 81e225852..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 2017 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.
@@ -76,9 +79,13 @@ This rule enforces a consistent indentation style in ``. The default s
- `type` (`number | "tab"`) ... The type of indentation. Default is `2`. If this is a number, it's the number of spaces for one indent. If this is `"tab"`, it uses one tab for one indent.
- `attribute` (`integer`) ... The multiplier of indentation for attributes. Default is `1`.
- `baseIndent` (`integer`) ... The multiplier of indentation for top-level statements. Default is `1`.
-- `closeBracket` (`integer`) ... The multiplier of indentation for right brackets. Default is `0`.
+- `closeBracket` (`integer | object`) ... The multiplier of indentation for right brackets. Default is `0`.
+ You can apply all of the following by setting a number value.
+ - `closeBracket.startTag` (`integer`) ... The multiplier of indentation for right brackets of start tags (`
`). Default is `0`.
+ - `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}`
@@ -186,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..26542db38
--- /dev/null
+++ b/docs/rules/index.md
@@ -0,0 +1,651 @@
+---
+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..5ba837f41
--- /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 identifier. Note that "matches" means that the imported name matches either the PascalCase or kebab-case version of the components object property identifier. 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 4dc5ba8d5..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,13 +319,19 @@ 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)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/max-len.js)
+
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/max-len)
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..4e6e999b1 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,11 +143,14 @@ This rule enforces a line break before and after the contents of a multiline ele
-## :books: Further reading
+## :couple: Related Rules
+
+- [vue/singleline-html-element-content-newline](./singleline-html-element-content-newline.md)
+- [no-multiple-empty-lines](https://eslint.org/docs/rules/no-multiple-empty-lines)
-- [no-multiple-empty-lines]
+## :rocket: Version
-[no-multiple-empty-lines]: https://eslint.org/docs/rules/no-multiple-empty-lines
+This rule was introduced in eslint-plugin-vue v5.0.0
## :mag: Implementation
diff --git a/docs/rules/multiline-ternary.md b/docs/rules/multiline-ternary.md
new file mode 100644
index 000000000..687138e77
--- /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/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/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
new file mode 100644
index 000000000..23a23c116
--- /dev/null
+++ b/docs/rules/no-bare-strings-in-template.md
@@ -0,0 +1,94 @@
+---
+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 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
+
+
+
{{ $t('foo.bar') }}
+
{{ foo }}
+
+
+
+
Lorem ipsum
+
+
+
+
+
+
+
{{ 'Lorem ipsum' }}
+
+
+```
+
+
+
+:::tip
+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.
+:::
+
+## :wrench: Options
+
+```js
+{
+ "vue/no-bare-strings-in-template": ["error", {
+ "allowlist": [
+ "(", ")", ",", ".", "&", "+", "-", "=", "*", "/", "#", "%", "!", "?", ":", "[", "]", "{", "}", "<", ">", "\u00b7", "\u2022", "\u2010", "\u2013", "\u2014", "\u2212", "|"
+ ],
+ "attributes": {
+ "/.+/": ["title", "aria-label", "aria-placeholder", "aria-roledescription", "aria-valuetext"],
+ "input": ["placeholder"],
+ "img": ["alt"]
+ },
+ "directives": ["v-text"]
+ }]
+}
+```
+
+- `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
+
+- [vue/no-useless-v-bind]
+- [vue/no-useless-mustaches]
+
+[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)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/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
new file mode 100644
index 000000000..9c681e0ea
--- /dev/null
+++ b/docs/rules/no-deprecated-destroyed-lifecycle.md
@@ -0,0 +1,55 @@
+---
+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/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
+
+```
+
+
+
+## :wrench: Options
+
+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)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/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
new file mode 100644
index 000000000..45e67d480
--- /dev/null
+++ b/docs/rules/no-deprecated-dollar-scopedslots-api.md
@@ -0,0 +1,57 @@
+---
+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/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
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :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)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/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..34f941ec4 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,90 @@ 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"],
+ "ignoreParents": ["my-web-component"],
+ }]
+}
+```
+
+- `"ignore"` (`string[]`) An array of tags or regular expression patterns (e.g. `/^custom-/`) that ignore these rules. This option will check both kebab-case and PascalCase versions of the given tag names. Default is empty.
+- `"ignoreParents"` (`string[]`) An array of tags or regular expression patterns (e.g. `/^custom-/`) for parents that ignore these rules. This option is especially useful for [Web-Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components). Default is empty.
+
+### `"ignore": ["my-component"]`
+
+
+
+```vue
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+```
+
+
+
+### `"ignoreParents": ["my-web-component"]`
+
+
+
+```vue
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+```
+
+
+
+## :books: Further Reading
+
+- [API - slot](https://v2.vuejs.org/v2/api/#slot-deprecated)
+- [Web - slot](https://developer.mozilla.org/en-US/docs/Web/API/Element/slot)
+
+## :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
new file mode 100644
index 000000000..344bd0cba
--- /dev/null
+++ b/docs/rules/no-empty-component-block.md
@@ -0,0 +1,76 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-empty-component-block
+description: disallow the `` `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+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)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-empty-component-block.js)
diff --git a/docs/rules/no-empty-pattern.md b/docs/rules/no-empty-pattern.md
index dd23c9f5b..6434a7bff 100644
--- a/docs/rules/no-empty-pattern.md
+++ b/docs/rules/no-empty-pattern.md
@@ -2,20 +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/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 4726907d3..fbb0a3202 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 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.
-This rule is the same rule as core [no-extra-parens] rule but it applies to the expressions in ``.
+[@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,13 +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/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 Stylistic](https://eslint.style/rules/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..751e189b9
--- /dev/null
+++ b/docs/rules/no-implicit-coercion.md
@@ -0,0 +1,33 @@
+---
+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.
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+
+This rule is the same rule as core [no-implicit-coercion] rule but it applies to the expressions in ``.
+
+## :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 7fd4efcd9..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,13 +160,19 @@ 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)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-irregular-whitespace.js)
+
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/no-irregular-whitespace)
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
new file mode 100644
index 000000000..13f1f591b
--- /dev/null
+++ b/docs/rules/no-multiple-objects-in-class.md
@@ -0,0 +1,44 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-multiple-objects-in-class
+description: disallow passing multiple objects in an array to class
+since: v7.0.0
+---
+
+# vue/no-multiple-objects-in-class
+
+> disallow passing multiple objects in an array to class
+
+## :book: Rule Details
+
+This rule disallows to pass multiple objects into array to class.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+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)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/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
new file mode 100644
index 000000000..74d6c2931
--- /dev/null
+++ b/docs/rules/no-multiple-slot-args.md
@@ -0,0 +1,56 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-multiple-slot-args
+description: disallow passing multiple arguments to scoped slots
+since: v7.0.0
+---
+
+# vue/no-multiple-slot-args
+
+> 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
+
+This rule disallows to pass multiple arguments to scoped slots.
+In details, it reports call expressions if a call of `this.$scopedSlots` members has 2 or more arguments.
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :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)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/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-negated-condition.md b/docs/rules/no-negated-condition.md
new file mode 100644
index 000000000..17ffdba5d
--- /dev/null
+++ b/docs/rules/no-negated-condition.md
@@ -0,0 +1,37 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-negated-condition
+description: Disallow negated conditions in ``
+since: v10.4.0
+---
+
+# vue/no-negated-condition
+
+> Disallow negated conditions in ``
+
+## :book: Rule Details
+
+This rule is the same rule as core [no-negated-condition] rule but it applies to the expressions in ``.
+
+## :couple: Related Rules
+
+- [`vue/no-negated-v-if-condition`](https://eslint.vuejs.org/rules/no-negated-v-if-condition.html)
+- [unicorn/no-negated-condition](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-negated-condition.md)
+
+## :books: Further Reading
+
+- [no-negated-condition]
+
+[no-negated-condition]: https://eslint.org/docs/rules/no-negated-condition
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v10.4.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-negated-condition.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-negated-condition.js)
+
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/latest/rules/no-negated-condition)
diff --git a/docs/rules/no-negated-v-if-condition.md b/docs/rules/no-negated-v-if-condition.md
new file mode 100644
index 000000000..21592793d
--- /dev/null
+++ b/docs/rules/no-negated-v-if-condition.md
@@ -0,0 +1,68 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-negated-v-if-condition
+description: disallow negated conditions in v-if/v-else
+since: v10.4.0
+---
+
+# vue/no-negated-v-if-condition
+
+> disallow negated conditions in v-if/v-else
+
+- :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 disallows negated conditions in `v-if` and `v-else-if` directives which have an `v-else` branch.
+
+Negated conditions make the code less readable. When there's an `else` clause, it's better to use a positive condition and switch the branches.
+
+
+
+```vue
+
+
+
First
+
Second
+
+
First
+
Second
+
+
Content
+
+
Not equal
+
+
+
First
+
Second
+
+
First
+
Second
+
+
First
+
Second
+
Third
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [no-negated-condition](https://eslint.org/docs/latest/rules/no-negated-condition)
+- [vue/no-negated-condition](https://eslint.vuejs.org/rules/no-negated-condition.html)
+- [unicorn/no-negated-condition](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-negated-condition.md)
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v10.4.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-negated-v-if-condition.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-negated-v-if-condition.js)
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 e347676db..17e21c818 100644
--- a/docs/rules/no-potential-component-option-typo.md
+++ b/docs/rules/no-potential-component-option-typo.md
@@ -3,23 +3,27 @@ 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
{
"vue/no-potential-component-option-typo": ["error", {
"presets": ["all"],
"custom": ["test"]
- }
- ]
+ }]
}
```
@@ -30,21 +34,21 @@ This rule disallow a potential typo in your component options
export default {
/* ✓ GOOD */
props: {
-
+
},
- /* × BAD */
+ /* ✗ BAD */
method: {
},
/* ✓ GOOD */
data: {
-
+
},
- /* × BAD */
+ /* ✗ BAD */
beforeRouteEnteR() {
},
- /* × BAD due to custom option 'test'*/
+ /* ✗ BAD due to custom option 'test' */
testt: {
}
@@ -54,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
{
@@ -72,19 +76,19 @@ export default {
```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 c0ca04175..8aa43e19b 100644
--- a/lib/rules/camelcase.js
+++ b/lib/rules/camelcase.js
@@ -5,5 +5,5 @@
const { wrapCoreRule } = require('../utils')
-// eslint-disable-next-line
-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 cfd0a4185..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
-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 4f7f32b2d..815bd23be 100644
--- a/lib/rules/comma-style.js
+++ b/lib/rules/comma-style.js
@@ -3,14 +3,17 @@
*/
'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) {
- coreHandlers.FunctionExpression(node)
+ if (baseHandlers.FunctionExpression) {
+ // @ts-expect-error -- Process params of VSlotScopeExpression as FunctionExpression.
+ baseHandlers.FunctionExpression(node)
+ }
}
}
}
diff --git a/lib/rules/comment-directive.js b/lib/rules/comment-directive.js
index 9b3842ca9..655e222bd 100644
--- a/lib/rules/comment-directive.js
+++ b/lib/rules/comment-directive.js
@@ -1,16 +1,21 @@
/**
* @author Toru Nagashima
*/
-/* eslint-disable eslint-plugin/report-message-format, consistent-docs-description */
+/* eslint-disable eslint-plugin/report-message-format */
'use strict'
-// -----------------------------------------------------------------------------
-// Helpers
-// -----------------------------------------------------------------------------
+const utils = require('../utils')
-const COMMENT_DIRECTIVE_B = /^\s*(eslint-(?:en|dis)able)(?:\s+(\S|\S[\s\S]*\S))?\s*$/
-const COMMENT_DIRECTIVE_L = /^\s*(eslint-disable(?:-next)?-line)(?:\s+(\S|\S[\s\S]*\S))?\s*$/
+/**
+ * @typedef {object} RuleAndLocation
+ * @property {string} RuleAndLocation.ruleId
+ * @property {number} RuleAndLocation.index
+ * @property {string} [RuleAndLocation.key]
+ */
+
+const COMMENT_DIRECTIVE_B = /^\s*(eslint-(?:en|dis)able)(?:\s+|$)/
+const COMMENT_DIRECTIVE_L = /^\s*(eslint-disable(?:-next)?-line)(?:\s+|$)/
/**
* Remove the ignored part from a given directive comment and trim it.
@@ -18,26 +23,40 @@ const COMMENT_DIRECTIVE_L = /^\s*(eslint-disable(?:-next)?-line)(?:\s+(\S|\S[\s\
* @returns {string} The stripped text.
*/
function stripDirectiveComment(value) {
- return value.split(/\s-{2,}\s/u)[0].trim()
+ return value.split(/\s-{2,}\s/u)[0]
}
/**
* Parse a given comment.
* @param {RegExp} pattern The RegExp pattern to parse.
* @param {string} comment The comment value to parse.
- * @returns {({type:string,rules:string[]})|null} The parsing result.
+ * @returns {({type:string,rules:RuleAndLocation[]})|null} The parsing result.
*/
function parse(pattern, comment) {
- const match = pattern.exec(stripDirectiveComment(comment))
+ const text = stripDirectiveComment(comment)
+ const match = pattern.exec(text)
if (match == null) {
return null
}
const type = match[1]
- const rules = (match[2] || '')
- .split(',')
- .map((s) => s.trim())
- .filter(Boolean)
+
+ /** @type {RuleAndLocation[]} */
+ const rules = []
+
+ const rulesRe = /([^\s,]+)[\s,]*/g
+ let startIndex = match[0].length
+ rulesRe.lastIndex = startIndex
+
+ let res
+ while ((res = rulesRe.exec(text))) {
+ const ruleId = res[1].trim()
+ rules.push({
+ ruleId,
+ index: startIndex
+ })
+ startIndex = rulesRe.lastIndex
+ }
return { type, rules }
}
@@ -46,18 +65,21 @@ function parse(pattern, comment) {
* Enable rules.
* @param {RuleContext} context The rule context.
* @param {{line:number,column:number}} loc The location information to enable.
- * @param {string} group The group to enable.
- * @param {string[]} rules The rule IDs to enable.
+ * @param { 'block' | 'line' } group The group to enable.
+ * @param {string | null} rule The rule ID to enable.
* @returns {void}
*/
-function enable(context, loc, group, rules) {
- if (rules.length === 0) {
- context.report({ loc, message: '++ {{group}}', data: { group } })
+function enable(context, loc, group, rule) {
+ if (rule) {
+ context.report({
+ loc,
+ messageId: group === 'block' ? 'enableBlockRule' : 'enableLineRule',
+ data: { rule }
+ })
} else {
context.report({
loc,
- message: '+ {{group}} {{rules}}',
- data: { group, rules: rules.join(' ') }
+ messageId: group === 'block' ? 'enableBlock' : 'enableLine'
})
}
}
@@ -66,18 +88,23 @@ function enable(context, loc, group, rules) {
* Disable rules.
* @param {RuleContext} context The rule context.
* @param {{line:number,column:number}} loc The location information to disable.
- * @param {string} group The group to disable.
- * @param {string[]} rules The rule IDs to disable.
+ * @param { 'block' | 'line' } group The group to disable.
+ * @param {string | null} rule The rule ID to disable.
+ * @param {string} key The disable directive key.
* @returns {void}
*/
-function disable(context, loc, group, rules) {
- if (rules.length === 0) {
- context.report({ loc, message: '-- {{group}}', data: { group } })
+function disable(context, loc, group, rule, key) {
+ if (rule) {
+ context.report({
+ loc,
+ messageId: group === 'block' ? 'disableBlockRule' : 'disableLineRule',
+ data: { rule, key }
+ })
} else {
context.report({
loc,
- message: '- {{group}} {{rules}}',
- data: { group, rules: rules.join(' ') }
+ messageId: group === 'block' ? 'disableBlock' : 'disableLine',
+ data: { key }
})
}
}
@@ -87,15 +114,40 @@ function disable(context, loc, group, rules) {
* If the comment is `eslint-disable` or `eslint-enable` then it reports the comment.
* @param {RuleContext} context The rule context.
* @param {Token} comment The comment token to process.
+ * @param {boolean} reportUnusedDisableDirectives To report unused eslint-disable comments.
* @returns {void}
*/
-function processBlock(context, comment) {
+function processBlock(context, comment, reportUnusedDisableDirectives) {
const parsed = parse(COMMENT_DIRECTIVE_B, comment.value)
- if (parsed != null) {
- if (parsed.type === 'eslint-disable') {
- disable(context, comment.loc.start, 'block', parsed.rules)
+ 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 {
- enable(context, comment.loc.start, 'block', parsed.rules)
+ 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)
}
}
}
@@ -105,26 +157,101 @@ function processBlock(context, comment) {
* If the comment is `eslint-disable-line` or `eslint-disable-next-line` then it reports the comment.
* @param {RuleContext} context The rule context.
* @param {Token} comment The comment token to process.
+ * @param {boolean} reportUnusedDisableDirectives To report unused eslint-disable comments.
* @returns {void}
*/
-function processLine(context, comment) {
+function processLine(context, comment, reportUnusedDisableDirectives) {
const parsed = parse(COMMENT_DIRECTIVE_L, comment.value)
if (parsed != null && comment.loc.start.line === comment.loc.end.line) {
const line =
comment.loc.start.line + (parsed.type === 'eslint-disable-line' ? 0 : 1)
const column = -1
- disable(context, { line, column }, 'line', parsed.rules)
- enable(context, { line: line + 1, column }, 'line', parsed.rules)
+ if (parsed.rules.length > 0) {
+ const rules = reportUnusedDisableDirectives
+ ? reportUnusedRules(context, comment, parsed.type, parsed.rules)
+ : parsed.rules
+ for (const rule of rules) {
+ disable(context, { line, column }, 'line', rule.ruleId, rule.key || '')
+ enable(context, { line: line + 1, column }, 'line', rule.ruleId)
+ }
+ } else {
+ const key = reportUnusedDisableDirectives
+ ? reportUnused(context, comment, parsed.type)
+ : ''
+ disable(context, { line, column }, 'line', null, key)
+ enable(context, { line: line + 1, column }, 'line', null)
+ }
}
}
+/**
+ * Reports unused disable directive.
+ * Do not check the use of directives here. Filter the directives used with postprocess.
+ * @param {RuleContext} context The rule context.
+ * @param {Token} comment The comment token to report.
+ * @param {string} kind The comment directive kind.
+ * @returns {string} The report key
+ */
+function reportUnused(context, comment, kind) {
+ const loc = comment.loc
+
+ context.report({
+ loc,
+ messageId: 'unused',
+ data: { kind }
+ })
+
+ return locToKey(loc.start)
+}
+
+/**
+ * Reports unused disable directive rules.
+ * Do not check the use of directives here. Filter the directives used with postprocess.
+ * @param {RuleContext} context The rule context.
+ * @param {Token} comment The comment token to report.
+ * @param {string} kind The comment directive kind.
+ * @param {RuleAndLocation[]} rules To report rule.
+ * @returns { { ruleId: string, key: string }[] }
+ */
+function reportUnusedRules(context, comment, kind, rules) {
+ const sourceCode = context.getSourceCode()
+ const commentStart = comment.range[0] + 4 /* '."
}
},
-
+ /** @param {RuleContext} context */
create(context) {
const option = parseOption(context.options[0])
return htmlComments.defineVisitor(
context,
context.options[1],
(comment) => {
- if (!comment.value) {
+ const { value, openDecoration, closeDecoration } = comment
+ if (!value) {
return
}
- const startLine = comment.openDecoration
- ? comment.openDecoration.loc.end.line
- : comment.value.loc.start.line
- const endLine = comment.closeDecoration
- ? comment.closeDecoration.loc.start.line
- : comment.value.loc.end.line
+
+ const startLine = openDecoration
+ ? openDecoration.loc.end.line
+ : value.loc.start.line
+ const endLine = closeDecoration
+ ? closeDecoration.loc.start.line
+ : value.loc.end.line
const newlineType =
startLine === endLine ? option.singleline : option.multiline
if (newlineType === 'ignore') {
@@ -115,92 +108,94 @@ module.exports = {
/**
* Reports the newline before the contents of a given comment if it's invalid.
- * @param {HTMLComment} comment - comment data.
+ * @param {ParsedHTMLComment} comment - comment data.
* @param {boolean} requireNewline - `true` if line breaks are required.
* @returns {void}
*/
function checkCommentOpen(comment, requireNewline) {
- const beforeToken = comment.openDecoration || comment.open
+ const { value, openDecoration, open } = comment
+ if (!value) {
+ return
+ }
+ const beforeToken = openDecoration || open
if (requireNewline) {
- if (beforeToken.loc.end.line < comment.value.loc.start.line) {
+ if (beforeToken.loc.end.line < value.loc.start.line) {
// Is valid
return
}
context.report({
loc: {
start: beforeToken.loc.end,
- end: comment.value.loc.start
+ end: value.loc.start
},
- messageId: comment.openDecoration
+ messageId: openDecoration
? 'expectedAfterExceptionBlock'
: 'expectedAfterHTMLCommentOpen',
- fix: comment.openDecoration
+ fix: openDecoration
? undefined
: (fixer) => fixer.insertTextAfter(beforeToken, '\n')
})
} else {
- if (beforeToken.loc.end.line === comment.value.loc.start.line) {
+ if (beforeToken.loc.end.line === value.loc.start.line) {
// Is valid
return
}
context.report({
loc: {
start: beforeToken.loc.end,
- end: comment.value.loc.start
+ end: value.loc.start
},
messageId: 'unexpectedAfterHTMLCommentOpen',
fix: (fixer) =>
- fixer.replaceTextRange(
- [beforeToken.range[1], comment.value.range[0]],
- ' '
- )
+ fixer.replaceTextRange([beforeToken.range[1], value.range[0]], ' ')
})
}
}
/**
* Reports the space after the contents of a given comment if it's invalid.
- * @param {HTMLComment} comment - comment data.
+ * @param {ParsedHTMLComment} comment - comment data.
* @param {boolean} requireNewline - `true` if line breaks are required.
* @returns {void}
*/
function checkCommentClose(comment, requireNewline) {
- const afterToken = comment.closeDecoration || comment.close
+ const { value, closeDecoration, close } = comment
+ if (!value) {
+ return
+ }
+ const afterToken = closeDecoration || close
if (requireNewline) {
- if (comment.value.loc.end.line < afterToken.loc.start.line) {
+ if (value.loc.end.line < afterToken.loc.start.line) {
// Is valid
return
}
context.report({
loc: {
- start: comment.value.loc.end,
+ start: value.loc.end,
end: afterToken.loc.start
},
- messageId: comment.closeDecoration
+ messageId: closeDecoration
? 'expectedBeforeExceptionBlock'
: 'expectedBeforeHTMLCommentOpen',
- fix: comment.closeDecoration
+ fix: closeDecoration
? undefined
: (fixer) => fixer.insertTextBefore(afterToken, '\n')
})
} else {
- if (comment.value.loc.end.line === afterToken.loc.start.line) {
+ if (value.loc.end.line === afterToken.loc.start.line) {
// Is valid
return
}
context.report({
loc: {
- start: comment.value.loc.end,
+ start: value.loc.end,
end: afterToken.loc.start
},
messageId: 'unexpectedBeforeHTMLCommentOpen',
fix: (fixer) =>
- fixer.replaceTextRange(
- [comment.value.range[1], afterToken.range[0]],
- ' '
- )
+ fixer.replaceTextRange([value.range[1], afterToken.range[0]], ' ')
})
}
}
diff --git a/lib/rules/html-comment-content-spacing.js b/lib/rules/html-comment-content-spacing.js
index 250fd7ae9..b220bd79c 100644
--- a/lib/rules/html-comment-content-spacing.js
+++ b/lib/rules/html-comment-content-spacing.js
@@ -4,20 +4,12 @@
*/
'use strict'
-// -----------------------------------------------------------------------------
-// Requirements
-// -----------------------------------------------------------------------------
-
const htmlComments = require('../utils/html-comments')
/**
- * @typedef { import('../utils/html-comments').HTMLComment } HTMLComment
+ * @typedef { import('../utils/html-comments').ParsedHTMLComment } ParsedHTMLComment
*/
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'layout',
@@ -54,7 +46,7 @@ module.exports = {
unexpectedBeforeHTMLCommentOpen: "Unexpected space before '-->'."
}
},
-
+ /** @param {RuleContext} context */
create(context) {
// Unless the first option is never, require a space
const requireSpace = context.options[0] !== 'never'
@@ -62,9 +54,6 @@ module.exports = {
context,
context.options[1],
(comment) => {
- if (!comment.value) {
- return
- }
checkCommentOpen(comment)
checkCommentClose(comment)
},
@@ -73,100 +62,108 @@ module.exports = {
/**
* Reports the space before the contents of a given comment if it's invalid.
- * @param {HTMLComment} comment - comment data.
+ * @param {ParsedHTMLComment} comment - comment data.
* @returns {void}
*/
function checkCommentOpen(comment) {
- const beforeToken = comment.openDecoration || comment.open
- if (beforeToken.loc.end.line !== comment.value.loc.start.line) {
+ const { value, openDecoration, open } = comment
+ if (!value) {
+ return
+ }
+ const beforeToken = openDecoration || open
+ if (beforeToken.loc.end.line !== value.loc.start.line) {
// Ignore newline
return
}
if (requireSpace) {
- if (beforeToken.range[1] < comment.value.range[0]) {
+ if (beforeToken.range[1] < value.range[0]) {
// Is valid
return
}
context.report({
loc: {
start: beforeToken.loc.end,
- end: comment.value.loc.start
+ end: value.loc.start
},
- messageId: comment.openDecoration
+ messageId: openDecoration
? 'expectedAfterExceptionBlock'
: 'expectedAfterHTMLCommentOpen',
- fix: comment.openDecoration
+ fix: openDecoration
? undefined
: (fixer) => fixer.insertTextAfter(beforeToken, ' ')
})
} else {
- if (comment.openDecoration) {
+ if (openDecoration) {
// Ignore expection block
return
}
- if (beforeToken.range[1] === comment.value.range[0]) {
+ if (beforeToken.range[1] === value.range[0]) {
// Is valid
return
}
context.report({
loc: {
start: beforeToken.loc.end,
- end: comment.value.loc.start
+ end: value.loc.start
},
messageId: 'unexpectedAfterHTMLCommentOpen',
fix: (fixer) =>
- fixer.removeRange([beforeToken.range[1], comment.value.range[0]])
+ fixer.removeRange([beforeToken.range[1], value.range[0]])
})
}
}
/**
* Reports the space after the contents of a given comment if it's invalid.
- * @param {HTMLComment} comment - comment data.
+ * @param {ParsedHTMLComment} comment - comment data.
* @returns {void}
*/
function checkCommentClose(comment) {
- const afterToken = comment.closeDecoration || comment.close
- if (comment.value.loc.end.line !== afterToken.loc.start.line) {
+ const { value, closeDecoration, close } = comment
+ if (!value) {
+ return
+ }
+ const afterToken = closeDecoration || close
+ if (value.loc.end.line !== afterToken.loc.start.line) {
// Ignore newline
return
}
if (requireSpace) {
- if (comment.value.range[1] < afterToken.range[0]) {
+ if (value.range[1] < afterToken.range[0]) {
// Is valid
return
}
context.report({
loc: {
- start: comment.value.loc.end,
+ start: value.loc.end,
end: afterToken.loc.start
},
- messageId: comment.closeDecoration
+ messageId: closeDecoration
? 'expectedBeforeExceptionBlock'
: 'expectedBeforeHTMLCommentOpen',
- fix: comment.closeDecoration
+ fix: closeDecoration
? undefined
: (fixer) => fixer.insertTextBefore(afterToken, ' ')
})
} else {
- if (comment.closeDecoration) {
+ if (closeDecoration) {
// Ignore expection block
return
}
- if (comment.value.range[1] === afterToken.range[0]) {
+ if (value.range[1] === afterToken.range[0]) {
// Is valid
return
}
context.report({
loc: {
- start: comment.value.loc.end,
+ start: value.loc.end,
end: afterToken.loc.start
},
messageId: 'unexpectedBeforeHTMLCommentOpen',
fix: (fixer) =>
- fixer.removeRange([comment.value.range[1], afterToken.range[0]])
+ fixer.removeRange([value.range[1], afterToken.range[0]])
})
}
}
diff --git a/lib/rules/html-comment-indent.js b/lib/rules/html-comment-indent.js
index c3b60bfcc..2d4f613d3 100644
--- a/lib/rules/html-comment-indent.js
+++ b/lib/rules/html-comment-indent.js
@@ -4,33 +4,22 @@
*/
'use strict'
-// -----------------------------------------------------------------------------
-// Requirements
-// -----------------------------------------------------------------------------
-
const htmlComments = require('../utils/html-comments')
-/**
- * @typedef { import('../utils/html-comments').HTMLComment } HTMLComment
- */
-
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
/**
* Normalize options.
* @param {number|"tab"|undefined} type The type of indentation.
- * @returns {Object} Normalized options.
+ * @returns { { indentChar: string, indentSize: number, indentText: string } } Normalized options.
*/
function parseOptions(type) {
const ret = {
indentChar: ' ',
- indentSize: 2
+ indentSize: 2,
+ indentText: ''
}
if (Number.isSafeInteger(type)) {
- ret.indentSize = type
+ ret.indentSize = Number(type)
} else if (type === 'tab') {
ret.indentChar = '\t'
ret.indentSize = 1
@@ -40,20 +29,23 @@ function parseOptions(type) {
return ret
}
+/**
+ * @param {string} s
+ * @param {string} [unitChar]
+ */
function toDisplay(s, unitChar) {
if (s.length === 0 && unitChar) {
return `0 ${toUnit(unitChar)}s`
}
const char = s[0]
- if (char === ' ' || char === '\t') {
- if (s.split('').every((c) => c === char)) {
- return `${s.length} ${toUnit(char)}${s.length === 1 ? '' : 's'}`
- }
+ if ((char === ' ' || char === '\t') && [...s].every((c) => c === char)) {
+ return `${s.length} ${toUnit(char)}${s.length === 1 ? '' : 's'}`
}
return JSON.stringify(s)
}
+/** @param {string} char */
function toUnit(char) {
if (char === '\t') {
return 'tab'
@@ -64,10 +56,6 @@ function toUnit(char) {
return JSON.stringify(char)
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'layout',
@@ -80,7 +68,7 @@ module.exports = {
fixable: 'whitespace',
schema: [
{
- anyOf: [{ type: 'integer', minimum: 0 }, { enum: ['tab'] }]
+ oneOf: [{ type: 'integer', minimum: 0 }, { enum: ['tab'] }]
}
],
messages: {
@@ -96,7 +84,7 @@ module.exports = {
'Expected relative indentation of {{expected}} but found {{actual}}.'
}
},
-
+ /** @param {RuleContext} context */
create(context) {
const options = parseOptions(context.options[0])
const sourceCode = context.getSourceCode()
@@ -158,7 +146,7 @@ module.exports = {
* @param {number} line The number of line.
* @param {string} actualIndentText The actual indentation text.
* @param {string} expectedIndentText The expected indentation text.
- * @returns {function} The defined function.
+ * @returns { (fixer: RuleFixer) => Fix } The defined function.
*/
function defineFix(line, actualIndentText, expectedIndentText) {
return (fixer) => {
@@ -216,8 +204,8 @@ module.exports = {
)
// validate indent charctor
- for (let i = 0; i < actualOffsetIndentText.length; ++i) {
- if (actualOffsetIndentText[i] !== options.indentChar) {
+ for (const [i, char] of [...actualOffsetIndentText].entries()) {
+ if (char !== options.indentChar) {
context.report({
loc: {
start: { line, column: baseIndentText.length + i },
@@ -226,7 +214,7 @@ module.exports = {
messageId: 'unexpectedIndentationCharacter',
data: {
expected: toUnit(options.indentChar),
- actual: toUnit(actualOffsetIndentText[i])
+ actual: toUnit(char)
},
fix: defineFix(line, actualIndentText, expectedIndentText)
})
diff --git a/lib/rules/html-end-tags.js b/lib/rules/html-end-tags.js
index b40a53ce0..a0d313cf2 100644
--- a/lib/rules/html-end-tags.js
+++ b/lib/rules/html-end-tags.js
@@ -5,28 +5,23 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce end tag style',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
+ categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/html-end-tags.html'
},
fixable: 'code',
- schema: []
+ schema: [],
+ messages: {
+ missingEndTag: "'<{{name}}>' should have end tag."
+ }
},
-
+ /** @param {RuleContext} context */
create(context) {
let hasInvalidEOF = false
@@ -47,7 +42,7 @@ module.exports = {
context.report({
node: node.startTag,
loc: node.startTag.loc,
- message: "'<{{name}}>' should have end tag.",
+ messageId: 'missingEndTag',
data: { name },
fix: (fixer) => fixer.insertTextAfter(node, `${name}>`)
})
diff --git a/lib/rules/html-indent.js b/lib/rules/html-indent.js
index d62f6f3a5..05cdace92 100644
--- a/lib/rules/html-indent.js
+++ b/lib/rules/html-indent.js
@@ -5,46 +5,55 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const indentCommon = require('../utils/indent-common')
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
+ /** @param {RuleContext} context */
create(context) {
+ const sourceCode = context.getSourceCode()
const tokenStore =
- context.parserServices.getTemplateBodyTokenStore &&
- context.parserServices.getTemplateBodyTokenStore()
+ sourceCode.parserServices.getTemplateBodyTokenStore &&
+ sourceCode.parserServices.getTemplateBodyTokenStore()
const visitor = indentCommon.defineVisitor(context, tokenStore, {
baseIndent: 1
})
return utils.defineTemplateBodyVisitor(context, visitor)
},
+ // eslint-disable-next-line eslint-plugin/prefer-message-ids
meta: {
type: 'layout',
docs: {
description: 'enforce consistent indentation in ``',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
+ categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/html-indent.html'
},
+ // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
fixable: 'whitespace',
schema: [
{
- anyOf: [{ type: 'integer', minimum: 1 }, { enum: ['tab'] }]
+ oneOf: [{ type: 'integer', minimum: 1 }, { enum: ['tab'] }]
},
{
type: 'object',
properties: {
attribute: { type: 'integer', minimum: 0 },
baseIndent: { type: 'integer', minimum: 0 },
- closeBracket: { type: 'integer', minimum: 0 },
+ closeBracket: {
+ oneOf: [
+ { type: 'integer', minimum: 0 },
+ {
+ type: 'object',
+ properties: {
+ startTag: { type: 'integer', minimum: 0 },
+ endTag: { type: 'integer', minimum: 0 },
+ selfClosingTag: { type: 'integer', minimum: 0 }
+ },
+ additionalProperties: false
+ }
+ ]
+ },
switchCase: { type: 'integer', minimum: 0 },
alignAttributesVertically: { type: 'boolean' },
ignores: {
@@ -53,7 +62,7 @@ module.exports = {
allOf: [
{ type: 'string' },
{ not: { type: 'string', pattern: ':exit$' } },
- { not: { type: 'string', pattern: '^\\s*$' } }
+ { not: { type: 'string', pattern: String.raw`^\s*$` } }
]
},
uniqueItems: true,
diff --git a/lib/rules/html-quotes.js b/lib/rules/html-quotes.js
index 05bb272d5..f4f6d1c1d 100644
--- a/lib/rules/html-quotes.js
+++ b/lib/rules/html-quotes.js
@@ -5,22 +5,14 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce quotes style of HTML attributes',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
+ categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/html-quotes.html'
},
fixable: 'code',
@@ -35,9 +27,12 @@ module.exports = {
},
additionalProperties: false
}
- ]
+ ],
+ messages: {
+ expected: 'Expected to be enclosed by {{kind}}.'
+ }
},
-
+ /** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
const double = context.options[0] !== 'single'
@@ -45,6 +40,7 @@ module.exports = {
context.options[1] && context.options[1].avoidEscape === true
const quoteChar = double ? '"' : "'"
const quoteName = double ? 'double quotes' : 'single quotes'
+ /** @type {boolean} */
let hasInvalidEOF
return utils.defineTemplateBodyVisitor(
@@ -55,6 +51,11 @@ module.exports = {
return
}
+ if (utils.isVBindSameNameShorthand(node)) {
+ // v-bind same-name shorthand (Vue 3.4+)
+ return
+ }
+
const text = sourceCode.getText(node.value)
const firstChar = text[0]
@@ -70,17 +71,17 @@ module.exports = {
context.report({
node: node.value,
loc: node.value.loc,
- message: 'Expected to be enclosed by {{kind}}.',
+ messageId: 'expected',
data: { kind: quoteName },
fix(fixer) {
const contentText = quoted ? text.slice(1, -1) : text
- const fixToDouble =
- avoidEscape && !quoted && contentText.includes(quoteChar)
- ? double
- ? contentText.includes("'")
- : !contentText.includes('"')
- : double
+ let fixToDouble = double
+ if (avoidEscape && !quoted && contentText.includes(quoteChar)) {
+ fixToDouble = double
+ ? contentText.includes("'")
+ : !contentText.includes('"')
+ }
const quotePattern = fixToDouble ? /"/g : /'/g
const quoteEscaped = fixToDouble ? '"' : '''
diff --git a/lib/rules/html-self-closing.js b/lib/rules/html-self-closing.js
index f2fa0b277..c31c9ab70 100644
--- a/lib/rules/html-self-closing.js
+++ b/lib/rules/html-self-closing.js
@@ -5,67 +5,68 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
/**
* These strings wil be displayed in error messages.
*/
-const ELEMENT_TYPE = Object.freeze({
+const ELEMENT_TYPE_MESSAGES = Object.freeze({
NORMAL: 'HTML elements',
VOID: 'HTML void elements',
COMPONENT: 'Vue.js custom components',
SVG: 'SVG elements',
- MATH: 'MathML elements'
+ MATH: 'MathML elements',
+ UNKNOWN: 'unknown elements'
})
+/**
+ * @typedef {object} Options
+ * @property {'always' | 'never'} NORMAL
+ * @property {'always' | 'never'} VOID
+ * @property {'always' | 'never'} COMPONENT
+ * @property {'always' | 'never'} SVG
+ * @property {'always' | 'never'} MATH
+ * @property {null} UNKNOWN
+ */
+
/**
* Normalize the given options.
- * @param {Object|undefined} options The raw options object.
- * @returns {Object} Normalized options.
+ * @param {any} options The raw options object.
+ * @returns {Options} Normalized options.
*/
function parseOptions(options) {
return {
- [ELEMENT_TYPE.NORMAL]:
- (options && options.html && options.html.normal) || 'always',
- [ELEMENT_TYPE.VOID]:
- (options && options.html && options.html.void) || 'never',
- [ELEMENT_TYPE.COMPONENT]:
- (options && options.html && options.html.component) || 'always',
- [ELEMENT_TYPE.SVG]: (options && options.svg) || 'always',
- [ELEMENT_TYPE.MATH]: (options && options.math) || 'always'
+ NORMAL: (options && options.html && options.html.normal) || 'always',
+ VOID: (options && options.html && options.html.void) || 'never',
+ COMPONENT: (options && options.html && options.html.component) || 'always',
+ SVG: (options && options.svg) || 'always',
+ MATH: (options && options.math) || 'always',
+ UNKNOWN: null
}
}
/**
* Get the elementType of the given element.
* @param {VElement} node The element node to get.
- * @returns {string} The elementType of the element.
+ * @returns {keyof Options} The elementType of the element.
*/
function getElementType(node) {
if (utils.isCustomComponent(node)) {
- return ELEMENT_TYPE.COMPONENT
+ return 'COMPONENT'
}
if (utils.isHtmlElementNode(node)) {
if (utils.isHtmlVoidElementName(node.name)) {
- return ELEMENT_TYPE.VOID
+ return 'VOID'
}
- return ELEMENT_TYPE.NORMAL
+ return 'NORMAL'
}
if (utils.isSvgElementNode(node)) {
- return ELEMENT_TYPE.SVG
+ return 'SVG'
}
- if (utils.isMathMLElementNode(node)) {
- return ELEMENT_TYPE.MATH
+ if (utils.isMathElementNode(node)) {
+ return 'MATH'
}
- return 'unknown elements'
+ return 'UNKNOWN'
}
/**
@@ -77,21 +78,17 @@ function getElementType(node) {
*/
function isEmpty(node, sourceCode) {
const start = node.startTag.range[1]
- const end = node.endTag != null ? node.endTag.range[0] : node.range[1]
+ const end = node.endTag == null ? node.range[1] : node.endTag.range[0]
return sourceCode.text.slice(start, end).trim() === ''
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce self-closing style',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
+ categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/html-self-closing.html'
},
fixable: 'code',
@@ -122,9 +119,15 @@ module.exports = {
}
],
maxItems: 1
+ },
+ messages: {
+ requireSelfClosing:
+ 'Require self-closing on {{elementType}} (<{{name}}>).',
+ disallowSelfClosing:
+ 'Disallow self-closing on {{elementType}} (<{{name}}/>).'
}
},
-
+ /** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
const options = parseOptions(context.options[0])
@@ -134,7 +137,7 @@ module.exports = {
context,
{
VElement(node) {
- if (hasInvalidEOF) {
+ if (hasInvalidEOF || node.parent.type === 'VDocumentFragment') {
return
}
@@ -147,12 +150,15 @@ module.exports = {
isEmpty(node, sourceCode)
) {
context.report({
- node,
- loc: node.loc,
- message: 'Require self-closing on {{elementType}} (<{{name}}>).',
- data: { elementType, name: node.rawName },
- fix: (fixer) => {
- const tokens = context.parserServices.getTemplateBodyTokenStore()
+ node: node.endTag || node,
+ messageId: 'requireSelfClosing',
+ data: {
+ elementType: ELEMENT_TYPE_MESSAGES[elementType],
+ name: node.rawName
+ },
+ fix(fixer) {
+ const tokens =
+ sourceCode.parserServices.getTemplateBodyTokenStore()
const close = tokens.getLastToken(node.startTag)
if (close.type !== 'HTMLTagClose') {
return null
@@ -168,17 +174,26 @@ module.exports = {
if (mode === 'never' && node.startTag.selfClosing) {
context.report({
node,
- loc: node.loc,
- message:
- 'Disallow self-closing on {{elementType}} (<{{name}}/>).',
- data: { elementType, name: node.rawName },
- fix: (fixer) => {
- const tokens = context.parserServices.getTemplateBodyTokenStore()
+ loc: {
+ start: {
+ line: node.loc.end.line,
+ column: node.loc.end.column - 2
+ },
+ end: node.loc.end
+ },
+ messageId: 'disallowSelfClosing',
+ data: {
+ elementType: ELEMENT_TYPE_MESSAGES[elementType],
+ name: node.rawName
+ },
+ fix(fixer) {
+ const tokens =
+ sourceCode.parserServices.getTemplateBodyTokenStore()
const close = tokens.getLastToken(node.startTag)
if (close.type !== 'HTMLSelfClosingTagClose') {
return null
}
- if (elementType === ELEMENT_TYPE.VOID) {
+ if (elementType === 'VOID') {
return fixer.replaceText(close, '>')
}
// If only `close` is targeted for replacement, it conflicts with `component-name-in-template-casing`,
diff --git a/lib/rules/jsx-uses-vars.js b/lib/rules/jsx-uses-vars.js
index 2d335dde1..d9f46972a 100644
--- a/lib/rules/jsx-uses-vars.js
+++ b/lib/rules/jsx-uses-vars.js
@@ -30,32 +30,34 @@ SOFTWARE.
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+const utils = require('../utils')
module.exports = {
+ // eslint-disable-next-line eslint-plugin/prefer-message-ids
meta: {
type: 'problem',
docs: {
- description: 'prevent variables used in JSX to be marked as unused', // eslint-disable-line consistent-docs-description
+ description: 'prevent variables used in JSX to be marked as unused', // eslint-disable-line eslint-plugin/require-meta-docs-description
categories: ['base'],
url: 'https://eslint.vuejs.org/rules/jsx-uses-vars.html'
},
schema: []
},
-
+ /**
+ * @param {RuleContext} context - The rule context.
+ * @returns {RuleListener} AST event handlers.
+ */
create(context) {
return {
JSXOpeningElement(node) {
let name
- if (node.name.name) {
+ if (node.name.type === 'JSXIdentifier') {
//
name = node.name.name
- } else if (node.name.object) {
+ } else if (node.name.type === 'JSXMemberExpression') {
//
let parent = node.name.object
- while (parent.object) {
+ while (parent.type === 'JSXMemberExpression') {
parent = parent.object
}
name = parent.name
@@ -63,7 +65,7 @@ module.exports = {
return
}
- context.markVariableAsUsed(name)
+ utils.markVariableAsUsed(context, name, node)
}
}
}
diff --git a/lib/rules/key-spacing.js b/lib/rules/key-spacing.js
index 27e624517..a70a81420 100644
--- a/lib/rules/key-spacing.js
+++ b/lib/rules/key-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/key-spacing'), {
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapStylisticOrCoreRule('key-spacing', {
skipDynamicArguments: true
})
diff --git a/lib/rules/keyword-spacing.js b/lib/rules/keyword-spacing.js
index 0b0da6277..9cb4fd3fc 100644
--- a/lib/rules/keyword-spacing.js
+++ b/lib/rules/keyword-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/keyword-spacing'), {
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapStylisticOrCoreRule('keyword-spacing', {
skipDynamicArguments: true
})
diff --git a/lib/rules/match-component-file-name.js b/lib/rules/match-component-file-name.js
index 45d603727..181d88f1a 100644
--- a/lib/rules/match-component-file-name.js
+++ b/lib/rules/match-component-file-name.js
@@ -4,17 +4,22 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
const casing = require('../utils/casing')
const path = require('path')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+/**
+ * @param {Expression | SpreadElement} node
+ * @returns {node is (Literal | TemplateLiteral)}
+ */
+function canVerify(node) {
+ return (
+ node.type === 'Literal' ||
+ (node.type === 'TemplateLiteral' &&
+ node.expressions.length === 0 &&
+ node.quasis.length === 1)
+ )
+}
module.exports = {
meta: {
@@ -25,6 +30,7 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/match-component-file-name.html'
},
fixable: null,
+ hasSuggestions: true,
schema: [
{
type: 'object',
@@ -43,9 +49,13 @@ module.exports = {
},
additionalProperties: false
}
- ]
+ ],
+ messages: {
+ shouldMatchFileName:
+ 'Component name `{{name}}` should match file name `{{filename}}`.'
+ }
},
-
+ /** @param {RuleContext} context */
create(context) {
const options = context.options[0]
const shouldMatchCase = (options && options.shouldMatchCase) || false
@@ -57,6 +67,7 @@ module.exports = {
const extension = path.extname(context.getFilename())
const filename = path.basename(context.getFilename(), extension)
+ /** @type {Rule.ReportDescriptor[]} */
const errors = []
let componentCount = 0
@@ -64,10 +75,10 @@ module.exports = {
return {}
}
- // ----------------------------------------------------------------------
- // Private
- // ----------------------------------------------------------------------
-
+ /**
+ * @param {string} name
+ * @param {string} filename
+ */
function compareNames(name, filename) {
if (shouldMatchCase) {
return name === filename
@@ -79,36 +90,38 @@ module.exports = {
)
}
+ /**
+ * @param {Literal | TemplateLiteral} node
+ */
function verifyName(node) {
let name
if (node.type === 'TemplateLiteral') {
const quasis = node.quasis[0]
name = quasis.value.cooked
} else {
- name = node.value
+ name = `${node.value}`
}
if (!compareNames(name, filename)) {
errors.push({
node,
- message:
- 'Component name `{{name}}` should match file name `{{filename}}`.',
- data: { filename, name }
+ messageId: 'shouldMatchFileName',
+ data: { filename, name },
+ suggest: [
+ {
+ desc: 'Rename component to match file name.',
+ fix(fixer) {
+ const quote =
+ node.type === 'TemplateLiteral' ? '`' : node.raw[0]
+ return fixer.replaceText(node, `${quote}${filename}${quote}`)
+ }
+ }
+ ]
})
}
}
- function canVerify(node) {
- return (
- node.type === 'Literal' ||
- (node.type === 'TemplateLiteral' &&
- node.expressions.length === 0 &&
- node.quasis.length === 1)
- )
- }
-
- return Object.assign(
- {},
+ return utils.compositingVisitors(
utils.executeOnCallVueComponent(context, (node) => {
if (node.arguments.length === 2) {
const argument = node.arguments[0]
@@ -119,23 +132,31 @@ module.exports = {
}
}),
utils.executeOnVue(context, (object) => {
- const node = object.properties.find(
- (item) =>
- item.type === 'Property' &&
- item.key.name === 'name' &&
- canVerify(item.value)
- )
+ const node = utils.findProperty(object, 'name')
componentCount++
if (!node) return
+ if (!canVerify(node.value)) return
verifyName(node.value)
}),
+ utils.defineScriptSetupVisitor(context, {
+ onDefineOptionsEnter(node) {
+ componentCount++
+ if (node.arguments.length === 0) return
+ const define = node.arguments[0]
+ if (define.type !== 'ObjectExpression') return
+ const nameNode = utils.findProperty(define, 'name')
+ if (!nameNode) return
+ if (!canVerify(nameNode.value)) return
+ verifyName(nameNode.value)
+ }
+ }),
{
'Program:exit'() {
if (componentCount > 1) return
- errors.forEach((error) => context.report(error))
+ for (const error of errors) context.report(error)
}
}
)
diff --git a/lib/rules/match-component-import-name.js b/lib/rules/match-component-import-name.js
new file mode 100644
index 000000000..344661005
--- /dev/null
+++ b/lib/rules/match-component-import-name.js
@@ -0,0 +1,73 @@
+/**
+ * @author Doug Wade
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+/**
+ * @param {Identifier} identifier
+ * @return {Array}
+ */
+function getExpectedNames(identifier) {
+ return [casing.pascalCase(identifier.name), casing.kebabCase(identifier.name)]
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'require the registered component name to match the imported component name',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/match-component-import-name.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ unexpected:
+ 'Component alias {{importedName}} should be one of: {{expectedName}}.'
+ }
+ },
+ /**
+ * @param {RuleContext} context
+ * @returns {RuleListener}
+ */
+ create(context) {
+ return utils.executeOnVueComponent(context, (obj) => {
+ const components = utils.findProperty(obj, 'components')
+ if (
+ !components ||
+ !components.value ||
+ components.value.type !== 'ObjectExpression'
+ ) {
+ return
+ }
+
+ for (const property of components.value.properties) {
+ if (
+ property.type === 'SpreadElement' ||
+ property.value.type !== 'Identifier' ||
+ property.computed === true
+ ) {
+ continue
+ }
+
+ const importedName = utils.getStaticPropertyName(property) || ''
+ const expectedNames = getExpectedNames(property.value)
+ if (!expectedNames.includes(importedName)) {
+ context.report({
+ node: property,
+ messageId: 'unexpected',
+ data: {
+ importedName,
+ expectedName: expectedNames.join(', ')
+ }
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js
index 29b6a4e6b..8a9de1349 100644
--- a/lib/rules/max-attributes-per-line.js
+++ b/lib/rules/max-attributes-per-line.js
@@ -4,26 +4,77 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
const utils = require('../utils')
+/**
+ * @param {any} options
+ */
+function parseOptions(options) {
+ const defaults = {
+ singleline: 1,
+ multiline: 1
+ }
+
+ if (options) {
+ if (typeof options.singleline === 'number') {
+ defaults.singleline = options.singleline
+ } else if (
+ typeof options.singleline === 'object' &&
+ typeof options.singleline.max === 'number'
+ ) {
+ defaults.singleline = options.singleline.max
+ }
+
+ if (options.multiline) {
+ if (typeof options.multiline === 'number') {
+ defaults.multiline = options.multiline
+ } else if (
+ typeof options.multiline === 'object' &&
+ typeof options.multiline.max === 'number'
+ ) {
+ defaults.multiline = options.multiline.max
+ }
+ }
+ }
+
+ return defaults
+}
+
+/**
+ * @param {(VDirective | VAttribute)[]} attributes
+ */
+function groupAttrsByLine(attributes) {
+ const propsPerLine = [[attributes[0]]]
+
+ for (let index = 1; index < attributes.length; index++) {
+ const previous = attributes[index - 1]
+ const current = attributes[index]
+
+ if (previous.loc.end.line === current.loc.start.line) {
+ propsPerLine[propsPerLine.length - 1].push(current)
+ } else {
+ propsPerLine.push([current])
+ }
+ }
+
+ return propsPerLine
+}
+
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce the maximum number of attributes per line',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
+ categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/max-attributes-per-line.html'
},
- fixable: 'whitespace', // or "code" or "whitespace"
+ fixable: 'whitespace',
schema: [
{
type: 'object',
properties: {
singleline: {
- anyOf: [
+ oneOf: [
{
type: 'number',
minimum: 1
@@ -41,7 +92,7 @@ module.exports = {
]
},
multiline: {
- anyOf: [
+ oneOf: [
{
type: 'number',
minimum: 1
@@ -52,29 +103,29 @@ module.exports = {
max: {
type: 'number',
minimum: 1
- },
- allowFirstLine: {
- type: 'boolean'
}
},
additionalProperties: false
}
]
}
- }
+ },
+ additionalProperties: false
}
- ]
+ ],
+ messages: {
+ shouldBeOnNewLine: "'{{name}}' should be on a new line."
+ }
},
-
+ /** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
const configuration = parseOptions(context.options[0])
const multilineMaximum = configuration.multiline
const singlelinemMaximum = configuration.singleline
- const canHaveFirstLine = configuration.allowFirstLine
const template =
- context.parserServices.getTemplateBodyTokenStore &&
- context.parserServices.getTemplateBodyTokenStore()
+ sourceCode.parserServices.getTemplateBodyTokenStore &&
+ sourceCode.parserServices.getTemplateBodyTokenStore()
return utils.defineTemplateBodyVisitor(context, {
VStartTag(node) {
@@ -90,94 +141,43 @@ module.exports = {
}
if (!utils.isSingleLine(node)) {
- if (
- !canHaveFirstLine &&
- node.attributes[0].loc.start.line === node.loc.start.line
- ) {
- showErrors([node.attributes[0]])
- }
-
- groupAttrsByLine(node.attributes)
- .filter((attrs) => attrs.length > multilineMaximum)
- .forEach((attrs) => showErrors(attrs.splice(multilineMaximum)))
- }
- }
- })
-
- // ----------------------------------------------------------------------
- // Helpers
- // ----------------------------------------------------------------------
- function parseOptions(options) {
- const defaults = {
- singleline: 1,
- multiline: 1,
- allowFirstLine: false
- }
-
- if (options) {
- if (typeof options.singleline === 'number') {
- defaults.singleline = options.singleline
- } else if (options.singleline && options.singleline.max) {
- defaults.singleline = options.singleline.max
- }
-
- if (options.multiline) {
- if (typeof options.multiline === 'number') {
- defaults.multiline = options.multiline
- } else if (typeof options.multiline === 'object') {
- if (options.multiline.max) {
- defaults.multiline = options.multiline.max
- }
-
- if (options.multiline.allowFirstLine) {
- defaults.allowFirstLine = options.multiline.allowFirstLine
+ for (const attrs of groupAttrsByLine(node.attributes)) {
+ if (attrs.length > multilineMaximum) {
+ showErrors(attrs.splice(multilineMaximum))
}
}
}
}
+ })
- return defaults
- }
-
+ /**
+ * @param {(VDirective | VAttribute)[]} attributes
+ */
function showErrors(attributes) {
- attributes.forEach((prop, i) => {
- const fix = (fixer) => {
- if (i !== 0) return null
-
- // Find the closest token before the current prop
- // that is not a white space
- const prevToken = template.getTokenBefore(prop, {
- filter: (token) => token.type !== 'HTMLWhitespace'
- })
-
- const range = [prevToken.range[1], prop.range[0]]
-
- return fixer.replaceTextRange(range, '\n')
- }
-
+ for (const [i, prop] of attributes.entries()) {
context.report({
node: prop,
loc: prop.loc,
- message: "'{{name}}' should be on a new line.",
+ messageId: 'shouldBeOnNewLine',
data: { name: sourceCode.getText(prop.key) },
- fix
- })
- })
- }
+ fix(fixer) {
+ if (i !== 0) return null
- function groupAttrsByLine(attributes) {
- const propsPerLine = [[attributes[0]]]
+ // Find the closest token before the current prop
+ // that is not a white space
+ const prevToken = /** @type {Token} */ (
+ template.getTokenBefore(prop, {
+ filter: (token) => token.type !== 'HTMLWhitespace'
+ })
+ )
- attributes.reduce((previous, current) => {
- if (previous.loc.end.line === current.loc.start.line) {
- propsPerLine[propsPerLine.length - 1].push(current)
- } else {
- propsPerLine.push([current])
- }
- return current
- })
+ /** @type {Range} */
+ const range = [prevToken.range[1], prop.range[0]]
- return propsPerLine
+ return fixer.replaceTextRange(range, '\n')
+ }
+ })
+ }
}
}
}
diff --git a/lib/rules/max-len.js b/lib/rules/max-len.js
index 5e8998233..b5dce5eb1 100644
--- a/lib/rules/max-len.js
+++ b/lib/rules/max-len.js
@@ -4,16 +4,8 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Constants
-// ------------------------------------------------------------------------------
-
const OPTIONS_SCHEMA = {
type: 'object',
properties: {
@@ -65,7 +57,7 @@ const OPTIONS_SCHEMA = {
}
const OPTIONS_OR_INTEGER_SCHEMA = {
- anyOf: [
+ oneOf: [
OPTIONS_SCHEMA,
{
type: 'integer',
@@ -74,29 +66,28 @@ const OPTIONS_OR_INTEGER_SCHEMA = {
]
}
-// --------------------------------------------------------------------------
-// Helpers
-// --------------------------------------------------------------------------
-
/**
* Computes the length of a line that may contain tabs. The width of each
* tab will be the number of spaces to the next tab stop.
* @param {string} line The line.
- * @param {int} tabWidth The width of each tab stop in spaces.
- * @returns {int} The computed line length.
+ * @param {number} tabWidth The width of each tab stop in spaces.
+ * @returns {number} The computed line length.
* @private
*/
function computeLineLength(line, tabWidth) {
let extraCharacterCount = 0
- line.replace(/\t/gu, (match, offset) => {
+ const re = /\t/gu
+ let ret
+ while ((ret = re.exec(line))) {
+ const offset = ret.index
const totalOffset = offset + extraCharacterCount
const previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0
const spaceCount = tabWidth - previousTabStopOffset
-
extraCharacterCount += spaceCount - 1 // -1 for the replaced tab
- })
- return Array.from(line).length + extraCharacterCount
+ }
+
+ return [...line].length + extraCharacterCount
}
/**
@@ -104,16 +95,16 @@ function computeLineLength(line, tabWidth) {
* extends to or past the end of the current line.
* @param {string} line The source line we want to check for a trailing comment on
* @param {number} lineNumber The one-indexed line number for line
- * @param {ASTNode} comment The comment to inspect
- * @returns {boolean} If the comment is trailing on the given line
+ * @param {Token | null} comment The comment to inspect
+ * @returns {comment is Token} If the comment is trailing on the given line
*/
function isTrailingComment(line, lineNumber, comment) {
- return (
+ return Boolean(
comment &&
- comment.loc.start.line === lineNumber &&
- lineNumber <= comment.loc.end.line &&
- (comment.loc.end.line > lineNumber ||
- comment.loc.end.column === line.length)
+ comment.loc.start.line === lineNumber &&
+ lineNumber <= comment.loc.end.line &&
+ (comment.loc.end.line > lineNumber ||
+ comment.loc.end.column === line.length)
)
}
@@ -121,10 +112,13 @@ function isTrailingComment(line, lineNumber, comment) {
* Tells if a comment encompasses the entire line.
* @param {string} line The source line with a trailing comment
* @param {number} lineNumber The one-indexed line number this is on
- * @param {ASTNode} comment The comment to remove
+ * @param {Token | null} comment The comment to remove
* @returns {boolean} If the comment covers the entire line
*/
function isFullLineComment(line, lineNumber, comment) {
+ if (!comment) {
+ return false
+ }
const start = comment.loc.start
const end = comment.loc.end
const isFirstTokenOnLine = !line.slice(0, comment.loc.start.column).trim()
@@ -142,7 +136,7 @@ function isFullLineComment(line, lineNumber, comment) {
* Gets the line after the comment and any remaining trailing whitespace is
* stripped.
* @param {string} line The source line with a trailing comment
- * @param {ASTNode} comment The comment to remove
+ * @param {Token} comment The comment to remove
* @returns {string} Line without comment and trailing whitepace
*/
function stripTrailingComment(line, comment) {
@@ -151,48 +145,38 @@ function stripTrailingComment(line, comment) {
}
/**
- * Ensure that an array exists at [key] on `object`, and add `value` to it.
+ * Group AST nodes by line number, both start and end.
*
- * @param {Object} object the object to mutate
- * @param {string} key the object's key
- * @param {*} value the value to add
- * @returns {void}
+ * @param {Token[]} nodes the AST nodes in question
+ * @returns { { [key: number]: Token[] } } the grouped nodes
* @private
*/
-function ensureArrayAndPush(object, key, value) {
- if (!Array.isArray(object[key])) {
- object[key] = []
- }
- object[key].push(value)
-}
-
-/**
- * A reducer to group an AST node by line number, both start and end.
- *
- * @param {Object} acc the accumulator
- * @param {ASTNode} node the AST node in question
- * @returns {Object} the modified accumulator
- * @private
- */
-function groupByLineNumber(acc, node) {
- for (let i = node.loc.start.line; i <= node.loc.end.line; ++i) {
- ensureArrayAndPush(acc, i, node)
+function groupByLineNumber(nodes) {
+ /** @type { { [key: number]: Token[] } } */
+ const grouped = {}
+ for (const node of nodes) {
+ for (let i = node.loc.start.line; i <= node.loc.end.line; ++i) {
+ if (!Array.isArray(grouped[i])) {
+ grouped[i] = []
+ }
+ grouped[i].push(node)
+ }
}
- return acc
+ return grouped
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'layout',
docs: {
- description: 'enforce a maximum line length',
+ description: 'enforce a maximum line length in `.vue` files',
categories: undefined,
- url: 'https://eslint.vuejs.org/rules/max-len.html'
+ url: 'https://eslint.vuejs.org/rules/max-len.html',
+ extensionSource: {
+ url: 'https://eslint.org/docs/rules/max-len',
+ name: 'ESLint core'
+ }
},
schema: [
@@ -201,13 +185,15 @@ module.exports = {
OPTIONS_SCHEMA
],
messages: {
- max:
- 'This line has a length of {{lineLength}}. Maximum allowed is {{maxLength}}.',
+ max: 'This line has a length of {{lineLength}}. Maximum allowed is {{maxLength}}.',
maxComment:
'This line has a comment length of {{lineLength}}. Maximum allowed is {{maxCommentLength}}.'
}
},
-
+ /**
+ * @param {RuleContext} context - The rule context.
+ * @returns {RuleListener} AST event handlers.
+ */
create(context) {
/*
* Inspired by http://tools.ietf.org/html/rfc3986#appendix-B, however:
@@ -220,8 +206,11 @@ module.exports = {
const URL_REGEXP = /[^:/?#]:\/\/[^?#]/u
const sourceCode = context.getSourceCode()
+ /** @type {Token[]} */
const tokens = []
+ /** @type {(HTMLComment | HTMLBogusComment | Comment)[]} */
const comments = []
+ /** @type {VLiteral[]} */
const htmlAttributeValues = []
// The options object must be the last option specified…
@@ -239,9 +228,11 @@ module.exports = {
if (typeof context.options[1] === 'number') {
options.tabWidth = context.options[1]
}
-
+ /** @type {number} */
const scriptMaxLength = typeof options.code === 'number' ? options.code : 80
+ /** @type {number} */
const tabWidth = typeof options.tabWidth === 'number' ? options.tabWidth : 2 // default value of `vue/html-indent`
+ /** @type {number} */
const templateMaxLength =
typeof options.template === 'number' ? options.template : scriptMaxLength
const ignoreComments = !!options.ignoreComments
@@ -253,21 +244,19 @@ module.exports = {
const ignoreUrls = !!options.ignoreUrls
const ignoreHTMLAttributeValues = !!options.ignoreHTMLAttributeValues
const ignoreHTMLTextContents = !!options.ignoreHTMLTextContents
+ /** @type {number} */
const maxCommentLength = options.comments
+ /** @type {RegExp} */
let ignorePattern = options.ignorePattern || null
if (ignorePattern) {
ignorePattern = new RegExp(ignorePattern, 'u')
}
- // --------------------------------------------------------------------------
- // Helpers
- // --------------------------------------------------------------------------
-
/**
* Retrieves an array containing all strings (" or ') in the source code.
*
- * @returns {ASTNode[]} An array of string nodes.
+ * @returns {Token[]} An array of string nodes.
*/
function getAllStrings() {
return tokens.filter(
@@ -282,7 +271,7 @@ module.exports = {
/**
* Retrieves an array containing all template literals in the source code.
*
- * @returns {ASTNode[]} An array of template literal nodes.
+ * @returns {Token[]} An array of template literal nodes.
*/
function getAllTemplateLiterals() {
return tokens.filter((token) => token.type === 'Template')
@@ -291,7 +280,7 @@ module.exports = {
/**
* Retrieves an array containing all RegExp literals in the source code.
*
- * @returns {ASTNode[]} An array of RegExp literal nodes.
+ * @returns {Token[]} An array of RegExp literal nodes.
*/
function getAllRegExpLiterals() {
return tokens.filter((token) => token.type === 'RegularExpression')
@@ -300,7 +289,7 @@ module.exports = {
/**
* Retrieves an array containing all HTML texts in the source code.
*
- * @returns {ASTNode[]} An array of HTML text nodes.
+ * @returns {Token[]} An array of HTML text nodes.
*/
function getAllHTMLTextContents() {
return tokens.filter((token) => token.type === 'HTMLText')
@@ -308,7 +297,7 @@ module.exports = {
/**
* Check the program for max length
- * @param {ASTNode} node Node to examine
+ * @param {Program} node Node to examine
* @returns {void}
* @private
*/
@@ -320,8 +309,8 @@ module.exports = {
const scriptTokens = sourceCode.ast.tokens
const scriptComments = sourceCode.getAllComments()
- if (context.parserServices.getTemplateBodyTokenStore && templateBody) {
- const tokenStore = context.parserServices.getTemplateBodyTokenStore()
+ if (sourceCode.parserServices.getTemplateBodyTokenStore && templateBody) {
+ const tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore()
const templateTokens = tokenStore.getTokens(templateBody, {
includeComments: true
@@ -349,26 +338,26 @@ module.exports = {
}
}
+ /** @type {Range | undefined} */
let scriptLinesRange
- if (scriptTokens.length) {
- if (scriptComments.length) {
- scriptLinesRange = [
- Math.min(
- scriptTokens[0].loc.start.line,
- scriptComments[0].loc.start.line
- ),
- Math.max(
- scriptTokens[scriptTokens.length - 1].loc.end.line,
- scriptComments[scriptComments.length - 1].loc.end.line
- )
- ]
- } else {
- scriptLinesRange = [
- scriptTokens[0].loc.start.line,
- scriptTokens[scriptTokens.length - 1].loc.end.line
- ]
- }
- } else if (scriptComments.length) {
+ if (scriptTokens.length > 0) {
+ scriptLinesRange =
+ scriptComments.length > 0
+ ? [
+ Math.min(
+ scriptTokens[0].loc.start.line,
+ scriptComments[0].loc.start.line
+ ),
+ Math.max(
+ scriptTokens[scriptTokens.length - 1].loc.end.line,
+ scriptComments[scriptComments.length - 1].loc.end.line
+ )
+ ]
+ : [
+ scriptTokens[0].loc.start.line,
+ scriptTokens[scriptTokens.length - 1].loc.end.line
+ ]
+ } else if (scriptComments.length > 0) {
scriptLinesRange = [
scriptComments[0].loc.start.line,
scriptComments[scriptComments.length - 1].loc.end.line
@@ -383,31 +372,22 @@ module.exports = {
const lines = sourceCode.lines
const strings = getAllStrings()
- const stringsByLine = strings.reduce(groupByLineNumber, {})
+ const stringsByLine = groupByLineNumber(strings)
const templateLiterals = getAllTemplateLiterals()
- const templateLiteralsByLine = templateLiterals.reduce(
- groupByLineNumber,
- {}
- )
+ const templateLiteralsByLine = groupByLineNumber(templateLiterals)
const regExpLiterals = getAllRegExpLiterals()
- const regExpLiteralsByLine = regExpLiterals.reduce(groupByLineNumber, {})
+ const regExpLiteralsByLine = groupByLineNumber(regExpLiterals)
- const htmlAttributeValuesByLine = htmlAttributeValues.reduce(
- groupByLineNumber,
- {}
- )
+ const htmlAttributeValuesByLine = groupByLineNumber(htmlAttributeValues)
const htmlTextContents = getAllHTMLTextContents()
- const htmlTextContentsByLine = htmlTextContents.reduce(
- groupByLineNumber,
- {}
- )
+ const htmlTextContentsByLine = groupByLineNumber(htmlTextContents)
- const commentsByLine = comments.reduce(groupByLineNumber, {})
+ const commentsByLine = groupByLineNumber(comments)
- lines.forEach((line, i) => {
+ for (const [i, line] of lines.entries()) {
// i is zero-indexed, line numbers are one-indexed
const lineNumber = i + 1
@@ -422,14 +402,12 @@ module.exports = {
// check if line is inside a script or template.
if (!inScript && !inTemplate) {
// out of range.
- return
+ continue
}
- const maxLength =
- inScript && inTemplate
- ? Math.max(scriptMaxLength, templateMaxLength)
- : inScript
- ? scriptMaxLength
- : templateMaxLength
+ const maxLength = Math.max(
+ inScript ? scriptMaxLength : 0,
+ inTemplate ? templateMaxLength : 0
+ )
if (
(ignoreStrings && stringsByLine[lineNumber]) ||
@@ -440,7 +418,7 @@ module.exports = {
(ignoreHTMLTextContents && htmlTextContentsByLine[lineNumber])
) {
// ignore this line
- return
+ continue
}
/*
@@ -456,7 +434,7 @@ module.exports = {
if (commentsByLine[lineNumber]) {
const commentList = [...commentsByLine[lineNumber]]
- let comment = commentList.pop()
+ let comment = commentList.pop() || null
if (isFullLineComment(line, lineNumber, comment)) {
lineIsComment = true
@@ -468,7 +446,7 @@ module.exports = {
textToMeasure = stripTrailingComment(line, comment)
// ignore multiple trailing comments in the same line
- comment = commentList.pop()
+ comment = commentList.pop() || null
while (isTrailingComment(textToMeasure, lineNumber, comment)) {
textToMeasure = stripTrailingComment(textToMeasure, comment)
@@ -485,14 +463,14 @@ module.exports = {
(ignoreUrls && URL_REGEXP.test(textToMeasure))
) {
// ignore this line
- return
+ continue
}
const lineLength = computeLineLength(textToMeasure, tabWidth)
const commentLengthApplies = lineIsComment && maxCommentLength
if (lineIsComment && ignoreComments) {
- return
+ continue
}
if (commentLengthApplies) {
@@ -518,26 +496,21 @@ module.exports = {
}
})
}
- })
- }
-
- // --------------------------------------------------------------------------
- // Public API
- // --------------------------------------------------------------------------
-
- const bodyVisitor = utils.defineTemplateBodyVisitor(context, {
- 'VAttribute[directive=false] > VLiteral'(node) {
- htmlAttributeValues.push(node)
}
- })
+ }
- return Object.assign({}, bodyVisitor, {
- 'Program:exit'(node) {
- if (bodyVisitor['Program:exit']) {
- bodyVisitor['Program:exit'](node)
+ return utils.compositingVisitors(
+ utils.defineTemplateBodyVisitor(context, {
+ /** @param {VLiteral} node */
+ 'VAttribute[directive=false] > VLiteral'(node) {
+ htmlAttributeValues.push(node)
+ }
+ }),
+ {
+ 'Program:exit'(node) {
+ checkProgramForMaxLength(node)
}
- checkProgramForMaxLength(node)
}
- })
+ )
}
}
diff --git a/lib/rules/max-lines-per-block.js b/lib/rules/max-lines-per-block.js
new file mode 100644
index 000000000..c778ef40b
--- /dev/null
+++ b/lib/rules/max-lines-per-block.js
@@ -0,0 +1,111 @@
+/**
+ * @author lsdsjy
+ * @fileoverview Rule for checking the maximum number of lines in Vue SFC blocks.
+ */
+'use strict'
+
+const { SourceCode } = require('eslint')
+const utils = require('../utils')
+
+/**
+ * @param {string} text
+ */
+function isEmptyLine(text) {
+ return !text.trim()
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce maximum number of lines in Vue SFC blocks',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/max-lines-per-block.html'
+ },
+ fixable: null,
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ style: {
+ type: 'integer',
+ minimum: 1
+ },
+ template: {
+ type: 'integer',
+ minimum: 1
+ },
+ script: {
+ type: 'integer',
+ minimum: 1
+ },
+ skipBlankLines: {
+ type: 'boolean',
+ minimum: 0
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ tooManyLines:
+ 'Block has too many lines ({{lineCount}}). Maximum allowed is {{limit}}.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const option = context.options[0] || {}
+ /**
+ * @type {Record}
+ */
+ const limits = {
+ template: option.template,
+ script: option.script,
+ style: option.style
+ }
+
+ const code = context.getSourceCode()
+ const sourceCode = context.getSourceCode()
+ const documentFragment =
+ sourceCode.parserServices.getDocumentFragment &&
+ sourceCode.parserServices.getDocumentFragment()
+
+ function getTopLevelHTMLElements() {
+ if (documentFragment) {
+ return documentFragment.children.filter(utils.isVElement)
+ }
+ return []
+ }
+
+ return {
+ /** @param {Program} node */
+ Program(node) {
+ if (utils.hasInvalidEOF(node)) {
+ return
+ }
+ for (const block of getTopLevelHTMLElements()) {
+ if (limits[block.name]) {
+ // We suppose the start tag and end tag occupy one single line respectively
+ let lineCount = block.loc.end.line - block.loc.start.line - 1
+
+ if (option.skipBlankLines) {
+ const lines = SourceCode.splitLines(code.getText(block))
+ lineCount -= lines.filter(isEmptyLine).length
+ }
+
+ if (lineCount > limits[block.name]) {
+ context.report({
+ node: block,
+ messageId: 'tooManyLines',
+ data: {
+ limit: limits[block.name],
+ lineCount
+ }
+ })
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/rules/max-props.js b/lib/rules/max-props.js
new file mode 100644
index 000000000..0dc3043bf
--- /dev/null
+++ b/lib/rules/max-props.js
@@ -0,0 +1,81 @@
+/**
+ * @author kevsommer Kevin Sommer
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce maximum number of props in Vue component',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/max-props.html'
+ },
+ fixable: null,
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ maxProps: {
+ type: 'integer',
+ minimum: 1
+ }
+ },
+ additionalProperties: false,
+ minProperties: 1
+ }
+ ],
+ messages: {
+ tooManyProps:
+ 'Component has too many props ({{propCount}}). Maximum allowed is {{limit}}.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /** @type {Record} */
+ const option = context.options[0] || {}
+
+ /**
+ * @param {import('../utils').ComponentProp[]} props
+ * @param {CallExpression | Property} node
+ */
+ function checkMaxNumberOfProps(props, node) {
+ const uniqueProps = new Set(props.map((prop) => prop.propName))
+ const propCount = uniqueProps.size
+ if (propCount > option.maxProps && props[0].node) {
+ context.report({
+ node,
+ messageId: 'tooManyProps',
+ data: {
+ propCount,
+ limit: option.maxProps
+ }
+ })
+ }
+ }
+
+ return utils.compositingVisitors(
+ utils.executeOnVue(context, (node) => {
+ const propsNode = node.properties.find(
+ /** @returns {p is Property} */
+ (p) =>
+ p.type === 'Property' && utils.getStaticPropertyName(p) === 'props'
+ )
+
+ if (!propsNode) return
+
+ checkMaxNumberOfProps(
+ utils.getComponentPropsFromOptions(node),
+ propsNode
+ )
+ }),
+ utils.defineScriptSetupVisitor(context, {
+ onDefinePropsEnter(node, props) {
+ checkMaxNumberOfProps(props, node)
+ }
+ })
+ )
+ }
+}
diff --git a/lib/rules/max-template-depth.js b/lib/rules/max-template-depth.js
new file mode 100644
index 000000000..899ec75ce
--- /dev/null
+++ b/lib/rules/max-template-depth.js
@@ -0,0 +1,82 @@
+/**
+ * @author kevsommer Kevin Sommer
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce maximum depth of template',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/max-template-depth.html'
+ },
+ fixable: null,
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ maxDepth: {
+ type: 'integer',
+ minimum: 1
+ }
+ },
+ additionalProperties: false,
+ minProperties: 1
+ }
+ ],
+ messages: {
+ templateTooDeep:
+ 'Element is nested too deeply (depth of {{depth}}, maximum allowed is {{limit}}).'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const option = context.options[0] || {}
+
+ /**
+ * @param {VElement} element
+ * @param {number} curDepth
+ */
+ function checkMaxDepth(element, curDepth) {
+ if (curDepth > option.maxDepth) {
+ context.report({
+ node: element,
+ messageId: 'templateTooDeep',
+ data: {
+ depth: curDepth,
+ limit: option.maxDepth
+ }
+ })
+ }
+
+ if (!element.children) {
+ return
+ }
+
+ for (const child of element.children) {
+ if (child.type === 'VElement') {
+ checkMaxDepth(child, curDepth + 1)
+ }
+ }
+ }
+
+ return {
+ /** @param {Program} program */
+ Program(program) {
+ const element = program.templateBody
+
+ if (element == null) {
+ return
+ }
+
+ if (element.type !== 'VElement') {
+ return
+ }
+
+ checkMaxDepth(element, 0)
+ }
+ }
+ }
+}
diff --git a/lib/rules/multi-word-component-names.js b/lib/rules/multi-word-component-names.js
new file mode 100644
index 000000000..60f70a99e
--- /dev/null
+++ b/lib/rules/multi-word-component-names.js
@@ -0,0 +1,135 @@
+/**
+ * @author Marton Csordas
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const path = require('path')
+const casing = require('../utils/casing')
+const utils = require('../utils')
+
+const RESERVED_NAMES_IN_VUE3 = new Set(
+ require('../utils/vue3-builtin-components')
+)
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'require component names to be always multi-word',
+ categories: ['vue3-essential', 'vue2-essential'],
+ url: 'https://eslint.vuejs.org/rules/multi-word-component-names.html'
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ ignores: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true,
+ additionalItems: false
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ unexpected: 'Component name "{{value}}" should always be multi-word.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /** @type {Set} */
+ const ignores = new Set()
+ ignores.add('App')
+ ignores.add('app')
+ for (const ignore of (context.options[0] && context.options[0].ignores) ||
+ []) {
+ ignores.add(ignore)
+ if (casing.isPascalCase(ignore)) {
+ // PascalCase
+ ignores.add(casing.kebabCase(ignore))
+ }
+ }
+ let hasVue = utils.isScriptSetup(context)
+ let hasName = false
+
+ /**
+ * Returns true if the given component name is valid, otherwise false.
+ * @param {string} name
+ * */
+ function isValidComponentName(name) {
+ if (ignores.has(name) || RESERVED_NAMES_IN_VUE3.has(name)) {
+ return true
+ }
+ const elements = casing.kebabCase(name).split('-')
+ return elements.length > 1
+ }
+
+ /**
+ * @param {Expression | SpreadElement} nameNode
+ */
+ function validateName(nameNode) {
+ if (nameNode.type !== 'Literal') return
+ const componentName = `${nameNode.value}`
+ if (!isValidComponentName(componentName)) {
+ context.report({
+ node: nameNode,
+ messageId: 'unexpected',
+ data: {
+ value: componentName
+ }
+ })
+ }
+ }
+
+ return utils.compositingVisitors(
+ utils.executeOnCallVueComponent(context, (node) => {
+ hasVue = true
+ if (node.arguments.length !== 2) return
+ hasName = true
+ validateName(node.arguments[0])
+ }),
+ utils.executeOnVue(context, (obj) => {
+ hasVue = true
+ const node = utils.findProperty(obj, 'name')
+ if (!node) return
+ hasName = true
+ validateName(node.value)
+ }),
+ utils.defineScriptSetupVisitor(context, {
+ onDefineOptionsEnter(node) {
+ if (node.arguments.length === 0) return
+ const define = node.arguments[0]
+ if (define.type !== 'ObjectExpression') return
+ const nameNode = utils.findProperty(define, 'name')
+ if (!nameNode) return
+ hasName = true
+ validateName(nameNode.value)
+ }
+ }),
+ {
+ /** @param {Program} node */
+ 'Program:exit'(node) {
+ if (hasName) return
+ if (!hasVue && node.body.length > 0) return
+ const fileName = context.getFilename()
+ const componentName = path.basename(fileName, path.extname(fileName))
+ if (
+ utils.isVueFile(fileName) &&
+ !isValidComponentName(componentName)
+ ) {
+ context.report({
+ messageId: 'unexpected',
+ data: {
+ value: componentName
+ },
+ loc: { line: 1, column: 0 }
+ })
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/lib/rules/multiline-html-element-content-newline.js b/lib/rules/multiline-html-element-content-newline.js
index b2332ae5d..afe89bd2f 100644
--- a/lib/rules/multiline-html-element-content-newline.js
+++ b/lib/rules/multiline-html-element-content-newline.js
@@ -4,26 +4,24 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
const casing = require('../utils/casing')
const INLINE_ELEMENTS = require('../utils/inline-non-void-elements.json')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
+/**
+ * @param {VElement & { endTag: VEndTag }} element
+ */
function isMultilineElement(element) {
return element.loc.start.line < element.endTag.loc.start.line
}
+/**
+ * @param {any} options
+ */
function parseOptions(options) {
return Object.assign(
{
- ignores: ['pre', 'textarea'].concat(INLINE_ELEMENTS),
+ ignores: ['pre', 'textarea', ...INLINE_ELEMENTS],
ignoreWhenEmpty: true,
allowEmptyLines: false
},
@@ -31,18 +29,23 @@ function parseOptions(options) {
)
}
+/**
+ * @param {number} lineBreaks
+ */
function getPhrase(lineBreaks) {
switch (lineBreaks) {
- case 0:
+ case 0: {
return 'no'
- default:
+ }
+ default: {
return `${lineBreaks}`
+ }
}
}
/**
* Check whether the given element is empty or not.
* This ignores whitespaces, doesn't ignore comments.
- * @param {VElement} node The element node to check.
+ * @param {VElement & { endTag: VEndTag }} node The element node to check.
* @param {SourceCode} sourceCode The source code object of the current context.
* @returns {boolean} `true` if the element is empty.
*/
@@ -52,19 +55,14 @@ function isEmpty(node, sourceCode) {
return sourceCode.text.slice(start, end).trim() === ''
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'layout',
docs: {
description:
'require a line break before and after the contents of a multiline element',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
- url:
- 'https://eslint.vuejs.org/rules/multiline-html-element-content-newline.html'
+ categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
+ url: 'https://eslint.vuejs.org/rules/multiline-html-element-content-newline.html'
},
fixable: 'whitespace',
schema: [
@@ -94,19 +92,23 @@ module.exports = {
'Expected 1 line break before closing tag (`{{name}}>`), but {{actual}} line breaks found.'
}
},
-
+ /** @param {RuleContext} context */
create(context) {
const options = parseOptions(context.options[0])
const ignores = options.ignores
const ignoreWhenEmpty = options.ignoreWhenEmpty
const allowEmptyLines = options.allowEmptyLines
- const template =
- context.parserServices.getTemplateBodyTokenStore &&
- context.parserServices.getTemplateBodyTokenStore()
const sourceCode = context.getSourceCode()
+ const template =
+ sourceCode.parserServices.getTemplateBodyTokenStore &&
+ sourceCode.parserServices.getTemplateBodyTokenStore()
- let inIgnoreElement
+ /** @type {VElement | null} */
+ let inIgnoreElement = null
+ /**
+ * @param {VElement} node
+ */
function isIgnoredElement(node) {
return (
ignores.includes(node.name) ||
@@ -115,12 +117,11 @@ module.exports = {
)
}
+ /**
+ * @param {number} lineBreaks
+ */
function isInvalidLineBreaks(lineBreaks) {
- if (allowEmptyLines) {
- return lineBreaks === 0
- } else {
- return lineBreaks !== 1
- }
+ return allowEmptyLines ? lineBreaks === 0 : lineBreaks !== 1
}
return utils.defineTemplateBodyVisitor(context, {
@@ -138,73 +139,81 @@ module.exports = {
return
}
- if (!isMultilineElement(node)) {
+ const element = /** @type {VElement & { endTag: VEndTag }} */ (node)
+
+ if (!isMultilineElement(element)) {
return
}
+ /**
+ * @type {SourceCode.CursorWithCountOptions}
+ */
const getTokenOption = {
includeComments: true,
filter: (token) => token.type !== 'HTMLWhitespace'
}
if (
ignoreWhenEmpty &&
- node.children.length === 0 &&
+ element.children.length === 0 &&
template.getFirstTokensBetween(
- node.startTag,
- node.endTag,
+ element.startTag,
+ element.endTag,
getTokenOption
).length === 0
) {
return
}
- const contentFirst = template.getTokenAfter(
- node.startTag,
- getTokenOption
+ const contentFirst = /** @type {Token} */ (
+ template.getTokenAfter(element.startTag, getTokenOption)
+ )
+ const contentLast = /** @type {Token} */ (
+ template.getTokenBefore(element.endTag, getTokenOption)
)
- const contentLast = template.getTokenBefore(node.endTag, getTokenOption)
const beforeLineBreaks =
- contentFirst.loc.start.line - node.startTag.loc.end.line
+ contentFirst.loc.start.line - element.startTag.loc.end.line
const afterLineBreaks =
- node.endTag.loc.start.line - contentLast.loc.end.line
+ element.endTag.loc.start.line - contentLast.loc.end.line
if (isInvalidLineBreaks(beforeLineBreaks)) {
context.report({
- node: template.getLastToken(node.startTag),
+ node: template.getLastToken(element.startTag),
loc: {
- start: node.startTag.loc.end,
+ start: element.startTag.loc.end,
end: contentFirst.loc.start
},
messageId: 'unexpectedAfterClosingBracket',
data: {
- name: node.rawName,
+ name: element.rawName,
actual: getPhrase(beforeLineBreaks)
},
fix(fixer) {
- const range = [node.startTag.range[1], contentFirst.range[0]]
+ /** @type {Range} */
+ const range = [element.startTag.range[1], contentFirst.range[0]]
return fixer.replaceTextRange(range, '\n')
}
})
}
- if (isEmpty(node, sourceCode)) {
+ if (isEmpty(element, sourceCode)) {
return
}
if (isInvalidLineBreaks(afterLineBreaks)) {
context.report({
- node: template.getFirstToken(node.endTag),
+ node: template.getFirstToken(element.endTag),
loc: {
start: contentLast.loc.end,
- end: node.endTag.loc.start
+ end: element.endTag.loc.start
},
messageId: 'unexpectedBeforeOpeningBracket',
data: {
- name: node.name,
+ name: element.name,
actual: getPhrase(afterLineBreaks)
},
fix(fixer) {
- const range = [contentLast.range[1], node.endTag.range[0]]
+ /** @type {Range} */
+ const range = [contentLast.range[1], element.endTag.range[0]]
return fixer.replaceTextRange(range, '\n')
}
})
diff --git a/lib/rules/multiline-ternary.js b/lib/rules/multiline-ternary.js
new file mode 100644
index 000000000..84691627e
--- /dev/null
+++ b/lib/rules/multiline-ternary.js
@@ -0,0 +1,13 @@
+/**
+ * @author dev1437
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { wrapStylisticOrCoreRule } = require('../utils')
+
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapStylisticOrCoreRule('multiline-ternary', {
+ skipDynamicArguments: true,
+ applyDocument: true
+})
diff --git a/lib/rules/mustache-interpolation-spacing.js b/lib/rules/mustache-interpolation-spacing.js
index 83fb7886c..304b50c70 100644
--- a/lib/rules/mustache-interpolation-spacing.js
+++ b/lib/rules/mustache-interpolation-spacing.js
@@ -4,22 +4,14 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce unified spacing in mustache interpolations',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
+ categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/mustache-interpolation-spacing.html'
},
fixable: 'whitespace',
@@ -27,20 +19,24 @@ module.exports = {
{
enum: ['always', 'never']
}
- ]
+ ],
+ messages: {
+ expectedSpaceAfter: "Expected 1 space after '{{', but not found.",
+ expectedSpaceBefore: "Expected 1 space before '}}', but not found.",
+ unexpectedSpaceAfter: "Expected no space after '{{', but found.",
+ unexpectedSpaceBefore: "Expected no space before '}}', but found."
+ }
},
-
+ /** @param {RuleContext} context */
create(context) {
const options = context.options[0] || 'always'
+ const sourceCode = context.getSourceCode()
const template =
- context.parserServices.getTemplateBodyTokenStore &&
- context.parserServices.getTemplateBodyTokenStore()
-
- // ----------------------------------------------------------------------
- // Public
- // ----------------------------------------------------------------------
+ sourceCode.parserServices.getTemplateBodyTokenStore &&
+ sourceCode.parserServices.getTemplateBodyTokenStore()
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VExpressionContainer} node */
'VExpressionContainer[expression!=null]'(node) {
const openBrace = template.getFirstToken(node)
const closeBrace = template.getLastToken(node)
@@ -65,14 +61,14 @@ module.exports = {
if (openBrace.range[1] === firstToken.range[0]) {
context.report({
node: openBrace,
- message: "Expected 1 space after '{{', but not found.",
+ messageId: 'expectedSpaceAfter',
fix: (fixer) => fixer.insertTextAfter(openBrace, ' ')
})
}
if (closeBrace.range[0] === lastToken.range[1]) {
context.report({
node: closeBrace,
- message: "Expected 1 space before '}}', but not found.",
+ messageId: 'expectedSpaceBefore',
fix: (fixer) => fixer.insertTextBefore(closeBrace, ' ')
})
}
@@ -83,7 +79,7 @@ module.exports = {
start: openBrace.loc.start,
end: firstToken.loc.start
},
- message: "Expected no space after '{{', but found.",
+ messageId: 'unexpectedSpaceAfter',
fix: (fixer) =>
fixer.removeRange([openBrace.range[1], firstToken.range[0]])
})
@@ -94,7 +90,7 @@ module.exports = {
start: lastToken.loc.end,
end: closeBrace.loc.end
},
- message: "Expected no space before '}}', but found.",
+ messageId: 'unexpectedSpaceBefore',
fix: (fixer) =>
fixer.removeRange([lastToken.range[1], closeBrace.range[0]])
})
diff --git a/lib/rules/name-property-casing.js b/lib/rules/name-property-casing.js
deleted file mode 100644
index 6e5a52190..000000000
--- a/lib/rules/name-property-casing.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * @fileoverview Requires specific casing for the name property in Vue components
- * @author Armano
- */
-'use strict'
-
-const utils = require('../utils')
-const casing = require('../utils/casing')
-const allowedCaseOptions = ['PascalCase', 'kebab-case']
-
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
-module.exports = {
- meta: {
- type: 'suggestion',
- docs: {
- description:
- 'enforce specific casing for the name property in Vue components',
- categories: ['vue3-strongly-recommended', 'strongly-recommended'],
- url: 'https://eslint.vuejs.org/rules/name-property-casing.html',
- replacedBy: ['component-definition-name-casing']
- },
- deprecated: true,
- fixable: 'code', // or "code" or "whitespace"
- schema: [
- {
- enum: allowedCaseOptions
- }
- ]
- },
-
- create(context) {
- const options = context.options[0]
- const caseType =
- allowedCaseOptions.indexOf(options) !== -1 ? options : 'PascalCase'
-
- // ----------------------------------------------------------------------
- // Public
- // ----------------------------------------------------------------------
-
- return utils.executeOnVue(context, (obj) => {
- const node = obj.properties.find(
- (item) =>
- item.type === 'Property' &&
- item.key.name === 'name' &&
- item.value.type === 'Literal'
- )
-
- if (!node) return
-
- if (!casing.getChecker(caseType)(node.value.value)) {
- const value = casing.getExactConverter(caseType)(node.value.value)
- context.report({
- node: node.value,
- message: 'Property name "{{value}}" is not {{caseType}}.',
- data: {
- value: node.value.value,
- caseType
- },
- fix: (fixer) =>
- fixer.replaceText(
- node.value,
- node.value.raw.replace(node.value.value, value)
- )
- })
- }
- })
- }
-}
diff --git a/lib/rules/new-line-between-multi-line-property.js b/lib/rules/new-line-between-multi-line-property.js
new file mode 100644
index 000000000..54812869b
--- /dev/null
+++ b/lib/rules/new-line-between-multi-line-property.js
@@ -0,0 +1,154 @@
+/**
+ * @fileoverview Enforce new lines between multi-line properties in Vue components.
+ * @author IWANABETHATGUY
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @param {Token} node
+ */
+function isComma(node) {
+ return node.type === 'Punctuator' && node.value === ','
+}
+
+/**
+ * Check whether the between given nodes has empty line.
+ * @param {SourceCode} sourceCode
+ * @param {ASTNode} pre
+ * @param {ASTNode} cur
+ */
+function* iterateBetweenTokens(sourceCode, pre, cur) {
+ yield sourceCode.getLastToken(pre)
+ yield* sourceCode.getTokensBetween(pre, cur, {
+ includeComments: true
+ })
+ yield sourceCode.getFirstToken(cur)
+}
+
+/**
+ * Check whether the between given nodes has empty line.
+ * @param {SourceCode} sourceCode
+ * @param {ASTNode} pre
+ * @param {ASTNode} cur
+ */
+function hasEmptyLine(sourceCode, pre, cur) {
+ /** @type {Token|null} */
+ let preToken = null
+ for (const token of iterateBetweenTokens(sourceCode, pre, cur)) {
+ if (preToken && token.loc.start.line - preToken.loc.end.line >= 2) {
+ return true
+ }
+ preToken = token
+ }
+ return false
+}
+
+module.exports = {
+ meta: {
+ type: 'layout',
+ docs: {
+ description:
+ 'enforce new lines between multi-line properties in Vue components',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/new-line-between-multi-line-property.html'
+ },
+ fixable: 'whitespace',
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ // number of line you want to insert after multi-line property
+ minLineOfMultilineProperty: {
+ type: 'number',
+ minimum: 2
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ missingEmptyLine:
+ 'Enforce new lines between multi-line properties in Vue components.'
+ }
+ },
+
+ /** @param {RuleContext} context */
+ create(context) {
+ let minLineOfMultilineProperty = 2
+ if (
+ context.options &&
+ context.options[0] &&
+ context.options[0].minLineOfMultilineProperty
+ ) {
+ minLineOfMultilineProperty = context.options[0].minLineOfMultilineProperty
+ }
+
+ /** @type {CallExpression[]} */
+ const callStack = []
+ const sourceCode = context.getSourceCode()
+ return Object.assign(
+ utils.defineVueVisitor(context, {
+ CallExpression(node) {
+ callStack.push(node)
+ },
+ 'CallExpression:exit'() {
+ callStack.pop()
+ },
+
+ /**
+ * @param {ObjectExpression} node
+ */
+ ObjectExpression(node) {
+ if (callStack.length > 0) {
+ return
+ }
+ const properties = node.properties
+ for (let i = 1; i < properties.length; i++) {
+ const cur = properties[i]
+ const pre = properties[i - 1]
+
+ const lineCountOfPreProperty =
+ pre.loc.end.line - pre.loc.start.line + 1
+ if (lineCountOfPreProperty < minLineOfMultilineProperty) {
+ continue
+ }
+
+ if (hasEmptyLine(sourceCode, pre, cur)) {
+ continue
+ }
+
+ context.report({
+ node: pre,
+ loc: {
+ start: pre.loc.end,
+ end: cur.loc.start
+ },
+ messageId: 'missingEmptyLine',
+ fix(fixer) {
+ /** @type {Token|null} */
+ let preToken = null
+ for (const token of iterateBetweenTokens(
+ sourceCode,
+ pre,
+ cur
+ )) {
+ if (
+ preToken &&
+ preToken.loc.end.line < token.loc.start.line
+ ) {
+ return fixer.insertTextAfter(preToken, '\n')
+ }
+ preToken = token
+ }
+ const commaToken = sourceCode.getTokenAfter(pre, isComma)
+ return fixer.insertTextAfter(commaToken || pre, '\n\n')
+ }
+ })
+ }
+ }
+ })
+ )
+ }
+}
diff --git a/lib/rules/next-tick-style.js b/lib/rules/next-tick-style.js
new file mode 100644
index 000000000..08cc85a1b
--- /dev/null
+++ b/lib/rules/next-tick-style.js
@@ -0,0 +1,141 @@
+/**
+ * @fileoverview enforce Promise or callback style in `nextTick`
+ * @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')
+const { findVariable } = require('@eslint-community/eslint-utils')
+
+/**
+ * @param {Identifier} identifier
+ * @param {RuleContext} context
+ * @returns {CallExpression|undefined}
+ */
+function getVueNextTickCallExpression(identifier, context) {
+ // Instance API: this.$nextTick()
+ if (
+ identifier.name === '$nextTick' &&
+ identifier.parent.type === 'MemberExpression' &&
+ utils.isThis(identifier.parent.object, context) &&
+ identifier.parent.parent.type === 'CallExpression' &&
+ identifier.parent.parent.callee === identifier.parent
+ ) {
+ return identifier.parent.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' &&
+ identifier.parent.parent.type === 'CallExpression' &&
+ identifier.parent.parent.callee === identifier.parent
+ ) {
+ return identifier.parent.parent
+ }
+
+ // Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
+ if (
+ identifier.parent.type === 'CallExpression' &&
+ identifier.parent.callee === identifier
+ ) {
+ 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.parent
+ }
+ }
+ }
+
+ return undefined
+}
+
+/**
+ * @param {CallExpression} callExpression
+ * @returns {boolean}
+ */
+function isAwaitedPromise(callExpression) {
+ return (
+ callExpression.parent.type === 'AwaitExpression' ||
+ (callExpression.parent.type === 'MemberExpression' &&
+ callExpression.parent.property.type === 'Identifier' &&
+ callExpression.parent.property.name === 'then')
+ )
+}
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'enforce Promise or callback style in `nextTick`',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/next-tick-style.html'
+ },
+ fixable: 'code',
+ schema: [{ enum: ['promise', 'callback'] }],
+ messages: {
+ usePromise:
+ 'Use the Promise returned by `nextTick` instead of passing a callback function.',
+ useCallback:
+ 'Pass a callback function to `nextTick` instead of using the returned Promise.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const preferredStyle =
+ /** @type {string|undefined} */ (context.options[0]) || 'promise'
+
+ return utils.defineVueVisitor(context, {
+ /** @param {Identifier} node */
+ Identifier(node) {
+ const callExpression = getVueNextTickCallExpression(node, context)
+ if (!callExpression) {
+ return
+ }
+
+ if (preferredStyle === 'callback') {
+ if (
+ callExpression.arguments.length !== 1 ||
+ isAwaitedPromise(callExpression)
+ ) {
+ context.report({
+ node,
+ messageId: 'useCallback'
+ })
+ }
+
+ return
+ }
+
+ if (
+ callExpression.arguments.length > 0 ||
+ !isAwaitedPromise(callExpression)
+ ) {
+ context.report({
+ node,
+ messageId: 'usePromise',
+ fix(fixer) {
+ return fixer.insertTextAfter(node, '().then')
+ }
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/no-arrow-functions-in-watch.js b/lib/rules/no-arrow-functions-in-watch.js
index 2fce3f87f..b16b267f5 100644
--- a/lib/rules/no-arrow-functions-in-watch.js
+++ b/lib/rules/no-arrow-functions-in-watch.js
@@ -10,17 +10,20 @@ module.exports = {
type: 'problem',
docs: {
description: 'disallow using arrow functions to define watcher',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/no-arrow-functions-in-watch.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ noArrowFunctionsInWatch:
+ 'You should not use an arrow function to define a watcher.'
+ }
},
+ /** @param {RuleContext} context */
create(context) {
return utils.executeOnVue(context, (obj) => {
- const watchNode = obj.properties.find(
- (property) => utils.getStaticPropertyName(property) === 'watch'
- )
+ const watchNode = utils.findProperty(obj, 'watch')
if (watchNode == null) {
return
}
@@ -29,14 +32,17 @@ module.exports = {
return
}
for (const property of watchValue.properties) {
- if (
- property.type === 'Property' &&
- property.value.type === 'ArrowFunctionExpression'
- ) {
- context.report({
- node: property,
- message: 'You should not use an arrow function to define a watcher.'
- })
+ if (property.type !== 'Property') {
+ continue
+ }
+
+ for (const handler of utils.iterateWatchHandlerValues(property)) {
+ if (handler.type === 'ArrowFunctionExpression') {
+ context.report({
+ node: handler,
+ messageId: 'noArrowFunctionsInWatch'
+ })
+ }
}
}
})
diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js
index 34849d1c6..e8ed02f0e 100644
--- a/lib/rules/no-async-in-computed-properties.js
+++ b/lib/rules/no-async-in-computed-properties.js
@@ -3,93 +3,152 @@
* @author Armano
*/
'use strict'
-
+const { ReferenceTracker } = require('@eslint-community/eslint-utils')
const utils = require('../utils')
-const PROMISE_FUNCTIONS = ['then', 'catch', 'finally']
+/**
+ * @typedef {import('../utils').VueObjectData} VueObjectData
+ * @typedef {import('../utils').VueVisitor} VueVisitor
+ * @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty
+ */
+
+const PROMISE_FUNCTIONS = new Set(['then', 'catch', 'finally'])
-const PROMISE_METHODS = ['all', 'race', 'reject', 'resolve']
+const PROMISE_METHODS = new Set(['all', 'race', 'reject', 'resolve'])
-const TIMED_FUNCTIONS = [
+const TIMED_FUNCTIONS = new Set([
'setTimeout',
'setInterval',
'setImmediate',
'requestAnimationFrame'
-]
+])
+/**
+ * @param {CallExpression} node
+ */
function isTimedFunction(node) {
+ const callee = utils.skipChainExpression(node.callee)
return (
- ((node.type === 'CallExpression' &&
- node.callee.type === 'Identifier' &&
- TIMED_FUNCTIONS.indexOf(node.callee.name) !== -1) ||
- (node.type === 'CallExpression' &&
- node.callee.type === 'MemberExpression' &&
- node.callee.object.type === 'Identifier' &&
- node.callee.object.name === 'window' &&
- TIMED_FUNCTIONS.indexOf(node.callee.property.name) !== -1)) &&
- node.arguments.length
+ ((callee.type === 'Identifier' && TIMED_FUNCTIONS.has(callee.name)) ||
+ (callee.type === 'MemberExpression' &&
+ callee.object.type === 'Identifier' &&
+ callee.object.name === 'window' &&
+ TIMED_FUNCTIONS.has(utils.getStaticPropertyName(callee) || ''))) &&
+ node.arguments.length > 0
)
}
+/**
+ * @param {CallExpression} node
+ */
function isPromise(node) {
- if (
- node.type === 'CallExpression' &&
- node.callee.type === 'MemberExpression'
- ) {
+ const callee = utils.skipChainExpression(node.callee)
+ if (callee.type === 'MemberExpression') {
+ const name = utils.getStaticPropertyName(callee)
return (
+ name &&
// hello.PROMISE_FUNCTION()
- (node.callee.property.type === 'Identifier' &&
- PROMISE_FUNCTIONS.indexOf(node.callee.property.name) !== -1) || // Promise.PROMISE_METHOD()
- (node.callee.object.type === 'Identifier' &&
- node.callee.object.name === 'Promise' &&
- PROMISE_METHODS.indexOf(node.callee.property.name) !== -1)
+ (PROMISE_FUNCTIONS.has(name) ||
+ // Promise.PROMISE_METHOD()
+ (callee.object.type === 'Identifier' &&
+ callee.object.name === 'Promise' &&
+ PROMISE_METHODS.has(name)))
)
}
return false
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+/**
+ * @param {CallExpression} node
+ * @param {RuleContext} context
+ */
+function isNextTick(node, context) {
+ const callee = utils.skipChainExpression(node.callee)
+ if (callee.type === 'MemberExpression') {
+ const name = utils.getStaticPropertyName(callee)
+ return (
+ (utils.isThis(callee.object, context) && name === '$nextTick') ||
+ (callee.object.type === 'Identifier' &&
+ callee.object.name === 'Vue' &&
+ name === 'nextTick')
+ )
+ }
+ return false
+}
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow asynchronous actions in computed properties',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/no-async-in-computed-properties.html'
},
fixable: null,
- schema: []
+ schema: [],
+ messages: {
+ unexpectedInFunction:
+ 'Unexpected {{expressionName}} in computed function.',
+ unexpectedInProperty:
+ 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.'
+ }
},
-
+ /** @param {RuleContext} context */
create(context) {
+ /** @type {Map} */
const computedPropertiesMap = new Map()
- let scopeStack = { upper: null, body: null }
+ /** @type {(FunctionExpression | ArrowFunctionExpression)[]} */
+ const computedFunctionNodes = []
+
+ /**
+ * @typedef {object} ScopeStack
+ * @property {ScopeStack | null} upper
+ * @property {BlockStatement | Expression} body
+ */
+ /** @type {ScopeStack | null} */
+ let scopeStack = null
const expressionTypes = {
promise: 'asynchronous action',
+ nextTick: 'asynchronous action',
await: 'await operator',
async: 'async function declaration',
new: 'Promise object',
timed: 'timed function'
}
- function onFunctionEnter(node, { node: vueNode }) {
+ /**
+ * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+ * @param {VueObjectData|undefined} [info]
+ */
+ function onFunctionEnter(node, info) {
if (node.async) {
- verify(node, node.body, 'async', computedPropertiesMap.get(vueNode))
+ verify(
+ node,
+ node.body,
+ 'async',
+ info ? computedPropertiesMap.get(info.node) : null
+ )
}
- scopeStack = { upper: scopeStack, body: node.body }
+ scopeStack = {
+ upper: scopeStack,
+ body: node.body
+ }
}
function onFunctionExit() {
- scopeStack = scopeStack.upper
+ scopeStack = scopeStack && scopeStack.upper
}
+ /**
+ * @param {ESNode} node
+ * @param {BlockStatement | Expression} targetBody
+ * @param {keyof expressionTypes} type
+ * @param {ComponentComputedProperty[]|undefined|null} computedProperties
+ */
function verify(node, targetBody, type, computedProperties) {
- computedProperties.forEach((cp) => {
+ for (const cp of computedProperties || []) {
if (
cp.value &&
node.loc.start.line >= cp.value.loc.start.line &&
@@ -98,60 +157,136 @@ module.exports = {
) {
context.report({
node,
- message:
- 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
+ messageId: 'unexpectedInProperty',
data: {
expressionName: expressionTypes[type],
- propertyName: cp.key
+ propertyName: cp.key || 'unknown'
}
})
+ return
}
- })
+ }
+
+ for (const cf of computedFunctionNodes) {
+ if (
+ node.loc.start.line >= cf.body.loc.start.line &&
+ node.loc.end.line <= cf.body.loc.end.line &&
+ targetBody === cf.body
+ ) {
+ context.report({
+ node,
+ messageId: 'unexpectedInFunction',
+ data: {
+ expressionName: expressionTypes[type]
+ }
+ })
+ return
+ }
+ }
}
- return utils.defineVueVisitor(context, {
- onVueObjectEnter(node) {
- computedPropertiesMap.set(node, utils.getComputedProperties(node))
- },
+ const nodeVisitor = {
':function': onFunctionEnter,
':function:exit': onFunctionExit,
- NewExpression(node, { node: vueNode }) {
- if (node.callee.name === 'Promise') {
+ /**
+ * @param {NewExpression} node
+ * @param {VueObjectData|undefined} [info]
+ */
+ NewExpression(node, info) {
+ if (!scopeStack) {
+ return
+ }
+ if (
+ node.callee.type === 'Identifier' &&
+ node.callee.name === 'Promise'
+ ) {
verify(
node,
scopeStack.body,
'new',
- computedPropertiesMap.get(vueNode)
+ info ? computedPropertiesMap.get(info.node) : null
)
}
},
- CallExpression(node, { node: vueNode }) {
+ /**
+ * @param {CallExpression} node
+ * @param {VueObjectData|undefined} [info]
+ */
+ CallExpression(node, info) {
+ if (!scopeStack) {
+ return
+ }
if (isPromise(node)) {
verify(
node,
scopeStack.body,
'promise',
- computedPropertiesMap.get(vueNode)
+ info ? computedPropertiesMap.get(info.node) : null
)
} else if (isTimedFunction(node)) {
verify(
node,
scopeStack.body,
'timed',
- computedPropertiesMap.get(vueNode)
+ info ? computedPropertiesMap.get(info.node) : null
+ )
+ } else if (isNextTick(node, context)) {
+ verify(
+ node,
+ scopeStack.body,
+ 'nextTick',
+ info ? computedPropertiesMap.get(info.node) : null
)
}
},
- AwaitExpression(node, { node: vueNode }) {
+ /**
+ * @param {AwaitExpression} node
+ * @param {VueObjectData|undefined} [info]
+ */
+ AwaitExpression(node, info) {
+ if (!scopeStack) {
+ return
+ }
verify(
node,
scopeStack.body,
'await',
- computedPropertiesMap.get(vueNode)
+ info ? computedPropertiesMap.get(info.node) : null
)
}
- })
+ }
+
+ return utils.compositingVisitors(
+ {
+ /** @param {Program} program */
+ Program(program) {
+ const tracker = new ReferenceTracker(utils.getScope(context, program))
+ for (const { node } of utils.iterateReferencesTraceMap(tracker, {
+ computed: {
+ [ReferenceTracker.CALL]: true
+ }
+ })) {
+ if (node.type !== 'CallExpression') {
+ continue
+ }
+
+ const getter = utils.getGetterBodyFromComputedFunction(node)
+ if (getter) {
+ computedFunctionNodes.push(getter)
+ }
+ }
+ }
+ },
+ utils.isScriptSetup(context)
+ ? utils.defineScriptSetupVisitor(context, nodeVisitor)
+ : utils.defineVueVisitor(context, {
+ onVueObjectEnter(node) {
+ computedPropertiesMap.set(node, utils.getComputedProperties(node))
+ },
+ ...nodeVisitor
+ })
+ )
}
}
diff --git a/lib/rules/no-bare-strings-in-template.js b/lib/rules/no-bare-strings-in-template.js
new file mode 100644
index 000000000..8e4acc96d
--- /dev/null
+++ b/lib/rules/no-bare-strings-in-template.js
@@ -0,0 +1,293 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const regexp = require('../utils/regexp')
+const casing = require('../utils/casing')
+
+/**
+ * @typedef { { names: { [tagName in string]: Set }, regexps: { name: RegExp, attrs: Set }[], cache: { [tagName in string]: Set } } } TargetAttrs
+ */
+
+// https://dev.w3.org/html5/html-author/charref
+const DEFAULT_ALLOWLIST = [
+ '(',
+ ')',
+ ',',
+ '.',
+ '&',
+ '+',
+ '-',
+ '=',
+ '*',
+ '/',
+ '#',
+ '%',
+ '!',
+ '?',
+ ':',
+ '[',
+ ']',
+ '{',
+ '}',
+ '<',
+ '>',
+ '\u00B7', // "·"
+ '\u2022', // "•"
+ '\u2010', // "‐"
+ '\u2013', // "–"
+ '\u2014', // "—"
+ '\u2212', // "−"
+ '|'
+]
+
+const DEFAULT_ATTRIBUTES = {
+ '/.+/': [
+ 'title',
+ 'aria-label',
+ 'aria-placeholder',
+ 'aria-roledescription',
+ 'aria-valuetext'
+ ],
+ input: ['placeholder'],
+ img: ['alt']
+}
+
+const DEFAULT_DIRECTIVES = ['v-text']
+
+/**
+ * Parse attributes option
+ * @param {any} options
+ * @returns {TargetAttrs}
+ */
+function parseTargetAttrs(options) {
+ /** @type {TargetAttrs} */
+ const result = { names: {}, regexps: [], cache: {} }
+ for (const tagName of Object.keys(options)) {
+ /** @type { Set } */
+ const attrs = new Set(options[tagName])
+ if (regexp.isRegExp(tagName)) {
+ result.regexps.push({
+ name: regexp.toRegExp(tagName),
+ attrs
+ })
+ } else {
+ result.names[tagName] = attrs
+ }
+ }
+ return result
+}
+
+/**
+ * Get a string from given expression container node
+ * @param {VExpressionContainer} value
+ * @returns { string | null }
+ */
+function getStringValue(value) {
+ const expression = value.expression
+ if (!expression) {
+ return null
+ }
+ if (expression.type !== 'Literal') {
+ return null
+ }
+ if (typeof expression.value === 'string') {
+ return expression.value
+ }
+ return null
+}
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'disallow the use of bare strings in ``',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/no-bare-strings-in-template.html'
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ allowlist: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true
+ },
+ attributes: {
+ type: 'object',
+ patternProperties: {
+ '^(?:\\S+|/.*/[a-z]*)$': {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true
+ }
+ },
+ additionalProperties: false
+ },
+ directives: {
+ type: 'array',
+ items: { type: 'string', pattern: '^v-' },
+ uniqueItems: true
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ unexpected: 'Unexpected non-translated string used.',
+ unexpectedInAttr: 'Unexpected non-translated string used in `{{attr}}`.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /**
+ * @typedef { { upper: ElementStack | null, name: string, attrs: Set } } ElementStack
+ */
+ const opts = context.options[0] || {}
+ /** @type {string[]} */
+ const rawAllowlist = opts.allowlist || DEFAULT_ALLOWLIST
+ const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES)
+ const directives = opts.directives || DEFAULT_DIRECTIVES
+
+ /** @type {string[]} */
+ const stringAllowlist = []
+ /** @type {RegExp[]} */
+ const regexAllowlist = []
+
+ for (const item of rawAllowlist) {
+ if (regexp.isRegExp(item)) {
+ regexAllowlist.push(regexp.toRegExp(item))
+ } else {
+ stringAllowlist.push(item)
+ }
+ }
+
+ const allowlistRe =
+ stringAllowlist.length > 0
+ ? new RegExp(
+ stringAllowlist
+ .map((w) => regexp.escape(w))
+ .sort((a, b) => b.length - a.length)
+ .join('|'),
+ 'gu'
+ )
+ : null
+
+ /** @type {ElementStack | null} */
+ let elementStack = null
+ /**
+ * Gets the bare string from given string
+ * @param {string} str
+ */
+ function getBareString(str) {
+ let result = str.trim()
+
+ if (allowlistRe) {
+ result = result.replace(allowlistRe, '')
+ }
+
+ for (const regex of regexAllowlist) {
+ const flags = regex.flags.includes('g')
+ ? regex.flags
+ : `${regex.flags}g`
+ const globalRegex = new RegExp(regex.source, flags)
+ result = result.replace(globalRegex, '')
+ }
+
+ return result.trim()
+ }
+
+ /**
+ * Get the attribute to be verified from the element name.
+ * @param {string} tagName
+ * @returns {Set}
+ */
+ function getTargetAttrs(tagName) {
+ if (attributes.cache[tagName]) {
+ return attributes.cache[tagName]
+ }
+ /** @type {string[]} */
+ const result = []
+ if (attributes.names[tagName]) {
+ result.push(...attributes.names[tagName])
+ }
+ for (const { name, attrs } of attributes.regexps) {
+ name.lastIndex = 0
+ if (name.test(tagName)) {
+ result.push(...attrs)
+ }
+ }
+ if (casing.isKebabCase(tagName)) {
+ result.push(...getTargetAttrs(casing.pascalCase(tagName)))
+ }
+
+ return (attributes.cache[tagName] = new Set(result))
+ }
+
+ return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VText} node */
+ VText(node) {
+ if (getBareString(node.value)) {
+ context.report({
+ node,
+ messageId: 'unexpected'
+ })
+ }
+ },
+ /**
+ * @param {VElement} node
+ */
+ VElement(node) {
+ elementStack = {
+ upper: elementStack,
+ name: node.rawName,
+ attrs: getTargetAttrs(node.rawName)
+ }
+ },
+ 'VElement:exit'() {
+ elementStack = elementStack && elementStack.upper
+ },
+ /** @param {VAttribute|VDirective} node */
+ VAttribute(node) {
+ if (!node.value || !elementStack) {
+ return
+ }
+ if (node.directive === false) {
+ const attrs = elementStack.attrs
+ if (!attrs.has(node.key.rawName)) {
+ return
+ }
+
+ if (getBareString(node.value.value)) {
+ context.report({
+ node: node.value,
+ messageId: 'unexpectedInAttr',
+ data: {
+ attr: node.key.rawName
+ }
+ })
+ }
+ } else {
+ const directive = `v-${node.key.name.name}`
+ if (!directives.includes(directive)) {
+ return
+ }
+ const str = getStringValue(node.value)
+ if (str && getBareString(str)) {
+ context.report({
+ node: node.value,
+ messageId: 'unexpectedInAttr',
+ data: {
+ attr: directive
+ }
+ })
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/no-boolean-default.js b/lib/rules/no-boolean-default.js
index 1f1e192df..2ff6a2d06 100644
--- a/lib/rules/no-boolean-default.js
+++ b/lib/rules/no-boolean-default.js
@@ -6,37 +6,37 @@
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+/**
+ * @typedef {import('../utils').ComponentProp} ComponentProp
+ * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
+ */
-function isBooleanProp(prop) {
- return (
- prop.type === 'Property' &&
- prop.key.type === 'Identifier' &&
- prop.key.name === 'type' &&
- prop.value.type === 'Identifier' &&
- prop.value.name === 'Boolean'
- )
+/**
+ * @param {Expression|undefined} node
+ */
+function isBooleanIdentifier(node) {
+ return Boolean(node && node.type === 'Identifier' && node.name === 'Boolean')
}
-function getBooleanProps(props) {
- return props.filter(
- (prop) =>
- prop.value &&
- prop.value.properties &&
- prop.value.properties.find(isBooleanProp)
+/**
+ * Detects whether given prop node is a Boolean
+ * @param {ComponentObjectProp} prop
+ * @return {Boolean}
+ */
+function isBooleanProp(prop) {
+ const value = utils.skipTSAsExpression(prop.value)
+ return (
+ isBooleanIdentifier(value) ||
+ (value.type === 'ObjectExpression' &&
+ isBooleanIdentifier(utils.findProperty(value, 'type')?.value))
)
}
-function getDefaultNode(propDef) {
- return propDef.value.properties.find((p) => {
- return (
- p.type === 'Property' &&
- p.key.type === 'Identifier' &&
- p.key.name === 'default'
- )
- })
+/**
+ * @param {ObjectExpression} propDefValue
+ */
+function getDefaultNode(propDefValue) {
+ return utils.findProperty(propDefValue, 'default')
}
module.exports = {
@@ -47,47 +47,102 @@ module.exports = {
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-boolean-default.html'
},
- fixable: 'code',
+ fixable: null,
schema: [
{
enum: ['default-false', 'no-default']
}
- ]
+ ],
+ messages: {
+ noBooleanDefault:
+ 'Boolean prop should not set a default (Vue defaults it to false).',
+ defaultFalse: 'Boolean prop should only be defaulted to false.'
+ }
},
-
+ /** @param {RuleContext} context */
create(context) {
- return utils.executeOnVueComponent(context, (obj) => {
- const props = utils.getComponentProps(obj)
- const booleanProps = getBooleanProps(props)
-
- if (!booleanProps.length) return
-
- const booleanType = context.options[0] || 'no-default'
-
- booleanProps.forEach((propDef) => {
- const defaultNode = getDefaultNode(propDef)
+ const booleanType = context.options[0] || 'no-default'
+ /**
+ * @param {ComponentProp} prop
+ * @param {(propName: string) => Expression[]} otherDefaultProvider
+ */
+ function processProp(prop, otherDefaultProvider) {
+ if (prop.type === 'object') {
+ if (!isBooleanProp(prop)) {
+ return
+ }
+ if (prop.value.type === 'ObjectExpression') {
+ const defaultNode = getDefaultNode(prop.value)
+ if (defaultNode) {
+ verifyDefaultExpression(defaultNode.value)
+ }
+ }
+ if (prop.propName != null) {
+ for (const defaultNode of otherDefaultProvider(prop.propName)) {
+ verifyDefaultExpression(defaultNode)
+ }
+ }
+ } else if (prop.type === 'type') {
+ if (prop.types.length !== 1 || prop.types[0] !== 'Boolean') {
+ return
+ }
+ for (const defaultNode of otherDefaultProvider(prop.propName)) {
+ verifyDefaultExpression(defaultNode)
+ }
+ }
+ }
+ /**
+ * @param {ComponentProp[]} props
+ * @param {(propName: string) => Expression[]} otherDefaultProvider
+ */
+ function processProps(props, otherDefaultProvider) {
+ for (const prop of props) {
+ processProp(prop, otherDefaultProvider)
+ }
+ }
- switch (booleanType) {
- case 'no-default':
- if (defaultNode) {
- context.report({
- node: defaultNode,
- message:
- 'Boolean prop should not set a default (Vue defaults it to false).'
- })
- }
- break
+ /**
+ * @param {Expression} defaultNode
+ */
+ function verifyDefaultExpression(defaultNode) {
+ switch (booleanType) {
+ case 'no-default': {
+ context.report({
+ node: defaultNode,
+ messageId: 'noBooleanDefault'
+ })
+ break
+ }
- case 'default-false':
- if (defaultNode && defaultNode.value.value !== false) {
- context.report({
- node: defaultNode,
- message: 'Boolean prop should only be defaulted to false.'
- })
- }
- break
+ case 'default-false': {
+ if (defaultNode.type !== 'Literal' || defaultNode.value !== false) {
+ context.report({
+ node: defaultNode,
+ messageId: 'defaultFalse'
+ })
+ }
+ break
+ }
+ }
+ }
+ return utils.compositingVisitors(
+ utils.executeOnVueComponent(context, (obj) => {
+ processProps(utils.getComponentPropsFromOptions(obj), () => [])
+ }),
+ utils.defineScriptSetupVisitor(context, {
+ onDefinePropsEnter(node, props) {
+ const defaultsByWithDefaults =
+ utils.getWithDefaultsPropExpressions(node)
+ const defaultsByAssignmentPatterns =
+ utils.getDefaultPropExpressionsForPropsDestructure(node)
+ processProps(props, (propName) =>
+ [
+ defaultsByWithDefaults[propName],
+ defaultsByAssignmentPatterns[propName]?.expression
+ ].filter(utils.isDef)
+ )
}
})
- })
+ )
}
}
diff --git a/lib/rules/no-child-content.js b/lib/rules/no-child-content.js
new file mode 100644
index 000000000..df39c758e
--- /dev/null
+++ b/lib/rules/no-child-content.js
@@ -0,0 +1,163 @@
+/**
+ * @author Flo Edelmann
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+const { defineTemplateBodyVisitor } = require('../utils')
+
+/**
+ * @typedef {object} RuleOption
+ * @property {string[]} additionalDirectives
+ */
+
+/**
+ * @param {VNode | Token} node
+ * @returns {boolean}
+ */
+function isWhiteSpaceTextNode(node) {
+ return node.type === 'VText' && node.value.trim() === ''
+}
+
+/**
+ * @param {Position} pos1
+ * @param {Position} pos2
+ * @returns {'less' | 'equal' | 'greater'}
+ */
+function comparePositions(pos1, pos2) {
+ if (
+ pos1.line < pos2.line ||
+ (pos1.line === pos2.line && pos1.column < pos2.column)
+ ) {
+ return 'less'
+ }
+
+ if (
+ pos1.line > pos2.line ||
+ (pos1.line === pos2.line && pos1.column > pos2.column)
+ ) {
+ return 'greater'
+ }
+
+ return 'equal'
+}
+
+/**
+ * @param {(VNode | Token)[]} nodes
+ * @returns {SourceLocation | undefined}
+ */
+function getLocationRange(nodes) {
+ /** @type {Position | undefined} */
+ let start
+ /** @type {Position | undefined} */
+ let end
+
+ for (const node of nodes) {
+ if (!start || comparePositions(node.loc.start, start) === 'less') {
+ start = node.loc.start
+ }
+
+ if (!end || comparePositions(node.loc.end, end) === 'greater') {
+ end = node.loc.end
+ }
+ }
+
+ if (start === undefined || end === undefined) {
+ return undefined
+ }
+
+ return { start, end }
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ "disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text`",
+ categories: ['vue3-essential', 'vue2-essential'],
+ url: 'https://eslint.vuejs.org/rules/no-child-content.html'
+ },
+ fixable: null,
+ hasSuggestions: true,
+ schema: [
+ {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ additionalDirectives: {
+ type: 'array',
+ uniqueItems: true,
+ minItems: 1,
+ items: {
+ type: 'string'
+ }
+ }
+ },
+ required: ['additionalDirectives']
+ }
+ ],
+ messages: {
+ disallowedChildContent:
+ 'Child content is disallowed because it will be overwritten by the v-{{ directiveName }} directive.',
+ removeChildContent: 'Remove child content.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const directives = new Set(['html', 'text'])
+
+ /** @type {RuleOption | undefined} */
+ const option = context.options[0]
+ if (option !== undefined) {
+ for (const directive of option.additionalDirectives) {
+ directives.add(directive)
+ }
+ }
+
+ return defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} directiveNode */
+ 'VAttribute[directive=true]'(directiveNode) {
+ const directiveName = directiveNode.key.name.name
+ const elementNode = directiveNode.parent.parent
+
+ if (elementNode.endTag === null) {
+ return
+ }
+ const sourceCode = context.getSourceCode()
+ const tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore()
+ const elementComments = tokenStore.getTokensBetween(
+ elementNode.startTag,
+ elementNode.endTag,
+ {
+ includeComments: true,
+ filter: (token) => token.type === 'HTMLComment'
+ }
+ )
+
+ const childNodes = [...elementNode.children, ...elementComments]
+
+ if (
+ directives.has(directiveName) &&
+ childNodes.some((childNode) => !isWhiteSpaceTextNode(childNode))
+ ) {
+ context.report({
+ node: elementNode,
+ loc: getLocationRange(childNodes),
+ messageId: 'disallowedChildContent',
+ data: { directiveName },
+ suggest: [
+ {
+ messageId: 'removeChildContent',
+ *fix(fixer) {
+ for (const childNode of childNodes) {
+ yield fixer.remove(childNode)
+ }
+ }
+ }
+ ]
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/no-computed-properties-in-data.js b/lib/rules/no-computed-properties-in-data.js
new file mode 100644
index 000000000..472d45f4d
--- /dev/null
+++ b/lib/rules/no-computed-properties-in-data.js
@@ -0,0 +1,100 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @typedef {import('../utils').VueObjectData} VueObjectData
+ */
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'disallow accessing computed properties in `data`',
+ categories: ['vue3-essential', 'vue2-essential'],
+ url: 'https://eslint.vuejs.org/rules/no-computed-properties-in-data.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ cannotBeUsed:
+ 'The computed property cannot be used in `data()` because it is before initialization.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /** @type {Map}>} */
+ const contextMap = new Map()
+
+ /**
+ * @typedef {object} ScopeStack
+ * @property {ScopeStack | null} upper
+ * @property {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+ */
+ /** @type {ScopeStack | null} */
+ let scopeStack = null
+
+ return utils.compositingVisitors(
+ {
+ /**
+ * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+ */
+ ':function'(node) {
+ scopeStack = {
+ upper: scopeStack,
+ node
+ }
+ },
+ ':function:exit'() {
+ scopeStack = scopeStack && scopeStack.upper
+ }
+ },
+ utils.defineVueVisitor(context, {
+ onVueObjectEnter(node) {
+ const dataProperty = utils.findProperty(node, 'data')
+ if (
+ !dataProperty ||
+ (dataProperty.value.type !== 'FunctionExpression' &&
+ dataProperty.value.type !== 'ArrowFunctionExpression')
+ ) {
+ return
+ }
+ const computedNames = new Set()
+ for (const computed of utils.iterateProperties(
+ node,
+ new Set(['computed'])
+ )) {
+ computedNames.add(computed.name)
+ }
+
+ contextMap.set(node, { data: dataProperty.value, computedNames })
+ },
+ /**
+ * @param {MemberExpression} node
+ * @param {VueObjectData} vueData
+ */
+ MemberExpression(node, vueData) {
+ if (!scopeStack || !utils.isThis(node.object, context)) {
+ return
+ }
+ const ctx = contextMap.get(vueData.node)
+ if (!ctx || ctx.data !== scopeStack.node) {
+ return
+ }
+ const name = utils.getStaticPropertyName(node)
+ if (!name || !ctx.computedNames.has(name)) {
+ return
+ }
+ context.report({
+ node,
+ messageId: 'cannotBeUsed'
+ })
+ }
+ })
+ )
+ }
+}
diff --git a/lib/rules/no-confusing-v-for-v-if.js b/lib/rules/no-confusing-v-for-v-if.js
deleted file mode 100644
index 43951a090..000000000
--- a/lib/rules/no-confusing-v-for-v-if.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * @author Toru Nagashima
- * @copyright 2017 Toru Nagashima. All rights reserved.
- * See LICENSE file in root directory for full license.
- */
-'use strict'
-
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
-const utils = require('../utils')
-
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
-/**
- * Check whether the given `v-if` node is using the variable which is defined by the `v-for` directive.
- * @param {ASTNode} vIf The `v-if` attribute node to check.
- * @returns {boolean} `true` if the `v-if` is using the variable which is defined by the `v-for` directive.
- */
-function isUsingIterationVar(vIf) {
- const element = vIf.parent.parent
- return vIf.value.references.some((reference) =>
- element.variables.some(
- (variable) =>
- variable.id.name === reference.id.name && variable.kind === 'v-for'
- )
- )
-}
-
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
-module.exports = {
- meta: {
- type: 'suggestion',
- docs: {
- description: 'disallow confusing `v-for` and `v-if` on the same element',
- categories: ['vue3-recommended', 'recommended'],
- url: 'https://eslint.vuejs.org/rules/no-confusing-v-for-v-if.html',
- replacedBy: ['no-use-v-if-with-v-for']
- },
- deprecated: true,
- fixable: null,
- schema: []
- },
-
- create(context) {
- return utils.defineTemplateBodyVisitor(context, {
- "VAttribute[directive=true][key.name.name='if']"(node) {
- const element = node.parent.parent
-
- if (utils.hasDirective(element, 'for') && !isUsingIterationVar(node)) {
- context.report({
- node,
- loc: node.loc,
- message: "This 'v-if' should be moved to the wrapper element."
- })
- }
- }
- })
- }
-}
diff --git a/lib/rules/no-console.js b/lib/rules/no-console.js
new file mode 100644
index 000000000..caabebc4d
--- /dev/null
+++ b/lib/rules/no-console.js
@@ -0,0 +1,45 @@
+/**
+ * @author ItMaga
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = utils.wrapCoreRule('no-console', {
+ skipBaseHandlers: true,
+ create(context) {
+ const options = context.options[0] || {}
+ const allowed = options.allow || []
+
+ /**
+ * Copied from the core rule `no-console`.
+ * Checks whether the property name of the given MemberExpression node
+ * is allowed by options or not.
+ * @param {MemberExpression} node The MemberExpression node to check.
+ * @returns {boolean} `true` if the property name of the node is allowed.
+ */
+ function isAllowed(node) {
+ const propertyName = utils.getStaticPropertyName(node)
+
+ return propertyName && allowed.includes(propertyName)
+ }
+
+ return {
+ MemberExpression(node) {
+ if (
+ node.object.type === 'Identifier' &&
+ node.object.name === 'console' &&
+ !isAllowed(node)
+ ) {
+ context.report({
+ node: node.object,
+ loc: node.object.loc,
+ messageId: 'unexpected'
+ })
+ }
+ }
+ }
+ }
+})
diff --git a/lib/rules/no-constant-condition.js b/lib/rules/no-constant-condition.js
new file mode 100644
index 000000000..fc7e83946
--- /dev/null
+++ b/lib/rules/no-constant-condition.js
@@ -0,0 +1,29 @@
+/**
+ * @author Flo Edelmann
+ */
+'use strict'
+
+const { wrapCoreRule } = require('../utils')
+
+const conditionalDirectiveNames = new Set(['v-show', 'v-if', 'v-else-if'])
+
+// eslint-disable-next-line internal/no-invalid-meta
+module.exports = wrapCoreRule('no-constant-condition', {
+ create(_context, { baseHandlers }) {
+ return {
+ VDirectiveKey(node) {
+ if (
+ conditionalDirectiveNames.has(`v-${node.name.name}`) &&
+ node.parent.value &&
+ node.parent.value.expression &&
+ baseHandlers.IfStatement
+ ) {
+ baseHandlers.IfStatement({
+ // @ts-expect-error -- Process expression of VExpressionContainer as IfStatement.
+ test: node.parent.value.expression
+ })
+ }
+ }
+ }
+ }
+})
diff --git a/lib/rules/no-custom-modifiers-on-v-model.js b/lib/rules/no-custom-modifiers-on-v-model.js
index ea80fb7a6..cef3dfaef 100644
--- a/lib/rules/no-custom-modifiers-on-v-model.js
+++ b/lib/rules/no-custom-modifiers-on-v-model.js
@@ -4,28 +4,16 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
const VALID_MODIFIERS = new Set(['lazy', 'number', 'trim'])
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow custom modifiers on v-model used on the component',
- categories: ['essential'],
+ categories: ['vue2-essential'],
url: 'https://eslint.vuejs.org/rules/no-custom-modifiers-on-v-model.html'
},
fixable: null,
@@ -35,6 +23,7 @@ module.exports = {
"'v-model' directives don't support the modifier '{{name}}'."
}
},
+ /** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='model']"(node) {
diff --git a/lib/rules/no-deprecated-data-object-declaration.js b/lib/rules/no-deprecated-data-object-declaration.js
index 02422c7e5..1a9f846c5 100644
--- a/lib/rules/no-deprecated-data-object-declaration.js
+++ b/lib/rules/no-deprecated-data-object-declaration.js
@@ -4,20 +4,22 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
+/** @param {Token} token */
function isOpenParen(token) {
return token.type === 'Punctuator' && token.value === '('
}
+/** @param {Token} token */
function isCloseParen(token) {
return token.type === 'Punctuator' && token.value === ')'
}
+/**
+ * @param {Expression} node
+ * @param {SourceCode} sourceCode
+ */
function getFirstAndLastTokens(node, sourceCode) {
let first = sourceCode.getFirstToken(node)
let last = sourceCode.getLastToken(node)
@@ -35,10 +37,6 @@ function getFirstAndLastTokens(node, sourceCode) {
}
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
@@ -46,8 +44,7 @@ module.exports = {
description:
'disallow using deprecated object declaration on data (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
- url:
- 'https://eslint.vuejs.org/rules/no-deprecated-data-object-declaration.html'
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-data-object-declaration.html'
},
fixable: 'code',
schema: [],
@@ -56,35 +53,34 @@ module.exports = {
"Object declaration on 'data' property is deprecated. Using function declaration instead."
}
},
-
+ /** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
return utils.executeOnVue(context, (obj) => {
- obj.properties
- .filter(
- (p) =>
- p.type === 'Property' &&
- p.key.type === 'Identifier' &&
- p.key.name === 'data' &&
- p.value.type !== 'FunctionExpression' &&
- p.value.type !== 'ArrowFunctionExpression' &&
- p.value.type !== 'Identifier'
- )
- .forEach((p) => {
- context.report({
- node: p,
- messageId: 'objectDeclarationIsDeprecated',
- fix(fixer) {
- const tokens = getFirstAndLastTokens(p.value, sourceCode)
+ const invalidData = utils.findProperty(
+ obj,
+ 'data',
+ (p) =>
+ p.value.type !== 'FunctionExpression' &&
+ p.value.type !== 'ArrowFunctionExpression' &&
+ p.value.type !== 'Identifier'
+ )
+
+ if (invalidData) {
+ context.report({
+ node: invalidData,
+ messageId: 'objectDeclarationIsDeprecated',
+ fix(fixer) {
+ const tokens = getFirstAndLastTokens(invalidData.value, sourceCode)
- return [
- fixer.insertTextBefore(tokens.first, 'function() {\nreturn '),
- fixer.insertTextAfter(tokens.last, ';\n}')
- ]
- }
- })
+ return [
+ fixer.insertTextBefore(tokens.first, 'function() {\nreturn '),
+ fixer.insertTextAfter(tokens.last, ';\n}')
+ ]
+ }
})
+ }
})
}
}
diff --git a/lib/rules/no-deprecated-delete-set.js b/lib/rules/no-deprecated-delete-set.js
new file mode 100644
index 000000000..699c928e1
--- /dev/null
+++ b/lib/rules/no-deprecated-delete-set.js
@@ -0,0 +1,132 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const { ReferenceTracker } = require('@eslint-community/eslint-utils')
+
+/**
+ * @typedef {import('@eslint-community/eslint-utils').TYPES.TraceMap} TraceMap
+ */
+
+/** @type {TraceMap} */
+const deletedImportApisMap = {
+ set: {
+ [ReferenceTracker.CALL]: true
+ },
+ del: {
+ [ReferenceTracker.CALL]: true
+ }
+}
+const deprecatedApis = new Set(['set', 'delete'])
+const deprecatedDollarApis = new Set(['$set', '$delete'])
+
+/**
+ * @param {Expression|Super} node
+ */
+function isVue(node) {
+ return node.type === 'Identifier' && node.name === 'Vue'
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+)',
+ categories: ['vue3-essential'],
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-delete-set.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ deprecated: 'The `$delete`, `$set` is deprecated.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /**
+ * @param {Identifier} identifier
+ * @param {RuleContext} context
+ * @returns {CallExpression|undefined}
+ */
+ function getVueDeprecatedCallExpression(identifier, context) {
+ // Instance API: this.$set()
+ if (
+ deprecatedDollarApis.has(identifier.name) &&
+ identifier.parent.type === 'MemberExpression' &&
+ utils.isThis(identifier.parent.object, context) &&
+ identifier.parent.parent.type === 'CallExpression' &&
+ identifier.parent.parent.callee === identifier.parent
+ ) {
+ return identifier.parent.parent
+ }
+
+ // Vue 2 Global API: Vue.set()
+ if (
+ deprecatedApis.has(identifier.name) &&
+ identifier.parent.type === 'MemberExpression' &&
+ isVue(identifier.parent.object) &&
+ identifier.parent.parent.type === 'CallExpression' &&
+ identifier.parent.parent.callee === identifier.parent
+ ) {
+ return identifier.parent.parent
+ }
+
+ return undefined
+ }
+
+ const nodeVisitor = {
+ /** @param {Identifier} node */
+ Identifier(node) {
+ const callExpression = getVueDeprecatedCallExpression(node, context)
+ if (!callExpression) {
+ return
+ }
+
+ context.report({
+ node,
+ messageId: 'deprecated'
+ })
+ }
+ }
+
+ return utils.compositingVisitors(
+ utils.defineVueVisitor(context, nodeVisitor),
+ utils.defineScriptSetupVisitor(context, nodeVisitor),
+ {
+ /** @param {Program} node */
+ Program(node) {
+ const tracker = new ReferenceTracker(utils.getScope(context, node))
+
+ // import { set } from 'vue'; set()
+ const esmTraceMap = {
+ vue: {
+ [ReferenceTracker.ESM]: true,
+ ...deletedImportApisMap
+ }
+ }
+ // const { set } = require('vue'); set()
+ const cjsTraceMap = {
+ vue: {
+ ...deletedImportApisMap
+ }
+ }
+
+ for (const { node } of [
+ ...tracker.iterateEsmReferences(esmTraceMap),
+ ...tracker.iterateCjsReferences(cjsTraceMap)
+ ]) {
+ const refNode = /** @type {CallExpression} */ (node)
+ context.report({
+ node: refNode.callee,
+ messageId: 'deprecated'
+ })
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/lib/rules/no-deprecated-destroyed-lifecycle.js b/lib/rules/no-deprecated-destroyed-lifecycle.js
new file mode 100644
index 000000000..98986cd2e
--- /dev/null
+++ b/lib/rules/no-deprecated-destroyed-lifecycle.js
@@ -0,0 +1,78 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @param {RuleFixer} fixer
+ * @param {Property} property
+ * @param {string} newName
+ */
+function fix(fixer, property, newName) {
+ if (property.computed) {
+ if (
+ property.key.type === 'Literal' ||
+ property.key.type === 'TemplateLiteral'
+ ) {
+ return fixer.replaceTextRange(
+ [property.key.range[0] + 1, property.key.range[1] - 1],
+ newName
+ )
+ }
+ return null
+ }
+ if (property.shorthand) {
+ return fixer.insertTextBefore(property.key, `${newName}:`)
+ }
+ return fixer.replaceText(property.key, newName)
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+)',
+ categories: ['vue3-essential'],
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-destroyed-lifecycle.html'
+ },
+ fixable: 'code',
+ schema: [],
+ messages: {
+ deprecatedDestroyed:
+ 'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
+ deprecatedBeforeDestroy:
+ 'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ return utils.executeOnVue(context, (obj) => {
+ const destroyed = utils.findProperty(obj, 'destroyed')
+
+ if (destroyed) {
+ context.report({
+ node: destroyed.key,
+ messageId: 'deprecatedDestroyed',
+ fix(fixer) {
+ return fix(fixer, destroyed, 'unmounted')
+ }
+ })
+ }
+
+ const beforeDestroy = utils.findProperty(obj, 'beforeDestroy')
+ if (beforeDestroy) {
+ context.report({
+ node: beforeDestroy.key,
+ messageId: 'deprecatedBeforeDestroy',
+ fix(fixer) {
+ return fix(fixer, beforeDestroy, 'beforeUnmount')
+ }
+ })
+ }
+ })
+ }
+}
diff --git a/lib/rules/no-deprecated-dollar-listeners-api.js b/lib/rules/no-deprecated-dollar-listeners-api.js
index 7c60cbbe5..2db80a116 100644
--- a/lib/rules/no-deprecated-dollar-listeners-api.js
+++ b/lib/rules/no-deprecated-dollar-listeners-api.js
@@ -4,24 +4,15 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow using deprecated `$listeners` (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
- url:
- 'https://eslint.vuejs.org/rules/no-deprecated-dollar-listeners-api.html'
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-dollar-listeners-api.html'
},
fixable: null,
schema: [],
@@ -29,7 +20,7 @@ module.exports = {
deprecated: 'The `$listeners` is deprecated.'
}
},
-
+ /** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(
context,
diff --git a/lib/rules/no-deprecated-dollar-scopedslots-api.js b/lib/rules/no-deprecated-dollar-scopedslots-api.js
new file mode 100644
index 000000000..cca25942c
--- /dev/null
+++ b/lib/rules/no-deprecated-dollar-scopedslots-api.js
@@ -0,0 +1,70 @@
+/**
+ * @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:
+ 'disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+)',
+ categories: ['vue3-essential'],
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-dollar-scopedslots-api.html'
+ },
+ fixable: 'code',
+ schema: [],
+ messages: {
+ deprecated: 'The `$scopedSlots` is deprecated.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ return utils.defineTemplateBodyVisitor(
+ context,
+ {
+ VExpressionContainer(node) {
+ for (const reference of node.references) {
+ if (reference.variable != null) {
+ // Not vm reference
+ continue
+ }
+ if (reference.id.name === '$scopedSlots') {
+ context.report({
+ node: reference.id,
+ messageId: 'deprecated',
+ fix(fixer) {
+ return fixer.replaceText(reference.id, '$slots')
+ }
+ })
+ }
+ }
+ }
+ },
+ utils.defineVueVisitor(context, {
+ MemberExpression(node) {
+ if (
+ node.property.type !== 'Identifier' ||
+ node.property.name !== '$scopedSlots'
+ ) {
+ return
+ }
+ if (!utils.isThis(node.object, context)) {
+ return
+ }
+
+ context.report({
+ node: node.property,
+ messageId: 'deprecated',
+ fix(fixer) {
+ return fixer.replaceText(node.property, '$slots')
+ }
+ })
+ }
+ })
+ )
+ }
+}
diff --git a/lib/rules/no-deprecated-events-api.js b/lib/rules/no-deprecated-events-api.js
index 05d50bac1..fa602f335 100644
--- a/lib/rules/no-deprecated-events-api.js
+++ b/lib/rules/no-deprecated-events-api.js
@@ -4,16 +4,8 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
@@ -29,15 +21,29 @@ module.exports = {
'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.'
}
},
-
+ /** @param {RuleContext} context */
create(context) {
return utils.defineVueVisitor(context, {
- 'CallExpression > MemberExpression'(node) {
- const call = node.parent
+ /** @param {MemberExpression & ({parent: CallExpression} | {parent: ChainExpression & {parent: CallExpression}})} node */
+ 'CallExpression > MemberExpression, CallExpression > ChainExpression > MemberExpression'(
+ node
+ ) {
+ const call =
+ node.parent.type === 'ChainExpression'
+ ? node.parent.parent
+ : node.parent
+
+ if (call.optional) {
+ // It is OK because checking whether it is deprecated.
+ // e.g. `this.$on?.()`
+ return
+ }
+
if (
- call.callee !== node ||
- node.property.type !== 'Identifier' ||
- !['$on', '$off', '$once'].includes(node.property.name)
+ utils.skipChainExpression(call.callee) !== node ||
+ !['$on', '$off', '$once'].includes(
+ utils.getStaticPropertyName(node) || ''
+ )
) {
return
}
diff --git a/lib/rules/no-deprecated-filter.js b/lib/rules/no-deprecated-filter.js
index 0229d6e2e..5e7967385 100644
--- a/lib/rules/no-deprecated-filter.js
+++ b/lib/rules/no-deprecated-filter.js
@@ -4,16 +4,8 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
@@ -29,7 +21,7 @@ module.exports = {
noDeprecatedFilter: 'Filters are deprecated.'
}
},
-
+ /** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
VFilterSequenceExpression(node) {
diff --git a/lib/rules/no-deprecated-functional-template.js b/lib/rules/no-deprecated-functional-template.js
index cfaa8d957..5b3b9dea1 100644
--- a/lib/rules/no-deprecated-functional-template.js
+++ b/lib/rules/no-deprecated-functional-template.js
@@ -4,16 +4,8 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
@@ -21,8 +13,7 @@ module.exports = {
description:
'disallow using deprecated the `functional` template (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
- url:
- 'https://eslint.vuejs.org/rules/no-deprecated-functional-template.html'
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-functional-template.html'
},
fixable: null,
schema: [],
@@ -30,7 +21,10 @@ module.exports = {
unexpected: 'The `functional` template are deprecated.'
}
},
-
+ /**
+ * @param {RuleContext} context - The rule context.
+ * @returns {RuleListener} AST event handlers.
+ */
create(context) {
return {
Program(program) {
diff --git a/lib/rules/no-deprecated-html-element-is.js b/lib/rules/no-deprecated-html-element-is.js
index d62c3b479..5253ef2b7 100644
--- a/lib/rules/no-deprecated-html-element-is.js
+++ b/lib/rules/no-deprecated-html-element-is.js
@@ -4,16 +4,8 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
@@ -29,17 +21,37 @@ module.exports = {
unexpected: 'The `is` attribute on HTML element are deprecated.'
}
},
-
+ /** @param {RuleContext} context */
create(context) {
+ /** @param {VElement} node */
+ function isValidElement(node) {
+ return (
+ !utils.isHtmlWellKnownElementName(node.rawName) &&
+ !utils.isSvgWellKnownElementName(node.rawName) &&
+ !utils.isMathWellKnownElementName(node.rawName)
+ )
+ }
return utils.defineTemplateBodyVisitor(context, {
- "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is'], VAttribute[directive=false][key.name='is']"(
+ /** @param {VDirective} node */
+ "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']"(
node
) {
- const element = node.parent.parent
- if (
- !utils.isHtmlWellKnownElementName(element.rawName) &&
- !utils.isSvgWellKnownElementName(element.rawName)
- ) {
+ if (isValidElement(node.parent.parent)) {
+ return
+ }
+ context.report({
+ node,
+ loc: node.loc,
+ messageId: 'unexpected'
+ })
+ },
+ /** @param {VAttribute} node */
+ "VAttribute[directive=false][key.name='is']"(node) {
+ if (isValidElement(node.parent.parent)) {
+ return
+ }
+ if (node.value && node.value.value.startsWith('vue:')) {
+ // Usage on native elements 3.1+
return
}
context.report({
diff --git a/lib/rules/no-deprecated-inline-template.js b/lib/rules/no-deprecated-inline-template.js
index 69bdfda46..36923f2c2 100644
--- a/lib/rules/no-deprecated-inline-template.js
+++ b/lib/rules/no-deprecated-inline-template.js
@@ -4,16 +4,8 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
@@ -29,9 +21,10 @@ module.exports = {
unexpected: '`inline-template` are deprecated.'
}
},
-
+ /** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VIdentifier} node */
"VAttribute[directive=false] > VIdentifier[rawName='inline-template']"(
node
) {
diff --git a/lib/rules/no-deprecated-model-definition.js b/lib/rules/no-deprecated-model-definition.js
new file mode 100644
index 000000000..6c7e4dd8d
--- /dev/null
+++ b/lib/rules/no-deprecated-model-definition.js
@@ -0,0 +1,133 @@
+/**
+ * @author Flo Edelmann
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+const allowedPropNames = new Set(['modelValue', 'model-value'])
+const allowedEventNames = new Set(['update:modelValue', 'update:model-value'])
+
+/**
+ * @param {ObjectExpression} node
+ * @param {string} key
+ * @returns {Literal | TemplateLiteral | undefined}
+ */
+function findPropertyValue(node, key) {
+ const property = node.properties.find(
+ (property) =>
+ property.type === 'Property' &&
+ property.key.type === 'Identifier' &&
+ property.key.name === key
+ )
+ if (
+ !property ||
+ property.type !== 'Property' ||
+ !utils.isStringLiteral(property.value)
+ ) {
+ return undefined
+ }
+ return property.value
+}
+
+/**
+ * @param {RuleFixer} fixer
+ * @param {Literal | TemplateLiteral} node
+ * @param {string} text
+ */
+function replaceLiteral(fixer, node, text) {
+ return fixer.replaceTextRange([node.range[0] + 1, node.range[1] - 1], text)
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'disallow deprecated `model` definition (in Vue.js 3.0.0+)',
+ categories: ['vue3-essential'],
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-model-definition.html'
+ },
+ fixable: null,
+ hasSuggestions: true,
+ schema: [
+ {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ allowVue3Compat: {
+ type: 'boolean'
+ }
+ }
+ }
+ ],
+ messages: {
+ deprecatedModel: '`model` definition is deprecated.',
+ vue3Compat:
+ '`model` definition is deprecated. You may use the Vue 3-compatible `modelValue`/`update:modelValue` though.',
+ changeToModelValue: 'Change to `modelValue`/`update:modelValue`.',
+ changeToKebabModelValue: 'Change to `model-value`/`update:model-value`.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const allowVue3Compat = Boolean(context.options[0]?.allowVue3Compat)
+
+ return utils.executeOnVue(context, (obj) => {
+ const modelProperty = utils.findProperty(obj, 'model')
+ if (!modelProperty || modelProperty.value.type !== 'ObjectExpression') {
+ return
+ }
+
+ if (!allowVue3Compat) {
+ context.report({
+ node: modelProperty,
+ messageId: 'deprecatedModel'
+ })
+ return
+ }
+
+ const propName = findPropertyValue(modelProperty.value, 'prop')
+ const eventName = findPropertyValue(modelProperty.value, 'event')
+
+ if (
+ !propName ||
+ !eventName ||
+ !allowedPropNames.has(
+ utils.getStringLiteralValue(propName, true) ?? ''
+ ) ||
+ !allowedEventNames.has(
+ utils.getStringLiteralValue(eventName, true) ?? ''
+ )
+ ) {
+ context.report({
+ node: modelProperty,
+ messageId: 'vue3Compat',
+ suggest:
+ propName && eventName
+ ? [
+ {
+ messageId: 'changeToModelValue',
+ *fix(fixer) {
+ const newPropName = 'modelValue'
+ const newEventName = 'update:modelValue'
+ yield replaceLiteral(fixer, propName, newPropName)
+ yield replaceLiteral(fixer, eventName, newEventName)
+ }
+ },
+ {
+ messageId: 'changeToKebabModelValue',
+ *fix(fixer) {
+ const newPropName = 'model-value'
+ const newEventName = 'update:model-value'
+ yield replaceLiteral(fixer, propName, newPropName)
+ yield replaceLiteral(fixer, eventName, newEventName)
+ }
+ }
+ ]
+ : []
+ })
+ }
+ })
+ }
+}
diff --git a/lib/rules/no-deprecated-props-default-this.js b/lib/rules/no-deprecated-props-default-this.js
new file mode 100644
index 000000000..5583b12ad
--- /dev/null
+++ b/lib/rules/no-deprecated-props-default-this.js
@@ -0,0 +1,126 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @param {Expression|SpreadElement|null} node
+ */
+function isFunctionIdentifier(node) {
+ return node && node.type === 'Identifier' && node.name === 'Function'
+}
+
+/**
+ * @param {Expression} node
+ * @returns {boolean}
+ */
+function hasFunctionType(node) {
+ if (isFunctionIdentifier(node)) {
+ return true
+ }
+ if (node.type === 'ArrayExpression') {
+ return node.elements.some(isFunctionIdentifier)
+ }
+ return false
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'disallow deprecated `this` access in props default function (in Vue.js 3.0.0+)',
+ categories: ['vue3-essential'],
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-props-default-this.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ deprecated:
+ 'Props default value factory functions no longer have access to `this`.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /**
+ * @typedef {object} ScopeStack
+ * @property {ScopeStack | null} upper
+ * @property {FunctionExpression | FunctionDeclaration} node
+ * @property {boolean} propDefault
+ */
+ /** @type {Set} */
+ const propsDefault = new Set()
+ /** @type {ScopeStack | null} */
+ let scopeStack = null
+
+ /**
+ * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+ */
+ function onFunctionEnter(node) {
+ if (node.type === 'ArrowFunctionExpression') {
+ return
+ }
+ if (scopeStack) {
+ scopeStack = {
+ upper: scopeStack,
+ node,
+ propDefault: false
+ }
+ } else if (node.type === 'FunctionExpression' && propsDefault.has(node)) {
+ scopeStack = {
+ upper: scopeStack,
+ node,
+ propDefault: true
+ }
+ }
+ }
+
+ /**
+ * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+ */
+ function onFunctionExit(node) {
+ if (scopeStack && scopeStack.node === node) {
+ scopeStack = scopeStack.upper
+ }
+ }
+
+ return utils.defineVueVisitor(context, {
+ onVueObjectEnter(node) {
+ for (const prop of utils.getComponentPropsFromOptions(node)) {
+ if (prop.type !== 'object') {
+ continue
+ }
+ if (prop.value.type !== 'ObjectExpression') {
+ continue
+ }
+ const def = utils.findProperty(prop.value, 'default')
+ if (!def) {
+ continue
+ }
+ const type = utils.findProperty(prop.value, 'type')
+ if (type && hasFunctionType(type.value)) {
+ // ignore function type
+ continue
+ }
+ if (def.value.type !== 'FunctionExpression') {
+ continue
+ }
+ propsDefault.add(def.value)
+ }
+ },
+ ':function': onFunctionEnter,
+ ':function:exit': onFunctionExit,
+ ThisExpression(node) {
+ if (scopeStack && scopeStack.propDefault) {
+ context.report({
+ node,
+ messageId: 'deprecated'
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/no-deprecated-router-link-tag-prop.js b/lib/rules/no-deprecated-router-link-tag-prop.js
new file mode 100644
index 000000000..e2853f2af
--- /dev/null
+++ b/lib/rules/no-deprecated-router-link-tag-prop.js
@@ -0,0 +1,93 @@
+/**
+ * @author Marton Csordas
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+/** @param {RuleContext} context */
+function getComponentNames(context) {
+ let components = ['RouterLink']
+
+ if (context.options[0] && context.options[0].components) {
+ components = context.options[0].components
+ }
+
+ return new Set(
+ components.flatMap((component) => [
+ casing.kebabCase(component),
+ casing.pascalCase(component)
+ ])
+ )
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'disallow using deprecated `tag` property on `RouterLink` (in Vue.js 3.0.0+)',
+ categories: ['vue3-essential'],
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-router-link-tag-prop.html'
+ },
+ fixable: null,
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ components: {
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ uniqueItems: true,
+ minItems: 1
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ deprecated:
+ "'tag' property on '{{element}}' component is deprecated. Use scoped slots instead."
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const components = getComponentNames(context)
+
+ return utils.defineTemplateBodyVisitor(context, {
+ VElement(node) {
+ if (!components.has(node.rawName)) return
+
+ /** @type VIdentifier | null */
+ let tagKey = null
+
+ const tagAttr = utils.getAttribute(node, 'tag')
+ if (tagAttr) {
+ tagKey = tagAttr.key
+ } else {
+ const directive = utils.getDirective(node, 'bind', 'tag')
+ if (directive) {
+ const arg = directive.key.argument
+ if (arg && arg.type === 'VIdentifier') {
+ tagKey = arg
+ }
+ }
+ }
+
+ if (tagKey) {
+ context.report({
+ node: tagKey,
+ messageId: 'deprecated',
+ data: {
+ element: node.rawName
+ }
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/no-deprecated-scope-attribute.js b/lib/rules/no-deprecated-scope-attribute.js
index 24ef41042..80bcc02bd 100644
--- a/lib/rules/no-deprecated-scope-attribute.js
+++ b/lib/rules/no-deprecated-scope-attribute.js
@@ -15,16 +15,17 @@ module.exports = {
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-scope-attribute.html'
},
+ // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
fixable: 'code',
schema: [],
messages: {
forbiddenScopeAttribute: '`scope` attributes are deprecated.'
}
},
+ /** @param {RuleContext} context */
create(context) {
- const templateBodyVisitor = scopeAttribute.createTemplateBodyVisitor(
- context
- )
+ const templateBodyVisitor =
+ scopeAttribute.createTemplateBodyVisitor(context)
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
}
}
diff --git a/lib/rules/no-deprecated-slot-attribute.js b/lib/rules/no-deprecated-slot-attribute.js
index c7174c12a..44f3b68c1 100644
--- a/lib/rules/no-deprecated-slot-attribute.js
+++ b/lib/rules/no-deprecated-slot-attribute.js
@@ -15,12 +15,31 @@ module.exports = {
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-slot-attribute.html'
},
+ // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
fixable: 'code',
- schema: [],
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ ignore: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true
+ },
+ ignoreParents: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true
+ }
+ },
+ additionalProperties: false
+ }
+ ],
messages: {
forbiddenSlotAttribute: '`slot` attributes are deprecated.'
}
},
+ /** @param {RuleContext} context */
create(context) {
const templateBodyVisitor = slotAttribute.createTemplateBodyVisitor(context)
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
diff --git a/lib/rules/no-deprecated-slot-scope-attribute.js b/lib/rules/no-deprecated-slot-scope-attribute.js
index 1356016ed..20a4f00bd 100644
--- a/lib/rules/no-deprecated-slot-scope-attribute.js
+++ b/lib/rules/no-deprecated-slot-scope-attribute.js
@@ -14,15 +14,16 @@ module.exports = {
description:
'disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+)',
categories: ['vue3-essential'],
- url:
- 'https://eslint.vuejs.org/rules/no-deprecated-slot-scope-attribute.html'
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-slot-scope-attribute.html'
},
+ // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
fixable: 'code',
schema: [],
messages: {
forbiddenSlotScopeAttribute: '`slot-scope` are deprecated.'
}
},
+ /** @param {RuleContext} context */
create(context) {
const templateBodyVisitor = slotScopeAttribute.createTemplateBodyVisitor(
context,
diff --git a/lib/rules/no-deprecated-v-bind-sync.js b/lib/rules/no-deprecated-v-bind-sync.js
index b84ebf4de..f52c1aead 100644
--- a/lib/rules/no-deprecated-v-bind-sync.js
+++ b/lib/rules/no-deprecated-v-bind-sync.js
@@ -4,16 +4,8 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
@@ -30,6 +22,7 @@ module.exports = {
"'.sync' modifier on 'v-bind' directive is deprecated. Use 'v-model:propName' instead."
}
},
+ /** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='bind']"(node) {
@@ -38,11 +31,14 @@ module.exports = {
node,
loc: node.loc,
messageId: 'syncModifierIsDeprecated',
- fix: (fixer) => {
- const isUsingSpreadSyntax = node.key.argument == null
- const hasMultipleModifiers = node.key.modifiers.length > 1
- if (isUsingSpreadSyntax || hasMultipleModifiers) {
- return
+ fix(fixer) {
+ if (node.key.argument == null) {
+ // is using spread syntax
+ return null
+ }
+ if (node.key.modifiers.length > 1) {
+ // has multiple modifiers
+ return null
}
const bindArgument = context
diff --git a/lib/rules/no-deprecated-v-is.js b/lib/rules/no-deprecated-v-is.js
new file mode 100644
index 000000000..c497b0120
--- /dev/null
+++ b/lib/rules/no-deprecated-v-is.js
@@ -0,0 +1,29 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const vIs = require('./syntaxes/v-is')
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'disallow deprecated `v-is` directive (in Vue.js 3.1.0+)',
+ categories: ['vue3-essential'],
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-v-is.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ forbiddenVIs: '`v-is` directive is deprecated.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const templateBodyVisitor = vIs.createTemplateBodyVisitor(context)
+ return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
+ }
+}
diff --git a/lib/rules/no-deprecated-v-on-native-modifier.js b/lib/rules/no-deprecated-v-on-native-modifier.js
index 16f1987c5..929f8d1d8 100644
--- a/lib/rules/no-deprecated-v-on-native-modifier.js
+++ b/lib/rules/no-deprecated-v-on-native-modifier.js
@@ -4,16 +4,8 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
@@ -21,8 +13,7 @@ module.exports = {
description:
'disallow using deprecated `.native` modifiers (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
- url:
- 'https://eslint.vuejs.org/rules/no-deprecated-v-on-native-modifier.html'
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-v-on-native-modifier.html'
},
fixable: null,
schema: [],
@@ -30,9 +21,10 @@ module.exports = {
deprecated: "'.native' modifier on 'v-on' directive is deprecated."
}
},
-
+ /** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VIdentifier & {parent:VDirectiveKey} } node */
"VAttribute[directive=true][key.name.name='on'] > VDirectiveKey > VIdentifier[name='native']"(
node
) {
diff --git a/lib/rules/no-deprecated-v-on-number-modifiers.js b/lib/rules/no-deprecated-v-on-number-modifiers.js
index bf1fb88bd..1fdcdeaac 100644
--- a/lib/rules/no-deprecated-v-on-number-modifiers.js
+++ b/lib/rules/no-deprecated-v-on-number-modifiers.js
@@ -4,17 +4,9 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
const keyCodeToKey = require('../utils/keycode-to-key')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
@@ -22,8 +14,7 @@ module.exports = {
description:
'disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
- url:
- 'https://eslint.vuejs.org/rules/no-deprecated-v-on-number-modifiers.html'
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-v-on-number-modifiers.html'
},
fixable: 'code',
schema: [],
@@ -32,23 +23,24 @@ module.exports = {
"'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."
}
},
-
+ /** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirectiveKey} node */
"VAttribute[directive=true][key.name.name='on'] > VDirectiveKey"(node) {
const modifier = node.modifiers.find((mod) =>
- Number.isInteger(parseInt(mod.name, 10))
+ Number.isInteger(Number.parseInt(mod.name, 10))
)
if (!modifier) return
- const keyCodes = parseInt(modifier.name, 10)
+ const keyCodes = Number.parseInt(modifier.name, 10)
if (keyCodes > 9 || keyCodes < 0) {
context.report({
node: modifier,
messageId: 'numberModifierIsDeprecated',
- fix: (fixer) => {
+ fix(fixer) {
const key = keyCodeToKey[keyCodes]
- if (!key) return
+ if (!key) return null
return fixer.replaceText(modifier, `${key}`)
}
diff --git a/lib/rules/no-deprecated-vue-config-keycodes.js b/lib/rules/no-deprecated-vue-config-keycodes.js
index 0de109ef1..68af8430a 100644
--- a/lib/rules/no-deprecated-vue-config-keycodes.js
+++ b/lib/rules/no-deprecated-vue-config-keycodes.js
@@ -4,9 +4,7 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+const utils = require('../utils')
module.exports = {
meta: {
@@ -15,8 +13,7 @@ module.exports = {
description:
'disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
- url:
- 'https://eslint.vuejs.org/rules/no-deprecated-vue-config-keycodes.html'
+ url: 'https://eslint.vuejs.org/rules/no-deprecated-vue-config-keycodes.html'
},
fixable: null,
schema: [],
@@ -24,13 +21,14 @@ module.exports = {
unexpected: '`Vue.config.keyCodes` are deprecated.'
}
},
-
+ /** @param {RuleContext} context */
create(context) {
return {
+ /** @param {MemberExpression} node */
"MemberExpression[property.type='Identifier'][property.name='keyCodes']"(
node
) {
- const config = node.object
+ const config = utils.skipChainExpression(node.object)
if (
config.type !== 'MemberExpression' ||
config.property.type !== 'Identifier' ||
diff --git a/lib/rules/no-dupe-keys.js b/lib/rules/no-dupe-keys.js
index dfbebdedf..ecfa787cf 100644
--- a/lib/rules/no-dupe-keys.js
+++ b/lib/rules/no-dupe-keys.js
@@ -4,23 +4,96 @@
*/
'use strict'
+const { findVariable } = require('@eslint-community/eslint-utils')
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+/**
+ * @typedef {import('../utils').GroupName} GroupName
+ * @typedef {import('eslint').Scope.Variable} Variable
+ * @typedef {import('../utils').ComponentProp} ComponentProp
+ */
+/** @type {GroupName[]} */
const GROUP_NAMES = ['props', 'computed', 'data', 'methods', 'setup']
+/**
+ * Gets the props pattern node from given `defineProps()` node
+ * @param {CallExpression} node
+ * @returns {Pattern|null}
+ */
+function getPropsPattern(node) {
+ let target = node
+ if (
+ target.parent &&
+ target.parent.type === 'CallExpression' &&
+ target.parent.arguments[0] === target &&
+ target.parent.callee.type === 'Identifier' &&
+ target.parent.callee.name === 'withDefaults'
+ ) {
+ target = target.parent
+ }
+
+ if (
+ !target.parent ||
+ target.parent.type !== 'VariableDeclarator' ||
+ target.parent.init !== target
+ ) {
+ return null
+ }
+ return target.parent.id
+}
+
+/**
+ * Checks whether the initialization of the given variable declarator node contains one of the references.
+ * @param {VariableDeclarator} node
+ * @param {ESNode[]} references
+ */
+function isInsideInitializer(node, references) {
+ const init = node.init
+ if (!init) {
+ return false
+ }
+ return references.some(
+ (id) => init.range[0] <= id.range[0] && id.range[1] <= init.range[1]
+ )
+}
+
+/**
+ * Collects all renamed props from a pattern
+ * @param {Pattern | null} pattern - The destructuring pattern
+ * @returns {Set} - Set of prop names that have been renamed
+ */
+function collectRenamedProps(pattern) {
+ const renamedProps = new Set()
+
+ if (!pattern || pattern.type !== 'ObjectPattern') {
+ return renamedProps
+ }
+
+ for (const prop of pattern.properties) {
+ if (prop.type !== 'Property') continue
+
+ if (
+ prop.key.type === 'Identifier' &&
+ prop.value.type === 'Identifier' &&
+ prop.key.name !== prop.value.name
+ ) {
+ renamedProps.add(prop.key.name)
+ }
+ }
+
+ return renamedProps
+}
+
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow duplication of field names',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/no-dupe-keys.html'
},
- fixable: null, // or "code" or "whitespace"
+ fixable: null,
schema: [
{
type: 'object',
@@ -31,34 +104,105 @@ module.exports = {
},
additionalProperties: false
}
- ]
+ ],
+ messages: {
+ duplicateKey:
+ "Duplicate key '{{name}}'. May cause name collision in script or template tag."
+ }
},
-
+ /** @param {RuleContext} context */
create(context) {
const options = context.options[0] || {}
- const groups = new Set(GROUP_NAMES.concat(options.groups || []))
-
- // ----------------------------------------------------------------------
- // Public
- // ----------------------------------------------------------------------
-
- return utils.executeOnVue(context, (obj) => {
- const usedNames = []
- const properties = utils.iterateProperties(obj, groups)
-
- for (const o of properties) {
- if (usedNames.indexOf(o.name) !== -1) {
- context.report({
- node: o.node,
- message: "Duplicated key '{{name}}'.",
- data: {
- name: o.name
+ const groups = new Set([...GROUP_NAMES, ...(options.groups || [])])
+
+ return utils.compositingVisitors(
+ utils.executeOnVue(context, (obj) => {
+ const properties = utils.iterateProperties(obj, groups)
+ /** @type {Set} */
+ const usedNames = new Set()
+ for (const o of properties) {
+ if (usedNames.has(o.name)) {
+ context.report({
+ node: o.node,
+ messageId: 'duplicateKey',
+ data: {
+ name: o.name
+ }
+ })
+ }
+
+ usedNames.add(o.name)
+ }
+ }),
+ utils.defineScriptSetupVisitor(context, {
+ onDefinePropsEnter(node, props) {
+ const propsNode = getPropsPattern(node)
+ const propReferences = [
+ ...(propsNode ? extractReferences(propsNode) : []),
+ node
+ ]
+
+ const renamedProps = collectRenamedProps(propsNode)
+
+ for (const prop of props) {
+ if (!prop.propName) continue
+
+ if (renamedProps.has(prop.propName)) {
+ continue
}
- })
+
+ const variable = findVariable(
+ utils.getScope(context, node),
+ prop.propName
+ )
+ if (!variable || variable.defs.length === 0) continue
+
+ if (
+ variable.defs.some((def) => {
+ if (def.type !== 'Variable') return false
+ return isInsideInitializer(def.node, propReferences)
+ })
+ ) {
+ continue
+ }
+
+ context.report({
+ node: variable.defs[0].node,
+ messageId: 'duplicateKey',
+ data: {
+ name: prop.propName
+ }
+ })
+ }
}
+ })
+ )
- usedNames.push(o.name)
+ /**
+ * Extracts references from the given node.
+ * @param {Pattern} node
+ * @returns {Identifier[]} References
+ */
+ function extractReferences(node) {
+ if (node.type === 'Identifier') {
+ const variable = findVariable(utils.getScope(context, node), node)
+ if (!variable) {
+ return []
+ }
+ return variable.references.map((ref) => ref.identifier)
+ }
+ if (node.type === 'ObjectPattern') {
+ return node.properties.flatMap((prop) =>
+ extractReferences(prop.type === 'Property' ? prop.value : prop)
+ )
+ }
+ if (node.type === 'AssignmentPattern') {
+ return extractReferences(node.left)
+ }
+ if (node.type === 'RestElement') {
+ return extractReferences(node.argument)
}
- })
+ return []
+ }
}
}
diff --git a/lib/rules/no-dupe-v-else-if.js b/lib/rules/no-dupe-v-else-if.js
new file mode 100644
index 000000000..a20d5256b
--- /dev/null
+++ b/lib/rules/no-dupe-v-else-if.js
@@ -0,0 +1,181 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @typedef {NonNullable} VExpression
+ */
+/**
+ * @typedef {object} OrOperands
+ * @property {VExpression} OrOperands.node
+ * @property {AndOperands[]} OrOperands.operands
+ *
+ * @typedef {object} AndOperands
+ * @property {VExpression} AndOperands.node
+ * @property {VExpression[]} AndOperands.operands
+ */
+/**
+ * Splits the given node by the given logical operator.
+ * @param {string} operator Logical operator `||` or `&&`.
+ * @param {VExpression} node The node to split.
+ * @returns {VExpression[]} Array of conditions that makes the node when joined by the operator.
+ */
+function splitByLogicalOperator(operator, node) {
+ if (node.type === 'LogicalExpression' && node.operator === operator) {
+ return [
+ ...splitByLogicalOperator(operator, node.left),
+ ...splitByLogicalOperator(operator, node.right)
+ ]
+ }
+ return [node]
+}
+
+/**
+ * @param {VExpression} node
+ */
+function splitByOr(node) {
+ return splitByLogicalOperator('||', node)
+}
+/**
+ * @param {VExpression} node
+ */
+function splitByAnd(node) {
+ return splitByLogicalOperator('&&', node)
+}
+
+/**
+ * @param {VExpression} node
+ * @returns {OrOperands}
+ */
+function buildOrOperands(node) {
+ const orOperands = splitByOr(node)
+ return {
+ node,
+ operands: orOperands.map((orOperand) => {
+ const andOperands = splitByAnd(orOperand)
+ return {
+ node: orOperand,
+ operands: andOperands
+ }
+ })
+ }
+}
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'disallow duplicate conditions in `v-if` / `v-else-if` chains',
+ categories: ['vue3-essential', 'vue2-essential'],
+ url: 'https://eslint.vuejs.org/rules/no-dupe-v-else-if.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ unexpected:
+ 'This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const sourceCode = context.getSourceCode()
+ const tokenStore =
+ sourceCode.parserServices.getTemplateBodyTokenStore &&
+ sourceCode.parserServices.getTemplateBodyTokenStore()
+ /**
+ * Determines whether the two given nodes are considered to be equal. In particular, given that the nodes
+ * represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators.
+ * @param {VExpression} a First node.
+ * @param {VExpression} 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
+ }
+
+ if (
+ a.type === 'LogicalExpression' &&
+ b.type === 'LogicalExpression' &&
+ (a.operator === '||' || a.operator === '&&') &&
+ a.operator === b.operator
+ ) {
+ return (
+ (equal(a.left, b.left) && equal(a.right, b.right)) ||
+ (equal(a.left, b.right) && equal(a.right, b.left))
+ )
+ }
+
+ return utils.equalTokens(a, b, tokenStore)
+ }
+
+ /**
+ * Determines whether the first given AndOperands is a subset of the second given AndOperands.
+ *
+ * e.g. A: (a && b), B: (a && b && c): B is a subset of A.
+ *
+ * @param {AndOperands} operandsA The AndOperands to compare from.
+ * @param {AndOperands} operandsB The AndOperands to compare against.
+ * @returns {boolean} `true` if the `andOperandsA` is a subset of the `andOperandsB`.
+ */
+ function isSubset(operandsA, operandsB) {
+ return operandsA.operands.every((operandA) =>
+ operandsB.operands.some((operandB) => equal(operandA, operandB))
+ )
+ }
+
+ return utils.defineTemplateBodyVisitor(context, {
+ "VAttribute[directive=true][key.name.name='else-if']"(node) {
+ if (!node.value || !node.value.expression) {
+ return
+ }
+ const test = node.value.expression
+ const conditionsToCheck =
+ test.type === 'LogicalExpression' && test.operator === '&&'
+ ? [...splitByAnd(test), test]
+ : [test]
+ const listToCheck = conditionsToCheck.map(buildOrOperands)
+
+ /** @type {VElement | null} */
+ let current = node.parent.parent
+ while (current && (current = utils.prevSibling(current))) {
+ const vIf = utils.getDirective(current, 'if')
+ const currentTestDir = vIf || utils.getDirective(current, 'else-if')
+ if (!currentTestDir) {
+ return
+ }
+ if (currentTestDir.value && currentTestDir.value.expression) {
+ const currentOrOperands = buildOrOperands(
+ currentTestDir.value.expression
+ )
+
+ for (const condition of listToCheck) {
+ const operands = (condition.operands = condition.operands.filter(
+ (orOperand) =>
+ !currentOrOperands.operands.some((currentOrOperand) =>
+ isSubset(currentOrOperand, orOperand)
+ )
+ ))
+ if (operands.length === 0) {
+ context.report({
+ node: condition.node,
+ messageId: 'unexpected'
+ })
+ return
+ }
+ }
+ }
+
+ if (vIf) {
+ return
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/no-duplicate-attr-inheritance.js b/lib/rules/no-duplicate-attr-inheritance.js
index 78e00b326..31aef7e44 100644
--- a/lib/rules/no-duplicate-attr-inheritance.js
+++ b/lib/rules/no-duplicate-attr-inheritance.js
@@ -6,9 +6,32 @@
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+/** @param {VElement[]} elements */
+function isConditionalGroup(elements) {
+ if (elements.length < 2) {
+ return false
+ }
+
+ const firstElement = elements[0]
+ const lastElement = elements[elements.length - 1]
+ const inBetweenElements = elements.slice(1, -1)
+
+ return (
+ utils.hasDirective(firstElement, 'if') &&
+ (utils.hasDirective(lastElement, 'else-if') ||
+ utils.hasDirective(lastElement, 'else')) &&
+ inBetweenElements.every((element) => utils.hasDirective(element, 'else-if'))
+ )
+}
+
+/** @param {VElement[]} elements */
+function isMultiRootNodes(elements) {
+ if (elements.length > 1 && !isConditionalGroup(elements)) {
+ return true
+ }
+
+ return false
+}
module.exports = {
meta: {
@@ -17,38 +40,62 @@ module.exports = {
description:
'enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"`',
categories: undefined,
- recommended: false,
url: 'https://eslint.vuejs.org/rules/no-duplicate-attr-inheritance.html'
},
fixable: null,
schema: [
- // fill in your schema
- ]
+ {
+ type: 'object',
+ properties: {
+ checkMultiRootNodes: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ noDuplicateAttrInheritance: 'Set "inheritAttrs" to false.'
+ }
},
-
+ /** @param {RuleContext} context */
create(context) {
+ const options = context.options[0] || {}
+ const checkMultiRootNodes = options.checkMultiRootNodes === true
+
+ /** @type {Literal['value']} */
let inheritsAttrs = true
+ /** @type {VReference[]} */
+ const attrsRefs = []
- return Object.assign(
- utils.executeOnVue(context, (node) => {
- const inheritAttrsProp = node.properties.find(
- (prop) =>
- prop.type === 'Property' &&
- utils.getStaticPropertyName(prop) === 'inheritAttrs'
- )
+ /** @param {ObjectExpression} node */
+ function processOptions(node) {
+ const inheritAttrsProp = utils.findProperty(node, 'inheritAttrs')
- if (inheritAttrsProp && inheritAttrsProp.value.type === 'Literal') {
- inheritsAttrs = inheritAttrsProp.value.value
+ if (inheritAttrsProp && inheritAttrsProp.value.type === 'Literal') {
+ inheritsAttrs = inheritAttrsProp.value.value
+ }
+ }
+
+ return utils.compositingVisitors(
+ utils.executeOnVue(context, processOptions),
+ utils.defineScriptSetupVisitor(context, {
+ onDefineOptionsEnter(node) {
+ if (node.arguments.length === 0) return
+ const define = node.arguments[0]
+ if (define.type !== 'ObjectExpression') return
+ processOptions(define)
}
}),
utils.defineTemplateBodyVisitor(context, {
+ /** @param {VExpressionContainer} node */
"VAttribute[directive=true][key.name.name='bind'][key.argument=null] > VExpressionContainer"(
node
) {
if (!inheritsAttrs) {
return
}
- const attrsRef = node.references.find((reference) => {
+ const reference = node.references.find((reference) => {
if (reference.variable != null) {
// Not vm reference
return false
@@ -56,14 +103,32 @@ module.exports = {
return reference.id.name === '$attrs'
})
- if (attrsRef) {
- context.report({
- node: attrsRef.id,
- message: 'Set "inheritAttrs" to false.'
- })
+ if (reference) {
+ attrsRefs.push(reference)
+ }
+ }
+ }),
+ {
+ 'Program:exit'(program) {
+ const element = program.templateBody
+ if (element == null) {
+ return
+ }
+
+ const rootElements = element.children.filter(utils.isVElement)
+
+ if (!checkMultiRootNodes && isMultiRootNodes(rootElements)) return
+
+ if (attrsRefs.length > 0) {
+ for (const attrsRef of attrsRefs) {
+ context.report({
+ node: attrsRef.id,
+ messageId: 'noDuplicateAttrInheritance'
+ })
+ }
}
}
- })
+ }
)
}
}
diff --git a/lib/rules/no-duplicate-attributes.js b/lib/rules/no-duplicate-attributes.js
index 023dacf42..0c7d3eb55 100644
--- a/lib/rules/no-duplicate-attributes.js
+++ b/lib/rules/no-duplicate-attributes.js
@@ -5,41 +5,34 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
/**
* Get the name of the given attribute node.
- * @param {ASTNode} attribute The attribute node to get.
- * @returns {string} The name of the attribute.
+ * @param {VAttribute | VDirective} attribute The attribute node to get.
+ * @returns {string | null} The name of the attribute.
*/
function getName(attribute) {
if (!attribute.directive) {
return attribute.key.name
}
if (attribute.key.name.name === 'bind') {
- return (attribute.key.argument && attribute.key.argument.name) || null
+ return (
+ (attribute.key.argument &&
+ attribute.key.argument.type === 'VIdentifier' &&
+ attribute.key.argument.name) ||
+ null
+ )
}
return null
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow duplication of attributes',
- categories: ['vue3-essential', 'essential'],
+ categories: ['vue3-essential', 'vue2-essential'],
url: 'https://eslint.vuejs.org/rules/no-duplicate-attributes.html'
},
fixable: null,
@@ -54,19 +47,29 @@ module.exports = {
allowCoexistStyle: {
type: 'boolean'
}
- }
+ },
+ additionalProperties: false
}
- ]
+ ],
+ messages: {
+ duplicateAttribute: "Duplicate attribute '{{name}}'."
+ }
},
-
+ /** @param {RuleContext} context */
create(context) {
const options = context.options[0] || {}
const allowCoexistStyle = options.allowCoexistStyle !== false
const allowCoexistClass = options.allowCoexistClass !== false
+ /** @type {Set} */
const directiveNames = new Set()
+ /** @type {Set} */
const attributeNames = new Set()
+ /**
+ * @param {string} name
+ * @param {boolean} isDirective
+ */
function isDuplicate(name, isDirective) {
if (
(allowCoexistStyle && name === 'style') ||
@@ -92,7 +95,7 @@ module.exports = {
context.report({
node,
loc: node.loc,
- message: "Duplicate attribute '{{name}}'.",
+ messageId: 'duplicateAttribute',
data: { name }
})
}
diff --git a/lib/rules/no-empty-component-block.js b/lib/rules/no-empty-component-block.js
new file mode 100644
index 000000000..e7afc59f6
--- /dev/null
+++ b/lib/rules/no-empty-component-block.js
@@ -0,0 +1,104 @@
+/**
+ * @author tyankatsu
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { isVElement } = require('../utils')
+
+/**
+ * check whether has attribute `src`
+ * @param {VElement} componentBlock
+ */
+function hasAttributeSrc(componentBlock) {
+ const hasAttribute = componentBlock.startTag.attributes.length > 0
+
+ const hasSrc = componentBlock.startTag.attributes.some(
+ (attribute) =>
+ !attribute.directive &&
+ attribute.key.name === 'src' &&
+ attribute.value &&
+ attribute.value.value !== ''
+ )
+
+ return hasAttribute && hasSrc
+}
+
+/**
+ * check whether value under the component block is only whitespaces or break lines
+ * @param {VElement} componentBlock
+ */
+function isValueOnlyWhiteSpacesOrLineBreaks(componentBlock) {
+ return (
+ componentBlock.children.length === 1 &&
+ componentBlock.children[0].type === 'VText' &&
+ !componentBlock.children[0].value.trim()
+ )
+}
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description:
+ 'disallow the `` `\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 ae720d87a..e670fa3fc 100644
--- a/lib/rules/syntaxes/dynamic-directive-arguments.js
+++ b/lib/rules/syntaxes/dynamic-directive-arguments.js
@@ -4,22 +4,24 @@
*/
'use strict'
module.exports = {
- supported: '2.6.0',
+ supported: '>=2.6.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports dynamic argument node
- * @param {VExpressionContainer} dinamicArgument node of dynamic argument
+ * @param {VExpressionContainer} dynamicArgument node of dynamic argument
* @returns {void}
*/
- function reportDynamicArgument(dinamicArgument) {
+ function reportDynamicArgument(dynamicArgument) {
context.report({
- node: dinamicArgument,
+ node: dynamicArgument,
messageId: 'forbiddenDynamicDirectiveArguments'
})
}
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 b382eb40d..af7dbcd2f 100644
--- a/lib/rules/syntaxes/scope-attribute.js
+++ b/lib/rules/syntaxes/scope-attribute.js
@@ -5,6 +5,8 @@
'use strict'
module.exports = {
deprecated: '2.5.0',
+ supported: '<3.0.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports `scope` node
@@ -21,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 d20095a17..87fe9404c 100644
--- a/lib/rules/syntaxes/slot-attribute.js
+++ b/lib/rules/syntaxes/slot-attribute.js
@@ -3,10 +3,27 @@
* See LICENSE file in root directory for full license.
*/
'use strict'
+
+const canConvertToVSlot = require('./utils/can-convert-to-v-slot')
+const regexp = require('../../utils/regexp')
+const casing = require('../../utils/casing')
+const { isVElement } = require('../../utils')
+
module.exports = {
deprecated: '2.6.0',
+ supported: '<3.0.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
+ /** @type {{ ignore: string[], ignoreParents: string[] }} */
+ const options = context.options[0] || {}
+ const { ignore = [], ignoreParents = [] } = options
+ const isAnyIgnored = regexp.toRegExpGroupMatcher(ignore)
+ const isParentIgnored = regexp.toRegExpGroupMatcher(ignoreParents)
+
const sourceCode = context.getSourceCode()
+ const tokenStore =
+ sourceCode.parserServices.getTemplateBodyTokenStore &&
+ sourceCode.parserServices.getTemplateBodyTokenStore()
/**
* Checks whether the given node can convert to the `v-slot`.
@@ -14,24 +31,24 @@ 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)
}
/**
* Checks whether the given node can convert to the `v-slot`.
- * @param {VAttribute} slotAttr node of `v-bind:slot`
+ * @param {VDirective} slotAttr node of `v-bind:slot`
* @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
}
@@ -43,45 +60,64 @@ 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'
}
/**
* Convert to `v-slot`.
- * @param {object} fixer fixer
- * @param {VAttribute} slotAttr node of `slot`
+ * @param {RuleFixer} fixer fixer
+ * @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 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
@@ -89,22 +125,41 @@ module.exports = {
* @returns {void}
*/
function reportSlot(slotAttr) {
+ const component = slotAttr.parent.parent
+ const componentName = component.rawName
+
+ if (
+ isAnyIgnored(
+ componentName,
+ casing.pascalCase(componentName),
+ casing.kebabCase(componentName)
+ )
+ ) {
+ return
+ }
+
+ const parent = component.parent
+ const parentName = isVElement(parent) ? parent.rawName : null
+ if (parentName && isParentIgnored(parentName)) {
+ 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)
}
})
}
/**
* Reports `v-bind:slot` node
- * @param {VAttribute} slotAttr node of `v-bind:slot`
+ * @param {VDirective} slotAttr node of `v-bind:slot`
* @returns {void}
*/
function reportVBindSlot(slotAttr) {
@@ -112,22 +167,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 c4c20b771..5f8070498 100644
--- a/lib/rules/syntaxes/slot-scope-attribute.js
+++ b/lib/rules/syntaxes/slot-scope-attribute.js
@@ -3,11 +3,23 @@
* 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
+ * @param {boolean} [option.fixToUpgrade]
+ * @returns {TemplateListener}
+ */
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`.
@@ -15,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
}
@@ -33,6 +47,7 @@ module.exports = {
attr.directive === true &&
attr.key.name.name === 'bind' &&
attr.key.argument &&
+ attr.key.argument.type === 'VIdentifier' &&
attr.key.argument.name === 'slot'
)
if (vBindSlotAttr) {
@@ -45,38 +60,49 @@ module.exports = {
/**
* Convert to `v-slot`.
- * @param {object} fixer fixer
- * @param {VAttribute | null} scopeAttr node of `slot-scope`
- * @returns {*} fix data
+ * @param {RuleFixer} fixer fixer
+ * @param {VDirective} scopeAttr node of `slot-scope`
+ * @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
- * @param {VAttribute} scopeAttr node of `slot-scope`
+ * @param {VDirective} scopeAttr node of `slot-scope`
* @returns {void}
*/
function reportSlotScope(scopeAttr) {
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 386277002..645ed375d 100644
--- a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
+++ b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
@@ -3,18 +3,14 @@
* See LICENSE file in root directory for full license.
*/
'use strict'
-const { Range } = require('semver')
-const unsupported = new Range('<=2.5 || >=2.6.0')
module.exports = {
- // >=2.6.0-beta.1 <=2.6.0-beta.3
- 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) {
/**
* Reports `.prop` shorthand node
- * @param {VDirectiveKey} bindPropKey node of `.prop` shorthand
+ * @param { VDirectiveKey & { argument: VIdentifier } } bindPropKey node of `.prop` shorthand
* @returns {void}
*/
function reportPropModifierShorthand(bindPropKey) {
@@ -31,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
new file mode 100644
index 000000000..d1bd59738
--- /dev/null
+++ b/lib/rules/syntaxes/v-model-argument.js
@@ -0,0 +1,23 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+module.exports = {
+ supported: '>=3.0.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createTemplateBodyVisitor(context) {
+ return {
+ /** @param {VDirectiveKey & { argument: VExpressionContainer | VIdentifier }} node */
+ "VAttribute[directive=true] > VDirectiveKey[name.name='model'][argument!=null]"(
+ node
+ ) {
+ context.report({
+ node: node.argument,
+ messageId: 'forbiddenVModelArgument'
+ })
+ }
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/v-model-custom-modifiers.js b/lib/rules/syntaxes/v-model-custom-modifiers.js
new file mode 100644
index 000000000..4b17b47d6
--- /dev/null
+++ b/lib/rules/syntaxes/v-model-custom-modifiers.js
@@ -0,0 +1,29 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const BUILTIN_MODIFIERS = new Set(['lazy', 'number', 'trim'])
+
+module.exports = {
+ supported: '>=3.0.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createTemplateBodyVisitor(context) {
+ return {
+ /** @param {VDirectiveKey} node */
+ "VAttribute[directive=true] > VDirectiveKey[name.name='model'][modifiers.length>0]"(
+ node
+ ) {
+ for (const modifier of node.modifiers) {
+ if (!BUILTIN_MODIFIERS.has(modifier.name)) {
+ context.report({
+ node: modifier,
+ messageId: 'forbiddenVModelCustomModifiers'
+ })
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/v-slot.js b/lib/rules/syntaxes/v-slot.js
index 1b17f7c1c..a6a4b4ebd 100644
--- a/lib/rules/syntaxes/v-slot.js
+++ b/lib/rules/syntaxes/v-slot.js
@@ -3,31 +3,31 @@
* 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 {VAttribute} 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 {object} fixer fixer
- * @param {VAttribute} vSlotAttr node of `v-slot`
- * @returns {*} fix data
+ * @param {RuleFixer} fixer fixer
+ * @param {VDirective} vSlotAttr node of `v-slot`
+ * @returns {null|Fix} fix data
*/
function fixVSlotToSlot(fixer, vSlotAttr) {
const key = vSlotAttr.key
- if (key.modifiers.length) {
+ if (key.modifiers.length > 0) {
// unknown modifiers
return null
}
@@ -53,14 +53,14 @@ 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(' '))
}
/**
* Reports `v-slot` node
- * @param {VAttribute} vSlotAttr node of `v-slot`
+ * @param {VDirective} vSlotAttr node of `v-slot`
* @returns {void}
*/
function reportVSlot(vSlotAttr) {
@@ -68,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 cec83201e..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,81 +34,98 @@ module.exports = {
* @returns {Object} AST event handlers.
*/
create(context) {
- const options = context.options[0] !== 'always' ? 'never' : 'always'
- let scope = {
- parent: null,
- nodes: []
- }
+ const options = context.options[0] === 'always' ? 'always' : 'never'
+ /**
+ * @typedef {object} ScopeStack
+ * @property {ScopeStack | null} parent
+ * @property {Identifier[]} nodes
+ */
- return utils.defineTemplateBodyVisitor(
- context,
- Object.assign(
- {
- VElement(node) {
- scope = {
- parent: scope,
- nodes: scope.nodes.slice() // make copy
+ /** @type {ScopeStack | null} */
+ let scopeStack = null
+
+ return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VElement} node */
+ VElement(node) {
+ scopeStack = {
+ parent: scopeStack,
+ nodes: scopeStack
+ ? [...scopeStack.nodes] // make copy
+ : []
+ }
+ if (node.variables) {
+ for (const variable of node.variables) {
+ const varNode = variable.id
+ const name = varNode.name
+ if (!scopeStack.nodes.some((node) => node.name === name)) {
+ // Prevent adding duplicates
+ scopeStack.nodes.push(varNode)
}
- if (node.variables) {
- for (const variable of node.variables) {
- const varNode = variable.id
- const name = varNode.name
- if (!scope.nodes.some((node) => node.name === name)) {
- // Prevent adding duplicates
- scope.nodes.push(varNode)
- }
+ }
+ }
+ },
+ 'VElement:exit'() {
+ scopeStack = scopeStack && scopeStack.parent
+ },
+ ...(options === 'never'
+ ? {
+ /** @param { ThisExpression & { parent: MemberExpression } } node */
+ 'VExpressionContainer MemberExpression > ThisExpression'(node) {
+ if (!scopeStack) {
+ return
}
+ const propertyName = utils.getStaticPropertyName(node.parent)
+ if (
+ !propertyName ||
+ scopeStack.nodes.some((el) => el.name === propertyName) ||
+ RESERVED_NAMES.has(propertyName) || // this.class | this['class']
+ /^\d.*$|[^\w$]/.test(propertyName) // this['0aaaa'] | this['foo-bar bas']
+ ) {
+ return
+ }
+
+ context.report({
+ node,
+ loc: node.loc,
+ fix(fixer) {
+ // node.parent should be some code like `this.test`, `this?.['result']`
+ return fixer.replaceText(node.parent, propertyName)
+ },
+ messageId: 'unexpected'
+ })
}
- },
- 'VElement:exit'(node) {
- scope = scope.parent
}
- },
- options === 'never'
- ? {
- 'VExpressionContainer MemberExpression > ThisExpression'(node) {
- const propertyName = utils.getStaticPropertyName(
- node.parent.property
- )
- if (
- !propertyName ||
- scope.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']
- ) {
- return
- }
-
- context.report({
- node,
- loc: node.loc,
- message: "Unexpected usage of 'this'."
- })
+ : {
+ /** @param {VExpressionContainer} node */
+ VExpressionContainer(node) {
+ if (!scopeStack) {
+ return
}
- }
- : {
- VExpressionContainer(node) {
- if (node.parent.type === 'VDirectiveKey') {
- // We cannot use `.` in dynamic arguments because the right of the `.` becomes a modifier.
- // For example, In `:[this.prop]` case, `:[this` is an argument and `prop]` is a modifier.
- return
- }
- if (node.references) {
- for (const reference of node.references) {
- if (
- !scope.nodes.some((el) => el.name === reference.id.name)
- ) {
- context.report({
- node: reference.id,
- loc: reference.id.loc,
- message: "Expected 'this'."
- })
- }
+ if (node.parent.type === 'VDirectiveKey') {
+ // We cannot use `.` in dynamic arguments because the right of the `.` becomes a modifier.
+ // For example, In `:[this.prop]` case, `:[this` is an argument and `prop]` is a modifier.
+ return
+ }
+ if (node.references) {
+ for (const reference of node.references) {
+ if (
+ !scopeStack.nodes.some(
+ (el) => el.name === reference.id.name
+ )
+ ) {
+ context.report({
+ node: reference.id,
+ loc: reference.id.loc,
+ 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 baaaf8c60..f048b70ef 100644
--- a/lib/rules/use-v-on-exact.js
+++ b/lib/rules/use-v-on-exact.js
@@ -4,9 +4,9 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
+/**
+ * @typedef { {name: string, node: VDirectiveKey, modifiers: string[] } } EventDirective
+ */
const utils = require('../utils')
@@ -21,29 +21,21 @@ const GLOBAL_MODIFIERS = new Set([
'native'
])
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
/**
* Finds and returns all keys for event directives
*
- * @param {array} attributes Element attributes
+ * @param {VStartTag} startTag Element startTag
* @param {SourceCode} sourceCode The source code object.
- * @returns {array[object]} [{ name, node, modifiers }]
+ * @returns {EventDirective[]} [{ name, node, modifiers }]
*/
-function getEventDirectives(attributes, sourceCode) {
- return attributes
- .filter(
- (attribute) => attribute.directive && attribute.key.name.name === 'on'
- )
- .map((attribute) => ({
- name: attribute.key.argument
- ? sourceCode.getText(attribute.key.argument)
- : '',
- node: attribute.key,
- modifiers: attribute.key.modifiers.map((modifier) => modifier.name)
- }))
+function getEventDirectives(startTag, sourceCode) {
+ return utils.getDirectives(startTag, 'on').map((attribute) => ({
+ name: attribute.key.argument
+ ? sourceCode.getText(attribute.key.argument)
+ : '',
+ node: attribute.key,
+ modifiers: attribute.key.modifiers.map((modifier) => modifier.name)
+ }))
}
/**
@@ -70,7 +62,7 @@ function isSystemModifier(modifier) {
* Checks whether given any of provided modifiers
* has system modifier
*
- * @param {array} modifiers
+ * @param {string[]} modifiers
* @returns {boolean}
*/
function hasSystemModifier(modifiers) {
@@ -81,25 +73,25 @@ function hasSystemModifier(modifiers) {
* Groups all events in object,
* with keys represinting each event name
*
- * @param {array} events
- * @returns {object} { click: [], keypress: [] }
+ * @param {EventDirective[]} events
+ * @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
- }, {})
+ grouped[event.name].push(event)
+ }
+ return grouped
}
/**
* Creates alphabetically sorted string with system modifiers
*
- * @param {array[string]} modifiers
+ * @param {string[]} modifiers
* @returns {string} e.g. "alt,ctrl,del,shift"
*/
function getSystemModifiersString(modifiers) {
@@ -109,7 +101,7 @@ function getSystemModifiersString(modifiers) {
/**
* Creates alphabetically sorted string with key modifiers
*
- * @param {array[string]} modifiers
+ * @param {string[]} modifiers
* @returns {string} e.g. "enter,tab"
*/
function getKeyModifiersString(modifiers) {
@@ -120,8 +112,8 @@ function getKeyModifiersString(modifiers) {
* Compares two events based on their modifiers
* to detect possible event leakeage
*
- * @param {object} baseEvent
- * @param {object} event
+ * @param {EventDirective} baseEvent
+ * @param {EventDirective} event
* @returns {boolean}
*/
function hasConflictedModifiers(baseEvent, event) {
@@ -142,43 +134,44 @@ 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)
)
}
/**
* Searches for events that might conflict with each other
*
- * @param {array} events
- * @returns {array} conflicted events, without duplicates
+ * @param {EventDirective[]} events
+ * @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))
- ]
- }, [])
+ )
+ }
+ 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."
+ }
},
/**
@@ -191,11 +184,12 @@ module.exports = {
const sourceCode = context.getSourceCode()
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VStartTag} node */
VStartTag(node) {
if (node.attributes.length === 0) return
const isCustomComponent = utils.isCustomComponent(node.parent)
- let events = getEventDirectives(node.attributes, sourceCode)
+ let events = getEventDirectives(node, sourceCode)
if (isCustomComponent) {
// For components consider only events with `native` 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 1e7c50e69..752a8fdf0 100644
--- a/lib/rules/v-bind-style.js
+++ b/lib/rules/v-bind-style.js
@@ -5,70 +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, {
- "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..056890fa0
--- /dev/null
+++ b/lib/rules/v-on-event-hyphenation.js
@@ -0,0 +1,129 @@
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+const { toRegExpGroupMatcher } = 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) || []
+ const isIgnoredTag = toRegExpGroupMatcher(optionsPayload?.ignoreTags)
+ 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)
+ }
+
+ return utils.defineTemplateBodyVisitor(context, {
+ "VAttribute[directive=true][key.name.name='on']"(node) {
+ const element = node.parent.parent
+ if (
+ !utils.isCustomComponent(element) ||
+ isIgnoredTag(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 fe19f52da..000000000
--- a/lib/rules/v-on-function-call.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * @author Niklas Higi
- */
-'use strict'
-
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
-const utils = require('../utils')
-
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
-/**
- * Check whether the given token is a left parenthesis.
- * @param {Token} 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 === '('
-}
-
-// ------------------------------------------------------------------------------
-// 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'] }]
- },
-
- create(context) {
- const always = context.options[0] === 'always'
-
- return utils.defineTemplateBodyVisitor(context, {
- "VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer > Identifier"(
- node
- ) {
- if (!always) return
- context.report({
- node,
- loc: node.loc,
- message:
- "Method calls inside of 'v-on' directives must have parentheses."
- })
- },
-
- "VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression > ExpressionStatement > CallExpression"(
- node
- ) {
- if (
- !always &&
- node.arguments.length === 0 &&
- node.callee.type === 'Identifier'
- ) {
- context.report({
- node,
- loc: node.loc,
- message:
- "Method calls without arguments inside of 'v-on' directives must not have parentheses.",
- fix: (fixer) => {
- const tokenStore = context.parserServices.getTemplateBodyTokenStore()
- const rightToken = tokenStore.getLastToken(node)
- const leftToken = tokenStore.getTokenAfter(
- node.callee,
- isLeftParen
- )
- const tokens = tokenStore.getTokensBetween(
- leftToken,
- rightToken,
- { includeComments: true }
- )
-
- if (tokens.length) {
- // The comment is included and cannot be fixed.
- return null
- }
-
- return fixer.removeRange([
- leftToken.range[0],
- rightToken.range[1]
- ])
- }
- })
- }
- }
- })
- }
-}
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 bda26be94..eb5950026 100644
--- a/lib/rules/v-on-style.js
+++ b/lib/rules/v-on-style.js
@@ -5,32 +5,29 @@
*/
'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) {
const preferShorthand = context.options[0] !== 'longform'
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='on'][key.argument!=null]"(
node
) {
@@ -43,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 bbae1989e..964550b98 100644
--- a/lib/rules/v-slot-style.js
+++ b/lib/rules/v-slot-style.js
@@ -20,6 +20,7 @@ const utils = require('../utils')
* @returns {Options} The normalized options.
*/
function normalizeOptions(options) {
+ /** @type {Options} */
const normalized = {
atComponent: 'v-slot',
default: 'shorthand',
@@ -27,9 +28,14 @@ function normalizeOptions(options) {
}
if (typeof options === 'string') {
- normalized.atComponent = normalized.default = normalized.named = options
+ normalized.atComponent =
+ normalized.default =
+ normalized.named =
+ /** @type {"shorthand" | "longform"} */ (options)
} else if (options != null) {
- for (const key of ['atComponent', 'default', 'named']) {
+ /** @type {(keyof Options)[]} */
+ const keys = ['atComponent', 'default', 'named']
+ for (const key of keys) {
if (options[key] != null) {
normalized[key] = options[key]
}
@@ -42,7 +48,7 @@ function normalizeOptions(options) {
/**
* Get the expected style.
* @param {Options} options The options that defined expected types.
- * @param {VAttribute} node The `v-slot` node to check.
+ * @param {VDirective} node The `v-slot` node to check.
* @returns {"shorthand" | "longform" | "v-slot"} The expected style.
*/
function getExpectedStyle(options, node) {
@@ -60,7 +66,7 @@ function getExpectedStyle(options, node) {
/**
* Get the expected style.
- * @param {VAttribute} node The `v-slot` node to check.
+ * @param {VDirective} node The `v-slot` node to check.
* @returns {"shorthand" | "longform" | "v-slot"} The expected style.
*/
function getActualStyle(node) {
@@ -80,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',
@@ -107,12 +113,13 @@ module.exports = {
expectedVSlot: "Expected 'v-slot' instead of '{{actual}}'."
}
},
-
+ /** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
const options = normalizeOptions(context.options[0])
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='slot']"(node) {
const expected = getExpectedStyle(options, node)
const actual = getActualStyle(node)
@@ -121,6 +128,7 @@ module.exports = {
}
const { name, argument } = node.key
+ /** @type {Range} */
const range = [name.range[0], (argument || name).range[1]]
const argumentText = argument ? sourceCode.getText(argument) : 'default'
context.report({
@@ -133,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 f7928d294..c604a43a8 100644
--- a/lib/rules/valid-template-root.js
+++ b/lib/rules/valid-template-root.js
@@ -5,32 +5,30 @@
*/
'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) {
const sourceCode = context.getSourceCode()
return {
+ /** @param {Program} program */
Program(program) {
const element = program.templateBody
if (element == null) {
@@ -46,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 9d24442ac..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 {ASTNode} node The element node to check.
+ * @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,43 +20,84 @@ 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 can be LHS.
+ * @returns {boolean} `true` if the node is a MemberExpression containing an optional chaining.
*/
-function isLhs(node) {
+function isOptionalChainingMemberExpression(node) {
return (
- Boolean(node) &&
- (node.type === 'Identifier' || node.type === 'MemberExpression')
+ node.type === 'ChainExpression' &&
+ node.expression.type === 'MemberExpression'
)
}
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+/**
+ * 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) {
+ 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
+}
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."
}
},
-
+ /** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='bind']"(node) {
if (!node.key.modifiers.map((mod) => mod.name).includes('sync')) {
return
@@ -79,36 +108,50 @@ module.exports = {
if (!isValidElement(element)) {
context.report({
node,
- loc: node.loc,
messageId: 'unexpectedInvalidElement',
data: { name }
})
}
- if (node.value) {
- if (!isLhs(node.value.expression)) {
+ if (!node.value) {
+ return
+ }
+
+ const expression = node.value.expression
+ if (!expression) {
+ // Parsing error
+ return
+ }
+ if (isOptionalChainingMemberExpression(expression)) {
+ context.report({
+ 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) {
+ const id = reference.id
+ if (id.parent.type !== 'VExpressionContainer') {
+ continue
+ }
+ const variable = reference.variable
+ if (variable) {
context.report({
- node,
- loc: node.loc,
- messageId: 'unexpectedNonLhsExpression'
+ node: expression,
+ messageId: 'unexpectedUpdateIterationVariable',
+ data: { varName: id.name }
})
}
-
- for (const reference of node.value.references) {
- const id = reference.id
- if (id.parent.type !== 'VExpressionContainer') {
- continue
- }
- const variable = reference.variable
- if (variable) {
- context.report({
- node,
- loc: node.loc,
- messageId: 'unexpectedUpdateIterationVariable',
- data: { varName: id.name }
- })
- }
- }
}
}
})
diff --git a/lib/rules/valid-v-bind.js b/lib/rules/valid-v-bind.js
index dfb1f97e2..0f44dad26 100644
--- a/lib/rules/valid-v-bind.js
+++ b/lib/rules/valid-v-bind.js
@@ -5,54 +5,45 @@
*/
'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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='bind']"(node) {
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 }
})
}
}
- if (!utils.hasAttributeValue(node)) {
+ 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 b571ebd00..5ff956818 100644
--- a/lib/rules/valid-v-cloak.js
+++ b/lib/rules/valid-v-cloak.js
@@ -5,50 +5,49 @@
*/
'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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"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 b68caa746..b94437466 100644
--- a/lib/rules/valid-v-else-if.js
+++ b/lib/rules/valid-v-else-if.js
@@ -5,76 +5,75 @@
*/
'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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='else-if']"(node) {
const element = node.parent.parent
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 (!utils.hasAttributeValue(node)) {
+ 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 3379fa326..f35ac661d 100644
--- a/lib/rules/valid-v-else.js
+++ b/lib/rules/valid-v-else.js
@@ -5,76 +5,75 @@
*/
'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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='else']"(node) {
const element = node.parent.parent
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 (utils.hasAttributeValue(node)) {
+ 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 681a5dcf1..05183dd7e 100644
--- a/lib/rules/valid-v-for.js
+++ b/lib/rules/valid-v-for.js
@@ -5,20 +5,12 @@
*/
'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 {ASTNode} vFor The attribute node of `v-for` to check.
- * @param {ASTNode} vBindKey The attribute node of `v-bind:key` to check.
+ * @param {VDirective} vFor The attribute node of `v-for` to check.
+ * @param {VDirective} vBindKey The attribute node of `v-bind:key` to check.
* @returns {boolean} `true` if the node is using the variables which are defined by `v-for` directives.
*/
function isUsingIterationVar(vFor, vBindKey) {
@@ -38,14 +30,14 @@ function isUsingIterationVar(vFor, vBindKey) {
/**
* Check the child element in tempalte v-for about `v-bind:key` attributes.
* @param {RuleContext} context The rule context to report.
- * @param {ASTNode} vFor The attribute node of `v-for` to check.
- * @param {ASTNode} child The child node to check.
+ * @param {VDirective} vFor The attribute node of `v-for` to check.
+ * @param {VElement} child The child node to check.
*/
function checkChildKey(context, vFor, child) {
const childFor = utils.getDirective(child, 'for')
// if child has v-for, check if parent iterator is used in v-for
if (childFor != null) {
- const childForRefs = childFor.value.references
+ const childForRefs = (childFor.value && childFor.value.references) || []
const variables = vFor.parent.parent.variables
const usedInFor = childForRefs.some((cref) =>
variables.some(
@@ -66,11 +58,13 @@ function checkChildKey(context, vFor, child) {
/**
* Check the given element about `v-bind:key` attributes.
* @param {RuleContext} context The rule context to report.
- * @param {ASTNode} vFor The attribute node of `v-for` to check.
- * @param {ASTNode} element The element node to check.
+ * @param {VDirective} vFor The attribute node of `v-for` to check.
+ * @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,45 +73,50 @@ 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) {
const sourceCode = context.getSourceCode()
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='for']"(node) {
const element = node.parent.parent
@@ -125,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 (!utils.hasAttributeValue(node)) {
+ if (!node.value || utils.isEmptyValueDirective(node, context)) {
context.report({
node,
- loc: node.loc,
- message: "'v-for' directives require that attribute value."
+ messageId: 'expectedValue'
})
return
}
@@ -153,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
}
@@ -167,24 +165,21 @@ module.exports = {
if (value === null) {
context.report({
- node: value || expr,
- loc: value && value.loc,
- message: "Invalid alias ''."
+ node: expr,
+ 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 1bc1fcb2b..1df254033 100644
--- a/lib/rules/valid-v-html.js
+++ b/lib/rules/valid-v-html.js
@@ -5,50 +5,49 @@
*/
'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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"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 (!utils.hasAttributeValue(node)) {
+ 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 6c24cabc9..2e8651b44 100644
--- a/lib/rules/valid-v-if.js
+++ b/lib/rules/valid-v-if.js
@@ -5,68 +5,67 @@
*/
'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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='if']"(node) {
const element = node.parent.parent
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 (!utils.hasAttributeValue(node)) {
+ 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 7dc22a4d7..ca2830dc6 100644
--- a/lib/rules/valid-v-model.js
+++ b/lib/rules/valid-v-model.js
@@ -5,21 +5,13 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
const utils = require('../utils')
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
const VALID_MODIFIERS = new Set(['lazy', 'number', 'trim'])
/**
* Check whether the given node is valid or not.
- * @param {ASTNode} node The element node to check.
+ * @param {VElement} node The element node to check.
* @returns {boolean} `true` if the node is valid.
*/
function isValidElement(node) {
@@ -37,22 +29,65 @@ 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 can be LHS.
+ * @returns {boolean} `true` if the node is a MemberExpression containing an optional chaining.
*/
-function isLhs(node) {
+function isOptionalChainingMemberExpression(node) {
return (
- node != null &&
- (node.type === 'Identifier' || node.type === 'MemberExpression')
+ 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.
- * @param {ASTNode} leafNode The node to look up.
- * @returns {Variable|null} The found variable or null.
+ * @param {VElement} leafNode The node to look up.
+ * @returns {VVariable|null} The found variable or null.
*/
function getVariable(name, leafNode) {
let node = leafNode
@@ -65,30 +100,50 @@ function getVariable(name, leafNode) {
return variable
}
+ if (node.parent.type === 'VDocumentFragment') {
+ break
+ }
+
node = node.parent
}
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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='model']"(node) {
const element = node.parent.parent
const name = element.name
@@ -96,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 }
})
}
@@ -106,66 +159,72 @@ 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 }
})
}
}
}
- if (!utils.hasAttributeValue(node)) {
+ 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) {
- if (!isLhs(node.value.expression)) {
- context.report({
- node,
- loc: node.loc,
- message:
- "'v-model' directives require the attribute value which is valid as LHS."
- })
+ const expression = node.value.expression
+ if (!expression) {
+ // Parsing error
+ return
+ }
+ if (isOptionalChainingMemberExpression(expression)) {
+ context.report({
+ 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) {
+ const id = reference.id
+ if (id.parent.type !== 'VExpressionContainer') {
+ continue
}
- for (const reference of node.value.references) {
- const id = reference.id
- if (id.parent.type !== 'VExpressionContainer') {
- continue
- }
+ const variable = getVariable(id.name, element)
+ if (variable != null) {
+ context.report({
+ node: expression,
+ messageId: 'unexpectedUpdateIterationVariable',
- 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.",
- data: { varName: id.name }
- })
- }
+ data: { varName: id.name }
+ })
}
}
}
diff --git a/lib/rules/valid-v-on.js b/lib/rules/valid-v-on.js
index 4e014e00c..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',
@@ -46,15 +38,19 @@ const VERB_MODIFIERS = new Set(['stop', 'prevent'])
// https://www.w3.org/TR/uievents-key/
const KEY_ALIASES = new Set(keyAliases)
+/**
+ * @param {VIdentifier} modifierNode
+ * @param {Set} customModifiers
+ */
function isValidModifier(modifierNode, customModifiers) {
const modifier = modifierNode.name
return (
// 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
@@ -62,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,
@@ -85,49 +77,63 @@ 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) {
const options = context.options[0] || {}
+ /** @type {Set} */
const customModifiers = new Set(options.modifiers || [])
const sourceCode = context.getSourceCode()
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='on']"(node) {
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 }
})
}
}
if (
- !utils.hasAttributeValue(node) &&
+ (!node.value || !node.value.expression) &&
!node.key.modifiers.some((modifier) =>
VERB_MODIFIERS.has(modifier.name)
)
) {
- if (node.value && sourceCode.getText(node.value.expression)) {
- const value = sourceCode.getText(node.value)
- context.report({
- node,
- loc: node.loc,
- message:
- 'Avoid using JavaScript keyword as "v-on" value: {{value}}.',
- data: { value }
- })
+ if (node.value && !utils.isEmptyValueDirective(node, context)) {
+ const valueText = sourceCode.getText(node.value)
+ let innerText = valueText
+ if (
+ (valueText[0] === '"' || valueText[0] === "'") &&
+ valueText[0] === valueText[valueText.length - 1]
+ ) {
+ // quoted
+ innerText = valueText.slice(1, -1)
+ }
+ if (/^\w+$/.test(innerText)) {
+ context.report({
+ 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 aef2ca37d..9d3d8bf8f 100644
--- a/lib/rules/valid-v-once.js
+++ b/lib/rules/valid-v-once.js
@@ -5,50 +5,49 @@
*/
'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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"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 c975b1e2d..0505b9cb5 100644
--- a/lib/rules/valid-v-pre.js
+++ b/lib/rules/valid-v-pre.js
@@ -5,50 +5,49 @@
*/
'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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"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 51e4014fd..1da617806 100644
--- a/lib/rules/valid-v-show.js
+++ b/lib/rules/valid-v-show.js
@@ -5,50 +5,57 @@
*/
'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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"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,
+ messageId: 'expectedValue'
})
}
- if (!utils.hasAttributeValue(node)) {
+ if (node.parent.parent.name === 'template') {
context.report({
node,
- loc: node.loc,
- message: "'v-show' directives require that attribute value."
+ messageId: 'unexpectedTemplate'
})
}
}
diff --git a/lib/rules/valid-v-slot.js b/lib/rules/valid-v-slot.js
index 79b052269..d5c0efd1e 100644
--- a/lib/rules/valid-v-slot.js
+++ b/lib/rules/valid-v-slot.js
@@ -6,107 +6,184 @@
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.
- * @returns {VAttribute[]} The array of `v-slot` directives.
+ * @returns {VDirective[]} The array of `v-slot` directives.
*/
function getSlotDirectivesOnElement(node) {
- return node.startTag.attributes.filter(
- (attribute) => attribute.directive && attribute.key.name.name === 'slot'
- )
+ return utils.getDirectives(node, 'slot')
}
/**
* Get all `v-slot` directives on the children of a given element.
* @param {VElement} node The VElement node to check.
- * @returns {VAttribute[][]}
+ * @returns {VDirective[][]}
* The array of the group of `v-slot` directives.
* The group bundles `v-slot` directives of element sequence which is connected
* 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 }
- },
- { groups: [], vIf: false }
- )
- .groups.map((group) =>
- group
- .map((childElement) =>
- childElement.name === 'template'
- ? utils.getDirective(childElement, 'slot')
- : null
- )
- .filter(Boolean)
- )
- .filter((group) => group.length >= 1)
+ return groups
}
/**
- * Get the normalized name of a given `v-slot` directive node.
- * @param {VAttribute} node The `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 {VAttribute[][]} vSlotGroups The result of `getAllNamedSlotElements()`.
- * @param {VElement} currentVSlot The current `v-slot` directive node.
- * @returns {VAttribute[][]} The array of the group of `v-slot` directives.
+ * @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 > 0)
+}
+
+/**
+ * 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]
)
- .filter((slots) => slots.length >= 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
}
/**
- * Check whether a given argument node is using an iteration variable that the element defined.
+ * 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 (
@@ -115,16 +192,16 @@ 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
}
/**
* Check whether a given argument node is using an scope variable that the directive defined.
- * @param {VAttribute} vSlot The `v-slot` directive to check.
+ * @param {VDirective} vSlot The `v-slot` directive to check.
* @returns {boolean} `true` if that argument node is using a scope variable the directive defined.
*/
function isUsingScopeVar(vSlot) {
@@ -143,6 +220,20 @@ 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 = {
@@ -150,11 +241,21 @@ module.exports = {
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.",
@@ -173,18 +274,29 @@ module.exports = {
"'v-slot' directive on a custom element requires that attribute value."
}
},
-
+ /** @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 */
"VAttribute[directive=true][key.name.name='slot']"(node) {
const isDefaultSlot =
- node.key.argument == null || node.key.argument.name === 'default'
+ node.key.argument == null ||
+ (node.key.argument.type === 'VIdentifier' &&
+ node.key.argument.name === 'default')
const element = node.parent.parent
const parentElement = element.parent
const ownerElement =
element.name === 'template' ? parentElement : element
+ if (ownerElement.type === 'VDocumentFragment') {
+ return
+ }
const vSlotsOnElement = getSlotDirectivesOnElement(element)
const vSlotGroupsOnChildren = getSlotDirectivesOnChildren(ownerElement)
@@ -202,7 +314,7 @@ module.exports = {
messageId: 'namedSlotMustBeOnTemplate'
})
}
- if (ownerElement === element && vSlotGroupsOnChildren.length >= 1) {
+ if (ownerElement === element && vSlotGroupsOnChildren.length > 0) {
context.report({
node,
messageId: 'defaultSlotMustBeOnTemplate'
@@ -218,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)
@@ -235,7 +353,7 @@ module.exports = {
messageId: 'disallowDuplicateSlotsOnChildren'
})
}
- if (vFor && !isUsingIterationVar(node.key.argument, element)) {
+ if (vFor && !vSlotVForVar) {
// E.g.,
context.report({
node,
@@ -253,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'
})
}
@@ -264,7 +387,9 @@ module.exports = {
if (
ownerElement === element &&
isDefaultSlot &&
- !utils.hasAttributeValue(node)
+ (!node.value ||
+ utils.isEmptyValueDirective(node, context) ||
+ utils.isEmptyExpressionValueDirective(node, context))
) {
context.report({
node,
diff --git a/lib/rules/valid-v-text.js b/lib/rules/valid-v-text.js
index 2e1881ce3..b41e7f2b3 100644
--- a/lib/rules/valid-v-text.js
+++ b/lib/rules/valid-v-text.js
@@ -5,50 +5,49 @@
*/
'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) {
return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VDirective} node */
"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 (!utils.hasAttributeValue(node)) {
+ 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 4d5ca22aa..a47e978cd 100644
--- a/lib/utils/casing.js
+++ b/lib/utils/casing.js
@@ -1,23 +1,20 @@
-const assert = require('assert')
-
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
-
/**
* Capitalize a string.
+ * @param {string} str
*/
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
* Checks whether the given string has symbols.
+ * @param {string} str
*/
function hasSymbols(str) {
return /[!"#%&'()*+,./:;<=>?@[\\\]^`{|}]/u.exec(str) // without " ", "$", "-" and "_"
}
/**
* Checks whether the given string has upper.
+ * @param {string} str
*/
function hasUpper(str) {
return /[A-Z]/u.exec(str)
@@ -37,17 +34,15 @@ function kebabCase(str) {
/**
* Checks whether the given string is kebab-case.
+ * @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)
+ )
}
/**
@@ -64,12 +59,10 @@ function snakeCase(str) {
/**
* Checks whether the given string is snake_case.
+ * @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)
}
/**
@@ -86,16 +79,10 @@ function camelCase(str) {
/**
* Checks whether the given string is camelCase.
+ * @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)
}
/**
@@ -109,16 +96,10 @@ function pascalCase(str) {
/**
* Checks whether the given string is PascalCase.
+ * @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 = {
@@ -136,23 +117,19 @@ const checkersMap = {
}
/**
* Return case checker
- * @param {string} name type of checker to return ('camelCase', 'kebab-case', 'PascalCase')
- * @return {isKebabCase|isCamelCase|isPascalCase}
+ * @param { 'camelCase' | 'kebab-case' | 'PascalCase' | 'snake_case' } name type of checker to return ('camelCase', 'kebab-case', 'PascalCase')
+ * @return {isKebabCase|isCamelCase|isPascalCase|isSnakeCase}
*/
function getChecker(name) {
- assert(typeof name === 'string')
-
return checkersMap[name] || isPascalCase
}
/**
* Return case converter
- * @param {string} name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
- * @return {kebabCase|camelCase|pascalCase}
+ * @param { 'camelCase' | 'kebab-case' | 'PascalCase' | 'snake_case' } name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
+ * @return {kebabCase|camelCase|pascalCase|snakeCase}
*/
function getConverter(name) {
- assert(typeof name === 'string')
-
return convertersMap[name] || pascalCase
}
@@ -162,22 +139,22 @@ module.exports = {
/**
* Return case converter
* @param {string} name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
- * @return {kebabCase|camelCase|pascalCase}
+ * @return {kebabCase|camelCase|pascalCase|snakeCase}
*/
getConverter,
/**
* Return case checker
* @param {string} name type of checker to return ('camelCase', 'kebab-case', 'PascalCase')
- * @return {isKebabCase|isCamelCase|isPascalCase}
+ * @return {isKebabCase|isCamelCase|isPascalCase|isSnakeCase}
*/
getChecker,
/**
* Return case exact converter.
* If the converted result is not the correct case, the original value is returned.
- * @param {string} name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
- * @return {kebabCase|camelCase|pascalCase}
+ * @param { 'camelCase' | 'kebab-case' | 'PascalCase' | 'snake_case' } name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
+ * @return {kebabCase|camelCase|pascalCase|snakeCase}
*/
getExactConverter(name) {
const converter = getConverter(name)
@@ -196,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 f214b7374..53f7df5ae 100644
--- a/lib/utils/html-comments.js
+++ b/lib/utils/html-comments.js
@@ -1,36 +1,20 @@
-/**
- * @typedef { import('eslint').SourceCode } SourceCode
- * @typedef { import('eslint').Rule.RuleContext } RuleContext
- * @typedef { import('vue-eslint-parser').AST.Token } ASTToken
- * @typedef { import('vue-eslint-parser').AST.HasLocation } HasLocation
- */
-
/**
* @typedef { { exceptions?: string[] } } CommentParserConfig
- * @typedef { (comment: HTMLComment) => void } HTMLCommentVisitor
+ * @typedef { (comment: ParsedHTMLComment) => void } HTMLCommentVisitor
* @typedef { { includeDirectives?: boolean } } CommentVisitorOption
- * @typedef { ASTToken & { type: 'HTMLComment' } } HTMLCommentToken
*
- * @typedef { ASTToken & { type: 'HTMLCommentOpen' } } HTMLCommentOpen
- * @typedef { ASTToken & { type: 'HTMLCommentOpenDecoration' } } HTMLCommentOpenDecoration
- * @typedef { ASTToken & { type: 'HTMLCommentValue' } } HTMLCommentValue
- * @typedef { ASTToken & { type: 'HTMLCommentClose' } } HTMLCommentClose
- * @typedef { ASTToken & { type: 'HTMLCommentCloseDecoration' } } HTMLCommentCloseDecoration
- * @typedef { { open: HTMLCommentOpen, openDecoration: HTMLCommentOpenDecoration | null, value: HTMLCommentValue | null, closeDecoration: HTMLCommentCloseDecoration | null, close: HTMLCommentClose } } HTMLComment
+ * @typedef { Token & { type: 'HTMLCommentOpen' } } HTMLCommentOpen
+ * @typedef { Token & { type: 'HTMLCommentOpenDecoration' } } HTMLCommentOpenDecoration
+ * @typedef { Token & { type: 'HTMLCommentValue' } } HTMLCommentValue
+ * @typedef { Token & { type: 'HTMLCommentClose' } } HTMLCommentClose
+ * @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'
@@ -44,7 +28,7 @@ const TYPE_HTML_COMMENT_CLOSE = 'HTMLCommentClose'
const TYPE_HTML_COMMENT_CLOSE_DECORATION = 'HTMLCommentCloseDecoration'
/**
- * @param {HTMLCommentToken} comment
+ * @param {HTMLComment} comment
* @returns {boolean}
*/
function isCommentDirective(comment) {
@@ -52,7 +36,7 @@ function isCommentDirective(comment) {
}
/**
- * @param {HTMLCommentToken} comment
+ * @param {HTMLComment} comment
* @returns {boolean}
*/
function isIEConditionalComment(comment) {
@@ -66,8 +50,8 @@ function isIEConditionalComment(comment) {
* Define HTML comment parser
*
* @param {SourceCode} sourceCode The source code instance.
- * @param {CommentParserConfig} config The config.
- * @returns { (node: ASTToken) => (HTMLComment | null) } HTML comment parser.
+ * @param {CommentParserConfig | null} config The config.
+ * @returns { (node: Token) => (ParsedHTMLComment | null) } HTML comment parser.
*/
function defineParser(sourceCode, config) {
config = config || {}
@@ -118,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') {
@@ -164,9 +148,10 @@ function defineParser(sourceCode, config) {
* @returns {any}
*/
const createToken = (type, value) => {
- /** @type {[number,number]} */
+ /** @type {Range} */
const range = [tokenIndex, tokenIndex + value.length]
tokenIndex = range[1]
+ /** @type {SourceLocation} */
let loc
return {
type,
@@ -222,15 +207,15 @@ function defineParser(sourceCode, config) {
* Define HTML comment visitor
*
* @param {RuleContext} context The rule context.
- * @param {CommentParserConfig} config The config.
+ * @param {CommentParserConfig | null} config The config.
* @param {HTMLCommentVisitor} visitHTMLComment The HTML comment visitor.
- * @param {CommentVisitorOption} visitorOption The option for visitor.
- * @returns {object} HTML comment visitor.
+ * @param {CommentVisitorOption} [visitorOption] The option for visitor.
+ * @returns {RuleListener} HTML comment visitor.
*/
function defineVisitor(context, config, visitHTMLComment, visitorOption) {
- visitorOption = visitorOption || {}
return {
Program(node) {
+ visitorOption = visitorOption || {}
if (utils.hasInvalidEOF(node)) {
return
}
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 1c8c3838a..d2879b120 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -4,59 +4,129 @@
*/
'use strict'
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
+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')
-const assert = require('assert')
-
-// ------------------------------------------------------------------------------
-// Helpers
-// ------------------------------------------------------------------------------
+/**
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/node').HasLocation} HasLocation
+ * @typedef { { type: string } & HasLocation } MaybeNode
+ */
-const KNOWN_NODES = new Set(['ArrayExpression', 'ArrayPattern', 'ArrowFunctionExpression', 'AssignmentExpression', 'AssignmentPattern', 'AwaitExpression', 'BinaryExpression', 'BlockStatement', 'BreakStatement', 'CallExpression', 'CatchClause', 'ClassBody', 'ClassDeclaration', 'ClassExpression', 'ConditionalExpression', 'ContinueStatement', 'DebuggerStatement', 'DoWhileStatement', 'EmptyStatement', 'ExperimentalRestProperty', 'ExperimentalSpreadProperty', 'ExportAllDeclaration', 'ExportDefaultDeclaration', 'ExportNamedDeclaration', 'ExportSpecifier', 'ExpressionStatement', 'ForInStatement', 'ForOfStatement', 'ForStatement', 'FunctionDeclaration', 'FunctionExpression', 'Identifier', 'IfStatement', 'ImportDeclaration', 'ImportDefaultSpecifier', '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 LT_CHAR = /[\r\n\u2028\u2029]/
-const LINES = /[^\r\n\u2028\u2029]+(?:$|\r\n|[\r\n\u2028\u2029])/g
+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 ITERATION_OPTS = Object.freeze({
+ includeComments: true,
+ filter: isNotWhitespace
+})
+const PREFORMATTED_ELEMENT_NAMES = new Set(['pre', 'textarea'])
+/**
+ * @typedef {object} IndentOptions
+ * @property { " " | "\t" } IndentOptions.indentChar
+ * @property {number} IndentOptions.indentSize
+ * @property {number} IndentOptions.baseIndent
+ * @property {number} IndentOptions.attribute
+ * @property {object} IndentOptions.closeBracket
+ * @property {number} IndentOptions.closeBracket.startTag
+ * @property {number} IndentOptions.closeBracket.endTag
+ * @property {number} IndentOptions.closeBracket.selfClosingTag
+ * @property {number} IndentOptions.switchCase
+ * @property {boolean} IndentOptions.alignAttributesVertically
+ * @property {string[]} IndentOptions.ignores
+ */
+/**
+ * @typedef {object} IndentUserOptions
+ * @property { " " | "\t" } [IndentUserOptions.indentChar]
+ * @property {number} [IndentUserOptions.indentSize]
+ * @property {number} [IndentUserOptions.baseIndent]
+ * @property {number} [IndentUserOptions.attribute]
+ * @property {IndentOptions['closeBracket'] | number} [IndentUserOptions.closeBracket]
+ * @property {number} [IndentUserOptions.switchCase]
+ * @property {boolean} [IndentUserOptions.alignAttributesVertically]
+ * @property {string[]} [IndentUserOptions.ignores]
+ */
/**
* Normalize options.
* @param {number|"tab"|undefined} type The type of indentation.
- * @param {Object} options Other options.
- * @param {Object} defaultOptions The default value of options.
- * @returns {{indentChar:" "|"\t",indentSize:number,baseIndent:number,attribute:number,closeBracket:number,switchCase:number,alignAttributesVertically:boolean,ignores:string[]}} Normalized options.
+ * @param {IndentUserOptions} options Other options.
+ * @param {Partial} defaultOptions The default value of options.
+ * @returns {IndentOptions} Normalized options.
*/
-function parseOptions (type, options, defaultOptions) {
- const ret = Object.assign({
- indentChar: ' ',
- indentSize: 2,
- baseIndent: 0,
- attribute: 1,
- closeBracket: 0,
- switchCase: 0,
- alignAttributesVertically: true,
- ignores: []
- }, defaultOptions)
+function parseOptions(type, options, defaultOptions) {
+ /** @type {IndentOptions} */
+ const ret = Object.assign(
+ {
+ indentChar: ' ',
+ indentSize: 2,
+ baseIndent: 0,
+ attribute: 1,
+ closeBracket: {
+ startTag: 0,
+ endTag: 0,
+ selfClosingTag: 0
+ },
+ switchCase: 0,
+ alignAttributesVertically: true,
+ ignores: []
+ },
+ defaultOptions
+ )
if (Number.isSafeInteger(type)) {
- ret.indentSize = type
+ ret.indentSize = Number(type)
} else if (type === 'tab') {
ret.indentChar = '\t'
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)) {
- ret.closeBracket = options.closeBracket
+ const num = Number(options.closeBracket)
+ ret.closeBracket = {
+ startTag: num,
+ endTag: num,
+ selfClosingTag: num
+ }
+ } else if (options.closeBracket) {
+ ret.closeBracket = Object.assign(
+ {
+ startTag: 0,
+ endTag: 0,
+ selfClosingTag: 0
+ },
+ options.closeBracket
+ )
}
- if (Number.isSafeInteger(options.switchCase)) {
+ if (options.switchCase != null && Number.isSafeInteger(options.switchCase)) {
ret.switchCase = options.switchCase
}
@@ -70,167 +140,14 @@ function parseOptions (type, options, defaultOptions) {
return ret
}
-/**
- * Check whether the given token is an arrow.
- * @param {Token} 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} 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} 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} 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} 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} 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} 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} 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} 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} 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} 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 whitespace.
- * @param {Token} 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} 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' || token.type.endsWith('Comment'))
-}
-
-/**
- * Check whether the given token is a comment.
- * @param {Token} 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' && !token.type.endsWith('Comment')
-}
-
-/**
- * Check whether the given node is not an empty text node.
- * @param {Node} 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} 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.
- * @param {Array} xs The array to get the last element.
- * @returns {any|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 {Node} 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 {Node[]} 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) {
+function isBeginningOfLine(node, index, nodes) {
if (node != null) {
for (let i = index - 1; i >= 0; --i) {
const prevNode = nodes[i]
@@ -249,50 +166,67 @@ function isBeginningOfLine (node, index, nodes) {
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a closing token.
*/
-function isClosingToken (token) {
- return token != null && (
- token.type === 'HTMLEndTagOpen' ||
- token.type === 'VExpressionEnd' ||
- (
- token.type === 'Punctuator' &&
- (
- token.value === ')' ||
- token.value === '}' ||
- token.value === ']'
- )
- )
+function isClosingToken(token) {
+ return (
+ token != null &&
+ (token.type === 'HTMLEndTagOpen' ||
+ token.type === 'VExpressionEnd' ||
+ (token.type === 'Punctuator' &&
+ (token.value === ')' || token.value === '}' || token.value === ']')))
)
}
+/**
+ * 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.
*
* @param {RuleContext} context The rule context.
- * @param {TokenStore} tokenStore The token store object to get tokens.
- * @param {Object} defaultOptions The default value of options.
- * @returns {object} AST event handlers.
+ * @param {ParserServices.TokenStore | SourceCode} tokenStore The token store object to get tokens.
+ * @param {Partial} defaultOptions The default value of options.
+ * @returns {NodeListener} AST event handlers.
*/
-module.exports.defineVisitor = function create (context, tokenStore, defaultOptions) {
+module.exports.defineVisitor = function create(
+ context,
+ tokenStore,
+ defaultOptions
+) {
if (!context.getFilename().endsWith('.vue')) return {}
- const options = parseOptions(context.options[0], context.options[1] || {}, defaultOptions)
+ const options = parseOptions(
+ context.options[0],
+ context.options[1] || {},
+ 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()
/**
* Set offset to the given tokens.
- * @param {Token|Token[]} token The token to set.
+ * @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.
- * @param {boolean} [trivial=false] The flag for trivial tokens.
* @returns {void}
*/
- function setOffset (token, offset, baseToken) {
- assert(baseToken != null, "'baseToken' should not be null or undefined.")
-
+ function setOffset(token, offset, baseToken) {
+ if (!token || token === baseToken) {
+ return
+ }
if (Array.isArray(token)) {
for (const t of token) {
+ if (!t || t === baseToken) continue
offsets.set(t, {
baseToken,
offset,
@@ -310,12 +244,39 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
}
}
+ /**
+ * 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.
* @returns {void}
*/
- function setBaseline (token) {
+ function setBaseline(token) {
const offsetInfo = offsets.get(token)
if (offsetInfo != null) {
offsetInfo.baseline = true
@@ -324,23 +285,30 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
/**
* Sets preformatted tokens to the given element node.
- * @param {Node} node The node to set.
+ * @param {VElement} node The node to set.
* @returns {void}
*/
- function setPreformattedTokens (node) {
- const endToken = (node.endTag && tokenStore.getFirstToken(node.endTag)) || tokenStore.getTokenAfter(node)
+ function setPreformattedTokens(node) {
+ const endToken =
+ (node.endTag && tokenStore.getFirstToken(node.endTag)) ||
+ tokenStore.getTokenAfter(node)
- const option = {
+ /** @type {SourceCode.CursorWithSkipOptions} */
+ const cursorOptions = {
includeComments: true,
- filter: token => token != null && (
- token.type === 'HTMLText' ||
- token.type === 'HTMLRCDataText' ||
- token.type === 'HTMLTagOpen' ||
- token.type === 'HTMLEndTagOpen' ||
- token.type === 'HTMLComment'
- )
+ filter: (token) =>
+ token != null &&
+ (token.type === 'HTMLText' ||
+ token.type === 'HTMLRCDataText' ||
+ token.type === 'HTMLTagOpen' ||
+ 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)
@@ -349,19 +317,25 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
/**
* Get the first and last tokens of the given node.
* If the node is parenthesized, this gets the outermost parentheses.
- * @param {Node} 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) {
- borderOffset |= 0
+ function getFirstAndLastTokens(node, borderOffset = 0) {
+ borderOffset = Math.trunc(borderOffset)
let firstToken = tokenStore.getFirstToken(node)
let lastToken = tokenStore.getLastToken(node)
// Get the outermost left parenthesis if it's parenthesized.
let t, u
- while ((t = tokenStore.getTokenBefore(firstToken)) != null && (u = tokenStore.getTokenAfter(lastToken)) != null && isLeftParen(t) && isRightParen(u) && t.range[0] >= borderOffset) {
+ while (
+ (t = tokenStore.getTokenBefore(firstToken)) != null &&
+ (u = tokenStore.getTokenAfter(lastToken)) != null &&
+ isOpeningParenToken(t) &&
+ isClosingParenToken(u) &&
+ t.range[0] >= borderOffset
+ ) {
firstToken = t
lastToken = u
}
@@ -373,31 +347,33 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
* Process the given node list.
* The first node is offsetted from the given left token.
* Rest nodes are adjusted to the first node.
- * @param {Node[]} nodeList The node to process.
- * @param {Node|Token|null} left The left parenthesis token.
- * @param {Node|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}
*/
- function processNodeList (nodeList, left, right, offset, alignVertically) {
+ function processNodeList(nodeList, left, right, offset, alignVertically) {
let t
- const leftToken = (left && tokenStore.getFirstToken(left)) || left
- const rightToken = (right && tokenStore.getFirstToken(right)) || right
+ 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)
+ const elementTokens = getFirstAndLastTokens(
+ node,
+ 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.
if (lastToken != null) {
@@ -464,7 +440,7 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
}
}
- if (rightToken != null) {
+ if (rightToken != null && leftToken != null) {
setOffset(rightToken, 0, leftToken)
}
}
@@ -472,45 +448,53 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
/**
* Process the given node as body.
* The body node maybe a block statement or an expression node.
- * @param {Node} node The body node to process.
+ * @param {ASTNode} node The body node to process.
* @param {Token} baseToken The base token.
* @returns {void}
*/
- function processMaybeBlock (node, baseToken) {
+ 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 = []
-
- 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)
+ }
}
/**
* Find the head of chaining nodes.
- * @param {Node} node The start node to find the head.
+ * @param {ASTNode} node The start node to find the head.
* @returns {Token} The head token of the chain.
*/
- function getChainHeadToken (node) {
+ function getChainHeadToken(node) {
const type = node.type
- while (node.parent.type === 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
}
@@ -529,44 +513,53 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
* - An expression of SequenceExpression
*
* @param {Token} token The token to check.
- * @param {Node} belongingNode The node that the token is belonging to.
+ * @param {ASTNode} 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) {
+ function isBeginningOfElement(token, belongingNode) {
let node = belongingNode
- while (node != null) {
+ while (node != null && node.parent != null) {
const parent = node.parent
- const t = parent && parent.type
- if (t != null && (t.endsWith('Statement') || t.endsWith('Declaration'))) {
+ if (
+ parent.type.endsWith('Statement') ||
+ parent.type.endsWith('Declaration')
+ ) {
return parent.range[0] === token.range[0]
}
- if (t === 'VExpressionContainer') {
+ if (parent.type === 'VExpressionContainer') {
if (node.range[0] !== token.range[0]) {
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 (t === 'CallExpression' || t === 'NewExpression') {
- const openParen = tokenStore.getTokenAfter(parent.callee, isNotRightParen)
- return parent.arguments.some(param =>
- getFirstAndLastTokens(param, openParen.range[1]).firstToken.range[0] === token.range[0]
+ if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
+ const openParen = /** @type {Token} */ (
+ tokenStore.getTokenAfter(parent.callee, isNotClosingParenToken)
+ )
+ return parent.arguments.some(
+ (param) =>
+ getFirstAndLastTokens(param, openParen.range[1]).firstToken
+ .range[0] === token.range[0]
)
}
- if (t === 'ArrayExpression') {
- return parent.elements.some(element =>
- element != null &&
- getFirstAndLastTokens(element).firstToken.range[0] === token.range[0]
+ if (parent.type === 'ArrayExpression') {
+ return parent.elements.some(
+ (element) =>
+ element != null &&
+ getFirstAndLastTokens(element).firstToken.range[0] ===
+ token.range[0]
)
}
- if (t === 'SequenceExpression') {
- return parent.expressions.some(expr =>
- getFirstAndLastTokens(expr).firstToken.range[0] === token.range[0]
+ if (parent.type === 'SequenceExpression') {
+ return parent.expressions.some(
+ (expr) =>
+ getFirstAndLastTokens(expr).firstToken.range[0] === token.range[0]
)
}
@@ -578,26 +571,31 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
/**
* Set the base indentation to a given top-level AST node.
- * @param {Node} node The node to set.
+ * @param {ASTNode} node The node to set.
* @param {number} expectedIndent The number of expected indent.
* @returns {void}
*/
- function processTopLevelNode (node, expectedIndent) {
+ function processTopLevelNode(node, expectedIndent) {
const token = tokenStore.getFirstToken(node)
const offsetInfo = offsets.get(token)
- if (offsetInfo != null) {
- offsetInfo.expectedIndent = expectedIndent
+ if (offsetInfo == null) {
+ offsets.set(token, {
+ baseToken: null,
+ offset: 0,
+ baseline: false,
+ expectedIndent
+ })
} else {
- offsets.set(token, { baseToken: null, offset: 0, baseline: false, expectedIndent })
+ offsetInfo.expectedIndent = expectedIndent
}
}
/**
* Ignore all tokens of the given node.
- * @param {Node} node The node to ignore.
+ * @param {ASTNode} node The node to ignore.
* @returns {void}
*/
- function ignore (node) {
+ function ignore(node) {
for (const token of tokenStore.getTokens(node)) {
offsets.delete(token)
ignoreTokens.add(token)
@@ -606,16 +604,17 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
/**
* Define functions to ignore nodes into the given visitor.
- * @param {Object} visitor The visitor to define functions to ignore nodes.
- * @returns {Object} The visitor.
+ * @param {NodeListener} visitor The visitor to define functions to ignore nodes.
+ * @returns {NodeListener} The visitor.
*/
- function processIgnores (visitor) {
+ function processIgnores(visitor) {
for (const ignorePattern of options.ignores) {
const key = `${ignorePattern}:exit`
if (visitor.hasOwnProperty(key)) {
const handler = visitor[key]
visitor[key] = function (node, ...args) {
+ // @ts-expect-error
const ret = handler.call(this, node, ...args)
ignore(node)
return ret
@@ -631,36 +630,42 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
/**
* Calculate correct indentation of the line of the given tokens.
* @param {Token[]} tokens Tokens which are on the same line.
- * @returns {object|null} Correct indentation. If it failed to calculate then `null`.
+ * @returns { { expectedIndent: number, expectedBaseIndent: number } |null } Correct indentation. If it failed to calculate then `null`.
*/
- function getExpectedIndents (tokens) {
+ 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 && baseOffsetInfo.expectedIndent != null && (i === 0 || !baseOffsetInfo.baseline)) {
- expectedIndents.push(baseOffsetInfo.expectedIndent + (offsetInfo.offset * options.indentSize))
+ if (
+ baseOffsetInfo != null &&
+ baseOffsetInfo.expectedIndent != null &&
+ (i === 0 || !baseOffsetInfo.baseline)
+ ) {
+ expectedIndents.push(
+ baseOffsetInfo.expectedIndent +
+ offsetInfo.offset * options.indentSize
+ )
if (baseOffsetInfo.baseline) {
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)
}
}
@@ -669,7 +674,7 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
* @param {Token} firstToken The first token on a line.
* @returns {string} The text of indentation part.
*/
- function getIndentText (firstToken) {
+ function getIndentText(firstToken) {
const text = sourceCode.text
let i = firstToken.range[0] - 1
@@ -683,29 +688,33 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
/**
* Define the function which fixes the problem.
* @param {Token} token The token to fix.
- * @param {number} actualIndent The number of actual indentaion.
+ * @param {number} actualIndent The number of actual indentation.
* @param {number} expectedIndent The number of expected indentation.
- * @returns {Function} The defined function.
+ * @returns { (fixer: RuleFixer) => Fix } The defined function.
*/
- function defineFix (token, actualIndent, expectedIndent) {
+ function defineFix(token, actualIndent, expectedIndent) {
if (token.type === 'Block' && token.loc.start.line !== token.loc.end.line) {
// Fix indentation in multiline block comments.
- const lines = sourceCode.getText(token).match(LINES)
+ const lines = sourceCode.getText(token).match(LINES) || []
const firstLine = lines.shift()
- if (lines.every(l => BLOCK_COMMENT_PREFIX.test(l))) {
- return fixer => {
+ if (lines.every((l) => BLOCK_COMMENT_PREFIX.test(l))) {
+ return (fixer) => {
+ /** @type {Range} */
const range = [token.range[0] - actualIndent, token.range[1]]
const indent = options.indentChar.repeat(expectedIndent)
return fixer.replaceTextRange(
range,
- `${indent}${firstLine}${lines.map(l => l.replace(BLOCK_COMMENT_PREFIX, `${indent} *`)).join('')}`
+ `${indent}${firstLine}${lines
+ .map((l) => l.replace(BLOCK_COMMENT_PREFIX, `${indent} *`))
+ .join('')}`
)
}
}
}
- return fixer => {
+ return (fixer) => {
+ /** @type {Range} */
const range = [token.range[0] - actualIndent, token.range[0]]
const indent = options.indentChar.repeat(expectedIndent)
return fixer.replaceTextRange(range, indent)
@@ -716,10 +725,10 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
* 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[]|undefined} optionalExpectedIndents The optional expected indentation.
+ * @param {[number, number?]} [optionalExpectedIndents] The optional expected indentation.
* @returns {void}
*/
- function validateCore (token, expectedIndent, optionalExpectedIndents) {
+ function validateCore(token, expectedIndent, optionalExpectedIndents) {
const line = token.loc.start.line
const indentText = getIndentText(token)
@@ -731,19 +740,20 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
}
const actualIndent = token.loc.start.column
- const unit = (options.indentChar === '\t' ? 'tab' : 'space')
+ 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 },
end: { line, column: i + 1 }
},
- message: 'Expected {{expected}} character, but found {{actual}} character.',
+ message:
+ '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)
})
@@ -751,19 +761,24 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
}
}
- if (actualIndent !== expectedIndent && (optionalExpectedIndents == null || !optionalExpectedIndents.includes(actualIndent))) {
+ if (
+ actualIndent !== expectedIndent &&
+ (optionalExpectedIndents == null ||
+ !optionalExpectedIndents.includes(actualIndent))
+ ) {
context.report({
loc: {
start: { line, column: 0 },
end: { line, column: actualIndent }
},
- message: 'Expected indentation of {{expectedIndent}} {{unit}}{{expectedIndentPlural}} but found {{actualIndent}} {{unit}}{{actualIndentPlural}}.',
+ message:
+ 'Expected indentation of {{expectedIndent}} {{unit}}{{expectedIndentPlural}} but found {{actualIndent}} {{unit}}{{actualIndentPlural}}.',
data: {
expectedIndent,
actualIndent,
unit,
- expectedIndentPlural: (expectedIndent === 1) ? '' : 's',
- actualIndentPlural: (actualIndent === 1) ? '' : 's'
+ expectedIndentPlural: expectedIndent === 1 ? '' : 's',
+ actualIndentPlural: actualIndent === 1 ? '' : 's'
},
fix: defineFix(token, actualIndent, expectedIndent)
})
@@ -772,12 +787,16 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
/**
* Get the expected indent of comments.
- * @param {Token|null} nextToken The next token of comments.
- * @param {number|undefined} nextExpectedIndent The expected indent of the next token.
+ * @param {Token} nextToken The next token of comments.
+ * @param {number} nextExpectedIndent The expected indent of the next token.
* @param {number|undefined} lastExpectedIndent The expected indent of the last token.
- * @returns {number[]}
+ * @returns {[number, number?]}
*/
- function getCommentExpectedIndents (nextToken, nextExpectedIndent, lastExpectedIndent) {
+ function getCommentExpectedIndents(
+ nextToken,
+ nextExpectedIndent,
+ lastExpectedIndent
+ ) {
if (typeof lastExpectedIndent === 'number' && isClosingToken(nextToken)) {
if (nextExpectedIndent === lastExpectedIndent) {
// For solo comment. E.g.,
@@ -810,7 +829,7 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
* @param {Token|null} lastToken The last validated token. Comments can adjust to the token.
* @returns {void}
*/
- function validate (tokens, comments, lastToken) {
+ function validate(tokens, comments, lastToken) {
// Calculate and save expected indentation.
const firstToken = tokens[0]
const actualIndent = firstToken.loc.start.column
@@ -843,17 +862,21 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
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).expectedIndent
+ offsetInfo.expectedIndent = /** @type {OffsetData} */ (
+ offsets.get(offsetInfo.baseToken)
+ ).expectedIndent
baseline.add(token)
} else {
// Otherwise, set the expected indent of this line.
@@ -871,17 +894,24 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
// It allows the same indent level with the previous line.
const lastOffsetInfo = offsets.get(lastToken)
const lastExpectedIndent = lastOffsetInfo && lastOffsetInfo.expectedIndent
- const commentOptionalExpectedIndents = getCommentExpectedIndents(firstToken, expectedIndent, lastExpectedIndent)
+ const commentOptionalExpectedIndents = getCommentExpectedIndents(
+ firstToken,
+ expectedIndent,
+ lastExpectedIndent
+ )
// Validate.
for (const comment of comments) {
const commentExpectedIndents = getExpectedIndents([comment])
- const commentExpectedIndent =
- commentExpectedIndents
- ? commentExpectedIndents.expectedIndent
- : commentOptionalExpectedIndents[0]
-
- validateCore(comment, commentExpectedIndent, commentOptionalExpectedIndents)
+ const commentExpectedIndent = commentExpectedIndents
+ ? commentExpectedIndents.expectedIndent
+ : commentOptionalExpectedIndents[0]
+
+ validateCore(
+ comment,
+ commentExpectedIndent,
+ commentOptionalExpectedIndents
+ )
}
validateCore(firstToken, expectedIndent)
}
@@ -890,8 +920,15 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
// Main
// ------------------------------------------------------------------------------
- return processIgnores({
- 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)
@@ -904,31 +941,41 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
}
}
},
-
- VElement (node) {
- if (!PREFORMATTED_ELEMENT_NAMES.includes(node.name)) {
- const isTopLevel = node.parent.type !== 'VElement'
- const offset = isTopLevel ? options.baseIndent : 1
- processNodeList(node.children.filter(isNotEmptyTextNode), node.startTag, node.endTag, offset, false)
- } else {
+ /** @param {VElement} node */
+ VElement(node) {
+ 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(
+ node.children.filter(isNotEmptyTextNode),
+ node.startTag,
+ node.endTag,
+ offset,
+ false
+ )
}
},
-
- VEndTag (node) {
- const openToken = tokenStore.getFirstToken(node)
+ /** @param {VEndTag} node */
+ VEndTag(node) {
+ const element = node.parent
+ const startTagOpenToken = tokenStore.getFirstToken(element.startTag)
const closeToken = tokenStore.getLastToken(node)
if (closeToken.type.endsWith('TagClose')) {
- setOffset(closeToken, options.closeBracket, openToken)
+ setOffset(closeToken, options.closeBracket.endTag, startTagOpenToken)
}
},
-
- VExpressionContainer (node) {
- if (node.expression != null && node.range[0] !== node.expression.range[0]) {
+ /** @param {VExpressionContainer} node */
+ VExpressionContainer(node) {
+ if (
+ node.expression != null &&
+ node.range[0] !== node.expression.range[0]
+ ) {
const startQuoteToken = tokenStore.getFirstToken(node)
const endQuoteToken = tokenStore.getLastToken(node)
const childToken = tokenStore.getTokenAfter(startQuoteToken)
@@ -937,23 +984,24 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
setOffset(endQuoteToken, 0, startQuoteToken)
}
},
-
- VFilter (node) {
+ /** @param {VFilter} node */
+ 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)
}
},
-
- VFilterSequenceExpression (node) {
+ /** @param {VFilterSequenceExpression} node */
+ VFilterSequenceExpression(node) {
if (node.filters.length === 0) {
return
}
const firstToken = tokenStore.getFirstToken(node)
+ /** @type {(Token|null)[]} */
const tokens = []
for (const filter of node.filters) {
@@ -965,26 +1013,31 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
setOffset(tokens, 1, firstToken)
},
-
- VForExpression (node) {
+ /** @param {VForExpression} node */
+ VForExpression(node) {
const firstToken = tokenStore.getFirstToken(node)
const lastOfLeft = last(node.left) || firstToken
- const inToken = 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)
setOffset(rightToken, 1, inToken)
},
-
- VOnExpression (node) {
+ /** @param {VOnExpression} node */
+ VOnExpression(node) {
processNodeList(node.body, null, null, 0)
},
-
- VStartTag (node) {
+ /** @param {VStartTag} node */
+ VStartTag(node) {
const openToken = tokenStore.getFirstToken(node)
const closeToken = tokenStore.getLastToken(node)
@@ -996,11 +1049,15 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
options.alignAttributesVertically
)
if (closeToken != null && closeToken.type.endsWith('TagClose')) {
- setOffset(closeToken, options.closeBracket, openToken)
+ const offset =
+ closeToken.type === 'HTMLSelfClosingTagClose'
+ ? options.closeBracket.selfClosingTag
+ : options.closeBracket.startTag
+ setOffset(closeToken, offset, openToken)
}
},
-
- VText (node) {
+ /** @param {VText} node */
+ VText(node) {
const tokens = tokenStore.getTokens(node, isNotWhitespace)
const firstTokenInfo = offsets.get(tokenStore.getFirstToken(node))
@@ -1008,73 +1065,149 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
offsets.set(token, Object.assign({}, firstTokenInfo))
}
},
-
- 'ArrayExpression, ArrayPattern' (node) {
- processNodeList(node.elements, tokenStore.getFirstToken(node), tokenStore.getLastToken(node), 1)
+ // ----------------------------------------------------------------------
+ // SINGLE TOKEN NODES
+ // ----------------------------------------------------------------------
+ VIdentifier() {},
+ VLiteral() {},
+ // ----------------------------------------------------------------------
+ // WRAPPER NODES
+ // ----------------------------------------------------------------------
+ VDirectiveKey() {},
+ VSlotScopeExpression() {},
+ // ----------------------------------------------------------------------
+ // ES NODES
+ // ----------------------------------------------------------------------
+ /** @param {ArrayExpression | ArrayPattern} node */
+ 'ArrayExpression, ArrayPattern'(node) {
+ const firstToken = tokenStore.getFirstToken(node)
+ const rightToken = tokenStore.getTokenAfter(
+ node.elements[node.elements.length - 1] || firstToken,
+ isClosingBracketToken
+ )
+ processNodeList(node.elements, firstToken, rightToken, 1)
},
-
- ArrowFunctionExpression (node) {
+ /** @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)) {
- const rightToken = tokenStore.getTokenAfter(last(node.params) || leftToken, isRightParen)
+ if (isOpeningParenToken(leftToken)) {
+ const rightToken = tokenStore.getTokenAfter(
+ last(node.params) || leftToken,
+ isClosingParenToken
+ )
processNodeList(node.params, leftToken, rightToken, 1)
}
setOffset(arrowToken, 1, firstToken)
processMaybeBlock(node.body, firstToken)
},
-
- 'AssignmentExpression, AssignmentPattern, BinaryExpression, LogicalExpression' (node) {
+ /** @param {AssignmentExpression | AssignmentPattern | BinaryExpression | LogicalExpression} node */
+ 'AssignmentExpression, AssignmentPattern, BinaryExpression, LogicalExpression'(
+ node
+ ) {
const leftToken = getChainHeadToken(node)
- const opToken = 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 = (
+ const shouldIndent =
prevToken == null ||
prevToken.loc.end.line === leftToken.loc.start.line ||
isBeginningOfElement(leftToken, node)
- )
setOffset([opToken, rightToken], shouldIndent ? 1 : 0, leftToken)
},
-
- 'AwaitExpression, RestElement, SpreadElement, UnaryExpression' (node) {
+ /** @param {AwaitExpression | RestElement | SpreadElement | UnaryExpression} node */
+ 'AwaitExpression, RestElement, SpreadElement, UnaryExpression'(node) {
const firstToken = tokenStore.getFirstToken(node)
const nextToken = tokenStore.getTokenAfter(firstToken)
setOffset(nextToken, 1, firstToken)
},
-
- 'BlockStatement, ClassBody' (node) {
- processNodeList(node.body, tokenStore.getFirstToken(node), tokenStore.getLastToken(node), 1)
+ /** @param {BlockStatement | ClassBody} node */
+ 'BlockStatement, ClassBody'(node) {
+ processNodeList(
+ node.body,
+ tokenStore.getFirstToken(node),
+ tokenStore.getLastToken(node),
+ 1
+ )
},
-
- 'BreakStatement, ContinueStatement, ReturnStatement, ThrowStatement' (node) {
- if (node.argument != null || node.label != null) {
+ 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 (
+ ((node.type === 'ReturnStatement' || node.type === 'ThrowStatement') &&
+ node.argument != null) ||
+ ((node.type === 'BreakStatement' ||
+ node.type === 'ContinueStatement') &&
+ node.label != null)
+ ) {
const firstToken = tokenStore.getFirstToken(node)
const nextToken = tokenStore.getTokenAfter(firstToken)
setOffset(nextToken, 1, firstToken)
}
},
-
- CallExpression (node) {
+ /** @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)
},
+ /** @param {ImportExpression} node */
+ ImportExpression(node) {
+ const firstToken = tokenStore.getFirstToken(node)
+ const rightToken = tokenStore.getLastToken(node)
+ const leftToken = tokenStore.getTokenAfter(
+ firstToken,
+ isOpeningParenToken
+ )
- CatchClause (node) {
+ setOffset(leftToken, 1, firstToken)
+ processNodeList([node.source], leftToken, rightToken, 1)
+ },
+ /** @param {CatchClause} node */
+ CatchClause(node) {
const firstToken = tokenStore.getFirstToken(node)
const bodyToken = tokenStore.getFirstToken(node.body)
@@ -1087,8 +1220,8 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
}
setOffset(bodyToken, 0, firstToken)
},
-
- 'ClassDeclaration, ClassExpression' (node) {
+ /** @param {ClassDeclaration | ClassExpression} node */
+ 'ClassDeclaration, ClassExpression'(node) {
const firstToken = tokenStore.getFirstToken(node)
const bodyToken = tokenStore.getFirstToken(node.body)
@@ -1096,20 +1229,26 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
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)
}
setOffset(bodyToken, 0, firstToken)
},
-
- ConditionalExpression (node) {
+ /** @param {ConditionalExpression} node */
+ ConditionalExpression(node) {
const prevToken = tokenStore.getTokenBefore(node)
const firstToken = tokenStore.getFirstToken(node)
- const questionToken = tokenStore.getTokenAfter(node.test, isNotRightParen)
+ const questionToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(node.test, isNotClosingParenToken)
+ )
const consequentToken = tokenStore.getTokenAfter(questionToken)
- const colonToken = tokenStore.getTokenAfter(node.consequent, isNotRightParen)
+ const colonToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(node.consequent, isNotClosingParenToken)
+ )
const alternateToken = tokenStore.getTokenAfter(colonToken)
const isFlat =
prevToken &&
@@ -1117,20 +1256,28 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
node.test.loc.end.line === node.consequent.loc.start.line
if (isFlat) {
- setOffset([questionToken, consequentToken, colonToken, alternateToken], 0, firstToken)
+ setOffset(
+ [questionToken, consequentToken, colonToken, alternateToken],
+ 0,
+ firstToken
+ )
} else {
setOffset([questionToken, colonToken], 1, firstToken)
setOffset([consequentToken, alternateToken], 1, questionToken)
}
},
-
- DoWhileStatement (node) {
+ /** @param {DoWhileStatement} node */
+ DoWhileStatement(node) {
const doToken = tokenStore.getFirstToken(node)
- const whileToken = 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) ? tokenStore.getTokenBefore(lastToken) : lastToken
+ const rightToken = isSemicolonToken(lastToken)
+ ? tokenStore.getTokenBefore(lastToken)
+ : lastToken
processMaybeBlock(node.body, doToken)
setOffset(whileToken, 0, doToken)
@@ -1138,59 +1285,150 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
setOffset(testToken, 1, leftToken)
setOffset(rightToken, 0, leftToken)
},
+ /** @param {ExportAllDeclaration} node */
+ ExportAllDeclaration(node) {
+ 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, exportToken)
+ setOffset(asToken, 1, starToken)
+ setOffset(exportedToken, 1, starToken)
+ setOffset(afterTokens, 1, exportToken)
+ } else {
+ setOffset(tokens, 1, exportToken)
+ }
- ExportAllDeclaration (node) {
- const tokens = tokenStore.getTokens(node)
- const firstToken = tokens.shift()
- if (isSemicolon(last(tokens))) {
- tokens.pop()
+ // 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)
+ }
}
- setOffset(tokens, 1, firstToken)
},
-
- ExportDefaultDeclaration (node) {
+ /** @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)
},
-
- ExportNamedDeclaration (node) {
+ /** @param {ExportNamedDeclaration} node */
+ ExportNamedDeclaration(node) {
const exportToken = tokenStore.getFirstToken(node)
if (node.declaration) {
// export var foo = 1;
const declarationToken = tokenStore.getFirstToken(node, 1)
setOffset(declarationToken, 1, exportToken)
} else {
- // export {foo, bar}; or export {foo, bar} from "mod";
- const leftParenToken = tokenStore.getFirstToken(node, 1)
- const rightParenToken = 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 firstSpecifier = node.specifiers[0]
+ if (!firstSpecifier || firstSpecifier.type === 'ExportSpecifier') {
+ // export {foo, bar}; or export {foo, bar} from "mod";
+ 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 parser
}
}
},
-
- ExportSpecifier (node) {
+ /** @param {ExportSpecifier | ImportSpecifier} node */
+ 'ExportSpecifier, ImportSpecifier'(node) {
const tokens = tokenStore.getTokens(node)
- const firstToken = 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)
},
-
- 'ForInStatement, ForOfStatement' (node) {
+ /** @param {ForInStatement | ForOfStatement} node */
+ 'ForInStatement, ForOfStatement'(node) {
const forToken = tokenStore.getFirstToken(node)
- const leftParenToken = tokenStore.getTokenAfter(forToken)
+ const awaitToken =
+ (node.type === 'ForOfStatement' &&
+ node.await &&
+ tokenStore.getTokenAfter(forToken)) ||
+ null
+ const leftParenToken = tokenStore.getTokenAfter(awaitToken || forToken)
const leftToken = tokenStore.getTokenAfter(leftParenToken)
- const inToken = tokenStore.getTokenAfter(leftToken, isNotRightParen)
+ const inToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(leftToken, isNotClosingParenToken)
+ )
const rightToken = tokenStore.getTokenAfter(inToken)
- const rightParenToken = tokenStore.getTokenBefore(node.body, isNotLeftParen)
+ const rightParenToken = tokenStore.getTokenBefore(
+ node.body,
+ isNotOpeningParenToken
+ )
+ if (awaitToken != null) {
+ setOffset(awaitToken, 0, forToken)
+ }
setOffset(leftParenToken, 1, forToken)
setOffset(leftToken, 1, leftParenToken)
setOffset(inToken, 1, leftToken)
@@ -1198,181 +1436,218 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
setOffset(rightParenToken, 0, leftParenToken)
processMaybeBlock(node.body, forToken)
},
-
- ForStatement (node) {
+ /** @param {ForStatement} node */
+ ForStatement(node) {
const forToken = tokenStore.getFirstToken(node)
const leftParenToken = tokenStore.getTokenAfter(forToken)
- const rightParenToken = tokenStore.getTokenBefore(node.body, isNotLeftParen)
+ const rightParenToken = tokenStore.getTokenBefore(
+ node.body,
+ isNotOpeningParenToken
+ )
setOffset(leftParenToken, 1, forToken)
- processNodeList([node.init, node.test, node.update], leftParenToken, rightParenToken, 1)
+ processNodeList(
+ [node.init, node.test, node.update],
+ leftParenToken,
+ rightParenToken,
+ 1
+ )
processMaybeBlock(node.body, forToken)
},
-
- 'FunctionDeclaration, FunctionExpression' (node) {
+ /** @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
}
- },
- IfStatement (node) {
+ 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) {
const ifToken = tokenStore.getFirstToken(node)
const ifLeftParenToken = tokenStore.getTokenAfter(ifToken)
- const ifRightParenToken = tokenStore.getTokenBefore(node.consequent, isRightParen)
+ const ifRightParenToken = tokenStore.getTokenBefore(
+ node.consequent,
+ isClosingParenToken
+ )
setOffset(ifLeftParenToken, 1, ifToken)
setOffset(ifRightParenToken, 0, ifLeftParenToken)
processMaybeBlock(node.consequent, ifToken)
if (node.alternate != null) {
- const elseToken = tokenStore.getTokenAfter(node.consequent, isNotRightParen)
+ const elseToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(node.consequent, isNotClosingParenToken)
+ )
setOffset(elseToken, 0, ifToken)
processMaybeBlock(node.alternate, elseToken)
}
},
-
- ImportDeclaration (node) {
- const firstSpecifier = node.specifiers[0]
- const secondSpecifier = node.specifiers[1]
+ /** @param {ImportDeclaration} node */
+ ImportDeclaration(node) {
const importToken = tokenStore.getFirstToken(node)
- const hasSemi = tokenStore.getLastToken(node).value === ';'
- 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)
- },
-
- ImportSpecifier (node) {
- if (node.local.range[0] !== node.imported.range[0]) {
- const tokens = tokenStore.getTokens(node)
- const firstToken = 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)
+ }
}
},
-
- ImportNamespaceSpecifier (node) {
+ /** @param {ImportNamespaceSpecifier} node */
+ ImportNamespaceSpecifier(node) {
const tokens = tokenStore.getTokens(node)
- const firstToken = tokens.shift()
+ const firstToken = /** @type {Token} */ (tokens.shift())
setOffset(tokens, 1, firstToken)
},
-
- LabeledStatement (node) {
+ /** @param {LabeledStatement} node */
+ LabeledStatement(node) {
const labelToken = tokenStore.getFirstToken(node)
const colonToken = tokenStore.getTokenAfter(labelToken)
const bodyToken = tokenStore.getTokenAfter(colonToken)
setOffset([colonToken, bodyToken], 1, labelToken)
},
-
- 'MemberExpression, MetaProperty' (node) {
+ /** @param {MemberExpression | MetaProperty} node */
+ 'MemberExpression, MetaProperty'(node) {
const objectToken = tokenStore.getFirstToken(node)
- if (node.computed) {
- const leftBracketToken = tokenStore.getTokenBefore(node.property, isLeftBracket)
+ if (node.type === 'MemberExpression' && node.computed) {
+ const leftBracketToken = /** @type {Token} */ (
+ tokenStore.getTokenBefore(node.property, isOpeningBracketToken)
+ )
const propertyToken = tokenStore.getTokenAfter(leftBracketToken)
- const rightBracketToken = tokenStore.getTokenAfter(node.property, isRightBracket)
+ const rightBracketToken = tokenStore.getTokenAfter(
+ node.property,
+ 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)
@@ -1384,97 +1659,115 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
setOffset([dotToken, propertyToken], 1, objectToken)
}
},
-
- '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)
- let lastKeyToken = null
+ let lastKeyToken
if (node.computed) {
- const keyLeftToken = tokenStore.getFirstToken(node, isLeftBracket)
- const keyToken = tokenStore.getTokenAfter(keyLeftToken)
- const keyRightToken = lastKeyToken = tokenStore.getTokenAfter(node.key, isRightBracket)
-
- if (hasPrefix) {
- setOffset(keyLeftToken, 0, last(prefixTokens))
- }
- setOffset(keyToken, 1, keyLeftToken)
- setOffset(keyRightToken, 0, keyLeftToken)
+ const leftBracketToken = tokenStore.getTokenBefore(keyTokens.firstToken)
+ const rightBracketToken = (lastKeyToken = tokenStore.getTokenAfter(
+ keyTokens.lastToken
+ ))
+ setOffset(leftBracketToken, 0, firstToken)
+ processNodeList([node.key], leftBracketToken, rightBracketToken, 1)
} else {
- const idToken = lastKeyToken = tokenStore.getFirstToken(node.key)
-
- if (hasPrefix) {
- setOffset(idToken, 0, last(prefixTokens))
- }
+ setOffset(keyTokens.firstToken, 0, firstToken)
+ lastKeyToken = keyTokens.lastToken
}
- if (isMethod) {
- const leftParenToken = tokenStore.getTokenAfter(lastKeyToken)
-
- setOffset(leftParenToken, 1, lastKeyToken)
- } else if (!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
+ )
}
},
-
- NewExpression (node) {
+ /** @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)
processNodeList(node.arguments, leftToken, rightToken, 1)
}
},
-
- 'ObjectExpression, ObjectPattern' (node) {
- processNodeList(node.properties, tokenStore.getFirstToken(node), tokenStore.getLastToken(node), 1)
+ /** @param {ObjectExpression | ObjectPattern} node */
+ 'ObjectExpression, ObjectPattern'(node) {
+ const firstToken = tokenStore.getFirstToken(node)
+ const rightToken = tokenStore.getTokenAfter(
+ node.properties[node.properties.length - 1] || firstToken,
+ isClosingBraceToken
+ )
+ processNodeList(node.properties, firstToken, rightToken, 1)
},
-
- SequenceExpression (node) {
+ /** @param {SequenceExpression} node */
+ SequenceExpression(node) {
processNodeList(node.expressions, null, null, 0)
},
-
- SwitchCase (node) {
+ /** @param {SwitchCase} node */
+ 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 (node.consequent.length === 1 && node.consequent[0].type === 'BlockStatement') {
+ if (
+ node.consequent.length === 1 &&
+ 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)
}
},
-
- SwitchStatement (node) {
+ /** @param {SwitchStatement} node */
+ SwitchStatement(node) {
const switchToken = tokenStore.getFirstToken(node)
const leftParenToken = tokenStore.getTokenAfter(switchToken)
const discriminantToken = tokenStore.getTokenAfter(leftParenToken)
- const leftBraceToken = tokenStore.getTokenAfter(node.discriminant, isLeftBrace)
+ const leftBraceToken = /** @type {Token} */ (
+ tokenStore.getTokenAfter(node.discriminant, isOpeningBraceToken)
+ )
const rightParenToken = tokenStore.getTokenBefore(leftBraceToken)
const rightBraceToken = tokenStore.getLastToken(node)
@@ -1482,26 +1775,35 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
setOffset(discriminantToken, 1, leftParenToken)
setOffset(rightParenToken, 0, leftParenToken)
setOffset(leftBraceToken, 0, switchToken)
- processNodeList(node.cases, leftBraceToken, rightBraceToken, options.switchCase)
+ processNodeList(
+ node.cases,
+ leftBraceToken,
+ rightBraceToken,
+ options.switchCase
+ )
},
-
- TaggedTemplateExpression (node) {
+ /** @param {TaggedTemplateExpression} node */
+ TaggedTemplateExpression(node) {
const tagTokens = getFirstAndLastTokens(node.tag, node.range[0])
const quasiToken = tokenStore.getTokenAfter(tagTokens.lastToken)
setOffset(quasiToken, 1, tagTokens.firstToken)
},
-
- TemplateLiteral (node) {
+ /** @param {TemplateLiteral} node */
+ TemplateLiteral(node) {
const firstToken = tokenStore.getFirstToken(node)
- const quasiTokens = node.quasis.slice(1).map(n => tokenStore.getFirstToken(n))
- const expressionToken = node.quasis.slice(0, -1).map(n => tokenStore.getTokenAfter(n))
+ const quasiTokens = node.quasis
+ .slice(1)
+ .map((n) => tokenStore.getFirstToken(n))
+ const expressionToken = node.quasis
+ .slice(0, -1)
+ .map((n) => tokenStore.getTokenAfter(n))
setOffset(quasiTokens, 0, firstToken)
setOffset(expressionToken, 1, firstToken)
},
-
- TryStatement (node) {
+ /** @param {TryStatement} node */
+ TryStatement(node) {
const tryToken = tokenStore.getFirstToken(node)
const tryBlockToken = tokenStore.getFirstToken(node.block)
@@ -1520,19 +1822,24 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
setOffset([finallyToken, finallyBlockToken], 0, tryToken)
}
},
-
- UpdateExpression (node) {
+ /** @param {UpdateExpression} node */
+ UpdateExpression(node) {
const firstToken = tokenStore.getFirstToken(node)
const nextToken = tokenStore.getTokenAfter(firstToken)
setOffset(nextToken, 1, firstToken)
},
-
- VariableDeclaration (node) {
- processNodeList(node.declarations, tokenStore.getFirstToken(node), null, 1)
+ /** @param {VariableDeclaration} node */
+ VariableDeclaration(node) {
+ processNodeList(
+ node.declarations,
+ tokenStore.getFirstToken(node),
+ null,
+ 1
+ )
},
-
- VariableDeclarator (node) {
+ /** @param {VariableDeclarator} node */
+ VariableDeclarator(node) {
if (node.init != null) {
const idToken = tokenStore.getFirstToken(node)
const eqToken = tokenStore.getTokenAfter(node.id)
@@ -1541,18 +1848,21 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
setOffset([eqToken, initToken], 1, idToken)
}
},
-
- 'WhileStatement, WithStatement' (node) {
+ /** @param {WhileStatement | WithStatement} node */
+ '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)
processMaybeBlock(node.body, firstToken)
},
-
- YieldExpression (node) {
+ /** @param {YieldExpression} node */
+ YieldExpression(node) {
if (node.argument != null) {
const yieldToken = tokenStore.getFirstToken(node)
@@ -1562,34 +1872,45 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
}
}
},
-
+ // ----------------------------------------------------------------------
+ // 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)
@@ -1599,49 +1920,69 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
}
},
+ .../** @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)) {
+ '*:exit'(node) {
+ if (!knownNodes.has(node.type)) {
ignore(node)
}
},
-
+ /** @param {Program} node */
// Top-level process.
- Program (node) {
+ Program(node) {
const firstToken = node.tokens[0]
- const isScriptTag = (
+ const isScriptTag =
firstToken != null &&
firstToken.type === 'Punctuator' &&
firstToken.value === '
diff --git a/tests/fixtures/script-indent/class-fields-private-methods-01.vue b/tests/fixtures/script-indent/class-fields-private-methods-01.vue
new file mode 100644
index 000000000..46c6ae67c
--- /dev/null
+++ b/tests/fixtures/script-indent/class-fields-private-methods-01.vue
@@ -0,0 +1,64 @@
+
+
diff --git a/tests/fixtures/script-indent/class-fields-private-properties-01.vue b/tests/fixtures/script-indent/class-fields-private-properties-01.vue
new file mode 100644
index 000000000..c935f1904
--- /dev/null
+++ b/tests/fixtures/script-indent/class-fields-private-properties-01.vue
@@ -0,0 +1,22 @@
+
+
diff --git a/tests/fixtures/script-indent/class-fields-properties-01.vue b/tests/fixtures/script-indent/class-fields-properties-01.vue
new file mode 100644
index 000000000..ef193d1fe
--- /dev/null
+++ b/tests/fixtures/script-indent/class-fields-properties-01.vue
@@ -0,0 +1,16 @@
+
+
diff --git a/tests/fixtures/script-indent/class-fields-properties-02.vue b/tests/fixtures/script-indent/class-fields-properties-02.vue
new file mode 100644
index 000000000..3efe3bd25
--- /dev/null
+++ b/tests/fixtures/script-indent/class-fields-properties-02.vue
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/fixtures/script-indent/export-all-declaration-02.vue b/tests/fixtures/script-indent/export-all-declaration-02.vue
new file mode 100644
index 000000000..b85213a6b
--- /dev/null
+++ b/tests/fixtures/script-indent/export-all-declaration-02.vue
@@ -0,0 +1,16 @@
+
+
diff --git a/tests/fixtures/script-indent/for-await-of-01.vue b/tests/fixtures/script-indent/for-await-of-01.vue
new file mode 100644
index 000000000..b5c07e2c1
--- /dev/null
+++ b/tests/fixtures/script-indent/for-await-of-01.vue
@@ -0,0 +1,15 @@
+
+
diff --git a/tests/fixtures/script-indent/ignore-01.vue b/tests/fixtures/script-indent/ignore-01.vue
new file mode 100644
index 000000000..e323cdc08
--- /dev/null
+++ b/tests/fixtures/script-indent/ignore-01.vue
@@ -0,0 +1,19 @@
+
+
diff --git a/tests/fixtures/script-indent/ignore-02.vue b/tests/fixtures/script-indent/ignore-02.vue
new file mode 100644
index 000000000..18c557476
--- /dev/null
+++ b/tests/fixtures/script-indent/ignore-02.vue
@@ -0,0 +1,19 @@
+
+
diff --git a/tests/fixtures/script-indent/import-declaration-11.vue b/tests/fixtures/script-indent/import-declaration-11.vue
new file mode 100644
index 000000000..83289b1f4
--- /dev/null
+++ b/tests/fixtures/script-indent/import-declaration-11.vue
@@ -0,0 +1,7 @@
+
+
+
diff --git a/tests/fixtures/script-indent/import-declaration-12.vue b/tests/fixtures/script-indent/import-declaration-12.vue
new file mode 100644
index 000000000..fa622a696
--- /dev/null
+++ b/tests/fixtures/script-indent/import-declaration-12.vue
@@ -0,0 +1,11 @@
+
+
+
diff --git a/tests/fixtures/script-indent/import-expression-01.vue b/tests/fixtures/script-indent/import-expression-01.vue
new file mode 100644
index 000000000..3f7c7cb61
--- /dev/null
+++ b/tests/fixtures/script-indent/import-expression-01.vue
@@ -0,0 +1,7 @@
+
+
diff --git a/tests/fixtures/script-indent/import-expression-02.vue b/tests/fixtures/script-indent/import-expression-02.vue
new file mode 100644
index 000000000..8144c3ffa
--- /dev/null
+++ b/tests/fixtures/script-indent/import-expression-02.vue
@@ -0,0 +1,8 @@
+
+
diff --git a/tests/fixtures/script-indent/import-expression-03.vue b/tests/fixtures/script-indent/import-expression-03.vue
new file mode 100644
index 000000000..e82a6df42
--- /dev/null
+++ b/tests/fixtures/script-indent/import-expression-03.vue
@@ -0,0 +1,11 @@
+
+
diff --git a/tests/fixtures/script-indent/jsx-01.vue b/tests/fixtures/script-indent/jsx-01.vue
new file mode 100644
index 000000000..d5e73c650
--- /dev/null
+++ b/tests/fixtures/script-indent/jsx-01.vue
@@ -0,0 +1,16 @@
+
+
diff --git a/tests/fixtures/script-indent/jsx-02.vue b/tests/fixtures/script-indent/jsx-02.vue
new file mode 100644
index 000000000..a7656d430
--- /dev/null
+++ b/tests/fixtures/script-indent/jsx-02.vue
@@ -0,0 +1,23 @@
+
+
diff --git a/tests/fixtures/script-indent/method-definition-02.vue b/tests/fixtures/script-indent/method-definition-02.vue
new file mode 100644
index 000000000..d5c1d3725
--- /dev/null
+++ b/tests/fixtures/script-indent/method-definition-02.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/nullish-coalescing-operator-01.vue b/tests/fixtures/script-indent/nullish-coalescing-operator-01.vue
new file mode 100644
index 000000000..22427c7f6
--- /dev/null
+++ b/tests/fixtures/script-indent/nullish-coalescing-operator-01.vue
@@ -0,0 +1,6 @@
+
+
diff --git a/tests/fixtures/script-indent/object-expression-04.vue b/tests/fixtures/script-indent/object-expression-04.vue
new file mode 100644
index 000000000..25e8c2077
--- /dev/null
+++ b/tests/fixtures/script-indent/object-expression-04.vue
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/fixtures/script-indent/optional-chaining-01.vue b/tests/fixtures/script-indent/optional-chaining-01.vue
new file mode 100644
index 000000000..f0ea36ee4
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-01.vue
@@ -0,0 +1,7 @@
+
+
diff --git a/tests/fixtures/script-indent/optional-chaining-02.vue b/tests/fixtures/script-indent/optional-chaining-02.vue
new file mode 100644
index 000000000..11c77d518
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-02.vue
@@ -0,0 +1,7 @@
+
+
diff --git a/tests/fixtures/script-indent/optional-chaining-03.vue b/tests/fixtures/script-indent/optional-chaining-03.vue
new file mode 100644
index 000000000..01626acf0
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-03.vue
@@ -0,0 +1,7 @@
+
+
diff --git a/tests/fixtures/script-indent/optional-chaining-04.vue b/tests/fixtures/script-indent/optional-chaining-04.vue
new file mode 100644
index 000000000..65a42a74d
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-04.vue
@@ -0,0 +1,13 @@
+
+
diff --git a/tests/fixtures/script-indent/optional-chaining-05.vue b/tests/fixtures/script-indent/optional-chaining-05.vue
new file mode 100644
index 000000000..0e926b497
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-05.vue
@@ -0,0 +1,11 @@
+
+
diff --git a/tests/fixtures/script-indent/optional-chaining-06.vue b/tests/fixtures/script-indent/optional-chaining-06.vue
new file mode 100644
index 000000000..47898015b
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-06.vue
@@ -0,0 +1,25 @@
+
+
diff --git a/tests/fixtures/script-indent/optional-chaining-07.vue b/tests/fixtures/script-indent/optional-chaining-07.vue
new file mode 100644
index 000000000..1ba9fb0fe
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-07.vue
@@ -0,0 +1,25 @@
+
+
diff --git a/tests/fixtures/script-indent/optional-chaining-08.vue b/tests/fixtures/script-indent/optional-chaining-08.vue
new file mode 100644
index 000000000..61d071887
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-08.vue
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/fixtures/script-indent/optional-chaining-09.vue b/tests/fixtures/script-indent/optional-chaining-09.vue
new file mode 100644
index 000000000..c17562c0c
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-09.vue
@@ -0,0 +1,32 @@
+
+
diff --git a/tests/fixtures/script-indent/rest-properties-01.vue b/tests/fixtures/script-indent/rest-properties-01.vue
new file mode 100644
index 000000000..87ab2ab9b
--- /dev/null
+++ b/tests/fixtures/script-indent/rest-properties-01.vue
@@ -0,0 +1,8 @@
+
+
diff --git a/tests/fixtures/script-indent/static-block-01.vue b/tests/fixtures/script-indent/static-block-01.vue
new file mode 100644
index 000000000..87bfc6b34
--- /dev/null
+++ b/tests/fixtures/script-indent/static-block-01.vue
@@ -0,0 +1,8 @@
+
+
diff --git a/tests/fixtures/script-indent/try-statement-04.vue b/tests/fixtures/script-indent/try-statement-04.vue
new file mode 100644
index 000000000..4b3d9e866
--- /dev/null
+++ b/tests/fixtures/script-indent/try-statement-04.vue
@@ -0,0 +1,14 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-abstract-accessor-property-01.vue b/tests/fixtures/script-indent/ts-abstract-accessor-property-01.vue
new file mode 100644
index 000000000..01068057e
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-abstract-accessor-property-01.vue
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-abstract-class-property-01.vue b/tests/fixtures/script-indent/ts-abstract-class-property-01.vue
new file mode 100644
index 000000000..500b4c493
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-abstract-class-property-01.vue
@@ -0,0 +1,22 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-abstract-class-property-02.vue b/tests/fixtures/script-indent/ts-abstract-class-property-02.vue
new file mode 100644
index 000000000..47e4aba99
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-abstract-class-property-02.vue
@@ -0,0 +1,19 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-abstract-method-definition-01.vue b/tests/fixtures/script-indent/ts-abstract-method-definition-01.vue
new file mode 100644
index 000000000..cd700f1a9
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-abstract-method-definition-01.vue
@@ -0,0 +1,30 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-accessor-property-01.vue b/tests/fixtures/script-indent/ts-accessor-property-01.vue
new file mode 100644
index 000000000..97928b6b6
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-accessor-property-01.vue
@@ -0,0 +1,8 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-accessor-property-02.vue b/tests/fixtures/script-indent/ts-accessor-property-02.vue
new file mode 100644
index 000000000..da749a32e
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-accessor-property-02.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-accessor-property-03.vue b/tests/fixtures/script-indent/ts-accessor-property-03.vue
new file mode 100644
index 000000000..993e52527
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-accessor-property-03.vue
@@ -0,0 +1,9 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-accessor-property-04.vue b/tests/fixtures/script-indent/ts-accessor-property-04.vue
new file mode 100644
index 000000000..e69e294fc
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-accessor-property-04.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-accessor-property-05.vue b/tests/fixtures/script-indent/ts-accessor-property-05.vue
new file mode 100644
index 000000000..dc3503ca7
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-accessor-property-05.vue
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-as-expression-01.vue b/tests/fixtures/script-indent/ts-as-expression-01.vue
new file mode 100644
index 000000000..377dd3436
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-as-expression-01.vue
@@ -0,0 +1,6 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-call-expression-01.vue b/tests/fixtures/script-indent/ts-call-expression-01.vue
new file mode 100644
index 000000000..a12e28f7d
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-call-expression-01.vue
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-call-signature-declaration-01.vue b/tests/fixtures/script-indent/ts-call-signature-declaration-01.vue
new file mode 100644
index 000000000..b734c69c6
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-call-signature-declaration-01.vue
@@ -0,0 +1,13 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-call-signature-declaration-02.vue b/tests/fixtures/script-indent/ts-call-signature-declaration-02.vue
new file mode 100644
index 000000000..e2b6cd0b7
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-call-signature-declaration-02.vue
@@ -0,0 +1,13 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-declaration-01.vue b/tests/fixtures/script-indent/ts-class-declaration-01.vue
new file mode 100644
index 000000000..776e38aa3
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-declaration-01.vue
@@ -0,0 +1,11 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-declaration-02.vue b/tests/fixtures/script-indent/ts-class-declaration-02.vue
new file mode 100644
index 000000000..4b4eafbf1
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-declaration-02.vue
@@ -0,0 +1,16 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-declaration-03.vue b/tests/fixtures/script-indent/ts-class-declaration-03.vue
new file mode 100644
index 000000000..c1697f273
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-declaration-03.vue
@@ -0,0 +1,26 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-declaration-04.vue b/tests/fixtures/script-indent/ts-class-declaration-04.vue
new file mode 100644
index 000000000..c1697f273
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-declaration-04.vue
@@ -0,0 +1,26 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-declaration-05.vue b/tests/fixtures/script-indent/ts-class-declaration-05.vue
new file mode 100644
index 000000000..893584928
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-declaration-05.vue
@@ -0,0 +1,14 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-declaration-06.vue b/tests/fixtures/script-indent/ts-class-declaration-06.vue
new file mode 100644
index 000000000..06e42f0c6
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-declaration-06.vue
@@ -0,0 +1,28 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-declaration-07.vue b/tests/fixtures/script-indent/ts-class-declaration-07.vue
new file mode 100644
index 000000000..dfca03b8a
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-declaration-07.vue
@@ -0,0 +1,9 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-fields-private-methods-01.vue b/tests/fixtures/script-indent/ts-class-fields-private-methods-01.vue
new file mode 100644
index 000000000..ac0eda090
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-fields-private-methods-01.vue
@@ -0,0 +1,64 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-fields-private-properties-01.vue b/tests/fixtures/script-indent/ts-class-fields-private-properties-01.vue
new file mode 100644
index 000000000..26006fb4a
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-fields-private-properties-01.vue
@@ -0,0 +1,22 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-property-01.vue b/tests/fixtures/script-indent/ts-class-property-01.vue
new file mode 100644
index 000000000..a0e8b8583
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-property-01.vue
@@ -0,0 +1,15 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-property-02.vue b/tests/fixtures/script-indent/ts-class-property-02.vue
new file mode 100644
index 000000000..f2ae2e508
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-property-02.vue
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-class-property-03.vue b/tests/fixtures/script-indent/ts-class-property-03.vue
new file mode 100644
index 000000000..dfd437f38
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-class-property-03.vue
@@ -0,0 +1,36 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-conditional-type-01.vue b/tests/fixtures/script-indent/ts-conditional-type-01.vue
new file mode 100644
index 000000000..ca1334b18
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-conditional-type-01.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-conditional-type-02.vue b/tests/fixtures/script-indent/ts-conditional-type-02.vue
new file mode 100644
index 000000000..1e8d68b76
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-conditional-type-02.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-conditional-type-03.vue b/tests/fixtures/script-indent/ts-conditional-type-03.vue
new file mode 100644
index 000000000..e3230e948
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-conditional-type-03.vue
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-constructor-type-01.vue b/tests/fixtures/script-indent/ts-constructor-type-01.vue
new file mode 100644
index 000000000..97cc1a1d8
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-constructor-type-01.vue
@@ -0,0 +1,21 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-declare-function-01.vue b/tests/fixtures/script-indent/ts-declare-function-01.vue
new file mode 100644
index 000000000..2d1fa1f19
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-declare-function-01.vue
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-declare-function-02.vue b/tests/fixtures/script-indent/ts-declare-function-02.vue
new file mode 100644
index 000000000..1450b1b32
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-declare-function-02.vue
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-declare-function-03.vue b/tests/fixtures/script-indent/ts-declare-function-03.vue
new file mode 100644
index 000000000..7dabfe206
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-declare-function-03.vue
@@ -0,0 +1,14 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-declare-function-04.vue b/tests/fixtures/script-indent/ts-declare-function-04.vue
new file mode 100644
index 000000000..7ae12d894
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-declare-function-04.vue
@@ -0,0 +1,15 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-decorator-01.vue b/tests/fixtures/script-indent/ts-decorator-01.vue
new file mode 100644
index 000000000..c6f39dc35
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-decorator-01.vue
@@ -0,0 +1,205 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-decorator-02.vue b/tests/fixtures/script-indent/ts-decorator-02.vue
new file mode 100644
index 000000000..a314771ad
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-decorator-02.vue
@@ -0,0 +1,14 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-enum-01.vue b/tests/fixtures/script-indent/ts-enum-01.vue
new file mode 100644
index 000000000..b6b9ccae0
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-enum-01.vue
@@ -0,0 +1,9 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-enum-02.vue b/tests/fixtures/script-indent/ts-enum-02.vue
new file mode 100644
index 000000000..a0e25f12b
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-enum-02.vue
@@ -0,0 +1,9 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-enum-03.vue b/tests/fixtures/script-indent/ts-enum-03.vue
new file mode 100644
index 000000000..f46b6b179
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-enum-03.vue
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-enum-04.vue b/tests/fixtures/script-indent/ts-enum-04.vue
new file mode 100644
index 000000000..141e8d7f9
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-enum-04.vue
@@ -0,0 +1,7 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-enum-05.vue b/tests/fixtures/script-indent/ts-enum-05.vue
new file mode 100644
index 000000000..d40883ee9
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-enum-05.vue
@@ -0,0 +1,8 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-enum-06.vue b/tests/fixtures/script-indent/ts-enum-06.vue
new file mode 100644
index 000000000..b4d02a8f3
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-enum-06.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-enum-member-01.vue b/tests/fixtures/script-indent/ts-enum-member-01.vue
new file mode 100644
index 000000000..a646bda9b
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-enum-member-01.vue
@@ -0,0 +1,15 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-enum-member-02.vue b/tests/fixtures/script-indent/ts-enum-member-02.vue
new file mode 100644
index 000000000..0dfad0857
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-enum-member-02.vue
@@ -0,0 +1,26 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-export-assignment-01.vue b/tests/fixtures/script-indent/ts-export-assignment-01.vue
new file mode 100644
index 000000000..ee028cb48
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-export-assignment-01.vue
@@ -0,0 +1,5 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-export-assignment-02.vue b/tests/fixtures/script-indent/ts-export-assignment-02.vue
new file mode 100644
index 000000000..0d167f0df
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-export-assignment-02.vue
@@ -0,0 +1,13 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-export-namespace-01.vue b/tests/fixtures/script-indent/ts-export-namespace-01.vue
new file mode 100644
index 000000000..098f4b3cb
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-export-namespace-01.vue
@@ -0,0 +1,8 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-function-type-01.vue b/tests/fixtures/script-indent/ts-function-type-01.vue
new file mode 100644
index 000000000..1cd005f49
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-function-type-01.vue
@@ -0,0 +1,6 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-function-type-02.vue b/tests/fixtures/script-indent/ts-function-type-02.vue
new file mode 100644
index 000000000..bd5c27cb3
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-function-type-02.vue
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-assertion-01.vue b/tests/fixtures/script-indent/ts-import-assertion-01.vue
new file mode 100644
index 000000000..04cd9c99b
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-assertion-01.vue
@@ -0,0 +1,9 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-assertion-02.vue b/tests/fixtures/script-indent/ts-import-assertion-02.vue
new file mode 100644
index 000000000..c6e0cb866
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-assertion-02.vue
@@ -0,0 +1,15 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-assertion-03.vue b/tests/fixtures/script-indent/ts-import-assertion-03.vue
new file mode 100644
index 000000000..7cdf02501
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-assertion-03.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-assertion-04.vue b/tests/fixtures/script-indent/ts-import-assertion-04.vue
new file mode 100644
index 000000000..fc1bee6ee
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-assertion-04.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-equal-01.vue b/tests/fixtures/script-indent/ts-import-equal-01.vue
new file mode 100644
index 000000000..b35c28cd0
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-equal-01.vue
@@ -0,0 +1,6 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-equal-02.vue b/tests/fixtures/script-indent/ts-import-equal-02.vue
new file mode 100644
index 000000000..5d55cab24
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-equal-02.vue
@@ -0,0 +1,19 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-equal-03.vue b/tests/fixtures/script-indent/ts-import-equal-03.vue
new file mode 100644
index 000000000..d532454be
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-equal-03.vue
@@ -0,0 +1,13 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-type-01.vue b/tests/fixtures/script-indent/ts-import-type-01.vue
new file mode 100644
index 000000000..d8790ed57
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-type-01.vue
@@ -0,0 +1,8 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-type-02.vue b/tests/fixtures/script-indent/ts-import-type-02.vue
new file mode 100644
index 000000000..f19a97d89
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-type-02.vue
@@ -0,0 +1,18 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-type-03.vue b/tests/fixtures/script-indent/ts-import-type-03.vue
new file mode 100644
index 000000000..2724f2911
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-type-03.vue
@@ -0,0 +1,9 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-indexed-access-type-01.vue b/tests/fixtures/script-indent/ts-indexed-access-type-01.vue
new file mode 100644
index 000000000..f95b266fd
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-indexed-access-type-01.vue
@@ -0,0 +1,9 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-indexed-access-type-02.vue b/tests/fixtures/script-indent/ts-indexed-access-type-02.vue
new file mode 100644
index 000000000..6c641d0bf
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-indexed-access-type-02.vue
@@ -0,0 +1,11 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-infer-01.vue b/tests/fixtures/script-indent/ts-infer-01.vue
new file mode 100644
index 000000000..7cd8cb660
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-infer-01.vue
@@ -0,0 +1,7 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-instantiation-expression-01.vue b/tests/fixtures/script-indent/ts-instantiation-expression-01.vue
new file mode 100644
index 000000000..64e1d1073
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-instantiation-expression-01.vue
@@ -0,0 +1,8 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-interface-declaration-01.vue b/tests/fixtures/script-indent/ts-interface-declaration-01.vue
new file mode 100644
index 000000000..6dc683226
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-interface-declaration-01.vue
@@ -0,0 +1,7 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-interface-declaration-02.vue b/tests/fixtures/script-indent/ts-interface-declaration-02.vue
new file mode 100644
index 000000000..99d53c153
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-interface-declaration-02.vue
@@ -0,0 +1,15 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-interface-declaration-03.vue b/tests/fixtures/script-indent/ts-interface-declaration-03.vue
new file mode 100644
index 000000000..6490c79ec
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-interface-declaration-03.vue
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-interface-declaration-04.vue b/tests/fixtures/script-indent/ts-interface-declaration-04.vue
new file mode 100644
index 000000000..bb770b382
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-interface-declaration-04.vue
@@ -0,0 +1,19 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-interface-declaration-05.vue b/tests/fixtures/script-indent/ts-interface-declaration-05.vue
new file mode 100644
index 000000000..1c00cab83
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-interface-declaration-05.vue
@@ -0,0 +1,13 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-interface-declaration-06.vue b/tests/fixtures/script-indent/ts-interface-declaration-06.vue
new file mode 100644
index 000000000..3390cb963
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-interface-declaration-06.vue
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-interface-declaration-07.vue b/tests/fixtures/script-indent/ts-interface-declaration-07.vue
new file mode 100644
index 000000000..3dbcc88d3
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-interface-declaration-07.vue
@@ -0,0 +1,25 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-interface-declaration-08.vue b/tests/fixtures/script-indent/ts-interface-declaration-08.vue
new file mode 100644
index 000000000..0048fe129
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-interface-declaration-08.vue
@@ -0,0 +1,27 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-mapped-type-01.vue b/tests/fixtures/script-indent/ts-mapped-type-01.vue
new file mode 100644
index 000000000..6f7016231
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-mapped-type-01.vue
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-module-declaration-01.vue b/tests/fixtures/script-indent/ts-module-declaration-01.vue
new file mode 100644
index 000000000..ef89078fc
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-module-declaration-01.vue
@@ -0,0 +1,14 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-new-expression-01.vue b/tests/fixtures/script-indent/ts-new-expression-01.vue
new file mode 100644
index 000000000..3dd190fa0
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-new-expression-01.vue
@@ -0,0 +1,13 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-qualified-name-01.vue b/tests/fixtures/script-indent/ts-qualified-name-01.vue
new file mode 100644
index 000000000..aa8fa0a0f
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-qualified-name-01.vue
@@ -0,0 +1,7 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-satisfies-operators-01.vue b/tests/fixtures/script-indent/ts-satisfies-operators-01.vue
new file mode 100644
index 000000000..858834115
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-satisfies-operators-01.vue
@@ -0,0 +1,8 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-static-block-01.vue b/tests/fixtures/script-indent/ts-static-block-01.vue
new file mode 100644
index 000000000..5d585d6d0
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-static-block-01.vue
@@ -0,0 +1,8 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-template-literal-type-01.vue b/tests/fixtures/script-indent/ts-template-literal-type-01.vue
new file mode 100644
index 000000000..a260ecdae
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-template-literal-type-01.vue
@@ -0,0 +1,40 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-tuple-01.vue b/tests/fixtures/script-indent/ts-tuple-01.vue
new file mode 100644
index 000000000..d5d1bfdb0
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-tuple-01.vue
@@ -0,0 +1,9 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-tuple-02.vue b/tests/fixtures/script-indent/ts-tuple-02.vue
new file mode 100644
index 000000000..526755f48
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-tuple-02.vue
@@ -0,0 +1,15 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-alias-seclaration-01.vue b/tests/fixtures/script-indent/ts-type-alias-seclaration-01.vue
new file mode 100644
index 000000000..92037fb4c
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-alias-seclaration-01.vue
@@ -0,0 +1,21 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-annotation-01.vue b/tests/fixtures/script-indent/ts-type-annotation-01.vue
new file mode 100644
index 000000000..8d7ac5958
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-annotation-01.vue
@@ -0,0 +1,9 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-annotation-02.vue b/tests/fixtures/script-indent/ts-type-annotation-02.vue
new file mode 100644
index 000000000..5769cfd40
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-annotation-02.vue
@@ -0,0 +1,33 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-annotation-03.vue b/tests/fixtures/script-indent/ts-type-annotation-03.vue
new file mode 100644
index 000000000..2d18debaf
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-annotation-03.vue
@@ -0,0 +1,29 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-annotation-04.vue b/tests/fixtures/script-indent/ts-type-annotation-04.vue
new file mode 100644
index 000000000..015bfb0bb
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-annotation-04.vue
@@ -0,0 +1,25 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-annotation-05.vue b/tests/fixtures/script-indent/ts-type-annotation-05.vue
new file mode 100644
index 000000000..7bdec085b
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-annotation-05.vue
@@ -0,0 +1,21 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-annotation-06.vue b/tests/fixtures/script-indent/ts-type-annotation-06.vue
new file mode 100644
index 000000000..79cbf15d7
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-annotation-06.vue
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-assertion-01.vue b/tests/fixtures/script-indent/ts-type-assertion-01.vue
new file mode 100644
index 000000000..80f9aa78b
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-assertion-01.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-literal-01.vue b/tests/fixtures/script-indent/ts-type-literal-01.vue
new file mode 100644
index 000000000..61f481f9c
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-literal-01.vue
@@ -0,0 +1,7 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-only-import-export-01.vue b/tests/fixtures/script-indent/ts-type-only-import-export-01.vue
new file mode 100644
index 000000000..dea1377e9
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-only-import-export-01.vue
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-only-import-export-02.vue b/tests/fixtures/script-indent/ts-type-only-import-export-02.vue
new file mode 100644
index 000000000..b2bad678e
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-only-import-export-02.vue
@@ -0,0 +1,23 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-only-import-export-03.vue b/tests/fixtures/script-indent/ts-type-only-import-export-03.vue
new file mode 100644
index 000000000..4c9238dc9
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-only-import-export-03.vue
@@ -0,0 +1,26 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-paramater-01.vue b/tests/fixtures/script-indent/ts-type-paramater-01.vue
new file mode 100644
index 000000000..cbdd01bcf
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-paramater-01.vue
@@ -0,0 +1,24 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-parameter-seclaration-01.vue b/tests/fixtures/script-indent/ts-type-parameter-seclaration-01.vue
new file mode 100644
index 000000000..71ca145ab
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-parameter-seclaration-01.vue
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-union-intersection-01.vue b/tests/fixtures/script-indent/ts-union-intersection-01.vue
new file mode 100644
index 000000000..a808493e8
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-union-intersection-01.vue
@@ -0,0 +1,28 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-union-intersection-02.vue b/tests/fixtures/script-indent/ts-union-intersection-02.vue
new file mode 100644
index 000000000..62673b0a9
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-union-intersection-02.vue
@@ -0,0 +1,21 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-union-intersection-03.vue b/tests/fixtures/script-indent/ts-union-intersection-03.vue
new file mode 100644
index 000000000..022d2f1da
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-union-intersection-03.vue
@@ -0,0 +1,19 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-union-intersection-04.vue b/tests/fixtures/script-indent/ts-union-intersection-04.vue
new file mode 100644
index 000000000..378c95cdb
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-union-intersection-04.vue
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-union-intersection-05.vue b/tests/fixtures/script-indent/ts-union-intersection-05.vue
new file mode 100644
index 000000000..f12da960e
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-union-intersection-05.vue
@@ -0,0 +1,19 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-union-intersection-06.vue b/tests/fixtures/script-indent/ts-union-intersection-06.vue
new file mode 100644
index 000000000..8241ae123
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-union-intersection-06.vue
@@ -0,0 +1,27 @@
+
+
diff --git a/tests/fixtures/typescript/src/test.vue b/tests/fixtures/typescript/src/test.vue
new file mode 100644
index 000000000..5bf03f4a7
--- /dev/null
+++ b/tests/fixtures/typescript/src/test.vue
@@ -0,0 +1 @@
+
diff --git a/tests/fixtures/typescript/src/test01.ts b/tests/fixtures/typescript/src/test01.ts
new file mode 100644
index 000000000..917643c67
--- /dev/null
+++ b/tests/fixtures/typescript/src/test01.ts
@@ -0,0 +1,25 @@
+export type Props1 = {
+ foo: string
+ bar?: number
+ baz?: boolean
+}
+export type Emits1 = {
+ (e: 'foo' | 'bar', payload: string): void
+ (e: 'baz', payload: number): void
+}
+export type Props2 = {
+ a: string
+ b?: number
+ c?: boolean
+ d?: boolean
+ e?: number | string
+ f?: () => number
+ g?: { foo?: string }
+ h?: string[]
+ i?: readonly string[]
+}
+
+export type Slots1 = {
+ default(props: { msg: string }): any
+ foo(props: { msg: string }): any
+}
diff --git a/tests/fixtures/typescript/tsconfig.json b/tests/fixtures/typescript/tsconfig.json
new file mode 100644
index 000000000..c13ef64e3
--- /dev/null
+++ b/tests/fixtures/typescript/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "skipLibCheck": true
+ }
+}
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/result.js
new file mode 100644
index 000000000..805890a33
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/result.js
@@ -0,0 +1,16 @@
+let { a, b: c, d: [,f], g: [...h], ...i } = $(foo)
+let [ x, y = 42 ] = $(bar)
+
+console.log(
+ /*>*/a/*<{"escape":false,"method":"$"}*/,
+ b,
+ /*>*/c/*<{"escape":false,"method":"$"}*/,
+ d,
+ e,
+ /*>*/f/*<{"escape":false,"method":"$"}*/,
+ g,
+ /*>*/h/*<{"escape":false,"method":"$"}*/,
+ /*>*/i/*<{"escape":false,"method":"$"}*/,
+ /*>*/x/*<{"escape":false,"method":"$"}*/,
+ /*>*/y/*<{"escape":false,"method":"$"}*/
+)
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/source.js
new file mode 100644
index 000000000..bfb92574f
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/source.js
@@ -0,0 +1,16 @@
+let { a, b: c, d: [,f], g: [...h], ...i } = $(foo)
+let [ x, y = 42 ] = $(bar)
+
+console.log(
+ a,
+ b,
+ c,
+ d,
+ e,
+ f,
+ g,
+ h,
+ i,
+ x,
+ y
+)
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$/result.js
new file mode 100644
index 000000000..80d709dee
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$/result.js
@@ -0,0 +1,6 @@
+let v = $(foo)
+let { x, y } = $(bar)
+console.log(/*>*/v/*<{"escape":false,"method":"$"}*/, /*>*/x/*<{"escape":false,"method":"$"}*/, /*>*/y/*<{"escape":false,"method":"$"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$"}*/
+console.log(a)
+console.log($$({ /*>*/v/*<{"escape":true,"method":"$"}*/, /*>*/x/*<{"escape":true,"method":"$"}*/ }))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$/source.js
new file mode 100644
index 000000000..28c98cbc9
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$/source.js
@@ -0,0 +1,6 @@
+let v = $(foo)
+let { x, y } = $(bar)
+console.log(v, x, y)
+let a = v
+console.log(a)
+console.log($$({ v, x }))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/result.js
new file mode 100644
index 000000000..a45a0ae6f
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/result.js
@@ -0,0 +1,5 @@
+let v = $computed(() => 42)
+console.log(/*>*/v/*<{"escape":false,"method":"$computed"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$computed"}*/
+console.log(a)
+console.log($$(/*>*/v/*<{"escape":true,"method":"$computed"}*/))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/source.js
new file mode 100644
index 000000000..ff0f34766
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/source.js
@@ -0,0 +1,5 @@
+let v = $computed(() => 42)
+console.log(v)
+let a = v
+console.log(a)
+console.log($$(v))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/result.js
new file mode 100644
index 000000000..a8a3ae90a
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/result.js
@@ -0,0 +1,10 @@
+let v = $customRef((track, trigger) => {
+ return {
+ get() { return count },
+ set(newValue) { count = newValue }
+ }
+})
+console.log(/*>*/v/*<{"escape":false,"method":"$customRef"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$customRef"}*/
+console.log(a)
+console.log($$(/*>*/v/*<{"escape":true,"method":"$customRef"}*/))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/source.js
new file mode 100644
index 000000000..dd62754bc
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/source.js
@@ -0,0 +1,10 @@
+let v = $customRef((track, trigger) => {
+ return {
+ get() { return count },
+ set(newValue) { count = newValue }
+ }
+})
+console.log(v)
+let a = v
+console.log(a)
+console.log($$(v))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/result.js
new file mode 100644
index 000000000..518f0c56b
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/result.js
@@ -0,0 +1,5 @@
+let v = $ref(0)
+console.log(/*>*/v/*<{"escape":false,"method":"$ref"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$ref"}*/
+console.log(a)
+console.log($$(/*>*/v/*<{"escape":true,"method":"$ref"}*/))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/source.js
new file mode 100644
index 000000000..5c8d7248e
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/source.js
@@ -0,0 +1,5 @@
+let v = $ref(0)
+console.log(v)
+let a = v
+console.log(a)
+console.log($$(v))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/result.js
new file mode 100644
index 000000000..dbe345a19
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/result.js
@@ -0,0 +1,5 @@
+let v = $shallowRef({ count: 0 })
+console.log(/*>*/v/*<{"escape":false,"method":"$shallowRef"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$shallowRef"}*/
+console.log(a)
+console.log($$(/*>*/v/*<{"escape":true,"method":"$shallowRef"}*/))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/source.js
new file mode 100644
index 000000000..da4c19320
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/source.js
@@ -0,0 +1,5 @@
+let v = $shallowRef({ count: 0 })
+console.log(v)
+let a = v
+console.log(a)
+console.log($$(v))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/result.js
new file mode 100644
index 000000000..a7bff1219
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/result.js
@@ -0,0 +1,5 @@
+let v = $toRef(foo, 'bar')
+console.log(/*>*/v/*<{"escape":false,"method":"$toRef"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$toRef"}*/
+console.log(a)
+console.log($$(/*>*/v/*<{"escape":true,"method":"$toRef"}*/))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/source.js
new file mode 100644
index 000000000..b94ddea93
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/source.js
@@ -0,0 +1,5 @@
+let v = $toRef(foo, 'bar')
+console.log(v)
+let a = v
+console.log(a)
+console.log($$(v))
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/computed/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/computed/result.js
new file mode 100644
index 000000000..2217a8182
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/computed/result.js
@@ -0,0 +1,5 @@
+import { computed } from 'vue'
+let v = computed(() => 42)
+console.log(/*>*/v/*<{"type":"expression","method":"computed"}*/.value)
+let a = v
+console.log(/*>*/a/*<{"type":"expression","method":"computed"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/computed/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/computed/source.js
new file mode 100644
index 000000000..2b4a6412d
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/computed/source.js
@@ -0,0 +1,5 @@
+import { computed } from 'vue'
+let v = computed(() => 42)
+console.log(v.value)
+let a = v
+console.log(a.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/customRef/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/customRef/result.js
new file mode 100644
index 000000000..e7af0774e
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/customRef/result.js
@@ -0,0 +1,10 @@
+import { customRef } from 'vue'
+let v = customRef((track, trigger) => {
+ return {
+ get() { return count },
+ set(newValue) { count = newValue }
+ }
+})
+console.log(/*>*/v/*<{"type":"expression","method":"customRef"}*/.value)
+let a = v
+console.log(/*>*/a/*<{"type":"expression","method":"customRef"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/customRef/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/customRef/source.js
new file mode 100644
index 000000000..c94db730f
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/customRef/source.js
@@ -0,0 +1,10 @@
+import { customRef } from 'vue'
+let v = customRef((track, trigger) => {
+ return {
+ get() { return count },
+ set(newValue) { count = newValue }
+ }
+})
+console.log(v.value)
+let a = v
+console.log(a.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/defineModel01/result.vue b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel01/result.vue
new file mode 100644
index 000000000..a5d4293d8
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel01/result.vue
@@ -0,0 +1,24 @@
+
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/defineModel01/source.vue b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel01/source.vue
new file mode 100644
index 000000000..7cca9156c
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel01/source.vue
@@ -0,0 +1,24 @@
+
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/result.vue b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/result.vue
new file mode 100644
index 000000000..c2903d01f
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/result.vue
@@ -0,0 +1,16 @@
+
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/source.vue b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/source.vue
new file mode 100644
index 000000000..012cb9349
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/source.vue
@@ -0,0 +1,16 @@
+
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/result.js
new file mode 100644
index 000000000..a0681af4a
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/result.js
@@ -0,0 +1,9 @@
+import { ref } from 'vue'
+let v = ref(0)
+const /*>*/{ value: a }/*<{"type":"pattern","method":"ref"}*/ = v
+const /*>*/{ value = 42 }/*<{"type":"pattern","method":"ref"}*/ = v
+console.log(a)
+console.log(value)
+
+const [x] = v
+console.log(x)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/source.js
new file mode 100644
index 000000000..3a2c5c330
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/source.js
@@ -0,0 +1,9 @@
+import { ref } from 'vue'
+let v = ref(0)
+const { value: a } = v
+const { value = 42 } = v
+console.log(a)
+console.log(value)
+
+const [x] = v
+console.log(x)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/ref/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/ref/result.js
new file mode 100644
index 000000000..c356f2c2b
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/ref/result.js
@@ -0,0 +1,5 @@
+import { ref } from 'vue'
+let v = ref(0)
+console.log(/*>*/v/*<{"type":"expression","method":"ref"}*/.value)
+let a = v
+console.log(/*>*/a/*<{"type":"expression","method":"ref"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/ref/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/ref/source.js
new file mode 100644
index 000000000..836b45777
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/ref/source.js
@@ -0,0 +1,5 @@
+import { ref } from 'vue'
+let v = ref(0)
+console.log(v.value)
+let a = v
+console.log(a.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/result.js
new file mode 100644
index 000000000..206f3e88d
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/result.js
@@ -0,0 +1,5 @@
+import { shallowRef } from 'vue'
+let v = shallowRef({ count: 0 })
+console.log(/*>*/v/*<{"type":"expression","method":"shallowRef"}*/.value)
+let a = v
+console.log(/*>*/a/*<{"type":"expression","method":"shallowRef"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/source.js
new file mode 100644
index 000000000..98496ae7f
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/source.js
@@ -0,0 +1,5 @@
+import { shallowRef } from 'vue'
+let v = shallowRef({ count: 0 })
+console.log(v.value)
+let a = v
+console.log(a.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRef/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRef/result.js
new file mode 100644
index 000000000..28bea16dc
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRef/result.js
@@ -0,0 +1,5 @@
+import { toRef } from 'vue'
+let v = toRef(foo, 'bar')
+console.log(/*>*/v/*<{"type":"expression","method":"toRef"}*/.value)
+let a = v
+console.log(/*>*/a/*<{"type":"expression","method":"toRef"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRef/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRef/source.js
new file mode 100644
index 000000000..df884f003
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRef/source.js
@@ -0,0 +1,5 @@
+import { toRef } from 'vue'
+let v = toRef(foo, 'bar')
+console.log(v.value)
+let a = v
+console.log(a.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/result.js
new file mode 100644
index 000000000..527acdc78
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/result.js
@@ -0,0 +1,5 @@
+import { toRefs } from 'vue'
+let bar = toRefs(foo)
+const { x, y = 42 } = bar
+console.log(/*>*/x/*<{"type":"expression","method":"toRefs"}*/.value)
+console.log(/*>*/y/*<{"type":"expression","method":"toRefs"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/source.js
new file mode 100644
index 000000000..b2c39b4a9
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/source.js
@@ -0,0 +1,5 @@
+import { toRefs } from 'vue'
+let bar = toRefs(foo)
+const { x, y = 42 } = bar
+console.log(x.value)
+console.log(y.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/result.js
new file mode 100644
index 000000000..809dfc902
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/result.js
@@ -0,0 +1,18 @@
+import { toRefs } from 'vue'
+let { x, y } = toRefs(foo)
+console.log(/*>*/x/*<{"type":"expression","method":"toRefs"}*/.value)
+let a = y
+console.log(/*>*/a/*<{"type":"expression","method":"toRefs"}*/.value)
+console.log(/*>*/y/*<{"type":"expression","method":"toRefs"}*/.value)
+
+let bar = toRefs(foo)
+console.log(bar)
+console.log(/*>*/bar.x/*<{"type":"expression","method":"toRefs"}*/.value)
+console.log(/*>*/bar.y/*<{"type":"expression","method":"toRefs"}*/.value)
+
+const z = bar.z
+console.log(/*>*/z/*<{"type":"expression","method":"toRefs"}*/.value)
+
+let b;
+/*>*/b/*<{"type":"pattern","method":"toRefs"}*/ = bar.b
+console.log(/*>*/b/*<{"type":"expression","method":"toRefs"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/source.js
new file mode 100644
index 000000000..638e69296
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/source.js
@@ -0,0 +1,18 @@
+import { toRefs } from 'vue'
+let { x, y } = toRefs(foo)
+console.log(x.value)
+let a = y
+console.log(a.value)
+console.log(y.value)
+
+let bar = toRefs(foo)
+console.log(bar)
+console.log(bar.x.value)
+console.log(bar.y.value)
+
+const z = bar.z
+console.log(z.value)
+
+let b;
+b = bar.b
+console.log(b.value)
diff --git a/tests/fixtures/utils/selector/attr-contains/result.json b/tests/fixtures/utils/selector/attr-contains/result.json
new file mode 100644
index 000000000..a2627437e
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-contains/result.json
@@ -0,0 +1,9 @@
+{
+ "selector": "a[href*=\"example\"]",
+ "matches": [
+ "",
+ "",
+ ""
+ ],
+ "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-contains/source.vue b/tests/fixtures/utils/selector/attr-contains/source.vue
new file mode 100644
index 000000000..6c9a89dde
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-contains/source.vue
@@ -0,0 +1,10 @@
+
+
+