diff --git a/README.md b/README.md index b2e746eeb..f92b2633e 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,7 @@ These rules relate to style guidelines, and are therefore quite subjective: | Rule ID | Description | | |:--------|:------------|:---| | [svelte/first-attribute-linebreak](https://ota-meshi.github.io/eslint-plugin-svelte/rules/first-attribute-linebreak/) | enforce the location of first attribute | :wrench: | +| [svelte/html-closing-bracket-spacing](https://ota-meshi.github.io/eslint-plugin-svelte/rules/html-closing-bracket-spacing/) | require or disallow a space before tag's closing brackets | :wrench: | | [svelte/html-quotes](https://ota-meshi.github.io/eslint-plugin-svelte/rules/html-quotes/) | enforce quotes style of HTML attributes | :wrench: | | [svelte/indent](https://ota-meshi.github.io/eslint-plugin-svelte/rules/indent/) | enforce consistent indentation | :wrench: | | [svelte/max-attributes-per-line](https://ota-meshi.github.io/eslint-plugin-svelte/rules/max-attributes-per-line/) | enforce the maximum number of attributes per line | :wrench: | diff --git a/docs/rules.md b/docs/rules.md index 70a940c90..85077d75e 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -52,6 +52,7 @@ These rules relate to style guidelines, and are therefore quite subjective: | Rule ID | Description | | |:--------|:------------|:---| | [svelte/first-attribute-linebreak](./rules/first-attribute-linebreak.md) | enforce the location of first attribute | :wrench: | +| [svelte/html-closing-bracket-spacing](./rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets | :wrench: | | [svelte/html-quotes](./rules/html-quotes.md) | enforce quotes style of HTML attributes | :wrench: | | [svelte/indent](./rules/indent.md) | enforce consistent indentation | :wrench: | | [svelte/max-attributes-per-line](./rules/max-attributes-per-line.md) | enforce the maximum number of attributes per line | :wrench: | diff --git a/docs/rules/html-closing-bracket-spacing.md b/docs/rules/html-closing-bracket-spacing.md new file mode 100644 index 000000000..b514cdc05 --- /dev/null +++ b/docs/rules/html-closing-bracket-spacing.md @@ -0,0 +1,77 @@ +--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "svelte/html-closing-bracket-spacing" +description: "require or disallow a space before tag's closing brackets" +--- + +# svelte/html-closing-bracket-spacing + +> require or disallow a space before tag's closing brackets + +- :exclamation: **_This rule has not been released yet._** +- :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 + +You can choose either two styles for spacing before closing bracket + +- always: `
` +- never: `
` + + + + + + +```svelte + + + +
+

Hello

+
+
+ + +
+

Hello

+
+
+``` + + + + + +## :wrench: Options + +```json +{ + "svelte/html-closing-bracket-spacing": [ + "error", + { + "startTag": "never", // or "always" or "ignore" + "endTag": "never", // or "always" or "ignore" + "selfClosingTag": "always" // or "never" or "ignore" + } + ] +} +``` + +- `startTag` (`"never"` by default)... Spacing in start tags +- `endTag` (`"never"` by default)... Spacing in end tags +- `selfClosingTag` (`"always"` by default)... Spacing in self closing tags + +Every option can be set to +- "always" (`
`) +- "never" (`
`) +- "ignore" (either `
` or `
`) + +## :mag: Implementation + +- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/html-closing-bracket-spacing.ts) +- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/html-closing-bracket-spacing.ts) diff --git a/src/configs/prettier.ts b/src/configs/prettier.ts index 6c0eba387..fa92608da 100644 --- a/src/configs/prettier.ts +++ b/src/configs/prettier.ts @@ -7,6 +7,7 @@ export = { rules: { // eslint-plugin-svelte rules "svelte/first-attribute-linebreak": "off", + "svelte/html-closing-bracket-spacing": "off", "svelte/html-quotes": "off", "svelte/indent": "off", "svelte/max-attributes-per-line": "off", diff --git a/src/rules/html-closing-bracket-spacing.ts b/src/rules/html-closing-bracket-spacing.ts new file mode 100644 index 000000000..a69b16334 --- /dev/null +++ b/src/rules/html-closing-bracket-spacing.ts @@ -0,0 +1,109 @@ +import { createRule } from "../utils" +import type { AST } from "svelte-eslint-parser" + +export default createRule("html-closing-bracket-spacing", { + meta: { + docs: { + description: "require or disallow a space before tag's closing brackets", + category: "Stylistic Issues", + conflictWithPrettier: true, + recommended: false, + }, + schema: [ + { + type: "object", + properties: { + startTag: { + enum: ["always", "never", "ignore"], + }, + endTag: { + enum: ["always", "never", "ignore"], + }, + selfClosingTag: { + enum: ["always", "never", "ignore"], + }, + }, + additionalProperties: false, + }, + ], + messages: { + expectedSpace: "Expected space before '>', but not found.", + unexpectedSpace: "Expected no space before '>', but found.", + }, + fixable: "whitespace", + type: "layout", + }, + create(ctx) { + const options = { + startTag: "never", + endTag: "never", + selfClosingTag: "always", + ...ctx.options[0], + } + const src = ctx.getSourceCode() + + /** + * Returns true if string contains newline characters + */ + function containsNewline(string: string): boolean { + return string.includes("\n") + } + + /** + * Report + */ + function report( + node: AST.SvelteStartTag | AST.SvelteEndTag, + shouldHave: boolean, + ) { + const tagSrc = src.getText(node) + const match = /(\s*)\/?>$/.exec(tagSrc) + + const end = node.range[1] + const start = node.range[1] - match![0].length + const loc = { + start: src.getLocFromIndex(start), + end: src.getLocFromIndex(end), + } + + ctx.report({ + loc, + messageId: shouldHave ? "expectedSpace" : "unexpectedSpace", + *fix(fixer) { + if (shouldHave) { + yield fixer.insertTextBeforeRange([start, end], " ") + } else { + const spaces = match![1] + + yield fixer.removeRange([start, start + spaces.length]) + } + }, + }) + } + + return { + "SvelteStartTag, SvelteEndTag"( + node: AST.SvelteStartTag | AST.SvelteEndTag, + ) { + const tagType = + node.type === "SvelteEndTag" + ? "endTag" + : node.selfClosing + ? "selfClosingTag" + : "startTag" + + if (options[tagType] === "ignore") return + + const tagSrc = src.getText(node) + const match = /(\s*)\/?>$/.exec(tagSrc) + if (containsNewline(match![1])) return + + if (options[tagType] === "always" && !match![1]) { + report(node, true) + } else if (options[tagType] === "never" && match![1]) { + report(node, false) + } + }, + } + }, +}) diff --git a/src/utils/rules.ts b/src/utils/rules.ts index 19fe075fd..5bff9ab49 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -2,6 +2,7 @@ import type { RuleModule } from "../types" import buttonHasType from "../rules/button-has-type" import commentDirective from "../rules/comment-directive" import firstAttributeLinebreak from "../rules/first-attribute-linebreak" +import htmlClosingBracketSpacing from "../rules/html-closing-bracket-spacing" import htmlQuotes from "../rules/html-quotes" import indent from "../rules/indent" import maxAttributesPerLine from "../rules/max-attributes-per-line" @@ -32,6 +33,7 @@ export const rules = [ buttonHasType, commentDirective, firstAttributeLinebreak, + htmlClosingBracketSpacing, htmlQuotes, indent, maxAttributesPerLine, diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/_config.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/_config.json new file mode 100644 index 000000000..e5e1e4d84 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/_config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "selfClosingTag": "ignore" + } + ] +} diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-errors.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-errors.json new file mode 100644 index 000000000..8e0d94be0 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-errors.json @@ -0,0 +1,12 @@ +[ + { + "message": "Expected no space before '>', but found.", + "line": 2, + "column": 3 + }, + { + "message": "Expected no space before '>', but found.", + "line": 2, + "column": 14 + } +] diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-input.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-input.svelte new file mode 100644 index 000000000..0aa1e17af --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-input.svelte @@ -0,0 +1,5 @@ + +

Hello

+ +
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-output.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-output.svelte new file mode 100644 index 000000000..ee4523394 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-output.svelte @@ -0,0 +1,5 @@ + +

Hello

+ +
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/_config.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/_config.json new file mode 100644 index 000000000..c2b607b39 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/_config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "endTag": "ignore" + } + ] +} diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-errors.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-errors.json new file mode 100644 index 000000000..130ab5d28 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-errors.json @@ -0,0 +1,12 @@ +[ + { + "message": "Expected no space before '>', but found.", + "line": 2, + "column": 3 + }, + { + "message": "Expected space before '>', but not found.", + "line": 5, + "column": 5 + } +] diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-input.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-input.svelte new file mode 100644 index 000000000..97408e4d3 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-input.svelte @@ -0,0 +1,5 @@ + +

Hello

+

Hi

+ +
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-output.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-output.svelte new file mode 100644 index 000000000..8b8916c1b --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-output.svelte @@ -0,0 +1,5 @@ + +

Hello

+

Hi

+ +
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/_config.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/_config.json new file mode 100644 index 000000000..e0a9668b5 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/_config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "startTag": "ignore" + } + ] +} diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-errors.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-errors.json new file mode 100644 index 000000000..bb1c24ac3 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-errors.json @@ -0,0 +1,12 @@ +[ + { + "message": "Expected no space before '>', but found.", + "line": 2, + "column": 14 + }, + { + "message": "Expected space before '>', but not found.", + "line": 5, + "column": 5 + } +] diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-input.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-input.svelte new file mode 100644 index 000000000..97408e4d3 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-input.svelte @@ -0,0 +1,5 @@ + +

Hello

+

Hi

+ +
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-output.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-output.svelte new file mode 100644 index 000000000..644df5de0 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-output.svelte @@ -0,0 +1,5 @@ + +

Hello

+

Hi

+ +
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-errors.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-errors.json new file mode 100644 index 000000000..bababc39e --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-errors.json @@ -0,0 +1,17 @@ +[ + { + "message": "Expected no space before '>', but found.", + "line": 2, + "column": 3 + }, + { + "message": "Expected no space before '>', but found.", + "line": 2, + "column": 14 + }, + { + "message": "Expected space before '>', but not found.", + "line": 4, + "column": 5 + } +] diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-input.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-input.svelte new file mode 100644 index 000000000..7b0b7117d --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-input.svelte @@ -0,0 +1,10 @@ + +

Hello

+ +
+ +
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-output.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-output.svelte new file mode 100644 index 000000000..2cbd69667 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-output.svelte @@ -0,0 +1,10 @@ + +

Hello

+ +
+ +
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/valid/test-01-input.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/valid/test-01-input.svelte new file mode 100644 index 000000000..e40adb0e0 --- /dev/null +++ b/tests/fixtures/rules/html-closing-bracket-spacing/valid/test-01-input.svelte @@ -0,0 +1,11 @@ +

Hello

+
+ +
+ +
+
diff --git a/tests/src/rules/html-closing-bracket-spacing.ts b/tests/src/rules/html-closing-bracket-spacing.ts new file mode 100644 index 000000000..80cbe4562 --- /dev/null +++ b/tests/src/rules/html-closing-bracket-spacing.ts @@ -0,0 +1,16 @@ +import { RuleTester } from "eslint" +import rule from "../../../src/rules/html-closing-bracket-spacing" +import { loadTestCases } from "../../utils/utils" + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, +}) + +tester.run( + "html-closing-bracket-spacing", + rule as any, + loadTestCases("html-closing-bracket-spacing"), +)