@alveolus/arch 0.4.0 → 0.5.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/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as Cli, t as Docs } from "./docs-DcFgskuN.mjs";
2
+ import { r as Cli, t as Docs } from "./docs-B1Tzkjrn.mjs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  //#region src/bin.ts
5
5
  const docs = new Docs(fileURLToPath(new URL("../docs/", import.meta.url)));
@@ -166,22 +166,26 @@ var Member = class {
166
166
  kind;
167
167
  visibility;
168
168
  isStatic;
169
+ isReadonly;
169
170
  isParameterProperty;
170
171
  isCallable;
171
172
  valueTypes;
172
173
  parameterTypes;
173
174
  returns;
175
+ holdsCollection;
174
176
  constructor(props) {
175
177
  this.name = props.name;
176
178
  this.line = props.line;
177
179
  this.kind = props.kind;
178
180
  this.visibility = props.visibility;
179
181
  this.isStatic = props.isStatic;
182
+ this.isReadonly = props.isReadonly;
180
183
  this.isParameterProperty = props.isParameterProperty;
181
184
  this.isCallable = props.isCallable;
182
185
  this.valueTypes = props.valueTypes;
183
186
  this.parameterTypes = props.parameterTypes;
184
187
  this.returns = props.returns;
188
+ this.holdsCollection = props.holdsCollection;
185
189
  }
186
190
  get isPublicInstance() {
187
191
  return this.visibility === "public" && !this.isStatic;
@@ -227,6 +231,18 @@ var GlobalUse = class {
227
231
  }
228
232
  };
229
233
  //#endregion
234
+ //#region src/model/dependencies/wiring.ts
235
+ var Wiring = class {
236
+ line;
237
+ receiver;
238
+ links;
239
+ constructor(line, receiver, links) {
240
+ this.line = line;
241
+ this.receiver = receiver;
242
+ this.links = links;
243
+ }
244
+ };
245
+ //#endregion
230
246
  //#region src/model/project.ts
231
247
  var Project$1 = class {
232
248
  directory;
@@ -257,6 +273,7 @@ var SourceFile = class {
257
273
  statements;
258
274
  throws;
259
275
  globals;
276
+ wirings;
260
277
  disables;
261
278
  lines;
262
279
  constructor(props) {
@@ -266,6 +283,7 @@ var SourceFile = class {
266
283
  this.statements = props.statements;
267
284
  this.throws = props.throws;
268
285
  this.globals = props.globals;
286
+ this.wirings = props.wirings;
269
287
  this.disables = props.disables;
270
288
  this.lines = props.text.split(/\r?\n/);
271
289
  }
@@ -586,6 +604,60 @@ var CoreApi = class {
586
604
  }
587
605
  };
588
606
  //#endregion
607
+ //#region src/architecture/crossing.ts
608
+ var Crossings = class {
609
+ architecture;
610
+ constructor(architecture) {
611
+ this.architecture = architecture;
612
+ }
613
+ in(file) {
614
+ const crossings = /* @__PURE__ */ new Map();
615
+ for (const wiring of file.wirings) {
616
+ const crossing = this.of(wiring);
617
+ if (crossing !== void 0) crossings.set(`${crossing.line}:${crossing.expression}:${crossing.from}:${crossing.to}`, crossing);
618
+ }
619
+ return [...crossings.values()];
620
+ }
621
+ of(wiring) {
622
+ const receiver = this.architecture.locationOfPath(wiring.receiver);
623
+ if (!receiver.isInBoundedContext || receiver.context === void 0) return;
624
+ for (const link of wiring.links) {
625
+ const source = this.sourceOf(link, receiver);
626
+ if (source !== void 0) return {
627
+ expression: link.text,
628
+ from: receiver.context,
629
+ isOpenHostService: this.isOpenHostService(link, receiver),
630
+ line: wiring.line,
631
+ to: source
632
+ };
633
+ }
634
+ }
635
+ sourceOf(link, receiver) {
636
+ if (link.declaredIn !== void 0) {
637
+ const declared = this.architecture.locationOfPath(link.declaredIn);
638
+ if (declared.isOtherBoundedContextThan(receiver)) return declared.context;
639
+ }
640
+ return this.foreignTypesOf(link, receiver)[0]?.context;
641
+ }
642
+ isOpenHostService(link, receiver) {
643
+ const foreign = link.types.filter((type) => this.isForeignModel(type.declaredIn, receiver));
644
+ return foreign.length > 0 && foreign.every((type) => {
645
+ const declaration = this.architecture.classOf(type);
646
+ return declaration !== void 0 && this.architecture.implementsMarker(declaration, "OpenHostService");
647
+ });
648
+ }
649
+ foreignTypesOf(link, receiver) {
650
+ const locations = [];
651
+ for (const type of link.types) if (this.isForeignModel(type.declaredIn, receiver)) locations.push(this.architecture.locationOfPath(type.declaredIn ?? ""));
652
+ return locations;
653
+ }
654
+ isForeignModel(path, receiver) {
655
+ if (path === void 0) return false;
656
+ const location = this.architecture.locationOfPath(path);
657
+ return location.isOtherBoundedContextThan(receiver) && !location.isCompositionRoot;
658
+ }
659
+ };
660
+ //#endregion
589
661
  //#region src/architecture/layers.ts
590
662
  var Layers = class {
591
663
  isLayer(name) {
@@ -769,6 +841,7 @@ var Architecture = class {
769
841
  shape;
770
842
  layout;
771
843
  locations = /* @__PURE__ */ new Map();
844
+ crossings = new Crossings(this);
772
845
  constructor(project, settings) {
773
846
  this.project = project;
774
847
  this.settings = settings;
@@ -790,6 +863,12 @@ var Architecture = class {
790
863
  locationOf(file) {
791
864
  return this.locate(file.path);
792
865
  }
866
+ locationOfPath(path) {
867
+ return this.locate(path);
868
+ }
869
+ crossingsIn(file) {
870
+ return this.crossings.in(file);
871
+ }
793
872
  locationOfTarget(target) {
794
873
  if (target.visibility !== "analysed") return new Location({
795
874
  area: "outside",
@@ -882,10 +961,12 @@ var ContextMap = class {
882
961
  //#region src/rules/framework/wording.ts
883
962
  var Wording = class {
884
963
  target(target, architecture) {
964
+ if (target.visibility === "dynamic") return `code loaded at runtime with ${target.path}`;
885
965
  return `${architecture.relativePath(target.path)} (${this.location(architecture.locationOfTarget(target))})`;
886
966
  }
887
967
  location(location) {
888
968
  if (location.unseen === "ignored") return "ignored by the analysis";
969
+ if (location.unseen === "dynamic") return "loaded at runtime";
889
970
  if (location.unseen === "unresolved") return "not resolved by the analysis";
890
971
  if (location.isOutside) return "outside the declared bounded contexts and shared kernel";
891
972
  if (location.isAtRoot) return "at the root";
@@ -1089,6 +1170,7 @@ var NoOutwardImportRule = class extends ImportRule {
1089
1170
  description: "A dependency pointing away from the domain, a file outside the layers or in the wrong folder of its layer.",
1090
1171
  id: "layers/no-outward-import",
1091
1172
  messages: {
1173
+ compositionRootImport: "The composition root imports {target}: it wires its own context and the shared kernel only.",
1092
1174
  compositionRoots: "{context} has {count} composition roots ({names}): keep one, and move the rest into the layers.",
1093
1175
  directlyInLayer: "The file sits directly in {layer}/: put it in the folder of its kind, such as {example}.",
1094
1176
  drivenTooDeep: "The file is nested too deep: driven/ holds driven/<technology>/<folder>/, such as driven/pg/adapters/.",
@@ -1183,11 +1265,15 @@ var NoOutwardImportRule = class extends ImportRule {
1183
1265
  return findings;
1184
1266
  }
1185
1267
  layerProblem(from, to, architecture) {
1186
- if (from.isAtRoot) return to.isInBoundedContext && !to.isCompositionRoot ? {
1268
+ if (from.isAtRoot) return to.isOutside || to.isInBoundedContext && !to.isCompositionRoot ? {
1187
1269
  data: {},
1188
1270
  messageId: "rootImport"
1189
1271
  } : void 0;
1190
- if (from.isCompositionRoot || to.isOtherBoundedContextThan(from)) return;
1272
+ if (from.isCompositionRoot) return to.isOutside ? {
1273
+ data: {},
1274
+ messageId: "compositionRootImport"
1275
+ } : void 0;
1276
+ if (to.isOtherBoundedContextThan(from)) return;
1191
1277
  if (to.isCompositionRoot) return {
1192
1278
  data: {},
1193
1279
  messageId: "wiring"
@@ -1249,16 +1335,28 @@ var NoPortlessAdapterRule = class extends ClassRule {
1249
1335
  var NoCrossContextImportRule = class extends ImportRule {
1250
1336
  meta = {
1251
1337
  contexts: "every",
1252
- description: "An import from another bounded context that is not its open host service, a composition root that re-exports.",
1338
+ description: "An import from another bounded context that is not its open host service, a file the analysis does not see, a composition root that re-exports, a value of another context given in the wiring that is not its open host service.",
1253
1339
  id: "strategic/no-cross-context-import",
1254
1340
  messages: {
1341
+ dynamicLoad: "Loads code at runtime with {loader}: the analysis cannot tell which bounded context it reaches; use a static import.",
1255
1342
  notOpenHostService: "Imports {target}: only an OpenHostService of another bounded context may be imported.",
1256
1343
  outsideAntiCorruptionLayer: "Uses the open host service of {context} outside an AntiCorruptionLayer: a core context translates what it consumes in an anti-corruption layer.",
1257
1344
  publishedLanguage: "Imports the published language of {context}: redeclare the fields you read in your own published-language/.",
1258
1345
  reexport: "The composition root re-exports {names}: it exports its own module only, so that no other context reaches through it.",
1259
- sharedKernel: "The shared kernel imports no bounded context, but imports {target}."
1346
+ sharedKernel: "The shared kernel imports no bounded context, but imports {target}.",
1347
+ unseen: "Imports {target}: the analysis cannot tell which bounded context it reaches; move the file into a bounded context or the shared kernel.",
1348
+ wiredModel: "Gives {expression}, from {to}, to {from}: only an OpenHostService of another bounded context may cross, in an import or in the wiring."
1260
1349
  }
1261
1350
  };
1351
+ check(architecture) {
1352
+ const findings = super.check(architecture);
1353
+ for (const file of this.filesOf(architecture)) for (const crossing of architecture.crossingsIn(file)) if (!crossing.isOpenHostService) findings.push(this.finding(file, crossing.line, crossing.expression, "wiredModel", {
1354
+ expression: crossing.expression,
1355
+ from: crossing.from,
1356
+ to: crossing.to
1357
+ }));
1358
+ return findings;
1359
+ }
1262
1360
  appliesTo(file, architecture) {
1263
1361
  const location = architecture.locationOf(file);
1264
1362
  return location.isInBoundedContext || location.isInSharedKernel;
@@ -1269,6 +1367,8 @@ var NoCrossContextImportRule = class extends ImportRule {
1269
1367
  const target = dependency.target;
1270
1368
  if (target.kind !== "file") return;
1271
1369
  const to = architecture.locationOfTarget(target);
1370
+ if (target.visibility === "dynamic") return this.finding(file, dependency.line, dependency.label, "dynamicLoad", { loader: target.path });
1371
+ if (to.isOutside) return this.finding(file, dependency.line, dependency.label, "unseen", { target: this.wording.target(target, architecture) });
1272
1372
  if (!to.isOtherBoundedContextThan(from)) return;
1273
1373
  if (from.isInSharedKernel) return this.finding(file, dependency.line, dependency.label, "sharedKernel", { target: this.wording.target(target, architecture) });
1274
1374
  if (from.isCompositionRoot && to.isCompositionRoot) return;
@@ -1359,17 +1459,60 @@ var NoLeakyHostServiceRule = class extends ClassRule {
1359
1459
  }
1360
1460
  };
1361
1461
  //#endregion
1462
+ //#region src/rules/strategic/no-shared-state.rule.ts
1463
+ var NoSharedStateRule = class extends ClassRule {
1464
+ meta = {
1465
+ contexts: "every",
1466
+ description: "A static field of the shared kernel that holds state: one without readonly, or one holding a collection.",
1467
+ id: "strategic/no-shared-state",
1468
+ messages: {
1469
+ collection: "{class}.{field} holds a collection in a static field: every context reaches the same one, a channel the context map does not show. The shared kernel shares a model, not state: integrate through an open host service.",
1470
+ mutable: "{class}.{field} is a static field without readonly: every context reaches the same one, a channel the context map does not show. The shared kernel shares a model, not state: integrate through an open host service."
1471
+ }
1472
+ };
1473
+ findingsFor(codeClass, file, architecture) {
1474
+ if (!architecture.locationOf(file).isInSharedKernel) return [];
1475
+ const findings = [];
1476
+ for (const member of codeClass.members) {
1477
+ const messageId = this.stateOf(member);
1478
+ if (messageId !== void 0) findings.push(this.finding(file, member.line, member.name, messageId, {
1479
+ class: codeClass.name,
1480
+ field: member.name
1481
+ }));
1482
+ }
1483
+ return findings;
1484
+ }
1485
+ stateOf(member) {
1486
+ if (!member.isStatic || member.kind !== "field") return;
1487
+ if (!member.isReadonly) return "mutable";
1488
+ return member.holdsCollection ? "collection" : void 0;
1489
+ }
1490
+ };
1491
+ //#endregion
1362
1492
  //#region src/rules/strategic/no-unmapped-context.rule.ts
1363
1493
  var NoUnmappedContextRule = class extends Rule {
1364
1494
  meta = {
1365
1495
  contexts: "every",
1366
- description: "A bounded context consuming one the context map does not allow.",
1496
+ description: "A bounded context consuming one the context map does not allow, through an import or through the wiring.",
1367
1497
  id: "strategic/no-unmapped-context",
1368
- messages: { unmapped: "{from} consumes {to}, which the context map does not allow: reverse the dependency, or if {from} really is downstream of {to}, add {to} to contextMap.{from}.consumes." }
1498
+ messages: {
1499
+ unmapped: "{from} consumes {to}, which the context map does not allow: reverse the dependency, or if {from} really is downstream of {to}, add {to} to contextMap.{from}.consumes.",
1500
+ unmappedWiring: "{from} receives {expression} from {to} here, which the context map does not allow: reverse the dependency, or if {from} really is downstream of {to}, add {to} to contextMap.{from}.consumes."
1501
+ }
1369
1502
  };
1370
1503
  check(architecture) {
1371
1504
  const findings = [];
1372
1505
  for (const consumption of this.consumptionsIn(architecture)) if (!architecture.contextMap.allows(consumption.from, consumption.to)) findings.push(this.consumptionFinding(consumption));
1506
+ findings.push(...this.unmappedWirings(architecture));
1507
+ return findings;
1508
+ }
1509
+ unmappedWirings(architecture) {
1510
+ const findings = [];
1511
+ for (const file of this.filesOf(architecture)) for (const crossing of architecture.crossingsIn(file)) if (!architecture.contextMap.allows(crossing.from, crossing.to)) findings.push(this.finding(file, crossing.line, crossing.expression, "unmappedWiring", {
1512
+ expression: crossing.expression,
1513
+ from: crossing.from,
1514
+ to: crossing.to
1515
+ }));
1373
1516
  return findings;
1374
1517
  }
1375
1518
  consumptionsIn(architecture) {
@@ -1830,6 +1973,7 @@ const ruleIds = [
1830
1973
  "strategic/no-leaky-host-service",
1831
1974
  "strategic/no-unmapped-context",
1832
1975
  "strategic/no-fat-shared-kernel",
1976
+ "strategic/no-shared-state",
1833
1977
  "layers/no-impure-domain",
1834
1978
  "layers/no-outward-import",
1835
1979
  "layers/no-portless-adapter",
@@ -1850,6 +1994,7 @@ var RuleRegistry = class {
1850
1994
  new NoLeakyHostServiceRule(),
1851
1995
  new NoUnmappedContextRule(),
1852
1996
  new NoFatSharedKernelRule(),
1997
+ new NoSharedStateRule(),
1853
1998
  new NoImpureDomainRule(),
1854
1999
  new NoOutwardImportRule(),
1855
2000
  new NoPortlessAdapterRule(),
@@ -2088,6 +2233,7 @@ var Config = class {
2088
2233
  contextMap;
2089
2234
  ignored;
2090
2235
  rules;
2236
+ layout;
2091
2237
  constructor(config, projectDir) {
2092
2238
  this.projectDir = resolve(projectDir);
2093
2239
  this.rootDir = resolve(projectDir, config.root);
@@ -2105,6 +2251,7 @@ var Config = class {
2105
2251
  isSharedKernel: true,
2106
2252
  name: "shared kernel"
2107
2253
  }];
2254
+ this.layout = new Layout(this);
2108
2255
  }
2109
2256
  classifiedContexts(subdomains, boundedContexts) {
2110
2257
  const types = [
@@ -2149,6 +2296,10 @@ var Config = class {
2149
2296
  const projectPath = relative(this.projectDir, path).split(sep).join("/");
2150
2297
  return this.ignored.some((glob) => matchesGlob(projectPath, glob));
2151
2298
  }
2299
+ readsWiring(path) {
2300
+ const location = this.layout.locate(path);
2301
+ return location.isAtRoot || location.isCompositionRoot;
2302
+ }
2152
2303
  severityOf(rule) {
2153
2304
  return this.rules?.[rule] ?? "error";
2154
2305
  }
@@ -2253,6 +2404,7 @@ var ClassReader = class {
2253
2404
  const type = member.getType();
2254
2405
  const [signature] = type.getCallSignatures();
2255
2406
  return [this.member(member, "field", {
2407
+ holdsCollection: this.types.holdsCollection(type),
2256
2408
  isCallable: signature !== void 0,
2257
2409
  returns: signature === void 0 ? void 0 : this.types.returnShapeOf(signature.getReturnType()),
2258
2410
  valueTypes: this.types.classTypesIn(type)
@@ -2282,8 +2434,10 @@ var ClassReader = class {
2282
2434
  const isStatic = Node.isStaticable(member) && member.isStatic();
2283
2435
  const scope = Node.isScoped(member) ? member.getScope() : Scope.Public;
2284
2436
  return new Member({
2437
+ holdsCollection: types.holdsCollection ?? false,
2285
2438
  isCallable: types.isCallable ?? false,
2286
2439
  isParameterProperty: false,
2440
+ isReadonly: Node.isReadonlyable(member) && member.isReadonly(),
2287
2441
  isStatic,
2288
2442
  kind,
2289
2443
  line: member.getStartLineNumber(),
@@ -2299,8 +2453,10 @@ var ClassReader = class {
2299
2453
  }
2300
2454
  constructorParameter(parameter) {
2301
2455
  return new Member({
2456
+ holdsCollection: this.types.holdsCollection(parameter.getType()),
2302
2457
  isCallable: false,
2303
2458
  isParameterProperty: parameter.isParameterProperty(),
2459
+ isReadonly: parameter.isReadonly(),
2304
2460
  isStatic: false,
2305
2461
  kind: "constructor parameter",
2306
2462
  line: parameter.getStartLineNumber(),
@@ -2340,6 +2496,19 @@ var ClassReader = class {
2340
2496
  //#endregion
2341
2497
  //#region src/importer/ts-morph/readers/dependency-reader.ts
2342
2498
  const everything = ["*"];
2499
+ const loaderPackages = [
2500
+ "module",
2501
+ "node:module",
2502
+ "vm",
2503
+ "node:vm"
2504
+ ];
2505
+ const globalLoaders = ["eval", "Function"];
2506
+ const loaderMembers = [
2507
+ "eval",
2508
+ "Function",
2509
+ "require",
2510
+ "getBuiltinModule"
2511
+ ];
2343
2512
  var DependencyReader = class {
2344
2513
  project;
2345
2514
  scope;
@@ -2359,7 +2528,17 @@ var DependencyReader = class {
2359
2528
  for (const declaration of file.getExportDeclarations()) if (declaration.hasModuleSpecifier()) references.push(this.fromDeclaration(declaration, "re-export"));
2360
2529
  for (const declaration of file.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration)) references.push(...this.fromImportEquals(declaration));
2361
2530
  for (const node of file.getDescendantsOfKind(SyntaxKind.ImportType)) references.push(this.fromImportType(node));
2362
- for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) references.push(...this.fromCall(call));
2531
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
2532
+ references.push(...this.fromCall(call));
2533
+ references.push(...this.fromLoader(call));
2534
+ }
2535
+ for (const construction of file.getDescendantsOfKind(SyntaxKind.NewExpression)) references.push(...this.fromLoader(construction));
2536
+ for (const reference of file.getPathReferenceDirectives()) references.push({
2537
+ ...this.fromDirective(reference, file),
2538
+ isPath: true
2539
+ });
2540
+ for (const reference of file.getTypeReferenceDirectives()) references.push(this.fromDirective(reference, file));
2541
+ for (const declaration of file.getDescendantsOfKind(SyntaxKind.ModuleDeclaration)) references.push(...this.fromAugmentation(declaration));
2363
2542
  return references;
2364
2543
  }
2365
2544
  fromDeclaration(declaration, form) {
@@ -2399,6 +2578,47 @@ var DependencyReader = class {
2399
2578
  if (callee.isKind(SyntaxKind.Identifier) && callee.getText() === "require") return [this.reference(call, "require", argument, everything)];
2400
2579
  return [];
2401
2580
  }
2581
+ fromDirective(reference, file) {
2582
+ const line = file.getLineAndColumnAtPos(reference.getPos()).line;
2583
+ const specifier = reference.getFileName();
2584
+ return {
2585
+ form: "reference",
2586
+ line,
2587
+ names: everything,
2588
+ specifier,
2589
+ text: specifier
2590
+ };
2591
+ }
2592
+ fromAugmentation(declaration) {
2593
+ const name = declaration.getNameNode();
2594
+ if (!name.isKind(SyntaxKind.StringLiteral) || name.getLiteralValue().includes("*")) return [];
2595
+ return [this.reference(declaration, "augmentation", name, everything)];
2596
+ }
2597
+ fromLoader(call) {
2598
+ const loader = this.loaderOf(call.getExpression());
2599
+ if (loader === void 0) return [];
2600
+ return [{
2601
+ form: "dynamic load",
2602
+ line: call.getStartLineNumber(),
2603
+ names: [],
2604
+ specifier: loader,
2605
+ text: loader
2606
+ }];
2607
+ }
2608
+ loaderOf(callee) {
2609
+ const expression = this.unwrapped(callee);
2610
+ if (expression.isKind(SyntaxKind.Identifier)) return globalLoaders.includes(expression.getText()) && !this.isDeclaredInProject(expression) ? expression.getText() : void 0;
2611
+ if (expression.isKind(SyntaxKind.PropertyAccessExpression) && loaderMembers.includes(expression.getName())) return expression.getText();
2612
+ }
2613
+ unwrapped(expression) {
2614
+ if (expression.isKind(SyntaxKind.ParenthesizedExpression)) return this.unwrapped(expression.getExpression());
2615
+ if (expression.isKind(SyntaxKind.BinaryExpression) && expression.getOperatorToken().isKind(SyntaxKind.CommaToken)) return this.unwrapped(expression.getRight());
2616
+ return expression;
2617
+ }
2618
+ isDeclaredInProject(expression) {
2619
+ const declaration = expression.getSymbol()?.getDeclarations()[0];
2620
+ return declaration !== void 0 && this.isInsideProject(normalize(declaration.getSourceFile().getFilePath()));
2621
+ }
2402
2622
  reference(node, form, specifierNode, names) {
2403
2623
  const isLiteral = specifierNode.isKind(SyntaxKind.StringLiteral) || specifierNode.isKind(SyntaxKind.NoSubstitutionTemplateLiteral);
2404
2624
  return {
@@ -2410,15 +2630,20 @@ var DependencyReader = class {
2410
2630
  };
2411
2631
  }
2412
2632
  targetOf(reference, file) {
2633
+ if (reference.form === "dynamic load" || loaderPackages.includes(reference.specifier ?? "")) return {
2634
+ kind: "file",
2635
+ path: reference.text,
2636
+ visibility: "dynamic"
2637
+ };
2413
2638
  const directory = file.getDirectoryPath();
2414
2639
  if (reference.specifier === void 0) return {
2415
2640
  kind: "file",
2416
2641
  path: normalize(join(directory, reference.text)),
2417
2642
  visibility: "unresolved"
2418
2643
  };
2419
- const resolved = this.resolve(reference.specifier, file);
2644
+ const resolved = reference.isPath === true ? this.referencedPath(reference.specifier, directory) : this.resolve(reference.specifier, file);
2420
2645
  if (resolved !== void 0 && this.isInsideProject(resolved)) return this.fileTarget(resolved);
2421
- if (reference.specifier.startsWith(".")) return {
2646
+ if (reference.isPath === true || reference.specifier.startsWith(".")) return {
2422
2647
  kind: "file",
2423
2648
  path: normalize(join(directory, reference.specifier)),
2424
2649
  visibility: "unresolved"
@@ -2439,6 +2664,10 @@ var DependencyReader = class {
2439
2664
  const resolved = ts.resolveModuleName(specifier, file.getFilePath(), this.project.getCompilerOptions(), this.project.getModuleResolutionHost()).resolvedModule?.resolvedFileName;
2440
2665
  return resolved === void 0 ? void 0 : normalize(resolved);
2441
2666
  }
2667
+ referencedPath(specifier, directory) {
2668
+ const path = normalize(join(directory, specifier));
2669
+ return this.project.getModuleResolutionHost().fileExists(path) ? path : void 0;
2670
+ }
2442
2671
  packageNameOf(specifier) {
2443
2672
  const segments = specifier.split("/");
2444
2673
  const length = specifier.startsWith("@") ? 2 : 1;
@@ -2624,6 +2853,12 @@ var ThrowReader = class {
2624
2853
  };
2625
2854
  //#endregion
2626
2855
  //#region src/importer/ts-morph/readers/type-reader.ts
2856
+ const collections = [
2857
+ "Map",
2858
+ "Set",
2859
+ "WeakMap",
2860
+ "WeakSet"
2861
+ ];
2627
2862
  var TypeReader = class {
2628
2863
  packageNames;
2629
2864
  constructor(packageNames) {
@@ -2646,6 +2881,12 @@ var TypeReader = class {
2646
2881
  packageName: void 0
2647
2882
  } : this.namedSymbol(symbol);
2648
2883
  }
2884
+ holdsCollection(type) {
2885
+ if (type.isArray() || type.isTuple()) return !type.isReadonlyArray();
2886
+ if (collections.includes(type.getSymbol()?.getName() ?? "")) return true;
2887
+ if (type.getStringIndexType() !== void 0 || type.getNumberIndexType() !== void 0) return true;
2888
+ return type.isAnonymous() && type.getCallSignatures().length === 0 && type.getConstructSignatures().length === 0;
2889
+ }
2649
2890
  returnShapeOf(type) {
2650
2891
  if (type.getSymbol()?.getName() === "Promise") {
2651
2892
  const [inner] = type.getTypeArguments();
@@ -2710,17 +2951,98 @@ var TypeReader = class {
2710
2951
  }
2711
2952
  };
2712
2953
  //#endregion
2954
+ //#region src/importer/ts-morph/readers/wiring-reader.ts
2955
+ const references = [SyntaxKind.Identifier, SyntaxKind.PropertyAccessExpression];
2956
+ var WiringReader = class {
2957
+ types;
2958
+ scope;
2959
+ constructor(types, scope) {
2960
+ this.types = types;
2961
+ this.scope = scope;
2962
+ }
2963
+ read(file) {
2964
+ if (!this.scope.readsWiring(file.getFilePath())) return [];
2965
+ const wirings = [];
2966
+ for (const site of this.sitesOf(file)) {
2967
+ const receiver = this.declaringFileOf(site.receiver);
2968
+ if (receiver === void 0) continue;
2969
+ for (const value of site.values) for (const reference of this.referencesIn(value)) wirings.push(new Wiring(reference.getStartLineNumber(), receiver, this.linksOf(reference)));
2970
+ }
2971
+ return wirings;
2972
+ }
2973
+ sitesOf(file) {
2974
+ const sites = [];
2975
+ file.forEachDescendant((node) => {
2976
+ if (Node.isCallExpression(node) || Node.isNewExpression(node)) sites.push({
2977
+ receiver: node.getExpression(),
2978
+ values: node.getArguments()
2979
+ });
2980
+ else if (Node.isBinaryExpression(node) && node.getOperatorToken().isKind(SyntaxKind.EqualsToken)) sites.push({
2981
+ receiver: node.getLeft(),
2982
+ values: [node.getRight()]
2983
+ });
2984
+ else if ((Node.isVariableDeclaration(node) || Node.isPropertyDeclaration(node)) && node.getTypeNode() !== void 0) {
2985
+ const initializer = node.getInitializer();
2986
+ sites.push({
2987
+ receiver: node.getTypeNode(),
2988
+ values: initializer === void 0 ? [] : [initializer]
2989
+ });
2990
+ }
2991
+ });
2992
+ return sites;
2993
+ }
2994
+ referencesIn(value) {
2995
+ const found = [];
2996
+ const visit = (node) => {
2997
+ if (this.isOutermostReference(node)) found.push(node);
2998
+ node.forEachChild(visit);
2999
+ };
3000
+ visit(value);
3001
+ return found;
3002
+ }
3003
+ isOutermostReference(node) {
3004
+ if (!references.includes(node.getKind())) return false;
3005
+ const parent = node.getParent();
3006
+ if (parent === void 0 || Node.isPropertyAccessExpression(parent)) return false;
3007
+ if (Node.isTypeNode(parent) || Node.isQualifiedName(parent) || Node.isExpressionWithTypeArguments(parent)) return false;
3008
+ return Node.isShorthandPropertyAssignment(parent) || !(Node.hasName(parent) && parent.getNameNode() === node);
3009
+ }
3010
+ linksOf(reference) {
3011
+ if (Node.isPropertyAccessExpression(reference)) return [...this.linksOf(reference.getExpression()), this.link(reference, reference.getNameNode())];
3012
+ if (Node.isIdentifier(reference)) return [this.link(reference, reference)];
3013
+ return [];
3014
+ }
3015
+ link(expression, name) {
3016
+ return {
3017
+ declaredIn: this.declaringFileOf(name),
3018
+ text: expression.getText(),
3019
+ types: this.types.classTypesIn(expression.getType())
3020
+ };
3021
+ }
3022
+ declaringFileOf(node) {
3023
+ if (node === void 0) return;
3024
+ const named = Node.isTypeNode(node) ? node.getType().getAliasSymbol() ?? node.getType().getSymbol() : this.nameOf(node).getSymbol();
3025
+ const declaration = (named?.isAlias() === true ? named.getAliasedSymbol() : named)?.getDeclarations()[0];
3026
+ return declaration === void 0 ? void 0 : normalize(declaration.getSourceFile().getFilePath());
3027
+ }
3028
+ nameOf(node) {
3029
+ return Node.isPropertyAccessExpression(node) ? node.getNameNode() : node;
3030
+ }
3031
+ };
3032
+ //#endregion
2713
3033
  //#region src/importer/ts-morph/ts-morph-importer.ts
2714
3034
  var TsMorphImporter = class TsMorphImporter extends Importer {
2715
3035
  sources;
2716
3036
  classes;
3037
+ types;
2717
3038
  statements = new StatementReader();
2718
3039
  disables = new DisableReader();
2719
3040
  throws = new ThrowReader();
2720
3041
  constructor(sources) {
2721
3042
  super();
2722
3043
  this.sources = sources;
2723
- this.classes = new ClassReader(new TypeReader(new PackageNames(sources.getFileSystem())));
3044
+ this.types = new TypeReader(new PackageNames(sources.getFileSystem()));
3045
+ this.classes = new ClassReader(this.types);
2724
3046
  }
2725
3047
  static fromTsConfig(tsConfigFilePath) {
2726
3048
  if (!existsSync(tsConfigFilePath)) throw new Error(`No TypeScript configuration found at ${tsConfigFilePath}: pass --tsconfig, or set tsconfig in alveolus.config.ts.`);
@@ -2732,12 +3054,13 @@ var TsMorphImporter = class TsMorphImporter extends Importer {
2732
3054
  }
2733
3055
  read(scope) {
2734
3056
  const dependencies = new DependencyReader(this.sources, scope);
3057
+ const wirings = new WiringReader(this.types, scope);
2735
3058
  const globals = new GlobalReader(scope.projectDir);
2736
3059
  const files = [];
2737
- for (const file of this.sources.getSourceFiles()) if (this.isInside(scope.rootDir, file.getFilePath()) && !scope.isIgnored(file.getFilePath())) files.push(this.readFile(file, dependencies, globals));
3060
+ for (const file of this.sources.getSourceFiles()) if (this.isInside(scope.rootDir, file.getFilePath()) && !scope.isIgnored(file.getFilePath())) files.push(this.readFile(file, dependencies, globals, wirings));
2738
3061
  return new Project$1(scope.projectDir, files);
2739
3062
  }
2740
- readFile(file, dependencies, globals) {
3063
+ readFile(file, dependencies, globals, wirings) {
2741
3064
  const globalReferences = globals.read(file);
2742
3065
  return new SourceFile({
2743
3066
  classes: file.getClasses().map((declaration) => this.classes.read(declaration)),
@@ -2747,7 +3070,8 @@ var TsMorphImporter = class TsMorphImporter extends Importer {
2747
3070
  path: normalize(file.getFilePath()),
2748
3071
  statements: this.statements.read(file),
2749
3072
  text: file.getFullText(),
2750
- throws: this.throws.read(file)
3073
+ throws: this.throws.read(file),
3074
+ wirings: wirings.read(file)
2751
3075
  });
2752
3076
  }
2753
3077
  globalUsesOf(references) {
@@ -3042,4 +3366,4 @@ var Docs = class Docs {
3042
3366
  //#endregion
3043
3367
  export { TsMorphImporter as a, Config as c, RuleRegistry as d, Rule as f, Baseline as h, Init as i, Report as l, AllowedPackages as m, Page as n, Importer as o, Architecture as p, Cli as r, ConfigLoader as s, Docs as t, Checker as u };
3044
3368
 
3045
- //# sourceMappingURL=docs-DcFgskuN.mjs.map
3369
+ //# sourceMappingURL=docs-B1Tzkjrn.mjs.map