Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vue/first-attribute-linebreak rule #1587

Merged
merged 1 commit into from Oct 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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'),
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)
}
})
}
}