-
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(html-closing-bracket-new-line): add rule (#870)
Adds the `svelte/html-closing-bracket-newline` rule, which enforces that HTML tags must have a newline (or not) after the closing bracket. This rule is inspired by `vue/html-closing-bracket-newline`, and is implemented ensuring what's discussed in #590. Closes #590.
- Loading branch information
Showing
32 changed files
with
445 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
--- | ||
pageClass: 'rule-details' | ||
sidebarDepth: 0 | ||
title: 'svelte/html-closing-bracket-new-line' | ||
description: "Require or disallow a line break before tag's closing brackets" | ||
--- | ||
|
||
# svelte/html-closing-bracket-new-line | ||
|
||
> Require or disallow a line break before tag's closing brackets | ||
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge> | ||
- :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 enforces a line break (or no line break) before tag's closing brackets, which can also be configured to be enforced on self-closing tags. | ||
|
||
<ESLintCodeBlock fix> | ||
|
||
<!-- prettier-ignore-start --> | ||
<!--eslint-skip--> | ||
|
||
```svelte | ||
<script> | ||
/* eslint svelte/brackets-same-line: "error" */ | ||
</script> | ||
<!-- ✓ GOOD --> | ||
<div></div> | ||
<div | ||
multiline | ||
> | ||
Children | ||
</div> | ||
<SelfClosing /> | ||
<SelfClosing | ||
multiline | ||
/> | ||
<!-- ✗ BAD --> | ||
<div | ||
></div> | ||
<div | ||
multiline> | ||
Children | ||
</div> | ||
<SelfClosing | ||
/> | ||
<SelfClosing | ||
multiline/> | ||
``` | ||
|
||
<!-- prettier-ignore-end --> | ||
|
||
</ESLintCodeBlock> | ||
|
||
## :wrench: Options | ||
|
||
```jsonc | ||
{ | ||
"svelte/brackets-same-line": [ | ||
"error", | ||
{ | ||
"singleline": "never", // ["never", "always"] | ||
"multiline": "always", // ["never", "always"] | ||
"selfClosingTag": { | ||
"singleline": "never", // ["never", "always"] | ||
"multiline": "always" // ["never", "always"] | ||
} | ||
} | ||
] | ||
} | ||
``` | ||
|
||
- `singleline`: (`"never"` by default) Configuration for single-line elements. It's a single-line element if the element does not have attributes or the last attribute is on the same line as the opening bracket. | ||
- `multiline`: (`"always"` by default) Configuration for multi-line elements. It's a multi-line element if the last attribute is not on the same line of the opening bracket. | ||
- `selfClosingTag.singleline`: Configuration for single-line self closing elements. | ||
- `selfClosingTag.multiline`: Configuration for multi-line self closing elements. | ||
|
||
The `selfClosing` is optional, and by default it will use the same configuration as `singleline` and `multiline`, respectively. | ||
|
||
## :mag: Implementation | ||
|
||
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/html-closing-bracket-new-line.ts) | ||
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/html-closing-bracket-new-line.ts) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
163 changes: 163 additions & 0 deletions
163
packages/eslint-plugin-svelte/src/rules/html-closing-bracket-new-line.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import type { AST } from 'svelte-eslint-parser'; | ||
import { createRule } from '../utils'; | ||
import { getSourceCode } from '../utils/compat'; | ||
import type { SourceCode } from '../types'; | ||
|
||
type ExpectedNode = AST.SvelteStartTag | AST.SvelteEndTag; | ||
type OptionValue = 'always' | 'never'; | ||
type RuleOptions = { | ||
singleline: OptionValue; | ||
multiline: OptionValue; | ||
selfClosingTag?: Omit<RuleOptions, 'selfClosingTag'>; | ||
}; | ||
|
||
function getPhrase(lineBreaks: number) { | ||
switch (lineBreaks) { | ||
case 0: { | ||
return 'no line breaks'; | ||
} | ||
case 1: { | ||
return '1 line break'; | ||
} | ||
default: { | ||
return `${lineBreaks} line breaks`; | ||
} | ||
} | ||
} | ||
|
||
function getExpectedLineBreaks( | ||
node: ExpectedNode, | ||
options: RuleOptions, | ||
type: keyof Omit<RuleOptions, 'selfClosingTag'> | ||
) { | ||
const isSelfClosingTag = node.type === 'SvelteStartTag' && node.selfClosing; | ||
if (isSelfClosingTag && options.selfClosingTag && options.selfClosingTag[type]) { | ||
return options.selfClosingTag[type] === 'always' ? 1 : 0; | ||
} | ||
|
||
return options[type] === 'always' ? 1 : 0; | ||
} | ||
|
||
type NodeData = { | ||
actualLineBreaks: number; | ||
expectedLineBreaks: number; | ||
startToken: AST.Token; | ||
endToken: AST.Token; | ||
}; | ||
|
||
function getSelfClosingData( | ||
sourceCode: SourceCode, | ||
node: AST.SvelteStartTag, | ||
options: RuleOptions | ||
): NodeData | null { | ||
const tokens = sourceCode.getTokens(node); | ||
const closingToken = tokens[tokens.length - 2]; | ||
if (closingToken.value !== '/') { | ||
return null; | ||
} | ||
|
||
const prevToken = sourceCode.getTokenBefore(closingToken)!; | ||
const type = node.loc.start.line === prevToken.loc.end.line ? 'singleline' : 'multiline'; | ||
|
||
const expectedLineBreaks = getExpectedLineBreaks(node, options, type); | ||
const actualLineBreaks = closingToken.loc.start.line - prevToken.loc.end.line; | ||
|
||
return { actualLineBreaks, expectedLineBreaks, startToken: prevToken, endToken: closingToken }; | ||
} | ||
|
||
function getNodeData( | ||
sourceCode: SourceCode, | ||
node: ExpectedNode, | ||
options: RuleOptions | ||
): NodeData | null { | ||
const closingToken = sourceCode.getLastToken(node); | ||
if (closingToken.value !== '>') { | ||
return null; | ||
} | ||
|
||
const prevToken = sourceCode.getTokenBefore(closingToken)!; | ||
const type = node.loc.start.line === prevToken.loc.end.line ? 'singleline' : 'multiline'; | ||
|
||
const expectedLineBreaks = getExpectedLineBreaks(node, options, type); | ||
const actualLineBreaks = closingToken.loc.start.line - prevToken.loc.end.line; | ||
|
||
return { actualLineBreaks, expectedLineBreaks, startToken: prevToken, endToken: closingToken }; | ||
} | ||
|
||
export default createRule('html-closing-bracket-new-line', { | ||
meta: { | ||
docs: { | ||
description: "Require or disallow a line break before tag's closing brackets", | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
conflictWithPrettier: true | ||
}, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
singleline: { enum: ['always', 'never'] }, | ||
multiline: { enum: ['always', 'never'] }, | ||
selfClosingTag: { | ||
type: 'object', | ||
properties: { | ||
singleline: { enum: ['always', 'never'] }, | ||
multiline: { enum: ['always', 'never'] } | ||
}, | ||
additionalProperties: false, | ||
minProperties: 1 | ||
} | ||
}, | ||
additionalProperties: false | ||
} | ||
], | ||
messages: { | ||
expectedBeforeClosingBracket: | ||
'Expected {{expected}} before closing bracket, but {{actual}} found.' | ||
}, | ||
fixable: 'code', | ||
type: 'suggestion' | ||
}, | ||
create(context) { | ||
const options: RuleOptions = context.options[0] ?? {}; | ||
options.singleline ??= 'never'; | ||
options.multiline ??= 'always'; | ||
|
||
const sourceCode = getSourceCode(context); | ||
|
||
return { | ||
'SvelteStartTag, SvelteEndTag'(node: ExpectedNode) { | ||
const data = | ||
node.type === 'SvelteStartTag' && node.selfClosing | ||
? getSelfClosingData(sourceCode, node, options) | ||
: getNodeData(sourceCode, node, options); | ||
if (!data) { | ||
return; | ||
} | ||
|
||
const { actualLineBreaks, expectedLineBreaks, startToken, endToken } = data; | ||
if (actualLineBreaks !== expectedLineBreaks) { | ||
// For SvelteEndTag, does not make sense to add a line break, so we only fix if there are extra line breaks | ||
if (node.type === 'SvelteEndTag' && expectedLineBreaks !== 0) { | ||
return; | ||
} | ||
|
||
context.report({ | ||
node, | ||
loc: { start: startToken.loc.end, end: endToken.loc.start }, | ||
messageId: 'expectedBeforeClosingBracket', | ||
data: { | ||
expected: getPhrase(expectedLineBreaks), | ||
actual: getPhrase(actualLineBreaks) | ||
}, | ||
fix(fixer) { | ||
const range: AST.Range = [startToken.range[1], endToken.range[0]]; | ||
const text = '\n'.repeat(expectedLineBreaks); | ||
return fixer.replaceTextRange(range, text); | ||
} | ||
}); | ||
} | ||
} | ||
}; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
...e/tests/fixtures/rules/html-closing-bracket-new-line/invalid/multiline-never/_config.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"options": [{ "multiline": "never" }] | ||
} |
8 changes: 8 additions & 0 deletions
8
...s/fixtures/rules/html-closing-bracket-new-line/invalid/multiline-never/test01-errors.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
- message: Expected no line breaks before closing bracket, but 1 line break found. | ||
line: 2 | ||
column: 12 | ||
suggestions: null | ||
- message: Expected no line breaks before closing bracket, but 1 line break found. | ||
line: 7 | ||
column: 12 | ||
suggestions: null |
10 changes: 10 additions & 0 deletions
10
.../fixtures/rules/html-closing-bracket-new-line/invalid/multiline-never/test01-input.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<div | ||
class="foo" | ||
></div> | ||
<div | ||
class="bar"></div> | ||
<div | ||
class="bar" | ||
> | ||
Children | ||
</div> |
8 changes: 8 additions & 0 deletions
8
...fixtures/rules/html-closing-bracket-new-line/invalid/multiline-never/test01-output.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<div | ||
class="foo"></div> | ||
<div | ||
class="bar"></div> | ||
<div | ||
class="bar"> | ||
Children | ||
</div> |
3 changes: 3 additions & 0 deletions
3
...sts/fixtures/rules/html-closing-bracket-new-line/invalid/self-closing/always/_config.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"options": [{ "selfClosingTag": { "singleline": "always", "multiline": "always" } }] | ||
} |
8 changes: 8 additions & 0 deletions
8
...fixtures/rules/html-closing-bracket-new-line/invalid/self-closing/always/test-errors.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
- message: Expected 1 line break before closing bracket, but no line breaks found. | ||
line: 1 | ||
column: 18 | ||
suggestions: null | ||
- message: Expected 1 line break before closing bracket, but 2 line breaks found. | ||
line: 6 | ||
column: 12 | ||
suggestions: null |
8 changes: 8 additions & 0 deletions
8
...ixtures/rules/html-closing-bracket-new-line/invalid/self-closing/always/test-input.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<Custom foo="bar" /> | ||
<Custom | ||
foo="bar" | ||
/> | ||
<Custom | ||
foo="bar" | ||
|
||
/> |
8 changes: 8 additions & 0 deletions
8
...xtures/rules/html-closing-bracket-new-line/invalid/self-closing/always/test-output.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<Custom foo="bar" | ||
/> | ||
<Custom | ||
foo="bar" | ||
/> | ||
<Custom | ||
foo="bar" | ||
/> |
3 changes: 3 additions & 0 deletions
3
...ests/fixtures/rules/html-closing-bracket-new-line/invalid/self-closing/never/_config.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"options": [{ "selfClosingTag": { "singleline": "never", "multiline": "never" } }] | ||
} |
Oops, something went wrong.