Skip to content

Commit d59ea93

Browse files
committed
Add indent rule
1 parent 6b3ae52 commit d59ea93

File tree

83 files changed

+8060
-4
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+8060
-4
lines changed

README.md

+1
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/indent](https://ota-meshi.github.io/eslint-plugin-svelte/rules/indent.html) | enforce consistent indentation | :wrench: |
246247
| [@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: |
247248
| [@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: |
248249
| [@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: |

docs/rules/README.md

+1
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/indent](./indent.md) | enforce consistent indentation | :wrench: |
1617
| [@ota-meshi/svelte/max-attributes-per-line](./max-attributes-per-line.md) | enforce the maximum number of attributes per line | :wrench: |
1718
| [@ota-meshi/svelte/no-at-debug-tags](./no-at-debug-tags.md) | disallow the use of `{@debug}` | :star: |
1819
| [@ota-meshi/svelte/no-at-html-tags](./no-at-html-tags.md) | disallow use of `{@html}` to prevent XSS attack | :star: |

docs/rules/indent.md

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "@ota-meshi/svelte/indent"
5+
description: "enforce consistent indentation"
6+
---
7+
8+
# @ota-meshi/svelte/indent
9+
10+
> enforce consistent indentation
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+
This rule enforces a consistent indentation style in `.svelte`. The default style is 2 spaces.
18+
19+
- This rule checks all tags, also all expressions in directives and mustaches.
20+
- In the expressions, this rule supports ECMAScript 2020 syntaxes. It ignores unknown AST nodes, but it might be confused by non-standard syntaxes.
21+
22+
<eslint-code-block fix>
23+
24+
<!--eslint-skip-->
25+
<!-- prettier-ignore -->
26+
```html
27+
<script>
28+
/* eslint @ota-meshi/svelte/indent: "error" */
29+
function click() {}
30+
</script>
31+
32+
<!-- ✓ GOOD -->
33+
<button
34+
type="button"
35+
on:click="{click}"
36+
class="my-button primally"
37+
>
38+
CLICK ME!
39+
</button>
40+
41+
<!-- ✗ BAD -->
42+
<button
43+
type="button"
44+
on:click="{click}"
45+
class="my-button primally"
46+
>
47+
CLICK ME!
48+
</button>
49+
```
50+
51+
</eslint-code-block>
52+
53+
## :wrench: Options
54+
55+
```json
56+
{
57+
"@ota-meshi/svelte/indent": [
58+
"error",
59+
{
60+
"indent": 2
61+
}
62+
]
63+
}
64+
```
65+
66+
- `indent` (`number | "tab"`) ... The type of indentation. Default is `2`. If this is a number, it's the number of spaces for one indent. If this is `"tab"`, it uses one tab for one indent.
67+
68+
## :mag: Implementation
69+
70+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/indent.ts)
71+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/indent.ts)

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"homepage": "https://github.com/ota-meshi/eslint-plugin-svelte#readme",
4848
"dependencies": {
4949
"debug": "^4.3.1",
50+
"eslint-utils": "^3.0.0",
5051
"svelte-eslint-parser": "^0.3.0"
5152
},
5253
"peerDependencies": {

src/rules/indent-helpers/ast.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import type * as ESTree from "estree"
3+
type AnyToken = AST.Token | AST.Comment
4+
/**
5+
* Check whether the given token is a whitespace.
6+
*/
7+
export function isWhitespace(
8+
token: AnyToken | ESTree.Comment | null | undefined,
9+
): boolean {
10+
return token != null && token.type === "HTMLText" && !token.value.trim()
11+
}
12+
13+
/**
14+
* Check whether the given token is a not whitespace.
15+
*/
16+
export function isNotWhitespace(
17+
token: AnyToken | ESTree.Comment | null | undefined,
18+
): boolean {
19+
return (
20+
token != null && (token.type !== "HTMLText" || Boolean(token.value.trim()))
21+
)
22+
}

src/rules/indent-helpers/commons.ts

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type { ASTNode, SourceCode } from "../../types"
2+
import type { AST } from "svelte-eslint-parser"
3+
import { isOpeningParenToken, isClosingParenToken } from "eslint-utils"
4+
import { isNotWhitespace, isWhitespace } from "./ast"
5+
6+
type AnyToken = AST.Token | AST.Comment
7+
export type IndentContext = {
8+
sourceCode: SourceCode
9+
/**
10+
* Set offset to the given tokens.
11+
*/
12+
setOffset: (
13+
token: AnyToken | null | undefined | (AnyToken | null | undefined)[],
14+
offset: number,
15+
baseToken: AnyToken,
16+
) => void
17+
/**
18+
* Copy offset to the given tokens from srcToken.
19+
*/
20+
copyOffset: (
21+
token: AnyToken | null | undefined | (AnyToken | null | undefined)[],
22+
srcToken: AnyToken,
23+
) => void
24+
25+
/**
26+
* Set baseline offset to the given token.
27+
*/
28+
setOffsetBaseLine: (
29+
token: AnyToken | null | undefined | (AnyToken | null | undefined)[],
30+
offset: number,
31+
) => void
32+
/**
33+
* Ignore all tokens of the given node.
34+
*/
35+
ignore: (node: ASTNode) => void
36+
}
37+
38+
/**
39+
* Set offset to the given nodes.
40+
* The first node is offsetted from the given base token.
41+
*/
42+
export function setOffsetNodes(
43+
{ sourceCode, setOffset }: IndentContext,
44+
nodes: (ASTNode | AnyToken | null | undefined)[],
45+
baseNodeOrToken: ASTNode | AnyToken,
46+
lastNodeOrToken: ASTNode | AnyToken | null,
47+
offset: number,
48+
): void {
49+
const baseToken = sourceCode.getFirstToken(baseNodeOrToken)
50+
51+
let prevToken = sourceCode.getLastToken(baseNodeOrToken)
52+
for (const node of nodes) {
53+
if (node == null) {
54+
continue
55+
}
56+
const elementTokens = getFirstAndLastTokens(
57+
sourceCode,
58+
node,
59+
prevToken.range[1],
60+
)
61+
62+
let t: AnyToken | null = prevToken
63+
while (
64+
(t = sourceCode.getTokenAfter(t, {
65+
includeComments: true,
66+
filter: isNotWhitespace,
67+
})) != null &&
68+
t.range[1] <= elementTokens.firstToken.range[0]
69+
) {
70+
setOffset(t, offset, baseToken)
71+
}
72+
setOffset(elementTokens.firstToken, offset, baseToken)
73+
74+
prevToken = elementTokens.lastToken
75+
}
76+
77+
if (lastNodeOrToken) {
78+
const lastToken = sourceCode.getFirstToken(lastNodeOrToken)
79+
let t: AnyToken | null = prevToken
80+
while (
81+
(t = sourceCode.getTokenAfter(t, {
82+
includeComments: true,
83+
filter: isNotWhitespace,
84+
})) != null &&
85+
t.range[1] <= lastToken.range[0]
86+
) {
87+
setOffset(t, offset, baseToken)
88+
}
89+
setOffset(lastToken, 0, baseToken)
90+
}
91+
}
92+
93+
/**
94+
* Get the first and last tokens of the given node.
95+
* If the node is parenthesized, this gets the outermost parentheses.
96+
* If the node have whitespace at the start and the end, they will be skipped.
97+
*/
98+
export function getFirstAndLastTokens(
99+
sourceCode: SourceCode,
100+
node: ASTNode | AnyToken,
101+
borderOffset = 0,
102+
): { firstToken: AST.Token; lastToken: AST.Token } {
103+
let firstToken = sourceCode.getFirstToken(node)
104+
let lastToken = sourceCode.getLastToken(node)
105+
106+
// Get the outermost left parenthesis if it's parenthesized.
107+
let left: AST.Token | null, right: AST.Token | null
108+
while (
109+
(left = sourceCode.getTokenBefore(firstToken)) != null &&
110+
(right = sourceCode.getTokenAfter(lastToken)) != null &&
111+
isOpeningParenToken(left) &&
112+
isClosingParenToken(right) &&
113+
borderOffset <= left.range[0]
114+
) {
115+
firstToken = left
116+
lastToken = right
117+
}
118+
119+
while (isWhitespace(firstToken) && firstToken.range[0] < lastToken.range[0]) {
120+
firstToken = sourceCode.getTokenAfter(firstToken) as AST.Token
121+
}
122+
while (isWhitespace(lastToken) && firstToken.range[0] < lastToken.range[0]) {
123+
lastToken = sourceCode.getTokenBefore(lastToken) as AST.Token
124+
}
125+
126+
return { firstToken, lastToken }
127+
}

0 commit comments

Comments
 (0)