Skip to content

Commit f3ee4ec

Browse files
committed
feat(eslint): add sort-lit-get-properties rule
Migrate the sort-lit-get-properties logic from Prettier plugin to ESLint custom rule, as property ordering is a structural concern, not formatting.
1 parent 489c491 commit f3ee4ec

File tree

3 files changed

+254
-0
lines changed

3 files changed

+254
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/** @import { Rule } from 'eslint' */
2+
/** @import { Identifier } from 'estree' */
3+
4+
/** @type {Rule.RuleModule} */
5+
export default {
6+
meta: {
7+
type: 'suggestion',
8+
fixable: 'code',
9+
messages: {
10+
unsortedProperties:
11+
'Properties in static get properties() should be sorted: spreads first, then alphabetical, then _-prefixed.',
12+
},
13+
},
14+
create(context) {
15+
const sourceCode = context.sourceCode;
16+
17+
return {
18+
MethodDefinition(node) {
19+
if (!node.static || node.kind !== 'get' || node.key.type !== 'Identifier' || node.key.name !== 'properties') {
20+
return;
21+
}
22+
23+
const body = node.value.body.body;
24+
const firstStatement = body[0];
25+
if (
26+
firstStatement == null ||
27+
firstStatement.type !== 'ReturnStatement' ||
28+
firstStatement.argument?.type !== 'ObjectExpression'
29+
) {
30+
return;
31+
}
32+
33+
const properties = firstStatement.argument.properties;
34+
if (properties.length <= 1) {
35+
return;
36+
}
37+
38+
// Check for unknown spread elements — bail out if found
39+
for (const prop of properties) {
40+
if (prop.type === 'SpreadElement') {
41+
const isSuperProperties =
42+
prop.argument.type === 'MemberExpression' &&
43+
prop.argument.object.type === 'Super' &&
44+
prop.argument.property.type === 'Identifier' &&
45+
prop.argument.property.name === 'properties';
46+
47+
if (!isSuperProperties) {
48+
return;
49+
}
50+
}
51+
}
52+
53+
const sorted = [...properties].sort((pA, pB) => {
54+
if (pA.type === 'SpreadElement') {
55+
return -1;
56+
}
57+
if (pB.type === 'SpreadElement') {
58+
return 1;
59+
}
60+
61+
const pAName = /** @type {Identifier} */ (pA.key).name;
62+
const pBName = /** @type {Identifier} */ (pB.key).name;
63+
64+
if (pAName.startsWith('_') && !pBName.startsWith('_')) {
65+
return 1;
66+
}
67+
if (pBName.startsWith('_') && !pAName.startsWith('_')) {
68+
return -1;
69+
}
70+
if (pAName === pBName) {
71+
return 0;
72+
}
73+
74+
return pAName < pBName ? -1 : 1;
75+
});
76+
77+
// Check if order changed
78+
const isSorted = properties.every((prop, i) => prop === sorted[i]);
79+
if (isSorted) {
80+
return;
81+
}
82+
83+
context.report({
84+
node: firstStatement.argument,
85+
messageId: 'unsortedProperties',
86+
fix(fixer) {
87+
const originalTexts = properties.map((prop) => sourceCode.getText(prop));
88+
const sortedTexts = sorted.map((prop) => sourceCode.getText(prop));
89+
90+
const fixes = [];
91+
for (let i = 0; i < properties.length; i++) {
92+
if (originalTexts[i] !== sortedTexts[i]) {
93+
fixes.push(fixer.replaceText(properties[i], sortedTexts[i]));
94+
}
95+
}
96+
return fixes;
97+
},
98+
});
99+
},
100+
};
101+
},
102+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { RuleTester } from 'eslint';
2+
import sortLitGetPropertiesTestData from './sort-lit-get-properties.test-data.js';
3+
4+
const ruleTester = new RuleTester();
5+
6+
describe('eslint lit custom rules', () => {
7+
ruleTester.run(
8+
sortLitGetPropertiesTestData.name,
9+
sortLitGetPropertiesTestData.rule,
10+
sortLitGetPropertiesTestData.tests,
11+
);
12+
});
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import sortLitGetProperties from '../../eslint/lit/custom-rules/sort-lit-get-properties.js';
2+
3+
export default {
4+
name: 'sort-lit-get-properties',
5+
rule: sortLitGetProperties,
6+
tests: {
7+
valid: [
8+
{
9+
name: 'already sorted properties',
10+
code: `
11+
class MyComponent extends LitElement {
12+
static get properties() {
13+
return {
14+
...super.properties,
15+
a: { type: String },
16+
b: { type: String },
17+
c: { type: String },
18+
d: { type: String },
19+
e: { type: String },
20+
_myPrivA: { type: String },
21+
_myPrivB: { type: String },
22+
};
23+
}
24+
}
25+
`,
26+
},
27+
{
28+
name: 'no properties method',
29+
code: `
30+
class MyComponent extends LitElement {
31+
static get styles() {
32+
return css\`div { color: red; }\`;
33+
}
34+
}
35+
`,
36+
},
37+
{
38+
name: 'unknown spread element should be ignored',
39+
code: `
40+
class MyComponent extends LitElement {
41+
static get properties() {
42+
return {
43+
_myPrivA: { type: String },
44+
d: { type: String },
45+
a: { type: String },
46+
b: { type: String },
47+
e: { type: String },
48+
c: { type: String },
49+
_myPrivB: { type: String },
50+
...foobar,
51+
};
52+
}
53+
}
54+
`,
55+
},
56+
],
57+
invalid: [
58+
{
59+
name: 'should sort properties alphabetically',
60+
code: `
61+
class MyComponent extends LitElement {
62+
static get properties() {
63+
return {
64+
...super.properties,
65+
_myPrivA: { type: String },
66+
d: { type: String },
67+
a: { type: String },
68+
b: { type: String },
69+
e: { type: String },
70+
c: { type: String },
71+
_myPrivB: { type: String },
72+
};
73+
}
74+
}
75+
`,
76+
errors: [
77+
{
78+
messageId: 'unsortedProperties',
79+
},
80+
],
81+
output: `
82+
class MyComponent extends LitElement {
83+
static get properties() {
84+
return {
85+
...super.properties,
86+
a: { type: String },
87+
b: { type: String },
88+
c: { type: String },
89+
d: { type: String },
90+
e: { type: String },
91+
_myPrivA: { type: String },
92+
_myPrivB: { type: String },
93+
};
94+
}
95+
}
96+
`,
97+
},
98+
{
99+
name: 'should move ...super.properties to the top',
100+
code: `
101+
class MyComponent extends LitElement {
102+
static get properties() {
103+
return {
104+
_myPrivA: { type: String },
105+
d: { type: String },
106+
a: { type: String },
107+
b: { type: String },
108+
...super.properties,
109+
e: { type: String },
110+
c: { type: String },
111+
_myPrivB: { type: String },
112+
};
113+
}
114+
}
115+
`,
116+
errors: [
117+
{
118+
messageId: 'unsortedProperties',
119+
},
120+
],
121+
output: `
122+
class MyComponent extends LitElement {
123+
static get properties() {
124+
return {
125+
...super.properties,
126+
a: { type: String },
127+
b: { type: String },
128+
c: { type: String },
129+
d: { type: String },
130+
e: { type: String },
131+
_myPrivA: { type: String },
132+
_myPrivB: { type: String },
133+
};
134+
}
135+
}
136+
`,
137+
},
138+
],
139+
},
140+
};

0 commit comments

Comments
 (0)