Skip to content

Commit

Permalink
Add vue/first-attribute-linebreak rule (#1587)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Oct 20, 2021
1 parent 7cd2839 commit 14a17d4
Show file tree
Hide file tree
Showing 11 changed files with 553 additions and 255 deletions.
2 changes: 2 additions & 0 deletions docs/rules/README.md
Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down
164 changes: 164 additions & 0 deletions 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: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :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.

<eslint-code-block fix :rules="{'vue/first-attribute-linebreak': ['error']}">

```vue
<template>
<!-- ✓ GOOD -->
<MyComponent lorem="1"/>
<MyComponent lorem="1" ipsum="2"/>
<MyComponent
lorem="1"
ipsum="2"
/>
<!-- ✗ BAD -->
<MyComponent lorem="1"
ipsum="2"/>
</template>
```

</eslint-code-block>

## :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"`

<eslint-code-block fix :rules="{'vue/first-attribute-linebreak': ['error', {singleline: 'beside'}]}">

```vue
<template>
<!-- ✓ GOOD -->
<MyComponent lorem="1"/>
<MyComponent lorem="1" ipsum="2"/>
<!-- ✗ BAD -->
<MyComponent
lorem="1"/>
<MyComponent
lorem="1" ipsum="2"
/>
</template>
```

</eslint-code-block>

### `"singleline": "below"`

<eslint-code-block fix :rules="{'vue/first-attribute-linebreak': ['error', {singleline: 'below'}]}">

```vue
<template>
<!-- ✓ GOOD -->
<MyComponent
lorem="1"/>
<MyComponent
lorem="1" ipsum="2"
/>
<!-- ✗ BAD -->
<MyComponent lorem="1"/>
<MyComponent lorem="1" ipsum="2"/>
</template>
```

</eslint-code-block>

### `"multiline": "beside"`

<eslint-code-block fix :rules="{'vue/first-attribute-linebreak': ['error', {multiline: 'beside'}]}">

```vue
<template>
<!-- ✓ GOOD -->
<MyComponent lorem="1"
ipsum="2"/>
<MyComponent :lorem="{
a: 1
}"/>
<!-- ✗ BAD -->
<MyComponent
lorem="1"
ipsum="2"/>
<MyComponent
:lorem="{
a: 1
}"/>
</template>
```

</eslint-code-block>

### `"multiline": "below"`

<eslint-code-block fix :rules="{'vue/first-attribute-linebreak': ['error', {multiline: 'below'}]}">

```vue
<template>
<!-- ✓ GOOD -->
<MyComponent
lorem="1"
ipsum="2"/>
<MyComponent
:lorem="{
a: 1
}"/>
<!-- ✗ BAD -->
<MyComponent lorem="1"
ipsum="2"/>
<MyComponent :lorem="{
a: 1
}"/>
</template>
```

</eslint-code-block>

## :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)
42 changes: 4 additions & 38 deletions docs/rules/max-attributes-per-line.md
Expand Up @@ -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`

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

</eslint-code-block>

### `"singleline": 1, "allowFirstLine": false`

<eslint-code-block fix :rules="{'vue/max-attributes-per-line': ['error', {singleline: { allowFirstLine: false }}]}">

```vue
<template>
<!-- ✓ GOOD -->
<MyComponent
lorem="1"
/>
<!-- ✗ BAD -->
<MyComponent lorem="1" />
</template>
```

</eslint-code-block>

### `"multiline": 2`

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

</eslint-code-block>

### `"multiline": 1, "allowFirstLine": true`

<eslint-code-block fix :rules="{'vue/max-attributes-per-line': ['error', {multiline: { allowFirstLine: true }}]}">

```vue
<template>
<!-- ✓ GOOD -->
<MyComponent lorem="1"
ipsum="2"
dolor="3"
/>
</template>
```
## :couple: Related Rules

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

## :books: Further Reading

Expand Down
1 change: 1 addition & 0 deletions lib/configs/no-layout-rules.js
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions lib/configs/strongly-recommended.js
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions lib/configs/vue3-strongly-recommended.js
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -30,6 +30,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'),
Expand Down
98 changes: 98 additions & 0 deletions 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)
}
})
}
}

0 comments on commit 14a17d4

Please sign in to comment.