diff --git a/README.md b/README.md
index c6d848e9b..c2a1c6753 100644
--- a/README.md
+++ b/README.md
@@ -276,6 +276,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
| Rule ID | Description | |
|:--------|:------------|:---|
+| [@ota-meshi/svelte/first-attribute-linebreak](https://ota-meshi.github.io/eslint-plugin-svelte/rules/first-attribute-linebreak.html) | enforce the location of first attribute | :wrench: |
| [@ota-meshi/svelte/html-quotes](https://ota-meshi.github.io/eslint-plugin-svelte/rules/html-quotes.html) | enforce quotes style of HTML attributes | :wrench: |
| [@ota-meshi/svelte/indent](https://ota-meshi.github.io/eslint-plugin-svelte/rules/indent.html) | enforce consistent indentation | :wrench: |
| [@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: |
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 6a2d2cb02..edd61c36b 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -44,6 +44,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
| Rule ID | Description | |
|:--------|:------------|:---|
+| [@ota-meshi/svelte/first-attribute-linebreak](./first-attribute-linebreak.md) | enforce the location of first attribute | :wrench: |
| [@ota-meshi/svelte/html-quotes](./html-quotes.md) | enforce quotes style of HTML attributes | :wrench: |
| [@ota-meshi/svelte/indent](./indent.md) | enforce consistent indentation | :wrench: |
| [@ota-meshi/svelte/max-attributes-per-line](./max-attributes-per-line.md) | enforce the maximum number of attributes per line | :wrench: |
diff --git a/docs/rules/first-attribute-linebreak.md b/docs/rules/first-attribute-linebreak.md
new file mode 100644
index 000000000..3f6f033fc
--- /dev/null
+++ b/docs/rules/first-attribute-linebreak.md
@@ -0,0 +1,79 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "@ota-meshi/svelte/first-attribute-linebreak"
+description: "enforce the location of first attribute"
+---
+
+# @ota-meshi/svelte/first-attribute-linebreak
+
+> enforce the location of first attribute
+
+- :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
+
+This rule aims to enforce a consistent location for the first attribute.
+
+
+
+
+
+
+```svelte
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+
+
+## :wrench: Options
+
+```json
+{
+ "@ota-meshi/svelte/first-attribute-linebreak": [
+ "error",
+ {
+ "multiline": "below", // or "beside"
+ "singleline": "beside" // "below"
+ }
+ ]
+}
+```
+
+- `multiline` ... The location of the first attribute when the attributes span multiple lines. Default is `"below"`.
+ - `"below"` ... Requires a newline before the first attribute.
+ - `"beside"` ... Disallows a newline before the first attribute.
+- `singleline` ... The location of the first attribute when the attributes on single line. Default is `"beside"`.
+ - `"below"` ... Requires a newline before the first attribute.
+ - `"beside"` ... Disallows a newline before the first attribute.
+
+## :couple: Related Rules
+
+- [@ota-meshi/svelte/max-attributes-per-line]
+
+[@ota-meshi/svelte/max-attributes-per-line]: ./max-attributes-per-line.md
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/first-attribute-linebreak.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/first-attribute-linebreak.ts)
diff --git a/docs/rules/max-attributes-per-line.md b/docs/rules/max-attributes-per-line.md
index 359f386a6..5eba30368 100644
--- a/docs/rules/max-attributes-per-line.md
+++ b/docs/rules/max-attributes-per-line.md
@@ -74,8 +74,14 @@ There is a configurable number of attributes that are acceptable in one-line cas
}
```
-- `singleline` ... The number of maximum attributes per line when the opening tag is in a single line. Default is `1`.
- `multiline` ... The number of maximum attributes per line when the opening tag is in multiple lines. Default is `1`.
+- `singleline` ... The number of maximum attributes per line when the opening tag is in a single line. Default is `1`.
+
+## :couple: Related Rules
+
+- [@ota-meshi/svelte/first-attribute-linebreak]
+
+[@ota-meshi/svelte/first-attribute-linebreak]: ./first-attribute-linebreak.md
## :rocket: Version
diff --git a/src/rules/first-attribute-linebreak.ts b/src/rules/first-attribute-linebreak.ts
new file mode 100644
index 000000000..ed6a8fad8
--- /dev/null
+++ b/src/rules/first-attribute-linebreak.ts
@@ -0,0 +1,84 @@
+import type { AST } from "svelte-eslint-parser"
+import { createRule } from "../utils"
+
+export default createRule("first-attribute-linebreak", {
+ meta: {
+ docs: {
+ description: "enforce the location of first attribute",
+ category: "Stylistic Issues",
+ recommended: false,
+ },
+ fixable: "whitespace",
+ schema: [
+ {
+ type: "object",
+ properties: {
+ multiline: { enum: ["below", "beside"] },
+ singleline: { enum: ["below", "beside"] },
+ },
+ additionalProperties: false,
+ },
+ ],
+ messages: {
+ expected: "Expected a linebreak before this attribute.",
+ unexpected: "Expected no linebreak before this attribute.",
+ },
+ type: "layout",
+ },
+ create(context) {
+ const multiline: "below" | "beside" =
+ context.options[0]?.multiline || "below"
+ const singleline: "below" | "beside" =
+ context.options[0]?.singleline || "beside"
+ const sourceCode = context.getSourceCode()
+
+ /**
+ * Report attribute
+ */
+ function report(
+ firstAttribute: AST.SvelteStartTag["attributes"][number],
+ location: "below" | "beside",
+ ) {
+ context.report({
+ node: firstAttribute,
+ messageId: location === "beside" ? "unexpected" : "expected",
+ fix(fixer) {
+ const prevToken = sourceCode.getTokenBefore(firstAttribute, {
+ includeComments: true,
+ })!
+ return fixer.replaceTextRange(
+ [prevToken.range[1], firstAttribute.range[0]],
+ location === "beside" ? " " : "\n",
+ )
+ },
+ })
+ }
+
+ return {
+ SvelteStartTag(node) {
+ const firstAttribute = node.attributes[0]
+ if (!firstAttribute) return
+
+ const lastAttribute = node.attributes[node.attributes.length - 1]
+
+ const location =
+ firstAttribute.loc.start.line === lastAttribute.loc.end.line
+ ? singleline
+ : multiline
+
+ if (location === "beside") {
+ if (
+ node.parent.name.loc!.end.line === firstAttribute.loc.start.line
+ ) {
+ return
+ }
+ } else {
+ if (node.parent.name.loc!.end.line < firstAttribute.loc.start.line) {
+ return
+ }
+ }
+ report(firstAttribute, location)
+ },
+ }
+ },
+})
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index a9fa6e79b..5ef170ebe 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -1,6 +1,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 htmlQuotes from "../rules/html-quotes"
import indent from "../rules/indent"
import maxAttributesPerLine from "../rules/max-attributes-per-line"
@@ -20,6 +21,7 @@ import system from "../rules/system"
export const rules = [
buttonHasType,
commentDirective,
+ firstAttributeLinebreak,
htmlQuotes,
indent,
maxAttributesPerLine,
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/below/_config.json b/tests/fixtures/rules/first-attribute-linebreak/invalid/below/_config.json
new file mode 100644
index 000000000..a57d8d29b
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/below/_config.json
@@ -0,0 +1,3 @@
+{
+ "options": [{ "multiline": "below", "singleline": "below" }]
+}
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/below/test01-errors.json b/tests/fixtures/rules/first-attribute-linebreak/invalid/below/test01-errors.json
new file mode 100644
index 000000000..a5687643b
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/below/test01-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Expected a linebreak before this attribute.",
+ "line": 9,
+ "column": 9
+ },
+ {
+ "message": "Expected a linebreak before this attribute.",
+ "line": 13,
+ "column": 8
+ }
+]
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/below/test01-input.svelte b/tests/fixtures/rules/first-attribute-linebreak/invalid/below/test01-input.svelte
new file mode 100644
index 000000000..929227136
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/below/test01-input.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/below/test01-output.svelte b/tests/fixtures/rules/first-attribute-linebreak/invalid/below/test01-output.svelte
new file mode 100644
index 000000000..e767f2732
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/below/test01-output.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/_config.json b/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/_config.json
new file mode 100644
index 000000000..9777cb390
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/_config.json
@@ -0,0 +1,3 @@
+{
+ "options": [{ "multiline": "beside", "singleline": "beside" }]
+}
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/test01-errors.json b/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/test01-errors.json
new file mode 100644
index 000000000..6baa1b4ad
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/test01-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Expected no linebreak before this attribute.",
+ "line": 7,
+ "column": 3
+ },
+ {
+ "message": "Expected no linebreak before this attribute.",
+ "line": 16,
+ "column": 3
+ }
+]
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/test01-input.svelte b/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/test01-input.svelte
new file mode 100644
index 000000000..929227136
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/test01-input.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/test01-output.svelte b/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/test01-output.svelte
new file mode 100644
index 000000000..245ddd24e
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/beside/test01-output.svelte
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/test01-errors.json b/tests/fixtures/rules/first-attribute-linebreak/invalid/test01-errors.json
new file mode 100644
index 000000000..e096db2d9
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/test01-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Expected no linebreak before this attribute.",
+ "line": 7,
+ "column": 3
+ },
+ {
+ "message": "Expected a linebreak before this attribute.",
+ "line": 9,
+ "column": 9
+ }
+]
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/test01-input.svelte b/tests/fixtures/rules/first-attribute-linebreak/invalid/test01-input.svelte
new file mode 100644
index 000000000..929227136
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/test01-input.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/rules/first-attribute-linebreak/invalid/test01-output.svelte b/tests/fixtures/rules/first-attribute-linebreak/invalid/test01-output.svelte
new file mode 100644
index 000000000..ca1a0e11a
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/invalid/test01-output.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/rules/first-attribute-linebreak/valid/below/_config.json b/tests/fixtures/rules/first-attribute-linebreak/valid/below/_config.json
new file mode 100644
index 000000000..a57d8d29b
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/valid/below/_config.json
@@ -0,0 +1,3 @@
+{
+ "options": [{ "multiline": "below", "singleline": "below" }]
+}
diff --git a/tests/fixtures/rules/first-attribute-linebreak/valid/below/test01-input.svelte b/tests/fixtures/rules/first-attribute-linebreak/valid/below/test01-input.svelte
new file mode 100644
index 000000000..91cad4eab
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/valid/below/test01-input.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/rules/first-attribute-linebreak/valid/beside/_config.json b/tests/fixtures/rules/first-attribute-linebreak/valid/beside/_config.json
new file mode 100644
index 000000000..9777cb390
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/valid/beside/_config.json
@@ -0,0 +1,3 @@
+{
+ "options": [{ "multiline": "beside", "singleline": "beside" }]
+}
diff --git a/tests/fixtures/rules/first-attribute-linebreak/valid/beside/test01-input.svelte b/tests/fixtures/rules/first-attribute-linebreak/valid/beside/test01-input.svelte
new file mode 100644
index 000000000..8712844b0
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/valid/beside/test01-input.svelte
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/rules/first-attribute-linebreak/valid/test01-input.svelte b/tests/fixtures/rules/first-attribute-linebreak/valid/test01-input.svelte
new file mode 100644
index 000000000..3e17a196a
--- /dev/null
+++ b/tests/fixtures/rules/first-attribute-linebreak/valid/test01-input.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/src/rules/first-attribute-linebreak.ts b/tests/src/rules/first-attribute-linebreak.ts
new file mode 100644
index 000000000..d79639056
--- /dev/null
+++ b/tests/src/rules/first-attribute-linebreak.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/first-attribute-linebreak"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run(
+ "first-attribute-linebreak",
+ rule as any,
+ loadTestCases("first-attribute-linebreak"),
+)