Skip to content

Commit 895ffc1

Browse files
authored
Add max-attributes-per-line rule (#27)
1 parent e1dfd3e commit 895ffc1

File tree

16 files changed

+331
-3
lines changed

16 files changed

+331
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ The rules with the following star :star: are included in the configs.
243243
|:--------|:------------|:---|
244244
| [@ota-meshi/svelte/button-has-type](https://ota-meshi.github.io/eslint-plugin-svelte/rules/button-has-type.html) | disallow usage of button without an explicit type attribute | |
245245
| [@ota-meshi/svelte/comment-directive](https://ota-meshi.github.io/eslint-plugin-svelte/rules/comment-directive.html) | support comment-directives in HTML template | :star: |
246+
| [@ota-meshi/svelte/max-attributes-per-line](https://ota-meshi.github.io/eslint-plugin-svelte/rules/max-attributes-per-line.html) | enforce the maximum number of attributes per line | :wrench: |
246247
| [@ota-meshi/svelte/no-at-debug-tags](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-at-debug-tags.html) | disallow the use of `{@debug}` | :star: |
247248
| [@ota-meshi/svelte/no-at-html-tags](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-at-html-tags.html) | disallow use of `{@html}` to prevent XSS attack | :star: |
248249
| [@ota-meshi/svelte/no-dupe-else-if-blocks](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-else-if-blocks.html) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The rules with the following star :star: are included in the `plugin:@ota-meshi/
1313
|:--------|:------------|:---|
1414
| [@ota-meshi/svelte/button-has-type](./button-has-type.md) | disallow usage of button without an explicit type attribute | |
1515
| [@ota-meshi/svelte/comment-directive](./comment-directive.md) | support comment-directives in HTML template | :star: |
16+
| [@ota-meshi/svelte/max-attributes-per-line](./max-attributes-per-line.md) | enforce the maximum number of attributes per line | :wrench: |
1617
| [@ota-meshi/svelte/no-at-debug-tags](./no-at-debug-tags.md) | disallow the use of `{@debug}` | :star: |
1718
| [@ota-meshi/svelte/no-at-html-tags](./no-at-html-tags.md) | disallow use of `{@html}` to prevent XSS attack | :star: |
1819
| [@ota-meshi/svelte/no-dupe-else-if-blocks](./no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "@ota-meshi/svelte/max-attributes-per-line"
5+
description: "enforce the maximum number of attributes per line"
6+
---
7+
8+
# @ota-meshi/svelte/max-attributes-per-line
9+
10+
> enforce the maximum number of attributes per line
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
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+
Limits the maximum number of attributes/directives per line to improve readability.
18+
19+
This rule aims to enforce a number of attributes per line in templates.
20+
It checks all the elements in a template and verifies that the number of attributes per line does not exceed the defined maximum.
21+
An attribute is considered to be in a new line when there is a line break between two attributes.
22+
23+
There is a configurable number of attributes that are acceptable in one-line case (default 1), as well as how many attributes are acceptable per line in multi-line case (default 1).
24+
25+
<eslint-code-block fix>
26+
27+
<!--eslint-skip-->
28+
29+
```html
30+
<script>
31+
/* eslint @ota-meshi/svelte/max-attributes-per-line: "error" */
32+
</script>
33+
34+
<!-- ✓ GOOD -->
35+
<input
36+
type="text"
37+
bind:value="{text}"
38+
{maxlength}
39+
{...attrs}
40+
readonly
41+
size="20"
42+
/>
43+
<button
44+
type="button"
45+
on:click="{click}"
46+
{maxlength}
47+
{...attrs}
48+
disabled
49+
data-my-data="foo"
50+
>
51+
CLICK ME!
52+
</button>
53+
54+
<!-- ✗ BAD -->
55+
<input type="text" bind:value="{text}" {maxlength} {...attrs} readonly />
56+
<button type="button" on:click="{click}" {maxlength} {...attrs}>
57+
CLICK ME!
58+
</button>
59+
```
60+
61+
</eslint-code-block>
62+
63+
## :wrench: Options
64+
65+
```json
66+
{
67+
"@ota-meshi/svelte/max-attributes-per-line": [
68+
"error",
69+
{
70+
"multiline": 1,
71+
"singleline": 1
72+
}
73+
]
74+
}
75+
```
76+
77+
- `singleline` ... The number of maximum attributes per line when the opening tag is in a single line. Default is `1`.
78+
- `multiline` ... The number of maximum attributes per line when the opening tag is in multiple lines. Default is `1`.
79+
80+
## :mag: Implementation
81+
82+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/max-attributes-per-line.ts)
83+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/max-attributes-per-line.ts)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"homepage": "https://github.com/ota-meshi/eslint-plugin-svelte#readme",
4848
"dependencies": {
4949
"debug": "^4.3.1",
50-
"svelte-eslint-parser": "^0.1.0"
50+
"svelte-eslint-parser": "^0.2.0"
5151
},
5252
"peerDependencies": {
5353
"eslint": "^7.0.0",

src/rules/max-attributes-per-line.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import { createRule } from "../utils"
3+
4+
/**
5+
* Check whether the component is declared in a single line or not.
6+
*/
7+
function isSingleLine(node: AST.SvelteStartTag) {
8+
return node.loc.start.line === node.loc.end.line
9+
}
10+
11+
/**
12+
* Group attributes line by line.
13+
*/
14+
function groupAttributesByLine(attributes: AST.SvelteStartTag["attributes"]) {
15+
const group: AST.SvelteStartTag["attributes"][] = []
16+
for (const attr of attributes) {
17+
if (group[0]?.[0]?.loc.end.line === attr.loc.start.line) {
18+
group[0].push(attr)
19+
} else {
20+
group.unshift([attr])
21+
}
22+
}
23+
24+
return group.reverse()
25+
}
26+
27+
export default createRule("max-attributes-per-line", {
28+
meta: {
29+
docs: {
30+
description: "enforce the maximum number of attributes per line",
31+
recommended: false,
32+
},
33+
fixable: "whitespace",
34+
schema: [
35+
{
36+
type: "object",
37+
properties: {
38+
multiline: {
39+
type: "number",
40+
minimum: 1,
41+
},
42+
singleline: {
43+
type: "number",
44+
minimum: 1,
45+
},
46+
},
47+
additionalProperties: false,
48+
},
49+
],
50+
messages: {
51+
requireNewline: "'{{name}}' should be on a new line.",
52+
},
53+
type: "layout",
54+
},
55+
create(context) {
56+
const multilineMaximum = context.options[0]?.multiline ?? 1
57+
const singlelineMaximum = context.options[0]?.singleline ?? 1
58+
const sourceCode = context.getSourceCode()
59+
60+
/**
61+
* Report attributes
62+
*/
63+
function report(
64+
attribute: AST.SvelteStartTag["attributes"][number] | undefined,
65+
) {
66+
if (!attribute) {
67+
return
68+
}
69+
let name: string
70+
if (
71+
attribute.type === "SvelteAttribute" ||
72+
attribute.type === "SvelteShorthandAttribute" ||
73+
attribute.type === "SvelteDirective" ||
74+
attribute.type === "SvelteSpecialDirective"
75+
) {
76+
name = sourceCode.text.slice(...attribute.key.range!)
77+
} else {
78+
// if (attribute.type === "SvelteSpreadAttribute")
79+
name = sourceCode.text.slice(...attribute.range)
80+
}
81+
context.report({
82+
node: attribute,
83+
loc: attribute.loc,
84+
messageId: "requireNewline",
85+
data: { name },
86+
fix(fixer) {
87+
// Find the closest token before the current attribute
88+
// that is not a white space
89+
const prevToken = sourceCode.getTokenBefore(attribute, {
90+
includeComments: true,
91+
})!
92+
93+
const range: AST.Range = [prevToken.range[1], attribute.range[0]]
94+
95+
return fixer.replaceTextRange(range, "\n")
96+
},
97+
})
98+
}
99+
100+
return {
101+
SvelteStartTag(node) {
102+
const numberOfAttributes = node.attributes.length
103+
104+
if (!numberOfAttributes) return
105+
106+
if (isSingleLine(node)) {
107+
if (numberOfAttributes > singlelineMaximum) {
108+
report(node.attributes[singlelineMaximum])
109+
}
110+
} else {
111+
for (const attrs of groupAttributesByLine(node.attributes)) {
112+
report(attrs[multilineMaximum])
113+
}
114+
}
115+
},
116+
}
117+
},
118+
})

src/utils/ast-utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,15 @@ export function findBindDirective<N extends string>(
159159
name: N,
160160
):
161161
| (SvAST.SvelteBindingDirective & {
162-
name: SvAST.SvelteBindingDirective["name"] & { name: N }
162+
key: SvAST.SvelteDirectiveKey & {
163+
name: SvAST.SvelteDirectiveKey["name"] & { name: N }
164+
}
163165
})
164166
| null {
165167
const startTag = node.type === "SvelteStartTag" ? node : node.startTag
166168
for (const attr of startTag.attributes) {
167169
if (attr.type === "SvelteDirective") {
168-
if (attr.kind === "Binding" && attr.name.name === name) {
170+
if (attr.kind === "Binding" && attr.key.name.name === name) {
169171
return attr as never
170172
}
171173
}

src/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RuleModule } from "../types"
22
import buttonHasType from "../rules/button-has-type"
33
import commentDirective from "../rules/comment-directive"
4+
import maxAttributesPerLine from "../rules/max-attributes-per-line"
45
import noAtDebugTags from "../rules/no-at-debug-tags"
56
import noAtHtmlTags from "../rules/no-at-html-tags"
67
import noDupeElseIfBlocks from "../rules/no-dupe-else-if-blocks"
@@ -14,6 +15,7 @@ import system from "../rules/system"
1415
export const rules = [
1516
buttonHasType,
1617
commentDirective,
18+
maxAttributesPerLine,
1719
noAtDebugTags,
1820
noAtHtmlTags,
1921
noDupeElseIfBlocks,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"options": [{ "multiline": 3, "singleline": 3 }]
3+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "'{...attrs}' should be on a new line.",
4+
"line": 8,
5+
"column": 50
6+
}
7+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let text = "abc"
3+
const maxlength = 42
4+
const attrs = { disabled: true }
5+
function click() {}
6+
</script>
7+
8+
<input type="text" bind:value={text} {maxlength} {...attrs} readonly />
9+
<!-- prettier-ignore -->
10+
<button type="button" on:click={click}
11+
{maxlength} {...attrs}>
12+
CLICK ME!
13+
</button>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
let text = "abc"
3+
const maxlength = 42
4+
const attrs = { disabled: true }
5+
function click() {}
6+
</script>
7+
8+
<input type="text" bind:value={text} {maxlength}
9+
{...attrs} readonly />
10+
<!-- prettier-ignore -->
11+
<button type="button" on:click={click}
12+
{maxlength} {...attrs}>
13+
CLICK ME!
14+
</button>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"message": "'bind:value' should be on a new line.",
4+
"line": 8,
5+
"column": 20
6+
},
7+
{
8+
"message": "'on:click' should be on a new line.",
9+
"line": 10,
10+
"column": 23
11+
},
12+
{
13+
"message": "'{...attrs}' should be on a new line.",
14+
"line": 11,
15+
"column": 15
16+
}
17+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let text = "abc"
3+
const maxlength = 42
4+
const attrs = { disabled: true }
5+
function click() {}
6+
</script>
7+
8+
<input type="text" bind:value={text} {maxlength} {...attrs} readonly />
9+
<!-- prettier-ignore -->
10+
<button type="button" on:click={click}
11+
{maxlength} {...attrs}>
12+
CLICK ME!
13+
</button>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script>
2+
let text = "abc"
3+
const maxlength = 42
4+
const attrs = { disabled: true }
5+
function click() {}
6+
</script>
7+
8+
<input type="text"
9+
bind:value={text} {maxlength} {...attrs} readonly />
10+
<!-- prettier-ignore -->
11+
<button type="button"
12+
on:click={click}
13+
{maxlength}
14+
{...attrs}>
15+
CLICK ME!
16+
</button>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script>
2+
let text = "abc"
3+
const maxlength = 42
4+
const attrs = { disabled: true }
5+
function click() {}
6+
</script>
7+
8+
<!-- prettier-ignore -->
9+
<input
10+
type="text"
11+
bind:value={text}
12+
{maxlength}
13+
{...attrs}
14+
readonly />
15+
<!-- prettier-ignore -->
16+
<button
17+
type="button"
18+
on:click={click}
19+
{maxlength}
20+
{...attrs}>
21+
CLICK ME!
22+
</button>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../src/rules/max-attributes-per-line"
3+
import { loadTestCases } from "../../utils/utils"
4+
5+
const tester = new RuleTester({
6+
parserOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: "module",
9+
},
10+
})
11+
12+
tester.run(
13+
"max-attributes-per-line",
14+
rule as any,
15+
loadTestCases("max-attributes-per-line"),
16+
)

0 commit comments

Comments
 (0)