@diia-inhouse/oxc-config 3.0.4 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../oxlint/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,YAAY,EAAsC,MAAM,QAAQ,CAAA;AAE9E,KAAK,MAAM,GAAG,YAAY,GAAG;IAAE,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../oxlint/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,YAAY,EAAsC,MAAM,QAAQ,CAAA;AAE9E,KAAK,MAAM,GAAG,YAAY,GAAG;IAAE,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AA2dzD,eAAO,MAAM,IAAI,EAAE,MAAiD,CAAA;AAEpE,eAAO,MAAM,UAAU,EAAE,MAKb,CAAA;AAIZ;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,YAAY,KAAG,YAOnD,CAAA"}
|
package/dist/oxlint/config.js
CHANGED
|
@@ -3,9 +3,6 @@ import { defineConfig as defineOxlintConfig } from 'oxlint';
|
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
4
|
const resolve = (pkg) => require.resolve(pkg).replace(/\/[^/]+$/, '');
|
|
5
5
|
const resolveFile = (path) => new URL(path, import.meta.url).pathname;
|
|
6
|
-
// A package that ships `exports` without `main` cannot be loaded from its directory,
|
|
7
|
-
// so it is pointed at its entry file instead of being stripped back by `resolve`.
|
|
8
|
-
const resolveEntry = (pkg) => require.resolve(pkg);
|
|
9
6
|
const baseConfig = {
|
|
10
7
|
plugins: ['unicorn', 'typescript', 'import', 'promise', 'node', 'vitest', 'oxc'],
|
|
11
8
|
jsPlugins: [
|
|
@@ -13,12 +10,12 @@ const baseConfig = {
|
|
|
13
10
|
resolve('eslint-plugin-regexp'),
|
|
14
11
|
{ name: '@stylistic/js', specifier: resolve('@stylistic/eslint-plugin') },
|
|
15
12
|
resolve('@diia-inhouse/eslint-plugin'),
|
|
16
|
-
{ name: 'ts-eslint', specifier: resolveEntry('@typescript-eslint/eslint-plugin') },
|
|
17
13
|
{ name: '@diia-inhouse/package', specifier: resolveFile('./plugins/packageCheck.mjs') },
|
|
18
14
|
{ name: '@diia-inhouse/locale', specifier: resolveFile('./plugins/noHardcodedCyrillic.mjs') },
|
|
19
15
|
{ name: '@diia-inhouse/test', specifier: resolveFile('./plugins/testConventions.mjs') },
|
|
20
16
|
{ name: '@diia-inhouse/mongoose', specifier: resolveFile('./plugins/mongooseSchema.mjs') },
|
|
21
17
|
{ name: '@diia-inhouse/class', specifier: resolveFile('./plugins/classEncapsulation.mjs') },
|
|
18
|
+
{ name: '@diia-inhouse/class-conventions', specifier: resolveFile('./plugins/classConventions.mjs') },
|
|
22
19
|
{ name: '@diia-inhouse/temporal', specifier: resolveFile('./plugins/temporalConventions.mjs') },
|
|
23
20
|
{ name: '@diia-inhouse/code', specifier: resolveFile('./plugins/codeConventions.mjs') },
|
|
24
21
|
],
|
|
@@ -101,7 +98,7 @@ const baseConfig = {
|
|
|
101
98
|
'unicorn/prefer-spread': 'off',
|
|
102
99
|
'unicorn/require-post-message-target-origin': 'off',
|
|
103
100
|
'unicorn/no-array-sort': 'off',
|
|
104
|
-
'unicorn/no-array-reduce': 'error',
|
|
101
|
+
'unicorn/no-array-reduce': ['error', { allowSimpleOperations: false }],
|
|
105
102
|
'promise/always-return': 'error',
|
|
106
103
|
'promise/no-return-wrap': 'error',
|
|
107
104
|
'promise/param-names': 'error',
|
|
@@ -204,9 +201,7 @@ const baseConfig = {
|
|
|
204
201
|
'@diia-inhouse/mongoose/sub-schema-id-false': 'off',
|
|
205
202
|
'@diia-inhouse/class/no-module-level-const': 'warn',
|
|
206
203
|
'@diia-inhouse/class/no-interface-in-implementation': 'warn',
|
|
207
|
-
|
|
208
|
-
// equivalent, so the original rule runs as a JS plugin at its original severity.
|
|
209
|
-
'ts-eslint/member-ordering': 'error',
|
|
204
|
+
'@diia-inhouse/class-conventions/member-ordering': 'error',
|
|
210
205
|
'@diia-inhouse/temporal/workflow-single-param': 'error',
|
|
211
206
|
'@diia-inhouse/temporal/async-activity': 'error',
|
|
212
207
|
'@diia-inhouse/temporal/no-node-imports': 'error',
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
const accessibilities = ['public', 'protected', 'private', '#private']
|
|
2
|
+
|
|
3
|
+
const groupsOf = (type) => [
|
|
4
|
+
...accessibilities.map((accessibility) => `${accessibility}-static-${type}`),
|
|
5
|
+
...accessibilities.slice(0, 3).map((accessibility) => `${accessibility}-decorated-${type}`),
|
|
6
|
+
...accessibilities.map((accessibility) => `${accessibility}-instance-${type}`),
|
|
7
|
+
...accessibilities.slice(0, 2).map((accessibility) => `${accessibility}-abstract-${type}`),
|
|
8
|
+
...accessibilities.map((accessibility) => `${accessibility}-${type}`),
|
|
9
|
+
`static-${type}`,
|
|
10
|
+
`instance-${type}`,
|
|
11
|
+
`abstract-${type}`,
|
|
12
|
+
`decorated-${type}`,
|
|
13
|
+
type,
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
// Mirrors the default order of @typescript-eslint/member-ordering, which the eslint config enforced before the oxc migration
|
|
17
|
+
const memberOrder = [
|
|
18
|
+
'signature',
|
|
19
|
+
'call-signature',
|
|
20
|
+
...groupsOf('field'),
|
|
21
|
+
'static-initialization',
|
|
22
|
+
'public-constructor',
|
|
23
|
+
'protected-constructor',
|
|
24
|
+
'private-constructor',
|
|
25
|
+
'constructor',
|
|
26
|
+
...groupsOf('accessor'),
|
|
27
|
+
...groupsOf('get'),
|
|
28
|
+
...groupsOf('set'),
|
|
29
|
+
...groupsOf('method'),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const rankByGroup = new Map(memberOrder.map((group, rank) => [group, rank]))
|
|
33
|
+
|
|
34
|
+
const typeOf = (member) => {
|
|
35
|
+
switch (member.type) {
|
|
36
|
+
case 'TSIndexSignature': {
|
|
37
|
+
return 'signature'
|
|
38
|
+
}
|
|
39
|
+
case 'TSCallSignatureDeclaration': {
|
|
40
|
+
return 'call-signature'
|
|
41
|
+
}
|
|
42
|
+
case 'TSConstructSignatureDeclaration': {
|
|
43
|
+
return 'constructor'
|
|
44
|
+
}
|
|
45
|
+
case 'StaticBlock': {
|
|
46
|
+
return 'static-initialization'
|
|
47
|
+
}
|
|
48
|
+
case 'AccessorProperty':
|
|
49
|
+
case 'TSAbstractAccessorProperty': {
|
|
50
|
+
return 'accessor'
|
|
51
|
+
}
|
|
52
|
+
case 'TSPropertySignature': {
|
|
53
|
+
return 'field'
|
|
54
|
+
}
|
|
55
|
+
case 'PropertyDefinition':
|
|
56
|
+
case 'TSAbstractPropertyDefinition': {
|
|
57
|
+
return ['ArrowFunctionExpression', 'FunctionExpression'].includes(member.value?.type) ? 'method' : 'field'
|
|
58
|
+
}
|
|
59
|
+
case 'MethodDefinition': {
|
|
60
|
+
// Overload signatures can't be decorated, so ranking them would split a decorated implementation from its overloads
|
|
61
|
+
return member.value.type === 'TSEmptyBodyFunctionExpression' ? undefined : member.kind
|
|
62
|
+
}
|
|
63
|
+
case 'TSAbstractMethodDefinition':
|
|
64
|
+
case 'TSMethodSignature': {
|
|
65
|
+
return member.kind
|
|
66
|
+
}
|
|
67
|
+
default: {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const groupsOfMember = (member, type, supportsModifiers) => {
|
|
74
|
+
if (!supportsModifiers || type === 'signature' || type === 'static-initialization') {
|
|
75
|
+
return [type]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const accessibility = member.key.type === 'PrivateIdentifier' ? '#private' : (member.accessibility ?? 'public')
|
|
79
|
+
if (type === 'constructor') {
|
|
80
|
+
return [`${accessibility}-constructor`, 'constructor']
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const scope = member.static ? 'static' : member.type.startsWith('TSAbstract') ? 'abstract' : 'instance'
|
|
84
|
+
const decorated = member.decorators?.length > 0
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
...(decorated ? [`${accessibility}-decorated-${type}`, `decorated-${type}`] : []),
|
|
88
|
+
`${accessibility}-${scope}-${type}`,
|
|
89
|
+
`${scope}-${type}`,
|
|
90
|
+
`${accessibility}-${type}`,
|
|
91
|
+
type,
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rankOf = (member, supportsModifiers) => {
|
|
96
|
+
const type = typeOf(member)
|
|
97
|
+
if (!type) {
|
|
98
|
+
return -1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return rankByGroup.get(groupsOfMember(member, type, supportsModifiers).find((group) => rankByGroup.has(group)))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const hasInitializer = (member) => ['PropertyDefinition', 'AccessorProperty'].includes(member.type) && Boolean(member.value)
|
|
105
|
+
|
|
106
|
+
const initializes = (member) => member.type === 'StaticBlock' || hasInitializer(member)
|
|
107
|
+
|
|
108
|
+
const isStatic = (member) => member.type === 'StaticBlock' || Boolean(member.static)
|
|
109
|
+
|
|
110
|
+
const hasBlockBody = (member) =>
|
|
111
|
+
member.type === 'StaticBlock' || (member.type === 'MethodDefinition' && member.value.type !== 'TSEmptyBodyFunctionExpression')
|
|
112
|
+
|
|
113
|
+
const keylessMemberNames = {
|
|
114
|
+
StaticBlock: 'static block',
|
|
115
|
+
TSIndexSignature: 'index signature',
|
|
116
|
+
TSCallSignatureDeclaration: 'call',
|
|
117
|
+
TSConstructSignatureDeclaration: 'new',
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const disableDirective = /^\s*(?:eslint|oxlint)-disable(?:-next-line|-line)?(?:\s+([^]*?))?\s*(?:--|$)/
|
|
121
|
+
|
|
122
|
+
const disablesOrdering = (comment) => {
|
|
123
|
+
const match = disableDirective.exec(comment.value)
|
|
124
|
+
|
|
125
|
+
return Boolean(match) && (!match[1] || match[1].includes('member-ordering'))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default {
|
|
129
|
+
meta: { name: 'class-conventions' },
|
|
130
|
+
rules: {
|
|
131
|
+
'member-ordering': {
|
|
132
|
+
meta: {
|
|
133
|
+
type: 'suggestion',
|
|
134
|
+
fixable: 'code',
|
|
135
|
+
hasSuggestions: true,
|
|
136
|
+
messages: {
|
|
137
|
+
incorrectGroupOrder: 'Member {{ name }} should be declared before all {{ group }} definitions.',
|
|
138
|
+
sortMembers: 'Sort members by group. This changes the order in which field initializers run.',
|
|
139
|
+
sortExemptMembers: 'Sort members by group. This also moves members exempted by a disable comment.',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
create(context) {
|
|
143
|
+
const { sourceCode } = context
|
|
144
|
+
|
|
145
|
+
const chunkOf = (member, previousEnd) => {
|
|
146
|
+
const leading = sourceCode
|
|
147
|
+
.getCommentsBefore(member)
|
|
148
|
+
.find((comment) => sourceCode.text.slice(previousEnd, comment.range[0]).includes('\n'))
|
|
149
|
+
const trailing = sourceCode
|
|
150
|
+
.getCommentsAfter(member)
|
|
151
|
+
.findLast((comment) => !sourceCode.text.slice(member.range[1], comment.range[0]).includes('\n'))
|
|
152
|
+
|
|
153
|
+
return { start: leading?.range[0] ?? member.range[0], end: trailing?.range[1] ?? member.range[1] }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// A moved field can end up before `[computed]`, `*generator`, `in()` or `instanceof()`, which would continue its initializer expression
|
|
157
|
+
const needsSemicolon = (member, next, gap) => {
|
|
158
|
+
if (hasBlockBody(member) || [',', ';'].includes(sourceCode.getText(member).at(-1))) {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return !gap.includes('\n') || (hasInitializer(member) && /^(?:[*[]|in\b|instanceof\b)/.test(sourceCode.getText(next)))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const sortFixOf = (body, members, supportsModifiers) => {
|
|
166
|
+
const chunks = members.map((member, index) =>
|
|
167
|
+
chunkOf(member, index === 0 ? body.range[0] + 1 : members[index - 1].range[1]),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const units = []
|
|
171
|
+
let first = 0
|
|
172
|
+
for (const [index, member] of members.entries()) {
|
|
173
|
+
const rank = rankOf(member, supportsModifiers)
|
|
174
|
+
if (rank !== -1) {
|
|
175
|
+
units.push({ first, last: index, rank })
|
|
176
|
+
first = index + 1
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
units.at(-1).last = members.length - 1
|
|
181
|
+
|
|
182
|
+
const sorted = units.toSorted((a, b) => a.rank - b.rank)
|
|
183
|
+
const gaps = units
|
|
184
|
+
.slice(1)
|
|
185
|
+
.map((unit, index) => sourceCode.text.slice(chunks[units[index].last].end, chunks[unit.first].start))
|
|
186
|
+
const textOf = (unit, next, gap) => {
|
|
187
|
+
const text = sourceCode.text.slice(chunks[unit.first].start, chunks[unit.last].end)
|
|
188
|
+
if (!next || !needsSemicolon(members[unit.last], members[next.first], gap)) {
|
|
189
|
+
return text
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const semicolonAt = members[unit.last].range[1] - chunks[unit.first].start
|
|
193
|
+
|
|
194
|
+
return `${text.slice(0, semicolonAt)};${text.slice(semicolonAt)}`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const replacement = sorted
|
|
198
|
+
.map((unit, index) => textOf(unit, sorted[index + 1], gaps[index]) + (gaps[index] ?? ''))
|
|
199
|
+
.join('')
|
|
200
|
+
const fix = (fixer) => fixer.replaceTextRange([chunks[units[0].first].start, chunks.at(-1).end], replacement)
|
|
201
|
+
|
|
202
|
+
const initializationOrderOf = (ordered) =>
|
|
203
|
+
ordered
|
|
204
|
+
.flatMap((unit) => members.slice(unit.first, unit.last + 1))
|
|
205
|
+
.filter(initializes)
|
|
206
|
+
.toSorted((a, b) => Number(isStatic(a)) - Number(isStatic(b)))
|
|
207
|
+
const sortedInitializationOrder = initializationOrderOf(sorted)
|
|
208
|
+
const keepsInitializationOrder = initializationOrderOf(units).every(
|
|
209
|
+
(member, index) => member === sortedInitializationOrder[index],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if (!keepsInitializationOrder) {
|
|
213
|
+
return { suggest: [{ messageId: 'sortMembers', fix }] }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Sorting the whole body would override a member someone deliberately exempted from the rule
|
|
217
|
+
if (sourceCode.getCommentsInside(body).some(disablesOrdering)) {
|
|
218
|
+
return { suggest: [{ messageId: 'sortExemptMembers', fix }] }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { fix }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const nameOf = (member) => {
|
|
225
|
+
if (member.type in keylessMemberNames) {
|
|
226
|
+
return keylessMemberNames[member.type]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (member.key.type === 'PrivateIdentifier') {
|
|
230
|
+
return `#${member.key.name}`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return member.key.type === 'Identifier' && !member.computed ? member.key.name : context.sourceCode.getText(member.key)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const checkOrder = (body, members, supportsModifiers) => {
|
|
237
|
+
const ranks = []
|
|
238
|
+
const misplaced = []
|
|
239
|
+
for (const member of members) {
|
|
240
|
+
const rank = rankOf(member, supportsModifiers)
|
|
241
|
+
if (rank === -1) {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (ranks.length === 0 || rank > ranks.at(-1)) {
|
|
246
|
+
ranks.push(rank)
|
|
247
|
+
} else if (rank < ranks.at(-1)) {
|
|
248
|
+
misplaced.push({ member, expected: Math.min(...ranks.filter((previous) => previous > rank)) })
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (misplaced.length === 0) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Every report carries the fix, so it survives when some of them are suppressed by a disable comment
|
|
257
|
+
const sortFix = sortFixOf(body, members, supportsModifiers)
|
|
258
|
+
for (const { member, expected } of misplaced) {
|
|
259
|
+
context.report({
|
|
260
|
+
node: member,
|
|
261
|
+
messageId: 'incorrectGroupOrder',
|
|
262
|
+
data: { name: nameOf(member), group: memberOrder[expected].replaceAll('-', ' ') },
|
|
263
|
+
...sortFix,
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
ClassBody(node) {
|
|
270
|
+
checkOrder(node, node.body, true)
|
|
271
|
+
},
|
|
272
|
+
TSInterfaceBody(node) {
|
|
273
|
+
checkOrder(node, node.body, false)
|
|
274
|
+
},
|
|
275
|
+
TSTypeLiteral(node) {
|
|
276
|
+
checkOrder(node, node.members, false)
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
const accessibilities = ['public', 'protected', 'private', '#private']
|
|
2
|
+
|
|
3
|
+
const groupsOf = (type) => [
|
|
4
|
+
...accessibilities.map((accessibility) => `${accessibility}-static-${type}`),
|
|
5
|
+
...accessibilities.slice(0, 3).map((accessibility) => `${accessibility}-decorated-${type}`),
|
|
6
|
+
...accessibilities.map((accessibility) => `${accessibility}-instance-${type}`),
|
|
7
|
+
...accessibilities.slice(0, 2).map((accessibility) => `${accessibility}-abstract-${type}`),
|
|
8
|
+
...accessibilities.map((accessibility) => `${accessibility}-${type}`),
|
|
9
|
+
`static-${type}`,
|
|
10
|
+
`instance-${type}`,
|
|
11
|
+
`abstract-${type}`,
|
|
12
|
+
`decorated-${type}`,
|
|
13
|
+
type,
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
// Mirrors the default order of @typescript-eslint/member-ordering, which the eslint config enforced before the oxc migration
|
|
17
|
+
const memberOrder = [
|
|
18
|
+
'signature',
|
|
19
|
+
'call-signature',
|
|
20
|
+
...groupsOf('field'),
|
|
21
|
+
'static-initialization',
|
|
22
|
+
'public-constructor',
|
|
23
|
+
'protected-constructor',
|
|
24
|
+
'private-constructor',
|
|
25
|
+
'constructor',
|
|
26
|
+
...groupsOf('accessor'),
|
|
27
|
+
...groupsOf('get'),
|
|
28
|
+
...groupsOf('set'),
|
|
29
|
+
...groupsOf('method'),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const rankByGroup = new Map(memberOrder.map((group, rank) => [group, rank]))
|
|
33
|
+
|
|
34
|
+
const typeOf = (member) => {
|
|
35
|
+
switch (member.type) {
|
|
36
|
+
case 'TSIndexSignature': {
|
|
37
|
+
return 'signature'
|
|
38
|
+
}
|
|
39
|
+
case 'TSCallSignatureDeclaration': {
|
|
40
|
+
return 'call-signature'
|
|
41
|
+
}
|
|
42
|
+
case 'TSConstructSignatureDeclaration': {
|
|
43
|
+
return 'constructor'
|
|
44
|
+
}
|
|
45
|
+
case 'StaticBlock': {
|
|
46
|
+
return 'static-initialization'
|
|
47
|
+
}
|
|
48
|
+
case 'AccessorProperty':
|
|
49
|
+
case 'TSAbstractAccessorProperty': {
|
|
50
|
+
return 'accessor'
|
|
51
|
+
}
|
|
52
|
+
case 'TSPropertySignature': {
|
|
53
|
+
return 'field'
|
|
54
|
+
}
|
|
55
|
+
case 'PropertyDefinition':
|
|
56
|
+
case 'TSAbstractPropertyDefinition': {
|
|
57
|
+
return ['ArrowFunctionExpression', 'FunctionExpression'].includes(member.value?.type) ? 'method' : 'field'
|
|
58
|
+
}
|
|
59
|
+
case 'MethodDefinition': {
|
|
60
|
+
// Overload signatures can't be decorated, so ranking them would split a decorated implementation from its overloads
|
|
61
|
+
return member.value.type === 'TSEmptyBodyFunctionExpression' ? undefined : member.kind
|
|
62
|
+
}
|
|
63
|
+
case 'TSAbstractMethodDefinition':
|
|
64
|
+
case 'TSMethodSignature': {
|
|
65
|
+
return member.kind
|
|
66
|
+
}
|
|
67
|
+
default: {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const groupsOfMember = (member, type, supportsModifiers) => {
|
|
74
|
+
if (!supportsModifiers || type === 'signature' || type === 'static-initialization') {
|
|
75
|
+
return [type]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const accessibility = member.key.type === 'PrivateIdentifier' ? '#private' : (member.accessibility ?? 'public')
|
|
79
|
+
if (type === 'constructor') {
|
|
80
|
+
return [`${accessibility}-constructor`, 'constructor']
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const scope = member.static ? 'static' : member.type.startsWith('TSAbstract') ? 'abstract' : 'instance'
|
|
84
|
+
const decorated = member.decorators?.length > 0
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
...(decorated ? [`${accessibility}-decorated-${type}`, `decorated-${type}`] : []),
|
|
88
|
+
`${accessibility}-${scope}-${type}`,
|
|
89
|
+
`${scope}-${type}`,
|
|
90
|
+
`${accessibility}-${type}`,
|
|
91
|
+
type,
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rankOf = (member, supportsModifiers) => {
|
|
96
|
+
const type = typeOf(member)
|
|
97
|
+
if (!type) {
|
|
98
|
+
return -1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return rankByGroup.get(groupsOfMember(member, type, supportsModifiers).find((group) => rankByGroup.has(group)))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const hasInitializer = (member) => ['PropertyDefinition', 'AccessorProperty'].includes(member.type) && Boolean(member.value)
|
|
105
|
+
|
|
106
|
+
const initializes = (member) => member.type === 'StaticBlock' || hasInitializer(member)
|
|
107
|
+
|
|
108
|
+
const isStatic = (member) => member.type === 'StaticBlock' || Boolean(member.static)
|
|
109
|
+
|
|
110
|
+
const hasBlockBody = (member) =>
|
|
111
|
+
member.type === 'StaticBlock' || (member.type === 'MethodDefinition' && member.value.type !== 'TSEmptyBodyFunctionExpression')
|
|
112
|
+
|
|
113
|
+
const keylessMemberNames = {
|
|
114
|
+
StaticBlock: 'static block',
|
|
115
|
+
TSIndexSignature: 'index signature',
|
|
116
|
+
TSCallSignatureDeclaration: 'call',
|
|
117
|
+
TSConstructSignatureDeclaration: 'new',
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const disableDirective = /^\s*(?:eslint|oxlint)-disable(?:-next-line|-line)?(?:\s+([^]*?))?\s*(?:--|$)/
|
|
121
|
+
|
|
122
|
+
const disablesOrdering = (comment) => {
|
|
123
|
+
const match = disableDirective.exec(comment.value)
|
|
124
|
+
|
|
125
|
+
return Boolean(match) && (!match[1] || match[1].includes('member-ordering'))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default {
|
|
129
|
+
meta: { name: 'class-conventions' },
|
|
130
|
+
rules: {
|
|
131
|
+
'member-ordering': {
|
|
132
|
+
meta: {
|
|
133
|
+
type: 'suggestion',
|
|
134
|
+
fixable: 'code',
|
|
135
|
+
hasSuggestions: true,
|
|
136
|
+
messages: {
|
|
137
|
+
incorrectGroupOrder: 'Member {{ name }} should be declared before all {{ group }} definitions.',
|
|
138
|
+
sortMembers: 'Sort members by group. This changes the order in which field initializers run.',
|
|
139
|
+
sortExemptMembers: 'Sort members by group. This also moves members exempted by a disable comment.',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
create(context) {
|
|
143
|
+
const { sourceCode } = context
|
|
144
|
+
|
|
145
|
+
const chunkOf = (member, previousEnd) => {
|
|
146
|
+
const leading = sourceCode
|
|
147
|
+
.getCommentsBefore(member)
|
|
148
|
+
.find((comment) => sourceCode.text.slice(previousEnd, comment.range[0]).includes('\n'))
|
|
149
|
+
const trailing = sourceCode
|
|
150
|
+
.getCommentsAfter(member)
|
|
151
|
+
.findLast((comment) => !sourceCode.text.slice(member.range[1], comment.range[0]).includes('\n'))
|
|
152
|
+
|
|
153
|
+
return { start: leading?.range[0] ?? member.range[0], end: trailing?.range[1] ?? member.range[1] }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// A moved field can end up before `[computed]`, `*generator`, `in()` or `instanceof()`, which would continue its initializer expression
|
|
157
|
+
const needsSemicolon = (member, next, gap) => {
|
|
158
|
+
if (hasBlockBody(member) || [',', ';'].includes(sourceCode.getText(member).at(-1))) {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return !gap.includes('\n') || (hasInitializer(member) && /^(?:[*[]|in\b|instanceof\b)/.test(sourceCode.getText(next)))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const sortFixOf = (body, members, supportsModifiers) => {
|
|
166
|
+
const chunks = members.map((member, index) =>
|
|
167
|
+
chunkOf(member, index === 0 ? body.range[0] + 1 : members[index - 1].range[1]),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const units = []
|
|
171
|
+
let first = 0
|
|
172
|
+
for (const [index, member] of members.entries()) {
|
|
173
|
+
const rank = rankOf(member, supportsModifiers)
|
|
174
|
+
if (rank !== -1) {
|
|
175
|
+
units.push({ first, last: index, rank })
|
|
176
|
+
first = index + 1
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
units.at(-1).last = members.length - 1
|
|
181
|
+
|
|
182
|
+
const sorted = units.toSorted((a, b) => a.rank - b.rank)
|
|
183
|
+
const gaps = units
|
|
184
|
+
.slice(1)
|
|
185
|
+
.map((unit, index) => sourceCode.text.slice(chunks[units[index].last].end, chunks[unit.first].start))
|
|
186
|
+
const textOf = (unit, next, gap) => {
|
|
187
|
+
const text = sourceCode.text.slice(chunks[unit.first].start, chunks[unit.last].end)
|
|
188
|
+
if (!next || !needsSemicolon(members[unit.last], members[next.first], gap)) {
|
|
189
|
+
return text
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const semicolonAt = members[unit.last].range[1] - chunks[unit.first].start
|
|
193
|
+
|
|
194
|
+
return `${text.slice(0, semicolonAt)};${text.slice(semicolonAt)}`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const replacement = sorted
|
|
198
|
+
.map((unit, index) => textOf(unit, sorted[index + 1], gaps[index]) + (gaps[index] ?? ''))
|
|
199
|
+
.join('')
|
|
200
|
+
const fix = (fixer) => fixer.replaceTextRange([chunks[units[0].first].start, chunks.at(-1).end], replacement)
|
|
201
|
+
|
|
202
|
+
const initializationOrderOf = (ordered) =>
|
|
203
|
+
ordered
|
|
204
|
+
.flatMap((unit) => members.slice(unit.first, unit.last + 1))
|
|
205
|
+
.filter(initializes)
|
|
206
|
+
.toSorted((a, b) => Number(isStatic(a)) - Number(isStatic(b)))
|
|
207
|
+
const sortedInitializationOrder = initializationOrderOf(sorted)
|
|
208
|
+
const keepsInitializationOrder = initializationOrderOf(units).every(
|
|
209
|
+
(member, index) => member === sortedInitializationOrder[index],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if (!keepsInitializationOrder) {
|
|
213
|
+
return { suggest: [{ messageId: 'sortMembers', fix }] }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Sorting the whole body would override a member someone deliberately exempted from the rule
|
|
217
|
+
if (sourceCode.getCommentsInside(body).some(disablesOrdering)) {
|
|
218
|
+
return { suggest: [{ messageId: 'sortExemptMembers', fix }] }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { fix }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const nameOf = (member) => {
|
|
225
|
+
if (member.type in keylessMemberNames) {
|
|
226
|
+
return keylessMemberNames[member.type]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (member.key.type === 'PrivateIdentifier') {
|
|
230
|
+
return `#${member.key.name}`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return member.key.type === 'Identifier' && !member.computed ? member.key.name : context.sourceCode.getText(member.key)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const checkOrder = (body, members, supportsModifiers) => {
|
|
237
|
+
const ranks = []
|
|
238
|
+
const misplaced = []
|
|
239
|
+
for (const member of members) {
|
|
240
|
+
const rank = rankOf(member, supportsModifiers)
|
|
241
|
+
if (rank === -1) {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (ranks.length === 0 || rank > ranks.at(-1)) {
|
|
246
|
+
ranks.push(rank)
|
|
247
|
+
} else if (rank < ranks.at(-1)) {
|
|
248
|
+
misplaced.push({ member, expected: Math.min(...ranks.filter((previous) => previous > rank)) })
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (misplaced.length === 0) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Every report carries the fix, so it survives when some of them are suppressed by a disable comment
|
|
257
|
+
const sortFix = sortFixOf(body, members, supportsModifiers)
|
|
258
|
+
for (const { member, expected } of misplaced) {
|
|
259
|
+
context.report({
|
|
260
|
+
node: member,
|
|
261
|
+
messageId: 'incorrectGroupOrder',
|
|
262
|
+
data: { name: nameOf(member), group: memberOrder[expected].replaceAll('-', ' ') },
|
|
263
|
+
...sortFix,
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
ClassBody(node) {
|
|
270
|
+
checkOrder(node, node.body, true)
|
|
271
|
+
},
|
|
272
|
+
TSInterfaceBody(node) {
|
|
273
|
+
checkOrder(node, node.body, false)
|
|
274
|
+
},
|
|
275
|
+
TSTypeLiteral(node) {
|
|
276
|
+
checkOrder(node, node.members, false)
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diia-inhouse/oxc-config",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "OXC toolchain configs for Diia services: oxlint, oxfmt, oxc transformer",
|
|
5
5
|
"author": "Diia",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"@boundaries/eslint-plugin": "6.0.2",
|
|
34
34
|
"@diia-inhouse/eslint-plugin": "1.9.13",
|
|
35
35
|
"@stylistic/eslint-plugin": "5.10.0",
|
|
36
|
-
"@typescript-eslint/eslint-plugin": "8.60.1",
|
|
37
36
|
"eslint-import-resolver-oxc": "0.15.0",
|
|
38
37
|
"eslint-plugin-regexp": "3.1.0",
|
|
39
38
|
"eslint-plugin-security": "4.0.0",
|