@diia-inhouse/oxc-config 1.9.2

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.
@@ -0,0 +1,451 @@
1
+ import { createRequire } from 'node:module';
2
+ import { defineConfig } from 'oxlint';
3
+ const require = createRequire(import.meta.url);
4
+ const resolve = (pkg) => require.resolve(pkg).replace(/\/[^/]+$/, '');
5
+ const resolveFile = (path) => new URL(path, import.meta.url).pathname;
6
+ const baseConfig = {
7
+ plugins: ['unicorn', 'typescript', 'import', 'promise', 'node', 'vitest', 'oxc'],
8
+ jsPlugins: [
9
+ resolve('eslint-plugin-security'),
10
+ resolve('eslint-plugin-regexp'),
11
+ { name: '@stylistic/js', specifier: resolve('@stylistic/eslint-plugin') },
12
+ resolve('@diia-inhouse/eslint-plugin'),
13
+ { name: '@diia-inhouse/package', specifier: resolveFile('./plugins/packageCheck.mjs') },
14
+ { name: '@diia-inhouse/locale', specifier: resolveFile('./plugins/noHardcodedCyrillic.mjs') },
15
+ { name: '@diia-inhouse/test', specifier: resolveFile('./plugins/testConventions.mjs') },
16
+ { name: '@diia-inhouse/mongoose', specifier: resolveFile('./plugins/mongooseSchema.mjs') },
17
+ { name: '@diia-inhouse/class', specifier: resolveFile('./plugins/classEncapsulation.mjs') },
18
+ { name: '@diia-inhouse/temporal', specifier: resolveFile('./plugins/temporalConventions.mjs') },
19
+ { name: '@diia-inhouse/code', specifier: resolveFile('./plugins/codeConventions.mjs') },
20
+ ],
21
+ categories: {
22
+ correctness: 'error',
23
+ suspicious: 'warn',
24
+ },
25
+ env: { es6: true, node: true },
26
+ options: { typeAware: true, reportUnusedDisableDirectives: 'error' },
27
+ ignorePatterns: ['*.js', '*.mjs', 'dist', 'node_modules', 'coverage', 'src/generated/**', 'oxlint.config.ts', 'oxfmt.config.ts'],
28
+ rules: {
29
+ 'eslint/no-duplicate-imports': 'error',
30
+ 'eslint/no-redeclare': 'error',
31
+ 'eslint/no-implicit-coercion': 'error',
32
+ 'eslint/no-cond-assign': ['error', 'always'],
33
+ 'eslint/no-console': 'error',
34
+ 'eslint/no-underscore-dangle': ['warn', { allow: ['_id'] }],
35
+ // Projects use vitest; disable duplicate jest plugin diagnostics
36
+ 'jest/valid-title': 'off',
37
+ 'jest/no-conditional-expect': 'off',
38
+ 'jest/no-disabled-tests': 'off',
39
+ // Projects use Class.prototype.method.name as describe titles by convention
40
+ 'vitest/valid-title': 'off',
41
+ 'vitest/no-conditional-expect': 'off',
42
+ 'vitest/no-disabled-tests': 'off',
43
+ 'eslint/curly': ['error', 'all'],
44
+ 'eslint/eqeqeq': ['error', 'always'],
45
+ 'eslint/prefer-template': 'error',
46
+ 'eslint/no-restricted-imports': [
47
+ 'error',
48
+ {
49
+ paths: [
50
+ {
51
+ name: '@diia-inhouse/utils',
52
+ allowTypeImports: true,
53
+ message: 'Direct import of @diia-inhouse/utils is forbidden. Inject via DI and access through this.utils.*',
54
+ },
55
+ {
56
+ name: 'moleculer',
57
+ message: 'Moleculer is legacy. Use gRPC clients and proto definitions for inter-service communication.',
58
+ },
59
+ ],
60
+ },
61
+ ],
62
+ 'eslint/class-methods-use-this': 'off',
63
+ 'eslint/sort-imports': 'off',
64
+ 'eslint/no-invalid-regexp': 'off',
65
+ 'eslint/no-useless-backreference': 'off',
66
+ 'eslint/no-empty-character-class': 'off',
67
+ 'import/prefer-default-export': 'off',
68
+ 'import/named': 'off',
69
+ 'import/no-unassigned-import': ['warn', { allow: ['**/*.css', 'reflect-metadata', 'module-alias/register'] }],
70
+ 'typescript/consistent-type-assertions': ['error', { assertionStyle: 'as' }],
71
+ 'typescript/no-unsafe-type-assertion': 'off',
72
+ 'typescript/no-unused-vars': ['error', { ignoreRestSiblings: true, argsIgnorePattern: '^_' }],
73
+ 'typescript/explicit-function-return-type': 'error',
74
+ 'typescript/return-await': ['error', 'always'],
75
+ 'typescript/array-type': 'error',
76
+ 'typescript/no-inferrable-types': 'error',
77
+ 'typescript/no-floating-promises': 'error',
78
+ 'typescript/no-explicit-any': 'error',
79
+ 'typescript/consistent-return': 'off',
80
+ 'typescript/prefer-optional-chain': 'off',
81
+ 'typescript/no-require-imports': 'off',
82
+ 'typescript/no-empty-object-type': 'off',
83
+ 'typescript/no-useless-default-assignment': 'off',
84
+ 'typescript/no-unnecessary-type-parameters': 'off',
85
+ 'unicorn/custom-error-definition': 'error',
86
+ 'unicorn/no-nested-ternary': 'error',
87
+ 'unicorn/filename-case': ['error', { case: 'camelCase', ignore: ['^\\d+.*\\.ts$'] }],
88
+ 'unicorn/numeric-separators-style': 'off',
89
+ 'unicorn/catch-error-name': ['error', { name: 'err' }],
90
+ 'unicorn/no-null': 'off',
91
+ 'unicorn/no-anonymous-default-export': 'off',
92
+ 'unicorn/prefer-module': 'off',
93
+ 'unicorn/prefer-top-level-await': 'off',
94
+ 'unicorn/no-array-method-this-argument': 'off',
95
+ 'unicorn/no-array-callback-reference': 'off',
96
+ 'unicorn/prefer-spread': 'off',
97
+ 'unicorn/require-post-message-target-origin': 'off',
98
+ 'unicorn/no-array-sort': 'off',
99
+ 'unicorn/no-array-reduce': 'error',
100
+ 'promise/always-return': 'error',
101
+ 'promise/no-return-wrap': 'error',
102
+ 'promise/param-names': 'error',
103
+ 'promise/catch-or-return': 'error',
104
+ 'promise/no-nesting': 'warn',
105
+ 'promise/no-promise-in-callback': 'warn',
106
+ 'promise/no-callback-in-promise': 'warn',
107
+ 'promise/no-new-statics': 'error',
108
+ 'promise/no-return-in-finally': 'warn',
109
+ 'promise/valid-params': 'warn',
110
+ 'regexp/confusing-quantifier': 'warn',
111
+ 'regexp/control-character-escape': 'error',
112
+ 'regexp/match-any': 'error',
113
+ 'regexp/negation': 'error',
114
+ 'regexp/no-contradiction-with-assertion': 'error',
115
+ 'regexp/no-dupe-characters-character-class': 'error',
116
+ 'regexp/no-dupe-disjunctions': 'error',
117
+ 'regexp/no-empty-alternative': 'warn',
118
+ 'regexp/no-empty-capturing-group': 'error',
119
+ 'regexp/no-empty-character-class': 'error',
120
+ 'regexp/no-empty-group': 'error',
121
+ 'regexp/no-empty-lookarounds-assertion': 'error',
122
+ 'regexp/no-empty-string-literal': 'error',
123
+ 'regexp/no-escape-backspace': 'error',
124
+ 'regexp/no-extra-lookaround-assertions': 'error',
125
+ 'regexp/no-invalid-regexp': 'error',
126
+ 'regexp/no-invisible-character': 'error',
127
+ 'regexp/no-lazy-ends': 'warn',
128
+ 'regexp/no-legacy-features': 'error',
129
+ 'regexp/no-misleading-capturing-group': 'error',
130
+ 'regexp/no-misleading-unicode-character': 'error',
131
+ 'regexp/no-missing-g-flag': 'error',
132
+ 'regexp/no-non-standard-flag': 'error',
133
+ 'regexp/no-obscure-range': 'off',
134
+ 'regexp/no-optional-assertion': 'error',
135
+ 'regexp/no-potentially-useless-backreference': 'warn',
136
+ 'regexp/no-super-linear-backtracking': 'error',
137
+ 'regexp/no-trivially-nested-assertion': 'error',
138
+ 'regexp/no-trivially-nested-quantifier': 'error',
139
+ 'regexp/no-unused-capturing-group': 'error',
140
+ 'regexp/no-useless-assertions': 'error',
141
+ 'regexp/no-useless-backreference': 'error',
142
+ 'regexp/no-useless-character-class': 'error',
143
+ 'regexp/no-useless-dollar-replacements': 'error',
144
+ 'regexp/no-useless-escape': 'error',
145
+ 'regexp/no-useless-flag': 'warn',
146
+ 'regexp/no-useless-lazy': 'error',
147
+ 'regexp/no-useless-non-capturing-group': 'error',
148
+ 'regexp/no-useless-quantifier': 'error',
149
+ 'regexp/no-useless-range': 'error',
150
+ 'regexp/no-useless-set-operand': 'error',
151
+ 'regexp/no-useless-string-literal': 'error',
152
+ 'regexp/no-useless-two-nums-quantifier': 'error',
153
+ 'regexp/no-zero-quantifier': 'error',
154
+ 'regexp/optimal-lookaround-quantifier': 'warn',
155
+ 'regexp/optimal-quantifier-concatenation': 'error',
156
+ 'regexp/prefer-character-class': 'error',
157
+ 'regexp/prefer-d': 'error',
158
+ 'regexp/prefer-plus-quantifier': 'error',
159
+ 'regexp/prefer-predefined-assertion': 'error',
160
+ 'regexp/prefer-question-quantifier': 'error',
161
+ 'regexp/prefer-range': 'error',
162
+ 'regexp/prefer-set-operation': 'error',
163
+ 'regexp/prefer-star-quantifier': 'error',
164
+ 'regexp/prefer-unicode-codepoint-escapes': 'error',
165
+ 'regexp/prefer-w': 'error',
166
+ 'regexp/simplify-set-operations': 'error',
167
+ // regexp/sort-flags: not supported by oxlint's JS plugin bridge (uses token APIs)
168
+ 'regexp/strict': 'error',
169
+ 'regexp/use-ignore-case': 'error',
170
+ '@stylistic/js/comma-dangle': 'off',
171
+ '@stylistic/js/space-before-blocks': 'error',
172
+ '@stylistic/js/space-infix-ops': 'error',
173
+ '@stylistic/js/eol-last': ['error', 'always'],
174
+ '@stylistic/js/padding-line-between-statements': [
175
+ 'error',
176
+ { blankLine: 'always', prev: '*', next: 'return' },
177
+ { blankLine: 'always', prev: ['const', 'let'], next: '*' },
178
+ { blankLine: 'any', prev: ['const', 'let'], next: 'block-like' },
179
+ { blankLine: 'any', prev: ['const', 'let'], next: ['const', 'let'] },
180
+ { blankLine: 'always', prev: 'block-like', next: '*' },
181
+ { blankLine: 'never', prev: 'case', next: '*' },
182
+ { blankLine: 'always', prev: '*', next: 'export' },
183
+ ],
184
+ 'security/detect-object-injection': 'off',
185
+ 'security/detect-non-literal-fs-filename': 'error',
186
+ 'security/detect-non-literal-regexp': 'error',
187
+ '@diia-inhouse/logger-err-field': 'error',
188
+ '@diia-inhouse/package/no-service-in-package-name': 'error',
189
+ '@diia-inhouse/package/pinned-dependencies': 'error',
190
+ '@diia-inhouse/locale/no-hardcoded-cyrillic': 'error',
191
+ '@diia-inhouse/code/no-promise-settimeout': 'error',
192
+ '@diia-inhouse/code/no-inline-object-return-type': 'error',
193
+ '@diia-inhouse/code/prefer-as-const-object': 'error',
194
+ '@diia-inhouse/code/const-enum-naming': 'error',
195
+ '@diia-inhouse/code/const-enum-type-name': 'error',
196
+ '@diia-inhouse/mongoose/status-requires-history': 'error',
197
+ // Context-specific rules — off by default, enable per service in oxlint.config.ts
198
+ '@diia-inhouse/mongoose/schema-timestamps': 'off',
199
+ '@diia-inhouse/mongoose/sub-schema-id-false': 'off',
200
+ '@diia-inhouse/class/no-module-level-const': 'warn',
201
+ '@diia-inhouse/class/no-interface-in-implementation': 'warn',
202
+ '@diia-inhouse/temporal/workflow-single-param': 'error',
203
+ '@diia-inhouse/temporal/async-activity': 'error',
204
+ '@diia-inhouse/temporal/no-node-imports': 'error',
205
+ '@diia-inhouse/temporal/no-path-alias-imports': 'error',
206
+ },
207
+ overrides: [
208
+ {
209
+ files: ['**/bin/**'],
210
+ rules: {
211
+ 'security/detect-non-literal-fs-filename': 'off',
212
+ 'eslint/no-console': 'off',
213
+ '@stylistic/js/padding-line-between-statements': 'off',
214
+ },
215
+ },
216
+ {
217
+ files: ['**/*.d.ts'],
218
+ rules: {
219
+ 'import/no-unassigned-import': 'off',
220
+ },
221
+ },
222
+ {
223
+ files: ['**/deps/**', '**/deps.ts', 'src/index.ts'],
224
+ rules: {
225
+ 'eslint/no-restricted-imports': 'off',
226
+ },
227
+ },
228
+ {
229
+ files: ['tests/**'],
230
+ plugins: ['vitest'],
231
+ rules: {
232
+ 'eslint/no-restricted-imports': 'off',
233
+ 'vitest/no-conditional-in-test': 'error',
234
+ 'vitest/no-duplicate-hooks': 'error',
235
+ 'vitest/prefer-hooks-in-order': 'error',
236
+ 'vitest/prefer-hooks-on-top': 'error',
237
+ 'vitest/require-to-throw-message': 'error',
238
+ 'vitest/require-top-level-describe': 'error',
239
+ 'typescript/unbound-method': 'off',
240
+ '@diia-inhouse/test/no-persistent-mock': 'off',
241
+ '@diia-inhouse/test/no-vi-mock-in-workflows': 'error',
242
+ '@diia-inhouse/class/no-interface-in-implementation': 'off',
243
+ },
244
+ },
245
+ ],
246
+ };
247
+ const boundariesExtension = {
248
+ jsPlugins: [{ name: 'boundaries', specifier: resolve('@boundaries/eslint-plugin') }],
249
+ settings: {
250
+ 'import/resolver': { oxc: {} },
251
+ 'boundaries/elements': [
252
+ { type: 'actionsTypes', pattern: 'src/actions/**/*.types.ts', mode: 'file', capture: ['version', 'actionName'] },
253
+ { type: 'actions', pattern: 'src/actions/**/*.ts', mode: 'file', capture: ['version', 'actionName'] },
254
+ { type: 'viewsTypes', pattern: 'src/views/*types.ts', mode: 'file' },
255
+ { type: 'views', pattern: 'src/views/**' },
256
+ { type: 'schemas', pattern: 'src/**/*.schema.ts', mode: 'file' },
257
+ { type: 'providersTypes', pattern: 'src/providers/**/*types.ts', mode: 'file', capture: ['providerName'] },
258
+ { type: 'providers', pattern: 'src/providers/**', capture: ['providerName'] },
259
+ { type: 'repositories', pattern: 'src/repositories/**' },
260
+ { type: 'modelsTypes', pattern: 'src/models/*.types.ts', mode: 'file', capture: ['modelName'] },
261
+ { type: 'models', pattern: 'src/models/*.ts', mode: 'file', capture: ['modelName'] },
262
+ { type: 'servicesTypes', pattern: 'src/services/**/*types.ts', mode: 'file' },
263
+ { type: 'services', pattern: 'src/services/**' },
264
+ { type: 'configsTypes', pattern: 'src/configs/*types.ts', mode: 'file' },
265
+ { type: 'configs', pattern: 'src/configs/**' },
266
+ { type: 'locales', pattern: 'src/locales/**' },
267
+ { type: 'eventListenersTypes', pattern: 'src/eventListeners/*.types.ts', mode: 'file', capture: ['eventName'] },
268
+ { type: 'eventListeners', pattern: 'src/eventListeners/*.ts', mode: 'file', capture: ['eventName'] },
269
+ { type: 'externalEventListenersTypes', pattern: 'src/externalEventListeners/*.types.ts', mode: 'file', capture: ['eventName'] },
270
+ { type: 'externalEventListeners', pattern: 'src/externalEventListeners/*.ts', mode: 'file', capture: ['eventName'] },
271
+ { type: 'workerWorkflowsTypes', pattern: 'src/worker/workflows/**/*.types.ts', mode: 'file' },
272
+ { type: 'workerWorkflows', pattern: 'src/worker/workflows/**' },
273
+ { type: 'workerActivities', pattern: 'src/worker/activities/**' },
274
+ { type: 'workerSchedules', pattern: 'src/worker/schedules/**' },
275
+ { type: 'worker', pattern: 'src/worker/**' },
276
+ { type: 'depsTypes', pattern: 'src/deps/*types.ts', mode: 'file' },
277
+ { type: 'deps', pattern: 'src/deps/**', mode: 'file' },
278
+ { type: 'srcRoot', pattern: 'src/*', mode: 'file' },
279
+ { type: 'tests', pattern: 'tests/**' },
280
+ { type: 'migrations', pattern: 'migrations/**' },
281
+ { type: 'configFiles', pattern: '*.{json,md,mjs,mts,ts}', mode: 'full' },
282
+ { type: 'generated', pattern: 'src/generated/**' },
283
+ ],
284
+ },
285
+ rules: {
286
+ 'boundaries/no-unknown': 'error',
287
+ 'boundaries/no-unknown-files': 'error',
288
+ 'boundaries/entry-point': 'error',
289
+ 'boundaries/dependencies': [
290
+ 'error',
291
+ {
292
+ default: 'disallow',
293
+ rules: [
294
+ {
295
+ from: { type: 'actions' },
296
+ allow: {
297
+ to: [
298
+ { type: 'services' },
299
+ { type: 'servicesTypes' },
300
+ { type: 'views' },
301
+ { type: 'generated' },
302
+ { type: 'schemas' },
303
+ { type: 'modelsTypes' },
304
+ { type: 'actionsTypes', captured: { actionName: '{{ from.captured.actionName }}' } },
305
+ ],
306
+ },
307
+ },
308
+ { from: { type: 'actionsTypes' }, allow: { to: [{ type: 'generated' }] } },
309
+ {
310
+ from: { type: 'services' },
311
+ allow: {
312
+ to: [
313
+ { type: 'services' },
314
+ { type: 'servicesTypes' },
315
+ { type: 'providers' },
316
+ { type: 'providersTypes' },
317
+ { type: 'repositories' },
318
+ { type: 'modelsTypes' },
319
+ { type: 'configsTypes' },
320
+ { type: 'workerWorkflows' },
321
+ { type: 'workerWorkflowsTypes' },
322
+ ],
323
+ },
324
+ },
325
+ {
326
+ from: { type: 'providers' },
327
+ allow: {
328
+ to: [
329
+ { type: 'configsTypes' },
330
+ { type: 'schemas' },
331
+ { type: 'providersTypes', captured: { providerName: '{{ from.captured.providerName }}' } },
332
+ ],
333
+ },
334
+ },
335
+ { from: { type: 'providersTypes' }, allow: { to: [{ type: 'modelsTypes' }] } },
336
+ { from: { type: 'schemas' }, allow: [] },
337
+ {
338
+ from: { type: 'views' },
339
+ allow: { to: [{ type: 'viewsTypes' }, { type: 'servicesTypes' }, { type: 'modelsTypes' }, { type: 'generated' }] },
340
+ },
341
+ { from: { type: 'viewsTypes' }, allow: { to: [{ type: 'locales' }] } },
342
+ {
343
+ from: { type: 'repositories' },
344
+ allow: { to: [{ type: 'models' }, { type: 'configsTypes' }, { type: 'modelsTypes' }] },
345
+ },
346
+ {
347
+ from: { type: 'models' },
348
+ allow: { to: [{ type: 'modelsTypes', captured: { modelName: '{{ from.captured.modelName }}' } }] },
349
+ },
350
+ { from: { type: 'tests' }, allow: { to: [{ type: '*' }] } },
351
+ { from: { type: 'servicesTypes' }, allow: { to: [{ type: 'servicesTypes' }, { type: 'modelsTypes' }] } },
352
+ {
353
+ from: { type: 'srcRoot' },
354
+ allow: {
355
+ to: [
356
+ { type: 'srcRoot' },
357
+ { type: 'depsTypes' },
358
+ { type: 'configsTypes' },
359
+ { type: 'deps' },
360
+ { type: 'configs' },
361
+ { type: 'worker' },
362
+ ],
363
+ },
364
+ },
365
+ {
366
+ from: { type: 'deps' },
367
+ allow: {
368
+ to: [
369
+ { type: 'depsTypes' },
370
+ { type: 'configsTypes' },
371
+ { type: 'models' },
372
+ { type: 'viewsTypes' },
373
+ { type: 'providers' },
374
+ { type: 'services' },
375
+ ],
376
+ },
377
+ },
378
+ {
379
+ from: { type: 'depsTypes' },
380
+ allow: { to: [{ type: 'configsTypes' }, { type: 'viewsTypes' }, { type: 'providers' }] },
381
+ },
382
+ { from: { type: 'configs' }, allow: { to: [{ type: 'configsTypes' }] } },
383
+ { from: { type: 'configsTypes' }, allow: { to: [{ type: 'configs' }] } },
384
+ { from: { type: 'eventListenersTypes' }, allow: { to: [{ type: 'modelsTypes' }] } },
385
+ {
386
+ from: { type: 'eventListeners' },
387
+ allow: {
388
+ to: [
389
+ { type: 'services' },
390
+ { type: 'servicesTypes' },
391
+ { type: 'configsTypes' },
392
+ { type: 'schemas' },
393
+ { type: 'eventListenersTypes', captured: { eventName: '{{ from.captured.eventName }}' } },
394
+ ],
395
+ },
396
+ },
397
+ { from: { type: 'externalEventListenersTypes' }, allow: [] },
398
+ {
399
+ from: { type: 'externalEventListeners' },
400
+ allow: {
401
+ to: [
402
+ { type: 'services' },
403
+ { type: 'configsTypes' },
404
+ { type: 'schemas' },
405
+ { type: 'externalEventListenersTypes', captured: { eventName: '{{ from.captured.eventName }}' } },
406
+ ],
407
+ },
408
+ },
409
+ {
410
+ from: { type: 'worker' },
411
+ allow: { to: [{ type: 'configsTypes' }, { type: 'depsTypes' }, { type: 'workerActivities' }] },
412
+ },
413
+ {
414
+ from: { type: 'workerActivities' },
415
+ allow: {
416
+ to: [
417
+ { type: 'services' },
418
+ { type: 'servicesTypes' },
419
+ { type: 'repositories' },
420
+ { type: 'modelsTypes' },
421
+ { type: 'configsTypes' },
422
+ ],
423
+ },
424
+ },
425
+ { from: { type: 'workerWorkflowsTypes' }, allow: [] },
426
+ {
427
+ from: { type: 'workerWorkflows' },
428
+ allow: {
429
+ to: [
430
+ { type: 'services' },
431
+ { type: 'configsTypes' },
432
+ { type: 'workerActivities' },
433
+ { type: 'workerWorkflowsTypes' },
434
+ ],
435
+ },
436
+ },
437
+ { from: { type: 'workerSchedules' }, allow: { to: [{ type: 'configsTypes' }, { type: 'workerWorkflows' }] } },
438
+ { from: { type: 'migrations' }, allow: { to: [{ type: '*' }] } },
439
+ ],
440
+ },
441
+ ],
442
+ },
443
+ };
444
+ export const base = defineConfig(baseConfig);
445
+ export const boundaries = defineConfig({
446
+ ...baseConfig,
447
+ jsPlugins: [...(baseConfig.jsPlugins ?? []), ...(boundariesExtension.jsPlugins ?? [])],
448
+ settings: { ...boundariesExtension.settings },
449
+ rules: { ...baseConfig.rules, ...boundariesExtension.rules },
450
+ });
451
+ export { defineConfig };
@@ -0,0 +1,108 @@
1
+ export default {
2
+ meta: { name: '@diia-inhouse/oxlint-plugin-class' },
3
+ rules: {
4
+ 'no-module-level-const': {
5
+ meta: {
6
+ type: 'suggestion',
7
+ messages: {
8
+ forbidden:
9
+ 'Module-level "{{ kind }} {{ name }}" should be a private class property. Only types, interfaces, and re-exports belong at module level alongside a class.',
10
+ },
11
+ },
12
+ create(context) {
13
+ return {
14
+ 'Program:exit'(programNode) {
15
+ const hasDefaultClassExport = programNode.body.some(
16
+ (stmt) =>
17
+ stmt.type === 'ExportDefaultDeclaration' &&
18
+ stmt.declaration &&
19
+ stmt.declaration.type === 'ClassDeclaration',
20
+ )
21
+
22
+ if (!hasDefaultClassExport) {
23
+ return
24
+ }
25
+
26
+ for (const stmt of programNode.body) {
27
+ if (stmt.type !== 'VariableDeclaration') {
28
+ continue
29
+ }
30
+
31
+ for (const decl of stmt.declarations) {
32
+ const name = decl.id.type === 'Identifier' ? decl.id.name : '<destructured>'
33
+
34
+ context.report({
35
+ node: decl,
36
+ messageId: 'forbidden',
37
+ data: { kind: stmt.kind, name },
38
+ })
39
+ }
40
+ }
41
+ },
42
+ }
43
+ },
44
+ },
45
+
46
+ 'no-interface-in-implementation': {
47
+ meta: {
48
+ type: 'problem',
49
+ messages: {
50
+ forbidden:
51
+ '{{ kind }} "{{ name }}" must be in a separate file. Types and interfaces must not share a file with class implementation.',
52
+ },
53
+ },
54
+ create(context) {
55
+ return {
56
+ 'Program:exit'(programNode) {
57
+ const hasDefaultClassExport = programNode.body.some(
58
+ (stmt) =>
59
+ stmt.type === 'ExportDefaultDeclaration' &&
60
+ stmt.declaration &&
61
+ stmt.declaration.type === 'ClassDeclaration',
62
+ )
63
+
64
+ if (!hasDefaultClassExport) {
65
+ return
66
+ }
67
+
68
+ for (const stmt of programNode.body) {
69
+ if (stmt.type === 'TSInterfaceDeclaration') {
70
+ context.report({
71
+ node: stmt,
72
+ messageId: 'forbidden',
73
+ data: { kind: 'interface', name: stmt.id.name },
74
+ })
75
+ }
76
+
77
+ if (stmt.type === 'TSTypeAliasDeclaration') {
78
+ context.report({
79
+ node: stmt,
80
+ messageId: 'forbidden',
81
+ data: { kind: 'type', name: stmt.id.name },
82
+ })
83
+ }
84
+
85
+ if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration) {
86
+ if (stmt.declaration.type === 'TSInterfaceDeclaration') {
87
+ context.report({
88
+ node: stmt.declaration,
89
+ messageId: 'forbidden',
90
+ data: { kind: 'interface', name: stmt.declaration.id.name },
91
+ })
92
+ }
93
+
94
+ if (stmt.declaration.type === 'TSTypeAliasDeclaration') {
95
+ context.report({
96
+ node: stmt.declaration,
97
+ messageId: 'forbidden',
98
+ data: { kind: 'type', name: stmt.declaration.id.name },
99
+ })
100
+ }
101
+ }
102
+ }
103
+ },
104
+ }
105
+ },
106
+ },
107
+ },
108
+ }