diff --git a/docs/rules/README.md b/docs/rules/README.md
index f7a2e01d5..b1c72665d 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -117,6 +117,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
|:--------|:------------|:---|
| [vue/attribute-hyphenation](./attribute-hyphenation.md) | enforce attribute naming style on custom components in template | :wrench: |
| [vue/component-definition-name-casing](./component-definition-name-casing.md) | enforce specific casing for component definition name | :wrench: |
+| [vue/first-attribute-linebreak](./first-attribute-linebreak.md) | enforce the location of first attribute | :wrench: |
| [vue/html-closing-bracket-newline](./html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets | :wrench: |
| [vue/html-closing-bracket-spacing](./html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets | :wrench: |
| [vue/html-end-tags](./html-end-tags.md) | enforce end tag style | :wrench: |
@@ -228,6 +229,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
|:--------|:------------|:---|
| [vue/attribute-hyphenation](./attribute-hyphenation.md) | enforce attribute naming style on custom components in template | :wrench: |
| [vue/component-definition-name-casing](./component-definition-name-casing.md) | enforce specific casing for component definition name | :wrench: |
+| [vue/first-attribute-linebreak](./first-attribute-linebreak.md) | enforce the location of first attribute | :wrench: |
| [vue/html-closing-bracket-newline](./html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets | :wrench: |
| [vue/html-closing-bracket-spacing](./html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets | :wrench: |
| [vue/html-end-tags](./html-end-tags.md) | enforce end tag style | :wrench: |
diff --git a/docs/rules/first-attribute-linebreak.md b/docs/rules/first-attribute-linebreak.md
new file mode 100644
index 000000000..83aa4eda9
--- /dev/null
+++ b/docs/rules/first-attribute-linebreak.md
@@ -0,0 +1,164 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/first-attribute-linebreak
+description: enforce the location of first attribute
+---
+# vue/first-attribute-linebreak
+
+> enforce the location of first attribute
+
+- :exclamation: ***This rule has not been released yet.***
+- :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.
+
+## :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://v3.vuejs.org/style-guide/#multi-attribute-elements-strongly-recommended)
+
+## :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/max-attributes-per-line.md b/docs/rules/max-attributes-per-line.md
index dac9d2d72..a537eef33 100644
--- a/docs/rules/max-attributes-per-line.md
+++ b/docs/rules/max-attributes-per-line.md
@@ -58,21 +58,17 @@ There is a configurable number of attributes that are acceptable in one-line cas
{
"vue/max-attributes-per-line": ["error", {
"singleline": {
- "max": 1,
- "allowFirstLine": true
+ "max": 1
},
"multiline": {
- "max": 1,
- "allowFirstLine": false
+ "max": 1
}
}]
}
```
- `singleline.max` (`number`) ... The number of maximum attributes per line when the opening tag is in a single line. Default is `1`.
-- `singleline.allowFirstLine` (`boolean`) ... If `true`, it allows attributes on the same line as that tag name. Default is `true`.
- `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": 3`
@@ -90,24 +86,6 @@ There is a configurable number of attributes that are acceptable in one-line cas
-### `"singleline": 1, "allowFirstLine": false`
-
-
-
-```vue
-
-
-
-
-
-
-
-```
-
-
-
### `"multiline": 2`
@@ -130,21 +108,9 @@ There is a configurable number of attributes that are acceptable in one-line cas
-### `"multiline": 1, "allowFirstLine": true`
-
-
-
-```vue
-
-
-
-
-```
+## :couple: Related Rules
-
+- [vue/first-attribute-linebreak](./first-attribute-linebreak.md)
## :books: Further Reading
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index b7d33e0c1..73d857e54 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -15,6 +15,7 @@ module.exports = {
'vue/comma-spacing': 'off',
'vue/comma-style': 'off',
'vue/dot-location': 'off',
+ 'vue/first-attribute-linebreak': 'off',
'vue/func-call-spacing': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/html-closing-bracket-spacing': 'off',
diff --git a/lib/configs/strongly-recommended.js b/lib/configs/strongly-recommended.js
index 0d3d41246..d03984029 100644
--- a/lib/configs/strongly-recommended.js
+++ b/lib/configs/strongly-recommended.js
@@ -8,6 +8,7 @@ module.exports = {
rules: {
'vue/attribute-hyphenation': 'warn',
'vue/component-definition-name-casing': 'warn',
+ 'vue/first-attribute-linebreak': 'warn',
'vue/html-closing-bracket-newline': 'warn',
'vue/html-closing-bracket-spacing': 'warn',
'vue/html-end-tags': 'warn',
diff --git a/lib/configs/vue3-strongly-recommended.js b/lib/configs/vue3-strongly-recommended.js
index 9adfcd7c7..5b816c957 100644
--- a/lib/configs/vue3-strongly-recommended.js
+++ b/lib/configs/vue3-strongly-recommended.js
@@ -8,6 +8,7 @@ module.exports = {
rules: {
'vue/attribute-hyphenation': 'warn',
'vue/component-definition-name-casing': 'warn',
+ 'vue/first-attribute-linebreak': 'warn',
'vue/html-closing-bracket-newline': 'warn',
'vue/html-closing-bracket-spacing': 'warn',
'vue/html-end-tags': 'warn',
diff --git a/lib/index.js b/lib/index.js
index 1fe6e4eb3..bd51932a6 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -29,6 +29,7 @@ module.exports = {
'dot-notation': require('./rules/dot-notation'),
eqeqeq: require('./rules/eqeqeq'),
'experimental-script-setup-vars': require('./rules/experimental-script-setup-vars'),
+ 'first-attribute-linebreak': require('./rules/first-attribute-linebreak'),
'func-call-spacing': require('./rules/func-call-spacing'),
'html-button-has-type': require('./rules/html-button-has-type'),
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
diff --git a/lib/rules/first-attribute-linebreak.js b/lib/rules/first-attribute-linebreak.js
new file mode 100644
index 000000000..25f893fd2
--- /dev/null
+++ b/lib/rules/first-attribute-linebreak.js
@@ -0,0 +1,98 @@
+/**
+ * @fileoverview Enforce the location of first attribute
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'layout',
+ docs: {
+ description: 'enforce the location of first attribute',
+ categories: ['vue3-strongly-recommended', 'strongly-recommended'],
+ url: 'https://eslint.vuejs.org/rules/first-attribute-linebreak.html'
+ },
+ fixable: 'whitespace', // or "code" or "whitespace"
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ multiline: { enum: ['below', 'beside', 'ignore'] },
+ singleline: { enum: ['below', 'beside', 'ignore'] }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ expected: 'Expected a linebreak before this attribute.',
+ unexpected: 'Expected no linebreak before this attribute.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /** @type {"below" | "beside" | "ignore"} */
+ const singleline =
+ (context.options[0] && context.options[0].singleline) || 'ignore'
+ /** @type {"below" | "beside" | "ignore"} */
+ const multiline =
+ (context.options[0] && context.options[0].multiline) || 'below'
+
+ const template =
+ context.parserServices.getTemplateBodyTokenStore &&
+ context.parserServices.getTemplateBodyTokenStore()
+
+ /**
+ * Report attribute
+ * @param {VAttribute | VDirective} firstAttribute
+ * @param { "below" | "beside"} location
+ */
+ function report(firstAttribute, location) {
+ context.report({
+ node: firstAttribute,
+ messageId: location === 'beside' ? 'unexpected' : 'expected',
+ fix(fixer) {
+ const prevToken = template.getTokenBefore(firstAttribute, {
+ includeComments: true
+ })
+ return fixer.replaceTextRange(
+ [prevToken.range[1], firstAttribute.range[0]],
+ location === 'beside' ? ' ' : '\n'
+ )
+ }
+ })
+ }
+
+ return utils.defineTemplateBodyVisitor(context, {
+ VStartTag(node) {
+ const firstAttribute = node.attributes[0]
+ if (!firstAttribute) return
+
+ const lastAttribute = node.attributes[node.attributes.length - 1]
+
+ const location =
+ firstAttribute.loc.start.line === lastAttribute.loc.end.line
+ ? singleline
+ : multiline
+ if (location === 'ignore') {
+ return
+ }
+
+ if (location === 'beside') {
+ if (node.loc.start.line === firstAttribute.loc.start.line) {
+ return
+ }
+ } else {
+ if (node.loc.start.line < firstAttribute.loc.start.line) {
+ return
+ }
+ }
+ report(firstAttribute, location)
+ }
+ })
+ }
+}
diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js
index c21604a61..d0eb40fb3 100644
--- a/lib/rules/max-attributes-per-line.js
+++ b/lib/rules/max-attributes-per-line.js
@@ -34,9 +34,6 @@ module.exports = {
max: {
type: 'number',
minimum: 1
- },
- allowFirstLine: {
- type: 'boolean'
}
},
additionalProperties: false
@@ -55,9 +52,6 @@ module.exports = {
max: {
type: 'number',
minimum: 1
- },
- allowFirstLine: {
- type: 'boolean'
}
},
additionalProperties: false
@@ -75,8 +69,6 @@ module.exports = {
const configuration = parseOptions(context.options[0])
const multilineMaximum = configuration.multiline
const singlelinemMaximum = configuration.singleline
- const canHaveSinglelineFirstLine = configuration.singlelineAllowFirstLine
- const canHaveMultilineFirstLine = configuration.multilineAllowFirstLine
const template =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
@@ -88,26 +80,12 @@ module.exports = {
if (!numberOfAttributes) return
if (utils.isSingleLine(node)) {
- if (
- !canHaveSinglelineFirstLine &&
- node.attributes[0].loc.start.line === node.loc.start.line
- ) {
- showErrors([node.attributes[0]])
- }
-
if (numberOfAttributes > singlelinemMaximum) {
showErrors(node.attributes.slice(singlelinemMaximum))
}
}
if (!utils.isSingleLine(node)) {
- if (
- !canHaveMultilineFirstLine &&
- 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)))
@@ -124,9 +102,7 @@ module.exports = {
function parseOptions(options) {
const defaults = {
singleline: 1,
- singlelineAllowFirstLine: true,
- multiline: 1,
- multilineAllowFirstLine: false
+ multiline: 1
}
if (options) {
@@ -136,11 +112,6 @@ module.exports = {
if (typeof options.singleline.max === 'number') {
defaults.singleline = options.singleline.max
}
-
- if (typeof options.singleline.allowFirstLine === 'boolean') {
- defaults.singlelineAllowFirstLine =
- options.singleline.allowFirstLine
- }
}
if (options.multiline) {
@@ -150,11 +121,6 @@ module.exports = {
if (typeof options.multiline.max === 'number') {
defaults.multiline = options.multiline.max
}
-
- if (typeof options.multiline.allowFirstLine === 'boolean') {
- defaults.multilineAllowFirstLine =
- options.multiline.allowFirstLine
- }
}
}
}
diff --git a/tests/lib/rules/first-attribute-linebreak.js b/tests/lib/rules/first-attribute-linebreak.js
new file mode 100644
index 000000000..61cbb2ed9
--- /dev/null
+++ b/tests/lib/rules/first-attribute-linebreak.js
@@ -0,0 +1,278 @@
+/**
+ * @fileoverview Enforce the location of first attribute
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/first-attribute-linebreak')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: { ecmaVersion: 2015 }
+})
+
+ruleTester.run('first-attribute-linebreak', rule, {
+ valid: [
+ {
+ code: `
+
+
+ `
+ },
+ {
+ code: `
+
+
+
+
+ `
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ options: [{ singleline: 'ignore', multiline: 'ignore' }]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ options: [{ singleline: 'beside', multiline: 'ignore' }]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ options: [{ singleline: 'ignore', multiline: 'beside' }]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ options: [{ singleline: 'below', multiline: 'ignore' }]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ options: [{ singleline: 'ignore', multiline: 'below' }]
+ }
+ ],
+
+ invalid: [
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Expected a linebreak before this attribute.',
+ line: 8,
+ column: 20
+ }
+ ]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ options: [{ singleline: 'beside', multiline: 'beside' }],
+ errors: [
+ {
+ message: 'Expected no linebreak before this attribute.',
+ line: 4,
+ column: 11
+ },
+ {
+ message: 'Expected no linebreak before this attribute.',
+ line: 13,
+ column: 11
+ }
+ ]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ options: [{ singleline: 'below', multiline: 'below' }],
+ errors: [
+ {
+ message: 'Expected a linebreak before this attribute.',
+ line: 8,
+ column: 20
+ },
+ {
+ message: 'Expected a linebreak before this attribute.',
+ line: 15,
+ column: 20
+ }
+ ]
+ }
+ ]
+})
diff --git a/tests/lib/rules/max-attributes-per-line.js b/tests/lib/rules/max-attributes-per-line.js
index aedc6d6d8..9e14495a2 100644
--- a/tests/lib/rules/max-attributes-per-line.js
+++ b/tests/lib/rules/max-attributes-per-line.js
@@ -32,14 +32,6 @@ ruleTester.run('max-attributes-per-line', rule, {
job="Vet"
>`
},
- {
- code: ``,
- options: [{ multiline: { allowFirstLine: true } }]
- },
{
code: ``,
- options: [{ singleline: 1, multiline: { max: 1, allowFirstLine: false } }]
- },
- {
- code: ``,
- options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false } }]
- },
- {
- code: `
-
- `,
- options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: true } }]
- },
- {
- code: `
- `,
- options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false } }]
- },
- {
- code: `
-
- `,
- options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false } }]
+ `
}
],
@@ -131,26 +99,6 @@ v-if="something">`,
v-bind:age="user.age">`,
errors: ["'v-bind:age' should be on a new line."]
},
- {
- code: `
-
- `,
- output: `
-
- `,
- errors: [
- {
- message: "'job' should be on a new line.",
- type: 'VAttribute',
- line: 1
- }
- ]
- },
{
code: ``,
options: [{ singleline: { max: 2 } }],
@@ -164,70 +112,6 @@ job="Vet">`,
}
]
},
- {
- code: ``,
- options: [
- { singleline: 1, multiline: { max: 1, allowFirstLine: false } }
- ],
- output: ``,
- errors: [
- {
- message: "'age' should be on a new line.",
- type: 'VAttribute',
- line: 1
- },
- {
- message: "'job' should be on a new line.",
- type: 'VAttribute',
- line: 1
- }
- ]
- },
- {
- code: `
-
- `,
- options: [
- { singleline: 3, multiline: { max: 1, allowFirstLine: false } }
- ],
- output: `
-
- `,
- errors: [
- {
- message: "'name' should be on a new line.",
- type: 'VAttribute',
- line: 1
- }
- ]
- },
- {
- code: `
-
- `,
- options: [
- { singleline: 3, multiline: { max: 1, allowFirstLine: false } }
- ],
- output: `
-
- `,
- errors: [
- {
- message: "'age' should be on a new line.",
- type: 'VAttribute',
- line: 2
- }
- ]
- },
{
code: `
-
- `,
- options: [
- { singleline: 3, multiline: { max: 2, allowFirstLine: false } }
- ],
- output: `
-
- `,
- errors: [
- {
- message: "'petname' should be on a new line.",
- type: 'VAttribute',
- line: 3
- }
- ]
- },
- {
- code: `
-
- `,
- options: [
- { singleline: 3, multiline: { max: 2, allowFirstLine: false } }
- ],
- output: `
-
- `,
- errors: [
- {
- message: "'petname' should be on a new line.",
- type: 'VAttribute',
- line: 3
- },
- {
- message: "'extra' should be on a new line.",
- type: 'VAttribute',
- line: 3
- }
- ]
- },
- {
- code: ``,
- options: [{ singleline: { allowFirstLine: false } }],
- output: ``,
- errors: [
- {
- message: "'name' should be on a new line.",
- type: 'VAttribute',
- line: 1
- }
- ]
}
]
})