@noctcore/eslint-plugin-architecture 0.1.0 → 0.3.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.
- package/README.md +9 -2
- package/dist/index.cjs +1024 -30
- package/dist/index.d.cts +77 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +1024 -30
- package/docs/rules/barrel-purity.md +57 -0
- package/docs/rules/colocated-test-required.md +48 -0
- package/docs/rules/filename-matches-export.md +56 -0
- package/docs/rules/max-import-depth.md +52 -0
- package/docs/rules/single-semantic-module.md +180 -0
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -38,13 +38,26 @@ module.exports = __toCommonJS(index_exports);
|
|
|
38
38
|
|
|
39
39
|
// src/configs/recommended.ts
|
|
40
40
|
var recommended = {
|
|
41
|
+
"noctcore-architecture/barrel-purity": "error",
|
|
42
|
+
// Ships OFF: this rule does nothing until you name the source globs that must
|
|
43
|
+
// be tested (there is no universal "everything needs a test" default). Enable
|
|
44
|
+
// it with your own `include`, e.g.
|
|
45
|
+
// 'noctcore-architecture/colocated-test-required': ['error', { include: ['**/use*.ts', '**/*.service.ts'] }]
|
|
46
|
+
"noctcore-architecture/colocated-test-required": "off",
|
|
41
47
|
"noctcore-architecture/component-folder-structure": "error",
|
|
48
|
+
"noctcore-architecture/filename-matches-export": "error",
|
|
42
49
|
"noctcore-architecture/index-must-reexport-default": "error",
|
|
43
|
-
"noctcore-architecture/
|
|
50
|
+
"noctcore-architecture/max-import-depth": "error",
|
|
51
|
+
"noctcore-architecture/no-cross-feature-imports": "error",
|
|
52
|
+
// Ships OFF: which files it governs and which category mixes they may keep
|
|
53
|
+
// (a NestJS `.constants.ts` legitimately holds constants, types and enums) is
|
|
54
|
+
// a per-codebase decision best made from measured counts. Enable it with
|
|
55
|
+
// 'noctcore-architecture/single-semantic-module': ['error', { allow: [['constant', 'type', 'enum']] }]
|
|
56
|
+
"noctcore-architecture/single-semantic-module": "off"
|
|
44
57
|
};
|
|
45
58
|
|
|
46
|
-
// src/rules/
|
|
47
|
-
var
|
|
59
|
+
// src/rules/barrel-purity.ts
|
|
60
|
+
var import_utils = require("@typescript-eslint/utils");
|
|
48
61
|
|
|
49
62
|
// src/createRule.ts
|
|
50
63
|
var import_eslint_utils = require("@noctcore/eslint-utils");
|
|
@@ -136,8 +149,154 @@ function readDirSafe(dir) {
|
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
// src/rules/barrel-purity.ts
|
|
153
|
+
var RULE_NAME = "barrel-purity";
|
|
154
|
+
var DEFAULT_ALLOW = [];
|
|
155
|
+
var BARREL_BASENAME = /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
|
|
156
|
+
var optionSchema = {
|
|
157
|
+
type: "object",
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
properties: {
|
|
160
|
+
allow: { type: "array", items: { type: "string" }, uniqueItems: true }
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
function impurityOf(stmt) {
|
|
164
|
+
switch (stmt.type) {
|
|
165
|
+
case import_utils.AST_NODE_TYPES.ImportDeclaration:
|
|
166
|
+
return stmt.specifiers.length === 0 ? "a side-effect import" : null;
|
|
167
|
+
case import_utils.AST_NODE_TYPES.ExportAllDeclaration:
|
|
168
|
+
return null;
|
|
169
|
+
case import_utils.AST_NODE_TYPES.ExportNamedDeclaration:
|
|
170
|
+
if (stmt.source !== null) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (stmt.declaration === null) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return "a local declaration";
|
|
177
|
+
case import_utils.AST_NODE_TYPES.ExportDefaultDeclaration:
|
|
178
|
+
return stmt.declaration.type === import_utils.AST_NODE_TYPES.Identifier ? null : "a default-exported value";
|
|
179
|
+
case import_utils.AST_NODE_TYPES.ExpressionStatement:
|
|
180
|
+
return "a side-effect statement";
|
|
181
|
+
default:
|
|
182
|
+
return "non-re-export code";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
var barrelPurityRule = createRule({
|
|
186
|
+
name: RULE_NAME,
|
|
187
|
+
meta: {
|
|
188
|
+
type: "problem",
|
|
189
|
+
docs: {
|
|
190
|
+
description: "A barrel (`index.ts` / `index.tsx`) must contain only re-exports \u2014 never local declarations, side effects, or default-exported values."
|
|
191
|
+
},
|
|
192
|
+
schema: [optionSchema],
|
|
193
|
+
messages: {
|
|
194
|
+
impureBarrel: "A barrel must contain only re-exports; found {{kind}}. Move it into a sibling module and re-export it from here."
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
defaultOptions: [{ allow: [] }],
|
|
198
|
+
create(context, [options]) {
|
|
199
|
+
const allow = options.allow ?? DEFAULT_ALLOW;
|
|
200
|
+
const filename = context.filename;
|
|
201
|
+
if (!BARREL_BASENAME.test(getBasename(filename)) || isIgnoredPath(filename, allow)) {
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
Program(node) {
|
|
206
|
+
for (const stmt of node.body) {
|
|
207
|
+
const kind = impurityOf(stmt);
|
|
208
|
+
if (kind !== null) {
|
|
209
|
+
context.report({ node: stmt, messageId: "impureBarrel", data: { kind } });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// src/rules/colocated-test-required.ts
|
|
218
|
+
var import_node_fs2 = require("fs");
|
|
219
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
220
|
+
var RULE_NAME2 = "colocated-test-required";
|
|
221
|
+
var DEFAULT_INCLUDE = [];
|
|
222
|
+
var DEFAULT_IGNORE = [];
|
|
223
|
+
var TEST_SIBLING = /\.(test|spec)\.[^.]+$/;
|
|
224
|
+
var optionSchema2 = {
|
|
225
|
+
type: "object",
|
|
226
|
+
additionalProperties: false,
|
|
227
|
+
properties: {
|
|
228
|
+
include: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
229
|
+
ignore: { type: "array", items: { type: "string" }, uniqueItems: true }
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var dirCache = /* @__PURE__ */ new Map();
|
|
233
|
+
function readDirCached(dir) {
|
|
234
|
+
const cached = dirCache.get(dir);
|
|
235
|
+
if (cached !== void 0) {
|
|
236
|
+
return cached;
|
|
237
|
+
}
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = (0, import_node_fs2.readdirSync)(dir);
|
|
241
|
+
} catch {
|
|
242
|
+
entries = [];
|
|
243
|
+
}
|
|
244
|
+
dirCache.set(dir, entries);
|
|
245
|
+
return entries;
|
|
246
|
+
}
|
|
247
|
+
function stemOf(basename) {
|
|
248
|
+
const ext = import_node_path2.default.extname(basename);
|
|
249
|
+
return ext === "" ? basename : basename.slice(0, -ext.length);
|
|
250
|
+
}
|
|
251
|
+
function hasColocatedTest(dir, stem) {
|
|
252
|
+
const prefix = `${stem}.`;
|
|
253
|
+
return readDirCached(dir).some(
|
|
254
|
+
(entry) => entry.startsWith(prefix) && TEST_SIBLING.test(entry)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
var colocatedTestRequiredRule = createRule({
|
|
258
|
+
name: RULE_NAME2,
|
|
259
|
+
meta: {
|
|
260
|
+
type: "problem",
|
|
261
|
+
docs: {
|
|
262
|
+
description: "A source file matching an `include` glob must have a colocated `*.test.*` / `*.spec.*` sibling on disk. Off until `include` is configured."
|
|
263
|
+
},
|
|
264
|
+
schema: [optionSchema2],
|
|
265
|
+
messages: {
|
|
266
|
+
missingTest: "Source file `{{basename}}` has no colocated test. Add a sibling `{{stem}}.test.*` (or `.spec.*`) next to it."
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
defaultOptions: [{ include: [], ignore: [] }],
|
|
270
|
+
create(context, [options]) {
|
|
271
|
+
const include = options.include ?? DEFAULT_INCLUDE;
|
|
272
|
+
const ignore = options.ignore ?? DEFAULT_IGNORE;
|
|
273
|
+
const filename = context.filename;
|
|
274
|
+
if (include.length === 0 || !isIgnoredPath(filename, include)) {
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
277
|
+
const basename = getBasename(filename);
|
|
278
|
+
if (TEST_SIBLING.test(basename) || isIgnoredPath(filename, ignore)) {
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
const stem = stemOf(basename);
|
|
282
|
+
const dir = import_node_path2.default.dirname(filename);
|
|
283
|
+
return {
|
|
284
|
+
Program(node) {
|
|
285
|
+
if (!hasColocatedTest(dir, stem)) {
|
|
286
|
+
context.report({
|
|
287
|
+
node,
|
|
288
|
+
messageId: "missingTest",
|
|
289
|
+
data: { basename, stem }
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
139
297
|
// src/rules/component-folder-structure.ts
|
|
140
|
-
var
|
|
298
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
299
|
+
var RULE_NAME3 = "component-folder-structure";
|
|
141
300
|
var DEFAULT_COMPONENT_ROOT = "components";
|
|
142
301
|
var DEFAULT_IGNORE_PATHS = ["**/ui/**"];
|
|
143
302
|
var DEFAULT_REQUIRED_SIBLINGS = [
|
|
@@ -150,7 +309,7 @@ var DEFAULT_REQUIRED_SIBLINGS = [
|
|
|
150
309
|
function resolveSibling(template, name) {
|
|
151
310
|
return template.startsWith(".") ? `${name}${template}` : template;
|
|
152
311
|
}
|
|
153
|
-
var
|
|
312
|
+
var optionSchema3 = {
|
|
154
313
|
type: "object",
|
|
155
314
|
additionalProperties: false,
|
|
156
315
|
properties: {
|
|
@@ -160,13 +319,13 @@ var optionSchema = {
|
|
|
160
319
|
}
|
|
161
320
|
};
|
|
162
321
|
var componentFolderStructureRule = createRule({
|
|
163
|
-
name:
|
|
322
|
+
name: RULE_NAME3,
|
|
164
323
|
meta: {
|
|
165
324
|
type: "problem",
|
|
166
325
|
docs: {
|
|
167
326
|
description: "A component `<Name>/<Name>.tsx` under `<componentRoot>/<feature>/...` must have its sibling set (`.hooks.ts`, `.types.ts`, `.stories.tsx`, `.test.tsx`, `index.ts`) present on disk."
|
|
168
327
|
},
|
|
169
|
-
schema: [
|
|
328
|
+
schema: [optionSchema3],
|
|
170
329
|
messages: {
|
|
171
330
|
missingSiblings: "Component `{{name}}` is missing sibling file(s): {{missing}}. Every component folder must carry its hooks, types, stories, test, and index barrel."
|
|
172
331
|
}
|
|
@@ -190,7 +349,7 @@ var componentFolderStructureRule = createRule({
|
|
|
190
349
|
return {};
|
|
191
350
|
}
|
|
192
351
|
const name = getComponentName(filename);
|
|
193
|
-
const dir =
|
|
352
|
+
const dir = import_node_path3.default.dirname(filename);
|
|
194
353
|
const required = siblingTemplates.map((template) => resolveSibling(template, name));
|
|
195
354
|
const present = readDirSafe(dir);
|
|
196
355
|
const missing = required.filter((sibling) => !present.has(sibling));
|
|
@@ -208,12 +367,164 @@ var componentFolderStructureRule = createRule({
|
|
|
208
367
|
}
|
|
209
368
|
});
|
|
210
369
|
|
|
370
|
+
// src/rules/filename-matches-export.ts
|
|
371
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
372
|
+
var import_utils5 = require("@typescript-eslint/utils");
|
|
373
|
+
var RULE_NAME4 = "filename-matches-export";
|
|
374
|
+
var DEFAULT_IGNORE2 = [];
|
|
375
|
+
var VALID_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
376
|
+
var optionSchema4 = {
|
|
377
|
+
type: "object",
|
|
378
|
+
additionalProperties: false,
|
|
379
|
+
properties: {
|
|
380
|
+
ignore: { type: "array", items: { type: "string" }, uniqueItems: true }
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
function stemOf2(filename) {
|
|
384
|
+
const basename = getBasename(filename);
|
|
385
|
+
const ext = import_node_path4.default.extname(basename);
|
|
386
|
+
return ext === "" ? basename : basename.slice(0, -ext.length);
|
|
387
|
+
}
|
|
388
|
+
function normalize(value) {
|
|
389
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
390
|
+
}
|
|
391
|
+
function namedExportId(decl) {
|
|
392
|
+
if (decl.declaration === null) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
const d = decl.declaration;
|
|
396
|
+
switch (d.type) {
|
|
397
|
+
case import_utils5.AST_NODE_TYPES.FunctionDeclaration:
|
|
398
|
+
case import_utils5.AST_NODE_TYPES.ClassDeclaration:
|
|
399
|
+
return d.id;
|
|
400
|
+
case import_utils5.AST_NODE_TYPES.TSTypeAliasDeclaration:
|
|
401
|
+
case import_utils5.AST_NODE_TYPES.TSInterfaceDeclaration:
|
|
402
|
+
case import_utils5.AST_NODE_TYPES.TSEnumDeclaration:
|
|
403
|
+
return d.id;
|
|
404
|
+
case import_utils5.AST_NODE_TYPES.VariableDeclaration: {
|
|
405
|
+
const only = d.declarations.length === 1 ? d.declarations[0] : void 0;
|
|
406
|
+
return only !== void 0 && only.id.type === import_utils5.AST_NODE_TYPES.Identifier ? only.id : null;
|
|
407
|
+
}
|
|
408
|
+
default:
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function defaultExportId(decl) {
|
|
413
|
+
const d = decl.declaration;
|
|
414
|
+
if ((d.type === import_utils5.AST_NODE_TYPES.FunctionDeclaration || d.type === import_utils5.AST_NODE_TYPES.ClassDeclaration) && d.id !== null) {
|
|
415
|
+
return d.id;
|
|
416
|
+
}
|
|
417
|
+
return d.type === import_utils5.AST_NODE_TYPES.Identifier ? d : null;
|
|
418
|
+
}
|
|
419
|
+
function resolvePrimary(body) {
|
|
420
|
+
let hasDefault = false;
|
|
421
|
+
let defaultId = null;
|
|
422
|
+
const named = [];
|
|
423
|
+
for (const stmt of body) {
|
|
424
|
+
if (stmt.type === import_utils5.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
425
|
+
hasDefault = true;
|
|
426
|
+
defaultId = defaultExportId(stmt);
|
|
427
|
+
} else if (stmt.type === import_utils5.AST_NODE_TYPES.ExportNamedDeclaration && stmt.source === null) {
|
|
428
|
+
const id = namedExportId(stmt);
|
|
429
|
+
if (id !== null) {
|
|
430
|
+
named.push(id);
|
|
431
|
+
} else if (stmt.declaration === null) {
|
|
432
|
+
for (const spec of stmt.specifiers) {
|
|
433
|
+
if (spec.exported.type === import_utils5.AST_NODE_TYPES.Identifier) {
|
|
434
|
+
named.push(spec.exported);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (hasDefault) {
|
|
441
|
+
return defaultId === null ? null : { name: defaultId.name, node: defaultId };
|
|
442
|
+
}
|
|
443
|
+
const only = named.length === 1 ? named[0] : void 0;
|
|
444
|
+
return only !== void 0 ? { name: only.name, node: only } : null;
|
|
445
|
+
}
|
|
446
|
+
function moduleVariable(sourceCode, name) {
|
|
447
|
+
const globalScope = sourceCode.scopeManager?.globalScope ?? null;
|
|
448
|
+
if (globalScope === null) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
const moduleScope = globalScope.childScopes[0] ?? globalScope;
|
|
452
|
+
return moduleScope.variables.find((v) => v.name === name) ?? null;
|
|
453
|
+
}
|
|
454
|
+
var IMPORT_DEF_NODES = /* @__PURE__ */ new Set([
|
|
455
|
+
import_utils5.AST_NODE_TYPES.ImportSpecifier,
|
|
456
|
+
import_utils5.AST_NODE_TYPES.ImportDefaultSpecifier,
|
|
457
|
+
import_utils5.AST_NODE_TYPES.ImportNamespaceSpecifier
|
|
458
|
+
]);
|
|
459
|
+
function isImportBinding(variable) {
|
|
460
|
+
return variable.defs.some((def) => IMPORT_DEF_NODES.has(def.node.type));
|
|
461
|
+
}
|
|
462
|
+
var filenameMatchesExportRule = createRule({
|
|
463
|
+
name: RULE_NAME4,
|
|
464
|
+
meta: {
|
|
465
|
+
type: "suggestion",
|
|
466
|
+
hasSuggestions: true,
|
|
467
|
+
docs: {
|
|
468
|
+
description: "A file's basename must match its primary export (a default export, or the sole named export)."
|
|
469
|
+
},
|
|
470
|
+
schema: [optionSchema4],
|
|
471
|
+
messages: {
|
|
472
|
+
filenameMismatch: "File `{{basename}}` exports `{{name}}` as its primary export \u2014 the basename should match it (rename the file to `{{expected}}`, or the export).",
|
|
473
|
+
renameExport: "Rename the export to `{{expected}}` to match the filename."
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
defaultOptions: [{ ignore: [] }],
|
|
477
|
+
create(context, [options]) {
|
|
478
|
+
const ignore = options.ignore ?? DEFAULT_IGNORE2;
|
|
479
|
+
const filename = context.filename;
|
|
480
|
+
const stem = stemOf2(filename);
|
|
481
|
+
if (stem === "index" || isIgnoredPath(filename, ignore)) {
|
|
482
|
+
return {};
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
Program(node) {
|
|
486
|
+
const primary = resolvePrimary(node.body);
|
|
487
|
+
if (primary === null || normalize(primary.name) === normalize(stem)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const basename = getBasename(filename);
|
|
491
|
+
const canRename = VALID_IDENTIFIER.test(stem) && stem !== primary.name;
|
|
492
|
+
context.report({
|
|
493
|
+
node: primary.node,
|
|
494
|
+
messageId: "filenameMismatch",
|
|
495
|
+
data: { basename, name: primary.name, expected: stem },
|
|
496
|
+
suggest: canRename ? [
|
|
497
|
+
{
|
|
498
|
+
messageId: "renameExport",
|
|
499
|
+
data: { expected: stem },
|
|
500
|
+
fix: (fixer) => {
|
|
501
|
+
const variable = moduleVariable(context.sourceCode, primary.name);
|
|
502
|
+
if (variable !== null && !isImportBinding(variable)) {
|
|
503
|
+
const targets = /* @__PURE__ */ new Map();
|
|
504
|
+
for (const id of variable.identifiers) {
|
|
505
|
+
targets.set(id.range[0], id);
|
|
506
|
+
}
|
|
507
|
+
for (const ref of variable.references) {
|
|
508
|
+
targets.set(ref.identifier.range[0], ref.identifier);
|
|
509
|
+
}
|
|
510
|
+
return [...targets.values()].map((id) => fixer.replaceText(id, stem));
|
|
511
|
+
}
|
|
512
|
+
return [fixer.replaceText(primary.node, stem)];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
] : void 0
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
211
522
|
// src/rules/index-must-reexport-default.ts
|
|
212
|
-
var
|
|
213
|
-
var
|
|
214
|
-
var
|
|
523
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
524
|
+
var import_utils7 = require("@typescript-eslint/utils");
|
|
525
|
+
var RULE_NAME5 = "index-must-reexport-default";
|
|
215
526
|
var DEFAULT_IGNORE_PATHS2 = [];
|
|
216
|
-
var
|
|
527
|
+
var optionSchema5 = {
|
|
217
528
|
type: "object",
|
|
218
529
|
additionalProperties: false,
|
|
219
530
|
properties: {
|
|
@@ -225,17 +536,17 @@ function reexportsDefault(node) {
|
|
|
225
536
|
return false;
|
|
226
537
|
}
|
|
227
538
|
return node.specifiers.some(
|
|
228
|
-
(specifier) => specifier.local.type ===
|
|
539
|
+
(specifier) => specifier.local.type === import_utils7.AST_NODE_TYPES.Identifier && specifier.local.name === "default"
|
|
229
540
|
);
|
|
230
541
|
}
|
|
231
542
|
var indexMustReexportDefaultRule = createRule({
|
|
232
|
-
name:
|
|
543
|
+
name: RULE_NAME5,
|
|
233
544
|
meta: {
|
|
234
545
|
type: "problem",
|
|
235
546
|
docs: {
|
|
236
547
|
description: "A component folder's `index.ts` must re-export the component default (`export { default as <Name> } from './<Name>'`)."
|
|
237
548
|
},
|
|
238
|
-
schema: [
|
|
549
|
+
schema: [optionSchema5],
|
|
239
550
|
messages: {
|
|
240
551
|
missingDefaultReexport: "`index.ts` must re-export the {{name}} default: `export { default as {{name}} } from './{{name}}'`."
|
|
241
552
|
}
|
|
@@ -247,8 +558,8 @@ var indexMustReexportDefaultRule = createRule({
|
|
|
247
558
|
if (getBasename(filename) !== "index.ts" || isIgnoredPath(filename, ignorePaths)) {
|
|
248
559
|
return {};
|
|
249
560
|
}
|
|
250
|
-
const dir =
|
|
251
|
-
const folderName =
|
|
561
|
+
const dir = import_node_path5.default.dirname(filename);
|
|
562
|
+
const folderName = import_node_path5.default.basename(dir);
|
|
252
563
|
if (!isPascalCase(folderName) || !siblingExists(dir, `${folderName}.tsx`)) {
|
|
253
564
|
return {};
|
|
254
565
|
}
|
|
@@ -272,14 +583,120 @@ var indexMustReexportDefaultRule = createRule({
|
|
|
272
583
|
}
|
|
273
584
|
});
|
|
274
585
|
|
|
586
|
+
// src/rules/max-import-depth.ts
|
|
587
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
588
|
+
var import_utils9 = require("@typescript-eslint/utils");
|
|
589
|
+
var RULE_NAME6 = "max-import-depth";
|
|
590
|
+
var DEFAULT_MAX = 3;
|
|
591
|
+
var optionSchema6 = {
|
|
592
|
+
type: "object",
|
|
593
|
+
additionalProperties: false,
|
|
594
|
+
properties: {
|
|
595
|
+
max: { type: "integer", minimum: 0 },
|
|
596
|
+
alias: {
|
|
597
|
+
type: "object",
|
|
598
|
+
additionalProperties: { type: "string" }
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
function climbDepth(source) {
|
|
603
|
+
if (!source.startsWith(".")) {
|
|
604
|
+
return 0;
|
|
605
|
+
}
|
|
606
|
+
let depth = 0;
|
|
607
|
+
for (const segment of source.split("/")) {
|
|
608
|
+
if (segment === "..") {
|
|
609
|
+
depth += 1;
|
|
610
|
+
} else if (segment === ".") {
|
|
611
|
+
continue;
|
|
612
|
+
} else {
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return depth;
|
|
617
|
+
}
|
|
618
|
+
function aliasRewrite(source, currentFile, alias) {
|
|
619
|
+
const resolved = toPosix(import_node_path6.default.resolve(import_node_path6.default.dirname(currentFile), source));
|
|
620
|
+
for (const [anchor, prefix] of Object.entries(alias)) {
|
|
621
|
+
const marker = `/${anchor}/`;
|
|
622
|
+
const idx = resolved.lastIndexOf(marker);
|
|
623
|
+
if (idx === -1) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const rest = resolved.slice(idx + marker.length);
|
|
627
|
+
if (rest.length === 0) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
return `${prefix}/${rest}`;
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
var maxImportDepthRule = createRule({
|
|
635
|
+
name: RULE_NAME6,
|
|
636
|
+
meta: {
|
|
637
|
+
type: "suggestion",
|
|
638
|
+
fixable: "code",
|
|
639
|
+
docs: {
|
|
640
|
+
description: "A relative import may not climb more than `max` parent levels (default 3). Autofixed to a path alias when one is configured."
|
|
641
|
+
},
|
|
642
|
+
schema: [optionSchema6],
|
|
643
|
+
messages: {
|
|
644
|
+
tooDeep: "Relative import `{{source}}` climbs {{depth}} levels \u2014 over the limit of {{max}}. Use a path alias instead of reaching this far up the tree."
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
defaultOptions: [{ max: DEFAULT_MAX, alias: {} }],
|
|
648
|
+
create(context, [options]) {
|
|
649
|
+
const max = options.max ?? DEFAULT_MAX;
|
|
650
|
+
const alias = options.alias ?? {};
|
|
651
|
+
const filename = context.filename;
|
|
652
|
+
function check(sourceNode) {
|
|
653
|
+
if (sourceNode === null || sourceNode === void 0 || typeof sourceNode.value !== "string") {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const source = sourceNode.value;
|
|
657
|
+
const depth = climbDepth(source);
|
|
658
|
+
if (depth <= max) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const rewrite = aliasRewrite(source, filename, alias);
|
|
662
|
+
context.report({
|
|
663
|
+
node: sourceNode,
|
|
664
|
+
messageId: "tooDeep",
|
|
665
|
+
data: { source, depth, max },
|
|
666
|
+
fix: rewrite === null ? void 0 : (fixer) => {
|
|
667
|
+
const quote = sourceNode.raw.charAt(0);
|
|
668
|
+
return fixer.replaceText(sourceNode, `${quote}${rewrite}${quote}`);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function literalSource(node) {
|
|
673
|
+
return node !== null && node !== void 0 && node.type === import_utils9.AST_NODE_TYPES.Literal ? node : null;
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
ImportDeclaration(node) {
|
|
677
|
+
check(node.source);
|
|
678
|
+
},
|
|
679
|
+
ImportExpression(node) {
|
|
680
|
+
check(literalSource(node.source));
|
|
681
|
+
},
|
|
682
|
+
ExportNamedDeclaration(node) {
|
|
683
|
+
check(literalSource(node.source));
|
|
684
|
+
},
|
|
685
|
+
ExportAllDeclaration(node) {
|
|
686
|
+
check(literalSource(node.source));
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
275
692
|
// src/rules/no-cross-feature-imports.ts
|
|
276
|
-
var
|
|
277
|
-
var
|
|
278
|
-
var
|
|
693
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
694
|
+
var import_utils11 = require("@typescript-eslint/utils");
|
|
695
|
+
var RULE_NAME7 = "no-cross-feature-imports";
|
|
279
696
|
var DEFAULT_FEATURE_ROOT = "components";
|
|
280
697
|
var DEFAULT_ALIAS = "@/components";
|
|
281
698
|
var DEFAULT_SHARED_FEATURES = ["ui"];
|
|
282
|
-
var
|
|
699
|
+
var optionSchema7 = {
|
|
283
700
|
type: "object",
|
|
284
701
|
additionalProperties: false,
|
|
285
702
|
properties: {
|
|
@@ -301,19 +718,19 @@ function resolveTargetFeature(source, currentFile, aliasRe, featureRoot) {
|
|
|
301
718
|
return aliasMatch[1] ?? null;
|
|
302
719
|
}
|
|
303
720
|
if (source.startsWith(".")) {
|
|
304
|
-
const resolved =
|
|
721
|
+
const resolved = import_node_path7.default.resolve(import_node_path7.default.dirname(currentFile), source);
|
|
305
722
|
return getFeatureName(resolved, featureRoot);
|
|
306
723
|
}
|
|
307
724
|
return null;
|
|
308
725
|
}
|
|
309
726
|
var noCrossFeatureImportsRule = createRule({
|
|
310
|
-
name:
|
|
727
|
+
name: RULE_NAME7,
|
|
311
728
|
meta: {
|
|
312
729
|
type: "problem",
|
|
313
730
|
docs: {
|
|
314
731
|
description: "A file in one feature may not import runtime code from another feature. Move shared code to a shared module or a shared feature."
|
|
315
732
|
},
|
|
316
|
-
schema: [
|
|
733
|
+
schema: [optionSchema7],
|
|
317
734
|
messages: {
|
|
318
735
|
crossFeatureImport: "Cross-feature import: `{{current}}` may not import runtime code from `{{root}}/{{target}}`. Move shared code to a shared module or a shared feature (e.g. `{{root}}/ui`)."
|
|
319
736
|
}
|
|
@@ -356,25 +773,25 @@ var noCrossFeatureImportsRule = createRule({
|
|
|
356
773
|
}
|
|
357
774
|
return {
|
|
358
775
|
ImportDeclaration(node) {
|
|
359
|
-
if (node.source.type ===
|
|
776
|
+
if (node.source.type === import_utils11.AST_NODE_TYPES.Literal) {
|
|
360
777
|
checkSource(node.source, node.importKind === "type");
|
|
361
778
|
}
|
|
362
779
|
},
|
|
363
780
|
// Dynamic `import()` is runtime by nature — never type-only.
|
|
364
781
|
ImportExpression(node) {
|
|
365
|
-
if (node.source.type ===
|
|
782
|
+
if (node.source.type === import_utils11.AST_NODE_TYPES.Literal) {
|
|
366
783
|
checkSource(node.source, false);
|
|
367
784
|
}
|
|
368
785
|
},
|
|
369
786
|
// `export { x } from '…'` re-export laundering.
|
|
370
787
|
ExportNamedDeclaration(node) {
|
|
371
|
-
if (node.source !== null && node.source.type ===
|
|
788
|
+
if (node.source !== null && node.source.type === import_utils11.AST_NODE_TYPES.Literal) {
|
|
372
789
|
checkSource(node.source, node.exportKind === "type");
|
|
373
790
|
}
|
|
374
791
|
},
|
|
375
792
|
// `export * from '…'` re-export laundering.
|
|
376
793
|
ExportAllDeclaration(node) {
|
|
377
|
-
if (node.source.type ===
|
|
794
|
+
if (node.source.type === import_utils11.AST_NODE_TYPES.Literal) {
|
|
378
795
|
checkSource(node.source, node.exportKind === "type");
|
|
379
796
|
}
|
|
380
797
|
}
|
|
@@ -382,16 +799,593 @@ var noCrossFeatureImportsRule = createRule({
|
|
|
382
799
|
}
|
|
383
800
|
});
|
|
384
801
|
|
|
802
|
+
// src/semantic-module/classify.ts
|
|
803
|
+
var import_utils15 = require("@typescript-eslint/utils");
|
|
804
|
+
|
|
805
|
+
// src/semantic-module/ast.ts
|
|
806
|
+
var import_utils13 = require("@typescript-eslint/utils");
|
|
807
|
+
function getDeclarationName(node) {
|
|
808
|
+
if ("id" in node) {
|
|
809
|
+
const id = node.id;
|
|
810
|
+
if (isIdentifier(id)) {
|
|
811
|
+
return id.name;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return void 0;
|
|
815
|
+
}
|
|
816
|
+
function getVariableDeclaratorName(declarator) {
|
|
817
|
+
return declarator.id.type === import_utils13.AST_NODE_TYPES.Identifier ? declarator.id.name : void 0;
|
|
818
|
+
}
|
|
819
|
+
function isWrapperExpression(expression) {
|
|
820
|
+
return expression.type === import_utils13.AST_NODE_TYPES.TSAsExpression || expression.type === import_utils13.AST_NODE_TYPES.TSTypeAssertion || expression.type === import_utils13.AST_NODE_TYPES.TSNonNullExpression || expression.type === import_utils13.AST_NODE_TYPES.TSSatisfiesExpression || expression.type === import_utils13.AST_NODE_TYPES.TSInstantiationExpression;
|
|
821
|
+
}
|
|
822
|
+
function unwrapExpression(expression) {
|
|
823
|
+
let current = expression;
|
|
824
|
+
while (isWrapperExpression(current)) {
|
|
825
|
+
current = current.expression;
|
|
826
|
+
}
|
|
827
|
+
return current;
|
|
828
|
+
}
|
|
829
|
+
function isAmbientDeclaration(node) {
|
|
830
|
+
if ("declare" in node && node.declare === true) {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
return node.type === import_utils13.AST_NODE_TYPES.TSModuleDeclaration && node.kind === "global";
|
|
834
|
+
}
|
|
835
|
+
function functionReturnsJsx(node) {
|
|
836
|
+
if (node.type === import_utils13.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
837
|
+
if (!node.expression && node.body.type === import_utils13.AST_NODE_TYPES.BlockStatement) {
|
|
838
|
+
return blockReturnsJsx(node.body);
|
|
839
|
+
}
|
|
840
|
+
return containsJsx(node.body);
|
|
841
|
+
}
|
|
842
|
+
return blockReturnsJsx(node.body);
|
|
843
|
+
}
|
|
844
|
+
function blockReturnsJsx(block) {
|
|
845
|
+
return containsNode(block, (node) => {
|
|
846
|
+
if (node.type !== import_utils13.AST_NODE_TYPES.ReturnStatement || !node.argument) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
return containsJsx(node.argument);
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
function containsJsx(node) {
|
|
853
|
+
return containsNode(
|
|
854
|
+
node,
|
|
855
|
+
(candidate) => candidate.type === import_utils13.AST_NODE_TYPES.JSXElement || candidate.type === import_utils13.AST_NODE_TYPES.JSXFragment
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
var SKIPPED_KEYS = /* @__PURE__ */ new Set(["parent", "loc", "range", "tokens", "comments"]);
|
|
859
|
+
function containsNode(root, predicate) {
|
|
860
|
+
const stack = [root];
|
|
861
|
+
while (stack.length > 0) {
|
|
862
|
+
const current = stack.pop();
|
|
863
|
+
if (!current) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (predicate(current)) {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
for (const [key, value] of Object.entries(current)) {
|
|
870
|
+
if (SKIPPED_KEYS.has(key)) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (Array.isArray(value)) {
|
|
874
|
+
for (const item of value) {
|
|
875
|
+
if (isNodeLike(item)) {
|
|
876
|
+
stack.push(item);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
if (isNodeLike(value)) {
|
|
882
|
+
stack.push(value);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
function isNodeLike(value) {
|
|
889
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
890
|
+
}
|
|
891
|
+
function isIdentifier(value) {
|
|
892
|
+
return isNodeLike(value) && value.type === import_utils13.AST_NODE_TYPES.Identifier;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/semantic-module/classifiers.ts
|
|
896
|
+
var import_utils14 = require("@typescript-eslint/utils");
|
|
897
|
+
function isHookName(name, options) {
|
|
898
|
+
if (!options.hookDetection.enabled || !name) {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
return options.hookDetection.namePattern.test(name);
|
|
902
|
+
}
|
|
903
|
+
function isReactComponentName(name) {
|
|
904
|
+
return Boolean(name && /^[A-Z][A-Za-z0-9]*$/u.test(name));
|
|
905
|
+
}
|
|
906
|
+
function isReactComponentFunction(node, name, options, isDefaultExport = false) {
|
|
907
|
+
if (!options.reactComponentDetection.enabled) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
if (!isReactComponentName(name) && !isDefaultExport) {
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
if (node.returnType && typeReferencesJsxValue(node.returnType.typeAnnotation)) {
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
return functionReturnsJsx(node);
|
|
917
|
+
}
|
|
918
|
+
function isReactComponentVariable(declarator, options) {
|
|
919
|
+
if (!options.reactComponentDetection.enabled) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
const name = getVariableDeclaratorName(declarator);
|
|
923
|
+
if (!isReactComponentName(name)) {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
if (declarator.id.type === import_utils14.AST_NODE_TYPES.Identifier && declarator.id.typeAnnotation && typeReferencesReactComponent(declarator.id.typeAnnotation.typeAnnotation)) {
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
if (!declarator.init) {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
if (declarator.init.type === import_utils14.AST_NODE_TYPES.ArrowFunctionExpression || declarator.init.type === import_utils14.AST_NODE_TYPES.FunctionExpression) {
|
|
933
|
+
return isReactComponentFunction(declarator.init, name, options);
|
|
934
|
+
}
|
|
935
|
+
return containsJsx(declarator.init);
|
|
936
|
+
}
|
|
937
|
+
var REACT_COMPONENT_TYPES = /* @__PURE__ */ new Set([
|
|
938
|
+
"FC",
|
|
939
|
+
"FunctionComponent",
|
|
940
|
+
"React.FC",
|
|
941
|
+
"React.FunctionComponent"
|
|
942
|
+
]);
|
|
943
|
+
var JSX_VALUE_TYPES = /* @__PURE__ */ new Set([
|
|
944
|
+
"JSX.Element",
|
|
945
|
+
"React.ReactElement",
|
|
946
|
+
"React.ReactNode"
|
|
947
|
+
]);
|
|
948
|
+
function typeReferencesReactComponent(node) {
|
|
949
|
+
return containsNode(
|
|
950
|
+
node,
|
|
951
|
+
(candidate) => candidate.type === import_utils14.AST_NODE_TYPES.TSTypeReference && REACT_COMPONENT_TYPES.has(entityNameToString(candidate.typeName))
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
function typeReferencesJsxValue(node) {
|
|
955
|
+
return containsNode(
|
|
956
|
+
node,
|
|
957
|
+
(candidate) => candidate.type === import_utils14.AST_NODE_TYPES.TSTypeReference && JSX_VALUE_TYPES.has(entityNameToString(candidate.typeName))
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
function entityNameToString(entityName) {
|
|
961
|
+
if (entityName.type === import_utils14.AST_NODE_TYPES.Identifier) {
|
|
962
|
+
return entityName.name;
|
|
963
|
+
}
|
|
964
|
+
if (entityName.type === import_utils14.AST_NODE_TYPES.TSQualifiedName) {
|
|
965
|
+
return `${entityNameToString(entityName.left)}.${entityName.right.name}`;
|
|
966
|
+
}
|
|
967
|
+
return "this";
|
|
968
|
+
}
|
|
969
|
+
var SCHEMA_LIBRARY_MODULES = {
|
|
970
|
+
zod: ["zod"],
|
|
971
|
+
yup: ["yup"],
|
|
972
|
+
valibot: ["valibot"]
|
|
973
|
+
};
|
|
974
|
+
var SCHEMA_BUILDER_NAMES = /* @__PURE__ */ new Set([
|
|
975
|
+
"array",
|
|
976
|
+
"boolean",
|
|
977
|
+
"date",
|
|
978
|
+
"enum",
|
|
979
|
+
"literal",
|
|
980
|
+
"number",
|
|
981
|
+
"object",
|
|
982
|
+
"record",
|
|
983
|
+
"string",
|
|
984
|
+
"tuple",
|
|
985
|
+
"union"
|
|
986
|
+
]);
|
|
987
|
+
function collectSchemaImportContext(program, options) {
|
|
988
|
+
const namespaceIdentifiers = /* @__PURE__ */ new Set();
|
|
989
|
+
const builderIdentifiers = /* @__PURE__ */ new Set();
|
|
990
|
+
const enabledModules = new Set(
|
|
991
|
+
options.schemaLibraries.flatMap((library) => SCHEMA_LIBRARY_MODULES[library])
|
|
992
|
+
);
|
|
993
|
+
for (const statement of program.body) {
|
|
994
|
+
if (statement.type !== import_utils14.AST_NODE_TYPES.ImportDeclaration || statement.importKind === "type" || !enabledModules.has(String(statement.source.value))) {
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
for (const specifier of statement.specifiers) {
|
|
998
|
+
if (specifier.type === import_utils14.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils14.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
999
|
+
namespaceIdentifiers.add(specifier.local.name);
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
if (specifier.importKind === "type") {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
const importedName = specifier.imported.type === import_utils14.AST_NODE_TYPES.Identifier ? specifier.imported.name : String(specifier.imported.value);
|
|
1006
|
+
if (importedName === "z") {
|
|
1007
|
+
namespaceIdentifiers.add(specifier.local.name);
|
|
1008
|
+
}
|
|
1009
|
+
if (SCHEMA_BUILDER_NAMES.has(importedName)) {
|
|
1010
|
+
builderIdentifiers.add(specifier.local.name);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return { namespaceIdentifiers, builderIdentifiers };
|
|
1015
|
+
}
|
|
1016
|
+
function isSchemaExpression(expression, context) {
|
|
1017
|
+
const unwrapped = unwrapExpression(expression);
|
|
1018
|
+
if (unwrapped.type !== import_utils14.AST_NODE_TYPES.CallExpression) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
const rootName = expressionRootIdentifier(unwrapped.callee);
|
|
1022
|
+
if (!rootName) {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
return context.namespaceIdentifiers.has(rootName) || context.builderIdentifiers.has(rootName);
|
|
1026
|
+
}
|
|
1027
|
+
function expressionRootIdentifier(node) {
|
|
1028
|
+
switch (node.type) {
|
|
1029
|
+
case import_utils14.AST_NODE_TYPES.Identifier:
|
|
1030
|
+
return node.name;
|
|
1031
|
+
case import_utils14.AST_NODE_TYPES.MemberExpression:
|
|
1032
|
+
return expressionRootIdentifier(node.object);
|
|
1033
|
+
case import_utils14.AST_NODE_TYPES.CallExpression:
|
|
1034
|
+
return expressionRootIdentifier(node.callee);
|
|
1035
|
+
case import_utils14.AST_NODE_TYPES.ChainExpression:
|
|
1036
|
+
return expressionRootIdentifier(node.expression);
|
|
1037
|
+
default:
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
function getConstantReason(expression) {
|
|
1042
|
+
if (!expression) {
|
|
1043
|
+
return "top-level variable declaration without initializer";
|
|
1044
|
+
}
|
|
1045
|
+
switch (unwrapExpression(expression).type) {
|
|
1046
|
+
case import_utils14.AST_NODE_TYPES.Literal:
|
|
1047
|
+
return "literal runtime value";
|
|
1048
|
+
case import_utils14.AST_NODE_TYPES.ObjectExpression:
|
|
1049
|
+
return "object literal runtime value";
|
|
1050
|
+
case import_utils14.AST_NODE_TYPES.ArrayExpression:
|
|
1051
|
+
return "array literal runtime value";
|
|
1052
|
+
case import_utils14.AST_NODE_TYPES.TemplateLiteral:
|
|
1053
|
+
return "template literal runtime value";
|
|
1054
|
+
case import_utils14.AST_NODE_TYPES.CallExpression:
|
|
1055
|
+
return "computed top-level runtime value";
|
|
1056
|
+
default:
|
|
1057
|
+
return "top-level runtime value";
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/semantic-module/options.ts
|
|
1062
|
+
var SEMANTIC_CATEGORIES = [
|
|
1063
|
+
"type",
|
|
1064
|
+
"constant",
|
|
1065
|
+
"function",
|
|
1066
|
+
"class",
|
|
1067
|
+
"react-component",
|
|
1068
|
+
"hook",
|
|
1069
|
+
"schema",
|
|
1070
|
+
"enum"
|
|
1071
|
+
];
|
|
1072
|
+
var SCHEMA_LIBRARIES = ["zod", "yup", "valibot"];
|
|
1073
|
+
function sortCategories(categories) {
|
|
1074
|
+
const categorySet = new Set(categories);
|
|
1075
|
+
return SEMANTIC_CATEGORIES.filter((category) => categorySet.has(category));
|
|
1076
|
+
}
|
|
1077
|
+
var DEFAULT_HOOK_NAME_PATTERN = "^use[A-Z0-9].*";
|
|
1078
|
+
var DEFAULT_OPTIONS = {
|
|
1079
|
+
allow: [],
|
|
1080
|
+
enumCategory: "enum",
|
|
1081
|
+
debug: false,
|
|
1082
|
+
ignoreAmbientDeclarations: false,
|
|
1083
|
+
ignorePrivateDeclarations: true,
|
|
1084
|
+
schemaLibraries: SCHEMA_LIBRARIES,
|
|
1085
|
+
reactComponentDetection: { enabled: true },
|
|
1086
|
+
hookDetection: { enabled: true, namePattern: DEFAULT_HOOK_NAME_PATTERN }
|
|
1087
|
+
};
|
|
1088
|
+
function normalizeOptions(options) {
|
|
1089
|
+
return {
|
|
1090
|
+
allow: options.allow ?? DEFAULT_OPTIONS.allow,
|
|
1091
|
+
enumCategory: options.enumCategory ?? DEFAULT_OPTIONS.enumCategory,
|
|
1092
|
+
debug: options.debug ?? DEFAULT_OPTIONS.debug,
|
|
1093
|
+
ignoreAmbientDeclarations: options.ignoreAmbientDeclarations ?? DEFAULT_OPTIONS.ignoreAmbientDeclarations,
|
|
1094
|
+
ignorePrivateDeclarations: options.ignorePrivateDeclarations ?? DEFAULT_OPTIONS.ignorePrivateDeclarations,
|
|
1095
|
+
schemaLibraries: options.schemaLibraries ?? DEFAULT_OPTIONS.schemaLibraries,
|
|
1096
|
+
reactComponentDetection: { enabled: options.reactComponentDetection?.enabled ?? true },
|
|
1097
|
+
hookDetection: {
|
|
1098
|
+
enabled: options.hookDetection?.enabled ?? true,
|
|
1099
|
+
namePattern: compilePattern(options.hookDetection?.namePattern ?? DEFAULT_HOOK_NAME_PATTERN)
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function compilePattern(pattern) {
|
|
1104
|
+
try {
|
|
1105
|
+
return new RegExp(pattern);
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
`single-semantic-module: hookDetection.namePattern ${JSON.stringify(pattern)} is not a valid regular expression (${String(error)}).`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function isCategorySetAllowed(categories, allow) {
|
|
1113
|
+
if (categories.size <= 1) {
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
const detected = [...categories];
|
|
1117
|
+
return allow.some((group) => {
|
|
1118
|
+
const allowed = new Set(group);
|
|
1119
|
+
return detected.every((category) => allowed.has(category));
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/semantic-module/classify.ts
|
|
1124
|
+
function analyzeSemanticModule(program, rawOptions) {
|
|
1125
|
+
const options = normalizeOptions(rawOptions);
|
|
1126
|
+
const context = {
|
|
1127
|
+
options,
|
|
1128
|
+
schemaImports: collectSchemaImportContext(program, options),
|
|
1129
|
+
exportedNames: collectLocallyExportedNames(program)
|
|
1130
|
+
};
|
|
1131
|
+
const classifications = program.body.flatMap(
|
|
1132
|
+
(statement) => classifyTopLevelStatement(statement, context)
|
|
1133
|
+
);
|
|
1134
|
+
return {
|
|
1135
|
+
categories: new Set(classifications.map((classification2) => classification2.category)),
|
|
1136
|
+
classifications,
|
|
1137
|
+
options
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function collectLocallyExportedNames(program) {
|
|
1141
|
+
const names = /* @__PURE__ */ new Set();
|
|
1142
|
+
for (const statement of program.body) {
|
|
1143
|
+
if (statement.type === import_utils15.AST_NODE_TYPES.ExportNamedDeclaration && statement.source === null && statement.declaration === null) {
|
|
1144
|
+
for (const specifier of statement.specifiers) {
|
|
1145
|
+
if (specifier.local.type === import_utils15.AST_NODE_TYPES.Identifier) {
|
|
1146
|
+
names.add(specifier.local.name);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
} else if (statement.type === import_utils15.AST_NODE_TYPES.ExportDefaultDeclaration && statement.declaration.type === import_utils15.AST_NODE_TYPES.Identifier) {
|
|
1150
|
+
names.add(statement.declaration.name);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return names;
|
|
1154
|
+
}
|
|
1155
|
+
function classifyTopLevelStatement(statement, context) {
|
|
1156
|
+
switch (statement.type) {
|
|
1157
|
+
case import_utils15.AST_NODE_TYPES.ImportDeclaration:
|
|
1158
|
+
case import_utils15.AST_NODE_TYPES.EmptyStatement:
|
|
1159
|
+
case import_utils15.AST_NODE_TYPES.ExportAllDeclaration:
|
|
1160
|
+
return [];
|
|
1161
|
+
case import_utils15.AST_NODE_TYPES.ExportNamedDeclaration:
|
|
1162
|
+
return statement.declaration ? classifyDeclarationLike(statement.declaration, context) : [];
|
|
1163
|
+
case import_utils15.AST_NODE_TYPES.ExportDefaultDeclaration:
|
|
1164
|
+
return classifyDeclarationLike(statement.declaration, { ...context, isDefaultExport: true });
|
|
1165
|
+
default:
|
|
1166
|
+
if (!context.options.ignorePrivateDeclarations) {
|
|
1167
|
+
return classifyDeclarationLike(statement, context);
|
|
1168
|
+
}
|
|
1169
|
+
return classifyExportedByName(statement, context);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function classifyExportedByName(statement, context) {
|
|
1173
|
+
if (context.exportedNames.size === 0) {
|
|
1174
|
+
return [];
|
|
1175
|
+
}
|
|
1176
|
+
if (statement.type === import_utils15.AST_NODE_TYPES.VariableDeclaration) {
|
|
1177
|
+
const exported = statement.declarations.filter((declarator) => {
|
|
1178
|
+
const name2 = getVariableDeclaratorName(declarator);
|
|
1179
|
+
return name2 !== void 0 && context.exportedNames.has(name2);
|
|
1180
|
+
});
|
|
1181
|
+
return exported.map((declarator) => classifyVariableDeclarator(declarator, context));
|
|
1182
|
+
}
|
|
1183
|
+
const name = getDeclarationName(statement);
|
|
1184
|
+
return name !== void 0 && context.exportedNames.has(name) ? classifyDeclarationLike(statement, context) : [];
|
|
1185
|
+
}
|
|
1186
|
+
function classifyDeclarationLike(node, context) {
|
|
1187
|
+
if (isAmbientDeclaration(node)) {
|
|
1188
|
+
return context.options.ignoreAmbientDeclarations ? [] : [classification("type", node, getDeclarationName(node), "ambient declaration")];
|
|
1189
|
+
}
|
|
1190
|
+
switch (node.type) {
|
|
1191
|
+
case import_utils15.AST_NODE_TYPES.TSInterfaceDeclaration:
|
|
1192
|
+
case import_utils15.AST_NODE_TYPES.TSTypeAliasDeclaration:
|
|
1193
|
+
case import_utils15.AST_NODE_TYPES.TSModuleDeclaration:
|
|
1194
|
+
return [
|
|
1195
|
+
classification("type", node, getDeclarationName(node), "TypeScript type-space declaration")
|
|
1196
|
+
];
|
|
1197
|
+
case import_utils15.AST_NODE_TYPES.TSEnumDeclaration:
|
|
1198
|
+
return [
|
|
1199
|
+
classification(
|
|
1200
|
+
context.options.enumCategory,
|
|
1201
|
+
node,
|
|
1202
|
+
getDeclarationName(node),
|
|
1203
|
+
context.options.enumCategory === "type" ? "enum configured as type" : "enum declaration"
|
|
1204
|
+
)
|
|
1205
|
+
];
|
|
1206
|
+
case import_utils15.AST_NODE_TYPES.ClassDeclaration:
|
|
1207
|
+
return [classification("class", node, getDeclarationName(node), "class declaration")];
|
|
1208
|
+
case import_utils15.AST_NODE_TYPES.FunctionDeclaration:
|
|
1209
|
+
return [classifyFunction(node, getDeclarationName(node), context, "function declaration")];
|
|
1210
|
+
case import_utils15.AST_NODE_TYPES.VariableDeclaration:
|
|
1211
|
+
return node.declarations.map((declarator) => classifyVariableDeclarator(declarator, context));
|
|
1212
|
+
case import_utils15.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
1213
|
+
case import_utils15.AST_NODE_TYPES.FunctionExpression:
|
|
1214
|
+
return [classifyFunction(node, void 0, context, "function expression")];
|
|
1215
|
+
case import_utils15.AST_NODE_TYPES.ClassExpression:
|
|
1216
|
+
return [classification("class", node, getDeclarationName(node), "class expression")];
|
|
1217
|
+
case import_utils15.AST_NODE_TYPES.CallExpression:
|
|
1218
|
+
case import_utils15.AST_NODE_TYPES.ArrayExpression:
|
|
1219
|
+
case import_utils15.AST_NODE_TYPES.ObjectExpression:
|
|
1220
|
+
case import_utils15.AST_NODE_TYPES.Literal:
|
|
1221
|
+
case import_utils15.AST_NODE_TYPES.TemplateLiteral:
|
|
1222
|
+
return [classifyDefaultExpression(node, context)];
|
|
1223
|
+
case import_utils15.AST_NODE_TYPES.TSDeclareFunction:
|
|
1224
|
+
return [
|
|
1225
|
+
classification("function", node, getDeclarationName(node), "function overload signature")
|
|
1226
|
+
];
|
|
1227
|
+
default:
|
|
1228
|
+
return [];
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function classifyFunction(node, name, context, reason) {
|
|
1232
|
+
if (isHookName(name, context.options)) {
|
|
1233
|
+
return classification("hook", node, name, "function name matches hook pattern");
|
|
1234
|
+
}
|
|
1235
|
+
if (isReactComponentFunction(node, name, context.options, context.isDefaultExport === true)) {
|
|
1236
|
+
return classification(
|
|
1237
|
+
"react-component",
|
|
1238
|
+
node,
|
|
1239
|
+
name,
|
|
1240
|
+
`${reason === "function declaration" ? "function component" : "function expression"} returns JSX or React element`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
return classification("function", node, name, reason);
|
|
1244
|
+
}
|
|
1245
|
+
function classifyVariableDeclarator(declarator, context) {
|
|
1246
|
+
const name = getVariableDeclaratorName(declarator);
|
|
1247
|
+
const init = declarator.init ? unwrapExpression(declarator.init) : null;
|
|
1248
|
+
if (init && isSchemaExpression(init, context.schemaImports)) {
|
|
1249
|
+
return classification("schema", declarator, name, "schema builder expression");
|
|
1250
|
+
}
|
|
1251
|
+
if (isReactComponentVariable(declarator, context.options)) {
|
|
1252
|
+
return classification("react-component", declarator, name, "React component variable");
|
|
1253
|
+
}
|
|
1254
|
+
if (isHookName(name, context.options)) {
|
|
1255
|
+
return classification("hook", declarator, name, "variable name matches hook pattern");
|
|
1256
|
+
}
|
|
1257
|
+
if (init?.type === import_utils15.AST_NODE_TYPES.ArrowFunctionExpression || init?.type === import_utils15.AST_NODE_TYPES.FunctionExpression) {
|
|
1258
|
+
return classifyFunction(init, name, context, "function expression");
|
|
1259
|
+
}
|
|
1260
|
+
if (init?.type === import_utils15.AST_NODE_TYPES.ClassExpression) {
|
|
1261
|
+
return classification("class", declarator, name, "class expression");
|
|
1262
|
+
}
|
|
1263
|
+
return classification("constant", declarator, name, getConstantReason(init));
|
|
1264
|
+
}
|
|
1265
|
+
function classifyDefaultExpression(expression, context) {
|
|
1266
|
+
const unwrapped = unwrapExpression(expression);
|
|
1267
|
+
if (isSchemaExpression(unwrapped, context.schemaImports)) {
|
|
1268
|
+
return classification("schema", expression, void 0, "default schema expression");
|
|
1269
|
+
}
|
|
1270
|
+
if (unwrapped.type === import_utils15.AST_NODE_TYPES.ArrowFunctionExpression || unwrapped.type === import_utils15.AST_NODE_TYPES.FunctionExpression) {
|
|
1271
|
+
return classifyFunction(unwrapped, void 0, context, "function expression");
|
|
1272
|
+
}
|
|
1273
|
+
if (unwrapped.type === import_utils15.AST_NODE_TYPES.ClassExpression) {
|
|
1274
|
+
return classification("class", expression, void 0, "default class expression");
|
|
1275
|
+
}
|
|
1276
|
+
return classification("constant", expression, void 0, getConstantReason(unwrapped));
|
|
1277
|
+
}
|
|
1278
|
+
function classification(category, node, declarationName, reason) {
|
|
1279
|
+
return declarationName ? { category, node, reason, declarationName } : { category, node, reason };
|
|
1280
|
+
}
|
|
1281
|
+
function buildMixedCategoriesMessage(classifications, debug) {
|
|
1282
|
+
const categories = sortCategories(classifications.map((entry) => entry.category));
|
|
1283
|
+
const lines = [
|
|
1284
|
+
"Mixed semantic categories detected in module:",
|
|
1285
|
+
...categories.map((category) => `- ${category}`)
|
|
1286
|
+
];
|
|
1287
|
+
if (debug) {
|
|
1288
|
+
lines.push("", "Detected declarations:");
|
|
1289
|
+
for (const entry of classifications) {
|
|
1290
|
+
lines.push(`- ${entry.category}: ${entry.declarationName ?? "<anonymous>"} (${entry.reason})`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
lines.push(
|
|
1294
|
+
"",
|
|
1295
|
+
"A module must contain only one semantic concern.",
|
|
1296
|
+
"Move declarations into separate files/modules."
|
|
1297
|
+
);
|
|
1298
|
+
return lines.join("\n");
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/rules/single-semantic-module.ts
|
|
1302
|
+
var RULE_NAME8 = "single-semantic-module";
|
|
1303
|
+
var optionSchema8 = {
|
|
1304
|
+
type: "object",
|
|
1305
|
+
additionalProperties: false,
|
|
1306
|
+
properties: {
|
|
1307
|
+
allow: {
|
|
1308
|
+
type: "array",
|
|
1309
|
+
items: {
|
|
1310
|
+
type: "array",
|
|
1311
|
+
minItems: 2,
|
|
1312
|
+
uniqueItems: true,
|
|
1313
|
+
items: { type: "string", enum: [...SEMANTIC_CATEGORIES] }
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
enumCategory: { type: "string", enum: ["enum", "type"] },
|
|
1317
|
+
debug: { type: "boolean" },
|
|
1318
|
+
ignoreAmbientDeclarations: { type: "boolean" },
|
|
1319
|
+
ignorePrivateDeclarations: { type: "boolean" },
|
|
1320
|
+
schemaLibraries: {
|
|
1321
|
+
type: "array",
|
|
1322
|
+
uniqueItems: true,
|
|
1323
|
+
items: { type: "string", enum: [...SCHEMA_LIBRARIES] }
|
|
1324
|
+
},
|
|
1325
|
+
reactComponentDetection: {
|
|
1326
|
+
type: "object",
|
|
1327
|
+
additionalProperties: false,
|
|
1328
|
+
properties: { enabled: { type: "boolean" } }
|
|
1329
|
+
},
|
|
1330
|
+
hookDetection: {
|
|
1331
|
+
type: "object",
|
|
1332
|
+
additionalProperties: false,
|
|
1333
|
+
properties: {
|
|
1334
|
+
enabled: { type: "boolean" },
|
|
1335
|
+
namePattern: { type: "string" }
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
var singleSemanticModuleRule = createRule({
|
|
1341
|
+
name: RULE_NAME8,
|
|
1342
|
+
meta: {
|
|
1343
|
+
type: "suggestion",
|
|
1344
|
+
docs: {
|
|
1345
|
+
description: "Require each module to export only one semantic concern (types, constants, functions, classes, components, hooks, schemas or enums)."
|
|
1346
|
+
},
|
|
1347
|
+
schema: [optionSchema8],
|
|
1348
|
+
messages: {
|
|
1349
|
+
mixedSemanticCategories: "{{message}}"
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
1353
|
+
create(context, [options]) {
|
|
1354
|
+
return {
|
|
1355
|
+
Program(program) {
|
|
1356
|
+
const analysis = analyzeSemanticModule(program, options);
|
|
1357
|
+
if (isCategorySetAllowed(analysis.categories, analysis.options.allow)) {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
const [first] = analysis.classifications;
|
|
1361
|
+
const reportNode = analysis.classifications.find((entry) => entry.category !== first?.category)?.node ?? program;
|
|
1362
|
+
context.report({
|
|
1363
|
+
node: reportNode,
|
|
1364
|
+
messageId: "mixedSemanticCategories",
|
|
1365
|
+
data: {
|
|
1366
|
+
message: buildMixedCategoriesMessage(analysis.classifications, analysis.options.debug)
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
|
|
385
1374
|
// src/rules/index.ts
|
|
386
1375
|
var rules = {
|
|
1376
|
+
"barrel-purity": barrelPurityRule,
|
|
1377
|
+
"colocated-test-required": colocatedTestRequiredRule,
|
|
387
1378
|
"component-folder-structure": componentFolderStructureRule,
|
|
1379
|
+
"filename-matches-export": filenameMatchesExportRule,
|
|
388
1380
|
"index-must-reexport-default": indexMustReexportDefaultRule,
|
|
389
|
-
"
|
|
1381
|
+
"max-import-depth": maxImportDepthRule,
|
|
1382
|
+
"no-cross-feature-imports": noCrossFeatureImportsRule,
|
|
1383
|
+
"single-semantic-module": singleSemanticModuleRule
|
|
390
1384
|
};
|
|
391
1385
|
|
|
392
1386
|
// src/index.ts
|
|
393
1387
|
var NAMESPACE = "noctcore-architecture";
|
|
394
|
-
var VERSION = "0.
|
|
1388
|
+
var VERSION = "0.3.0";
|
|
395
1389
|
var plugin = {
|
|
396
1390
|
meta: { name: "@noctcore/eslint-plugin-architecture", version: VERSION },
|
|
397
1391
|
rules,
|