Skip to content

Commit 14a17d4

Browse files
authored
Add vue/first-attribute-linebreak rule (#1587)
1 parent 7cd2839 commit 14a17d4

11 files changed

+553
-255
lines changed

docs/rules/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
117117
|:--------|:------------|:---|
118118
| [vue/attribute-hyphenation](./attribute-hyphenation.md) | enforce attribute naming style on custom components in template | :wrench: |
119119
| [vue/component-definition-name-casing](./component-definition-name-casing.md) | enforce specific casing for component definition name | :wrench: |
120+
| [vue/first-attribute-linebreak](./first-attribute-linebreak.md) | enforce the location of first attribute | :wrench: |
120121
| [vue/html-closing-bracket-newline](./html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets | :wrench: |
121122
| [vue/html-closing-bracket-spacing](./html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets | :wrench: |
122123
| [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
228229
|:--------|:------------|:---|
229230
| [vue/attribute-hyphenation](./attribute-hyphenation.md) | enforce attribute naming style on custom components in template | :wrench: |
230231
| [vue/component-definition-name-casing](./component-definition-name-casing.md) | enforce specific casing for component definition name | :wrench: |
232+
| [vue/first-attribute-linebreak](./first-attribute-linebreak.md) | enforce the location of first attribute | :wrench: |
231233
| [vue/html-closing-bracket-newline](./html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets | :wrench: |
232234
| [vue/html-closing-bracket-spacing](./html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets | :wrench: |
233235
| [vue/html-end-tags](./html-end-tags.md) | enforce end tag style | :wrench: |
+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/first-attribute-linebreak
5+
description: enforce the location of first attribute
6+
---
7+
# vue/first-attribute-linebreak
8+
9+
> enforce the location of first attribute
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :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"`.
13+
- :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.
14+
15+
## :book: Rule Details
16+
17+
This rule aims to enforce a consistent location for the first attribute.
18+
19+
<eslint-code-block fix :rules="{'vue/first-attribute-linebreak': ['error']}">
20+
21+
```vue
22+
<template>
23+
<!-- ✓ GOOD -->
24+
<MyComponent lorem="1"/>
25+
<MyComponent lorem="1" ipsum="2"/>
26+
<MyComponent
27+
lorem="1"
28+
ipsum="2"
29+
/>
30+
31+
<!-- ✗ BAD -->
32+
<MyComponent lorem="1"
33+
ipsum="2"/>
34+
</template>
35+
```
36+
37+
</eslint-code-block>
38+
39+
## :wrench: Options
40+
41+
```json
42+
{
43+
"vue/first-attribute-linebreak": ["error", {
44+
"singleline": "ignore",
45+
"multiline": "below"
46+
}]
47+
}
48+
```
49+
50+
- `singleline` ... The location of the first attribute when the attributes on single line. Default is `"ignore"`.
51+
- `"below"` ... Requires a newline before the first attribute.
52+
- `"beside"` ... Disallows a newline before the first attribute.
53+
- `"ignore"` ... Ignores attribute checking.
54+
- `multiline` ... The location of the first attribute when the attributes span multiple lines. Default is `"below"`.
55+
- `"below"` ... Requires a newline before the first attribute.
56+
- `"beside"` ... Disallows a newline before the first attribute.
57+
- `"ignore"` ... Ignores attribute checking.
58+
59+
### `"singleline": "beside"`
60+
61+
<eslint-code-block fix :rules="{'vue/first-attribute-linebreak': ['error', {singleline: 'beside'}]}">
62+
63+
```vue
64+
<template>
65+
<!-- ✓ GOOD -->
66+
<MyComponent lorem="1"/>
67+
<MyComponent lorem="1" ipsum="2"/>
68+
69+
<!-- ✗ BAD -->
70+
<MyComponent
71+
lorem="1"/>
72+
<MyComponent
73+
lorem="1" ipsum="2"
74+
/>
75+
</template>
76+
```
77+
78+
</eslint-code-block>
79+
80+
### `"singleline": "below"`
81+
82+
<eslint-code-block fix :rules="{'vue/first-attribute-linebreak': ['error', {singleline: 'below'}]}">
83+
84+
```vue
85+
<template>
86+
<!-- ✓ GOOD -->
87+
<MyComponent
88+
lorem="1"/>
89+
<MyComponent
90+
lorem="1" ipsum="2"
91+
/>
92+
93+
<!-- ✗ BAD -->
94+
<MyComponent lorem="1"/>
95+
<MyComponent lorem="1" ipsum="2"/>
96+
</template>
97+
```
98+
99+
</eslint-code-block>
100+
101+
### `"multiline": "beside"`
102+
103+
<eslint-code-block fix :rules="{'vue/first-attribute-linebreak': ['error', {multiline: 'beside'}]}">
104+
105+
```vue
106+
<template>
107+
<!-- ✓ GOOD -->
108+
<MyComponent lorem="1"
109+
ipsum="2"/>
110+
<MyComponent :lorem="{
111+
a: 1
112+
}"/>
113+
114+
<!-- ✗ BAD -->
115+
<MyComponent
116+
lorem="1"
117+
ipsum="2"/>
118+
<MyComponent
119+
:lorem="{
120+
a: 1
121+
}"/>
122+
</template>
123+
```
124+
125+
</eslint-code-block>
126+
127+
### `"multiline": "below"`
128+
129+
<eslint-code-block fix :rules="{'vue/first-attribute-linebreak': ['error', {multiline: 'below'}]}">
130+
131+
```vue
132+
<template>
133+
<!-- ✓ GOOD -->
134+
<MyComponent
135+
lorem="1"
136+
ipsum="2"/>
137+
<MyComponent
138+
:lorem="{
139+
a: 1
140+
}"/>
141+
142+
<!-- ✗ BAD -->
143+
<MyComponent lorem="1"
144+
ipsum="2"/>
145+
<MyComponent :lorem="{
146+
a: 1
147+
}"/>
148+
</template>
149+
```
150+
151+
</eslint-code-block>
152+
153+
## :couple: Related Rules
154+
155+
- [vue/max-attributes-per-line](./max-attributes-per-line.md)
156+
157+
## :books: Further Reading
158+
159+
- [Style guide - Multi attribute elements](https://v3.vuejs.org/style-guide/#multi-attribute-elements-strongly-recommended)
160+
161+
## :mag: Implementation
162+
163+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/first-attribute-linebreak.js)
164+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/first-attribute-linebreak.js)

docs/rules/max-attributes-per-line.md

+4-38
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,17 @@ There is a configurable number of attributes that are acceptable in one-line cas
5858
{
5959
"vue/max-attributes-per-line": ["error", {
6060
"singleline": {
61-
"max": 1,
62-
"allowFirstLine": true
61+
"max": 1
6362
},
6463
"multiline": {
65-
"max": 1,
66-
"allowFirstLine": false
64+
"max": 1
6765
}
6866
}]
6967
}
7068
```
7169

7270
- `singleline.max` (`number`) ... The number of maximum attributes per line when the opening tag is in a single line. Default is `1`.
73-
- `singleline.allowFirstLine` (`boolean`) ... If `true`, it allows attributes on the same line as that tag name. Default is `true`.
7471
- `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.
75-
- `multiline.allowFirstLine` (`boolean`) ... If `true`, it allows attributes on the same line as that tag name. Default is `false`.
7672

7773
### `"singleline": 3`
7874

@@ -90,24 +86,6 @@ There is a configurable number of attributes that are acceptable in one-line cas
9086

9187
</eslint-code-block>
9288

93-
### `"singleline": 1, "allowFirstLine": false`
94-
95-
<eslint-code-block fix :rules="{'vue/max-attributes-per-line': ['error', {singleline: { allowFirstLine: false }}]}">
96-
97-
```vue
98-
<template>
99-
<!-- ✓ GOOD -->
100-
<MyComponent
101-
lorem="1"
102-
/>
103-
104-
<!-- ✗ BAD -->
105-
<MyComponent lorem="1" />
106-
</template>
107-
```
108-
109-
</eslint-code-block>
110-
11189
### `"multiline": 2`
11290

11391
<eslint-code-block fix :rules="{'vue/max-attributes-per-line': ['error', {multiline: 2}]}">
@@ -130,21 +108,9 @@ There is a configurable number of attributes that are acceptable in one-line cas
130108

131109
</eslint-code-block>
132110

133-
### `"multiline": 1, "allowFirstLine": true`
134-
135-
<eslint-code-block fix :rules="{'vue/max-attributes-per-line': ['error', {multiline: { allowFirstLine: true }}]}">
136-
137-
```vue
138-
<template>
139-
<!-- ✓ GOOD -->
140-
<MyComponent lorem="1"
141-
ipsum="2"
142-
dolor="3"
143-
/>
144-
</template>
145-
```
111+
## :couple: Related Rules
146112

147-
</eslint-code-block>
113+
- [vue/first-attribute-linebreak](./first-attribute-linebreak.md)
148114

149115
## :books: Further Reading
150116

lib/configs/no-layout-rules.js

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module.exports = {
1515
'vue/comma-spacing': 'off',
1616
'vue/comma-style': 'off',
1717
'vue/dot-location': 'off',
18+
'vue/first-attribute-linebreak': 'off',
1819
'vue/func-call-spacing': 'off',
1920
'vue/html-closing-bracket-newline': 'off',
2021
'vue/html-closing-bracket-spacing': 'off',

lib/configs/strongly-recommended.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module.exports = {
88
rules: {
99
'vue/attribute-hyphenation': 'warn',
1010
'vue/component-definition-name-casing': 'warn',
11+
'vue/first-attribute-linebreak': 'warn',
1112
'vue/html-closing-bracket-newline': 'warn',
1213
'vue/html-closing-bracket-spacing': 'warn',
1314
'vue/html-end-tags': 'warn',

lib/configs/vue3-strongly-recommended.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module.exports = {
88
rules: {
99
'vue/attribute-hyphenation': 'warn',
1010
'vue/component-definition-name-casing': 'warn',
11+
'vue/first-attribute-linebreak': 'warn',
1112
'vue/html-closing-bracket-newline': 'warn',
1213
'vue/html-closing-bracket-spacing': 'warn',
1314
'vue/html-end-tags': 'warn',

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module.exports = {
3030
'dot-notation': require('./rules/dot-notation'),
3131
eqeqeq: require('./rules/eqeqeq'),
3232
'experimental-script-setup-vars': require('./rules/experimental-script-setup-vars'),
33+
'first-attribute-linebreak': require('./rules/first-attribute-linebreak'),
3334
'func-call-spacing': require('./rules/func-call-spacing'),
3435
'html-button-has-type': require('./rules/html-button-has-type'),
3536
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* @fileoverview Enforce the location of first attribute
3+
* @author Yosuke Ota
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Rule Definition
9+
// ------------------------------------------------------------------------------
10+
const utils = require('../utils')
11+
12+
module.exports = {
13+
meta: {
14+
type: 'layout',
15+
docs: {
16+
description: 'enforce the location of first attribute',
17+
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
18+
url: 'https://eslint.vuejs.org/rules/first-attribute-linebreak.html'
19+
},
20+
fixable: 'whitespace', // or "code" or "whitespace"
21+
schema: [
22+
{
23+
type: 'object',
24+
properties: {
25+
multiline: { enum: ['below', 'beside', 'ignore'] },
26+
singleline: { enum: ['below', 'beside', 'ignore'] }
27+
},
28+
additionalProperties: false
29+
}
30+
],
31+
messages: {
32+
expected: 'Expected a linebreak before this attribute.',
33+
unexpected: 'Expected no linebreak before this attribute.'
34+
}
35+
},
36+
/** @param {RuleContext} context */
37+
create(context) {
38+
/** @type {"below" | "beside" | "ignore"} */
39+
const singleline =
40+
(context.options[0] && context.options[0].singleline) || 'ignore'
41+
/** @type {"below" | "beside" | "ignore"} */
42+
const multiline =
43+
(context.options[0] && context.options[0].multiline) || 'below'
44+
45+
const template =
46+
context.parserServices.getTemplateBodyTokenStore &&
47+
context.parserServices.getTemplateBodyTokenStore()
48+
49+
/**
50+
* Report attribute
51+
* @param {VAttribute | VDirective} firstAttribute
52+
* @param { "below" | "beside"} location
53+
*/
54+
function report(firstAttribute, location) {
55+
context.report({
56+
node: firstAttribute,
57+
messageId: location === 'beside' ? 'unexpected' : 'expected',
58+
fix(fixer) {
59+
const prevToken = template.getTokenBefore(firstAttribute, {
60+
includeComments: true
61+
})
62+
return fixer.replaceTextRange(
63+
[prevToken.range[1], firstAttribute.range[0]],
64+
location === 'beside' ? ' ' : '\n'
65+
)
66+
}
67+
})
68+
}
69+
70+
return utils.defineTemplateBodyVisitor(context, {
71+
VStartTag(node) {
72+
const firstAttribute = node.attributes[0]
73+
if (!firstAttribute) return
74+
75+
const lastAttribute = node.attributes[node.attributes.length - 1]
76+
77+
const location =
78+
firstAttribute.loc.start.line === lastAttribute.loc.end.line
79+
? singleline
80+
: multiline
81+
if (location === 'ignore') {
82+
return
83+
}
84+
85+
if (location === 'beside') {
86+
if (node.loc.start.line === firstAttribute.loc.start.line) {
87+
return
88+
}
89+
} else {
90+
if (node.loc.start.line < firstAttribute.loc.start.line) {
91+
return
92+
}
93+
}
94+
report(firstAttribute, location)
95+
}
96+
})
97+
}
98+
}

0 commit comments

Comments
 (0)