Skip to content

Commit

Permalink
feat: add sort-attributes rule (#423)
Browse files Browse the repository at this point in the history
* feat: add sort-attributes rule

* chore: add changeset
  • Loading branch information
azat-io authored Oct 15, 2024
1 parent dc107d3 commit d8d0c73
Show file tree
Hide file tree
Showing 19 changed files with 294 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-jobs-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-astro": minor
---

feat: add sort-attributes rule
59 changes: 59 additions & 0 deletions docs/rules/sort-attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: "astro/sort-attributes"
description: "Enforce sorted Astro attributes"
since: "v1.3.0"
---

# astro/sort-attributes

> Enforce sorted Astro attributes
- :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

Maintaining a consistent order of attributes in Astro elements is crucial for readability and maintainability. This rule ensures that attributes are sorted, making the structure of your elements more predictable and easier to manage.

Adopting this rule helps standardize code formatting across your project, facilitating better collaboration and reducing cognitive load for developers.

It’s safe. The rule considers spread elements in an attributes list and does not break component functionality.

Default:

<ESLintCodeBlock fix>

<!--eslint-skip-->

```astro
---
---
{/* ✓ GOOD */}
<Element a="a" b="b" c="c" />
{/* ✓ BAD */}
<Element c="c" b="b" a="a" />
```

</ESLintCodeBlock>

## :wrench: Options

```json
{
"astro/sort-attributes": [
"error",
{ "type": "alphabetical", "order": "asc", "ignoreCase": true }
]
}
```

## :rocket: Version

This rule was introduced in eslint-plugin-astro v1.3.0

## :mag: Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-astro/blob/main/src/rules/sort-attributes.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-astro/blob/main/tests/src/rules/sort-attributes.ts)
- [Test fixture sources](https://github.com/ota-meshi/eslint-plugin-astro/tree/main/tests/fixtures/rules/sort-attributes)
137 changes: 137 additions & 0 deletions src/rules/sort-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import type { AST } from "astro-eslint-parser"

import { createRule } from "../utils"

export default createRule("sort-attributes", {
meta: {
docs: {
description: "enforce sorting of attributes",
category: "Stylistic Issues",
recommended: false,
},
schema: [
{
type: "object",
properties: {
type: { type: "string", enum: ["alphabetical", "line-length"] },
ignoreCase: { type: "boolean" },
order: { type: "string", enum: ["asc", "desc"] },
},
additionalProperties: false,
},
],
messages: {
unexpectedAstroAttributesOrder:
'Expected "{{right}}" to come before "{{left}}".',
},
fixable: "code",
type: "suggestion",
},
create(context) {
if (!context.parserServices.isAstro) {

Check failure on line 31 in src/rules/sort-attributes.ts

View workflow job for this annotation

GitHub Actions / lint

'context.parserServices' is restricted from being used. Use src/utils/compat.ts
return {}
}

return {
JSXElement(node) {
const { openingElement } = node
const { attributes } = openingElement

if (attributes.length <= 1) {
return
}

const sourceCode = context.getSourceCode()

Check failure on line 44 in src/rules/sort-attributes.ts

View workflow job for this annotation

GitHub Actions / lint

'context.getSourceCode' is restricted from being used. Use src/utils/compat.ts

const pairwise = <T>(

Check failure on line 46 in src/rules/sort-attributes.ts

View workflow job for this annotation

GitHub Actions / lint

Expected a function declaration
nodes: T[],
callback: (left: T, right: T, iteration: number) => void,
) => {
if (nodes.length > 1) {
for (let i = 1; i < nodes.length; i++) {
let left = nodes.at(i - 1)

Check failure on line 52 in src/rules/sort-attributes.ts

View workflow job for this annotation

GitHub Actions / lint

'left' is never reassigned. Use 'const' instead
let right = nodes.at(i)

Check failure on line 53 in src/rules/sort-attributes.ts

View workflow job for this annotation

GitHub Actions / lint

'right' is never reassigned. Use 'const' instead

if (left && right) {
callback(left, right, i - 1)
}
}
}
}

type Node =
| AST.AstroShorthandAttribute
| AST.AstroTemplateLiteralAttribute
| AST.JSXAttribute

type SortingNode = {
name: string
node: Node
size: number
}

const compare = (left: SortingNode, right: SortingNode) => {

Check failure on line 73 in src/rules/sort-attributes.ts

View workflow job for this annotation

GitHub Actions / lint

Expected a function declaration
const compareFunc = (a: SortingNode, b: SortingNode) => {

Check failure on line 74 in src/rules/sort-attributes.ts

View workflow job for this annotation

GitHub Actions / lint

Expected a function declaration
if (context.options[0]?.type === "line-length") {
return a.size - b.size
}
const formatName = (name: string) =>

Check failure on line 78 in src/rules/sort-attributes.ts

View workflow job for this annotation

GitHub Actions / lint

Expected a function declaration
context.options[0]?.ignoreCase === false
? name
: name.toLowerCase()
return formatName(a.name).localeCompare(formatName(b.name))
}

const orderCoefficient = context.options[0]?.order === "desc" ? -1 : 1

return compareFunc(left, right) * orderCoefficient
}

const parts = attributes.reduce(
(accumulator: SortingNode[][], attribute) => {
if (attribute.type === "JSXSpreadAttribute") {
accumulator.push([])
return accumulator
}

const name =
typeof attribute.name.name === "string"
? attribute.name.name
: sourceCode.text.slice(...attribute.name.range)

accumulator[accumulator.length - 1].push({
name,
node: attribute,
size: attribute.range[1] - attribute.range[0],
})

return accumulator
},
[[]],
)

for (let nodes of parts) {

Check failure on line 113 in src/rules/sort-attributes.ts

View workflow job for this annotation

GitHub Actions / lint

'nodes' is never reassigned. Use 'const' instead
pairwise(nodes, (left, right) => {
if (compare(left, right) > 0) {
context.report({
node: left.node,
messageId: "unexpectedAstroAttributesOrder",
data: {
left: left.name,
right: right.name,
},
fix(fixer) {
return fixer.replaceTextRange(
[left.node.range[0], right.node.range[1]],
sourceCode.text.slice(...right.node.range) +
sourceCode.text.slice(...left.node.range),
)
},
})
}
})
}
},
}
},
})
2 changes: 2 additions & 0 deletions src/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import preferClassListDirective from "../rules/prefer-class-list-directive"
import preferObjectClassList from "../rules/prefer-object-class-list"
import preferSplitClassList from "../rules/prefer-split-class-list"
import semi from "../rules/semi"
import sortAttributes from "../rules/sort-attributes"
import validCompile from "../rules/valid-compile"
import { buildA11yRules } from "../a11y"

Expand All @@ -36,6 +37,7 @@ export const rules = [
preferObjectClassList,
preferSplitClassList,
semi,
sortAttributes,
validCompile,
...buildA11yRules(),
] as RuleModule[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"options": [{ "type": "alphabetical", "order": "asc" }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"message": "Expected \"b\" to come before \"c\".",
"line": 5,
"column": 6
},
{
"message": "Expected \"a\" to come before \"b\".",
"line": 5,
"column": 12
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
---

<div c="c" b="b" a="a"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
---

<div b="b"c="c" a="a"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"options": [{ "type": "line-length", "order": "desc" }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"message": "Expected \"bb\" to come before \"c\".",
"line": 5,
"column": 6
},
{
"message": "Expected \"aaa\" to come before \"bb\".",
"line": 5,
"column": 12
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
---

<div c="c" bb="b" aaa="a"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
---

<div bb="b"c="c" aaa="a"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"options": [{ "type": "alphabetical", "order": "asc" }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
---

<div a="a" b="b" c="c"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
const d = []
---

<div b="b" c="c" {...d} a="a"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"options": [{ "type": "line-length", "order": "desc" }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
---

<div aaa="a" bb="b" c="c"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
const d = []
---

<div bb="b" c="c" {...d} aaa="a"></div>
15 changes: 15 additions & 0 deletions tests/src/rules/sort-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { RuleTester } from "eslint"
import rule from "../../../src/rules/sort-attributes"
import { loadTestCases } from "../../utils/utils"

const tester = new RuleTester({
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
globals: {
Astro: false,
},
})

tester.run("sort-attributes", rule as any, loadTestCases("sort-attributes"))

0 comments on commit d8d0c73

Please sign in to comment.