-
-
Notifications
You must be signed in to change notification settings - Fork 38
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
feat(html-closing-bracket-new-line): add rule #870
Changes from all commits
b516c57
8e5baca
e40f893
157d6be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ota-meshi, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the syntax highlighting works fine on our website 👍 |
||
{ | ||
"svelte/brackets-same-line": [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be |
||
"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) |
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); | ||
} | ||
}); | ||
} | ||
} | ||
}; | ||
} | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"options": [{ "multiline": "never" }] | ||
} |
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 |
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> |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"options": [{ "selfClosingTag": { "singleline": "always", "multiline": "always" } }] | ||
} |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<Custom foo="bar" /> | ||
<Custom | ||
foo="bar" | ||
/> | ||
<Custom | ||
foo="bar" | ||
|
||
/> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<Custom foo="bar" | ||
/> | ||
<Custom | ||
foo="bar" | ||
/> | ||
<Custom | ||
foo="bar" | ||
/> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"options": [{ "selfClosingTag": { "singleline": "never", "multiline": "never" } }] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
svelte/html-closing-bracket-new-line
?