@alveolus/arch 0.4.0 → 0.6.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.
@@ -166,22 +166,32 @@ 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;
176
+ receivesFunction;
177
+ erasedType;
178
+ opaqueValue;
174
179
  constructor(props) {
175
180
  this.name = props.name;
176
181
  this.line = props.line;
177
182
  this.kind = props.kind;
178
183
  this.visibility = props.visibility;
179
184
  this.isStatic = props.isStatic;
185
+ this.isReadonly = props.isReadonly;
180
186
  this.isParameterProperty = props.isParameterProperty;
181
187
  this.isCallable = props.isCallable;
182
188
  this.valueTypes = props.valueTypes;
183
189
  this.parameterTypes = props.parameterTypes;
184
190
  this.returns = props.returns;
191
+ this.holdsCollection = props.holdsCollection;
192
+ this.receivesFunction = props.receivesFunction;
193
+ this.erasedType = props.erasedType;
194
+ this.opaqueValue = props.opaqueValue;
185
195
  }
186
196
  get isPublicInstance() {
187
197
  return this.visibility === "public" && !this.isStatic;
@@ -213,6 +223,18 @@ var Dependency = class {
213
223
  }
214
224
  };
215
225
  //#endregion
226
+ //#region src/model/dependencies/global-channel.ts
227
+ var GlobalChannel = class {
228
+ line;
229
+ name;
230
+ access;
231
+ constructor(line, name, access) {
232
+ this.line = line;
233
+ this.name = name;
234
+ this.access = access;
235
+ }
236
+ };
237
+ //#endregion
216
238
  //#region src/model/dependencies/global-use.ts
217
239
  var GlobalUse = class {
218
240
  line;
@@ -227,6 +249,32 @@ var GlobalUse = class {
227
249
  }
228
250
  };
229
251
  //#endregion
252
+ //#region src/model/dependencies/module-reach.ts
253
+ var ModuleReach = class {
254
+ line;
255
+ expression;
256
+ means;
257
+ module;
258
+ constructor(line, expression, means, module) {
259
+ this.line = line;
260
+ this.expression = expression;
261
+ this.means = means;
262
+ this.module = module;
263
+ }
264
+ };
265
+ //#endregion
266
+ //#region src/model/dependencies/wiring.ts
267
+ var Wiring = class {
268
+ line;
269
+ receiver;
270
+ links;
271
+ constructor(line, receiver, links) {
272
+ this.line = line;
273
+ this.receiver = receiver;
274
+ this.links = links;
275
+ }
276
+ };
277
+ //#endregion
230
278
  //#region src/model/project.ts
231
279
  var Project$1 = class {
232
280
  directory;
@@ -257,6 +305,9 @@ var SourceFile = class {
257
305
  statements;
258
306
  throws;
259
307
  globals;
308
+ globalChannels;
309
+ wirings;
310
+ moduleReaches;
260
311
  disables;
261
312
  lines;
262
313
  constructor(props) {
@@ -266,6 +317,9 @@ var SourceFile = class {
266
317
  this.statements = props.statements;
267
318
  this.throws = props.throws;
268
319
  this.globals = props.globals;
320
+ this.globalChannels = props.globalChannels;
321
+ this.wirings = props.wirings;
322
+ this.moduleReaches = props.moduleReaches;
269
323
  this.disables = props.disables;
270
324
  this.lines = props.text.split(/\r?\n/);
271
325
  }
@@ -586,6 +640,60 @@ var CoreApi = class {
586
640
  }
587
641
  };
588
642
  //#endregion
643
+ //#region src/architecture/crossing.ts
644
+ var Crossings = class {
645
+ architecture;
646
+ constructor(architecture) {
647
+ this.architecture = architecture;
648
+ }
649
+ in(file) {
650
+ const crossings = /* @__PURE__ */ new Map();
651
+ for (const wiring of file.wirings) {
652
+ const crossing = this.of(wiring);
653
+ if (crossing !== void 0) crossings.set(`${crossing.line}:${crossing.expression}:${crossing.from}:${crossing.to}`, crossing);
654
+ }
655
+ return [...crossings.values()];
656
+ }
657
+ of(wiring) {
658
+ const receiver = this.architecture.locationOfPath(wiring.receiver);
659
+ if (!receiver.isInBoundedContext || receiver.context === void 0) return;
660
+ for (const link of wiring.links) {
661
+ const source = this.sourceOf(link, receiver);
662
+ if (source !== void 0) return {
663
+ expression: link.text,
664
+ from: receiver.context,
665
+ isOpenHostService: this.isOpenHostService(link, receiver),
666
+ line: wiring.line,
667
+ to: source
668
+ };
669
+ }
670
+ }
671
+ sourceOf(link, receiver) {
672
+ if (link.declaredIn !== void 0) {
673
+ const declared = this.architecture.locationOfPath(link.declaredIn);
674
+ if (declared.isOtherBoundedContextThan(receiver)) return declared.context;
675
+ }
676
+ return this.foreignTypesOf(link, receiver)[0]?.context;
677
+ }
678
+ isOpenHostService(link, receiver) {
679
+ const foreign = link.types.filter((type) => this.isForeignModel(type.declaredIn, receiver));
680
+ return foreign.length > 0 && foreign.every((type) => {
681
+ const declaration = this.architecture.classOf(type);
682
+ return declaration !== void 0 && this.architecture.implementsMarker(declaration, "OpenHostService");
683
+ });
684
+ }
685
+ foreignTypesOf(link, receiver) {
686
+ const locations = [];
687
+ for (const type of link.types) if (this.isForeignModel(type.declaredIn, receiver)) locations.push(this.architecture.locationOfPath(type.declaredIn ?? ""));
688
+ return locations;
689
+ }
690
+ isForeignModel(path, receiver) {
691
+ if (path === void 0) return false;
692
+ const location = this.architecture.locationOfPath(path);
693
+ return location.isOtherBoundedContextThan(receiver) && !location.isCompositionRoot;
694
+ }
695
+ };
696
+ //#endregion
589
697
  //#region src/architecture/layers.ts
590
698
  var Layers = class {
591
699
  isLayer(name) {
@@ -769,6 +877,7 @@ var Architecture = class {
769
877
  shape;
770
878
  layout;
771
879
  locations = /* @__PURE__ */ new Map();
880
+ crossings = new Crossings(this);
772
881
  constructor(project, settings) {
773
882
  this.project = project;
774
883
  this.settings = settings;
@@ -790,6 +899,12 @@ var Architecture = class {
790
899
  locationOf(file) {
791
900
  return this.locate(file.path);
792
901
  }
902
+ locationOfPath(path) {
903
+ return this.locate(path);
904
+ }
905
+ crossingsIn(file) {
906
+ return this.crossings.in(file);
907
+ }
793
908
  locationOfTarget(target) {
794
909
  if (target.visibility !== "analysed") return new Location({
795
910
  area: "outside",
@@ -882,10 +997,12 @@ var ContextMap = class {
882
997
  //#region src/rules/framework/wording.ts
883
998
  var Wording = class {
884
999
  target(target, architecture) {
1000
+ if (target.visibility === "dynamic") return `code loaded at runtime with ${target.path}`;
885
1001
  return `${architecture.relativePath(target.path)} (${this.location(architecture.locationOfTarget(target))})`;
886
1002
  }
887
1003
  location(location) {
888
1004
  if (location.unseen === "ignored") return "ignored by the analysis";
1005
+ if (location.unseen === "dynamic") return "loaded at runtime";
889
1006
  if (location.unseen === "unresolved") return "not resolved by the analysis";
890
1007
  if (location.isOutside) return "outside the declared bounded contexts and shared kernel";
891
1008
  if (location.isAtRoot) return "at the root";
@@ -1089,6 +1206,7 @@ var NoOutwardImportRule = class extends ImportRule {
1089
1206
  description: "A dependency pointing away from the domain, a file outside the layers or in the wrong folder of its layer.",
1090
1207
  id: "layers/no-outward-import",
1091
1208
  messages: {
1209
+ compositionRootImport: "The composition root imports {target}: it wires its own context and the shared kernel only.",
1092
1210
  compositionRoots: "{context} has {count} composition roots ({names}): keep one, and move the rest into the layers.",
1093
1211
  directlyInLayer: "The file sits directly in {layer}/: put it in the folder of its kind, such as {example}.",
1094
1212
  drivenTooDeep: "The file is nested too deep: driven/ holds driven/<technology>/<folder>/, such as driven/pg/adapters/.",
@@ -1183,11 +1301,15 @@ var NoOutwardImportRule = class extends ImportRule {
1183
1301
  return findings;
1184
1302
  }
1185
1303
  layerProblem(from, to, architecture) {
1186
- if (from.isAtRoot) return to.isInBoundedContext && !to.isCompositionRoot ? {
1304
+ if (from.isAtRoot) return to.isOutside || to.isInBoundedContext && !to.isCompositionRoot ? {
1187
1305
  data: {},
1188
1306
  messageId: "rootImport"
1189
1307
  } : void 0;
1190
- if (from.isCompositionRoot || to.isOtherBoundedContextThan(from)) return;
1308
+ if (from.isCompositionRoot) return to.isOutside ? {
1309
+ data: {},
1310
+ messageId: "compositionRootImport"
1311
+ } : void 0;
1312
+ if (to.isOtherBoundedContextThan(from)) return;
1191
1313
  if (to.isCompositionRoot) return {
1192
1314
  data: {},
1193
1315
  messageId: "wiring"
@@ -1249,16 +1371,57 @@ var NoPortlessAdapterRule = class extends ClassRule {
1249
1371
  var NoCrossContextImportRule = class extends ImportRule {
1250
1372
  meta = {
1251
1373
  contexts: "every",
1252
- description: "An import from another bounded context that is not its open host service, a composition root that re-exports.",
1374
+ 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
1375
  id: "strategic/no-cross-context-import",
1254
1376
  messages: {
1377
+ dynamicLoad: "Loads code at runtime with {loader}: the analysis cannot tell which bounded context it reaches; use a static import.",
1378
+ globalChannel: "{access} {name}, which no file declares: two contexts can meet there without the context map showing it. Integrate through an open host service, or publish an integration event.",
1255
1379
  notOpenHostService: "Imports {target}: only an OpenHostService of another bounded context may be imported.",
1256
1380
  outsideAntiCorruptionLayer: "Uses the open host service of {context} outside an AntiCorruptionLayer: a core context translates what it consumes in an anti-corruption layer.",
1257
1381
  publishedLanguage: "Imports the published language of {context}: redeclare the fields you read in your own published-language/.",
1382
+ reachedModule: "Reaches into {expression} with {means}: in the wiring, a module of another context is read by its properties, so that what crosses can be seen.",
1258
1383
  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}."
1384
+ sharedKernel: "The shared kernel imports no bounded context, but imports {target}.",
1385
+ unseen: "Imports {target}: the analysis cannot tell which bounded context it reaches; move the file into a bounded context or the shared kernel.",
1386
+ wiredModel: "Gives {expression}, from {to}, to {from}: only an OpenHostService of another bounded context may cross, in an import or in the wiring."
1260
1387
  }
1261
1388
  };
1389
+ check(architecture) {
1390
+ const findings = super.check(architecture);
1391
+ for (const file of this.filesOf(architecture)) {
1392
+ findings.push(...this.reachedModules(file, architecture));
1393
+ findings.push(...this.globalChannels(file, architecture));
1394
+ for (const crossing of architecture.crossingsIn(file)) if (!crossing.isOpenHostService) findings.push(this.finding(file, crossing.line, crossing.expression, "wiredModel", {
1395
+ expression: crossing.expression,
1396
+ from: crossing.from,
1397
+ to: crossing.to
1398
+ }));
1399
+ }
1400
+ return findings;
1401
+ }
1402
+ globalChannels(file, architecture) {
1403
+ const location = architecture.locationOf(file);
1404
+ if (!location.isInBoundedContext && !location.isInSharedKernel && !location.isAtRoot) return [];
1405
+ return file.globalChannels.map((channel) => {
1406
+ const access = channel.access === "writes" ? "Writes" : "Reads";
1407
+ return this.finding(file, channel.line, channel.name, "globalChannel", {
1408
+ access,
1409
+ name: channel.name
1410
+ });
1411
+ });
1412
+ }
1413
+ reachedModules(file, architecture) {
1414
+ const from = architecture.locationOf(file);
1415
+ const findings = [];
1416
+ for (const reach of file.moduleReaches) {
1417
+ const module = architecture.locationOfPath(reach.module.declaredIn ?? "");
1418
+ if (module.isCompositionRoot && module.isInBoundedContext && !module.isSameContextAs(from)) findings.push(this.finding(file, reach.line, reach.expression, "reachedModule", {
1419
+ expression: reach.expression,
1420
+ means: reach.means
1421
+ }));
1422
+ }
1423
+ return findings;
1424
+ }
1262
1425
  appliesTo(file, architecture) {
1263
1426
  const location = architecture.locationOf(file);
1264
1427
  return location.isInBoundedContext || location.isInSharedKernel;
@@ -1269,6 +1432,8 @@ var NoCrossContextImportRule = class extends ImportRule {
1269
1432
  const target = dependency.target;
1270
1433
  if (target.kind !== "file") return;
1271
1434
  const to = architecture.locationOfTarget(target);
1435
+ if (target.visibility === "dynamic") return this.finding(file, dependency.line, dependency.label, "dynamicLoad", { loader: target.path });
1436
+ if (to.isOutside) return this.finding(file, dependency.line, dependency.label, "unseen", { target: this.wording.target(target, architecture) });
1272
1437
  if (!to.isOtherBoundedContextThan(from)) return;
1273
1438
  if (from.isInSharedKernel) return this.finding(file, dependency.line, dependency.label, "sharedKernel", { target: this.wording.target(target, architecture) });
1274
1439
  if (from.isCompositionRoot && to.isCompositionRoot) return;
@@ -1324,15 +1489,24 @@ var NoFatSharedKernelRule = class extends ClassRule {
1324
1489
  var NoLeakyHostServiceRule = class extends ClassRule {
1325
1490
  meta = {
1326
1491
  contexts: "every",
1327
- description: "An open host service that exposes a class of its context instead of the published language.",
1492
+ description: "An open host service that exposes a class of its context, receives a function or returns an erased type, instead of speaking the published language.",
1328
1493
  id: "strategic/no-leaky-host-service",
1329
- messages: { exposes: "{member} exposes {type}, {kind}{owner}: an open host service speaks the published language." }
1494
+ messages: {
1495
+ behaviour: "{member} receives a function: an open host service receives data, never behaviour, or the upstream context ends up running code of another one. To let another context react, publish an integration event.",
1496
+ erased: "{member} returns {type}: an open host service returns a type of its published language, so that what leaves the context can be seen.",
1497
+ exposes: "{member} exposes {type}, {kind}{owner}: an open host service speaks the published language."
1498
+ }
1330
1499
  };
1331
1500
  findingsFor(codeClass, file, architecture) {
1332
1501
  if (!architecture.implementsMarker(codeClass, "OpenHostService")) return [];
1333
1502
  const findings = /* @__PURE__ */ new Map();
1334
1503
  for (const member of codeClass.publicSurface) {
1335
1504
  const symbol = `${codeClass.name}.${member.name}`;
1505
+ if (member.receivesFunction) findings.set(`${symbol}:behaviour`, this.finding(file, member.line, symbol, "behaviour", { member: symbol }));
1506
+ if (member.erasedType !== void 0) findings.set(`${symbol}:erased`, this.finding(file, member.line, symbol, "erased", {
1507
+ member: symbol,
1508
+ type: member.erasedType
1509
+ }));
1336
1510
  for (const type of [...member.parameterTypes, ...member.valueTypes]) {
1337
1511
  const key = `${symbol}:${type.name}`;
1338
1512
  if (this.leaks(type, architecture) && !findings.has(key)) findings.set(key, this.finding(file, member.line, symbol, "exposes", this.describe(symbol, type, architecture)));
@@ -1359,17 +1533,83 @@ var NoLeakyHostServiceRule = class extends ClassRule {
1359
1533
  }
1360
1534
  };
1361
1535
  //#endregion
1536
+ //#region src/rules/strategic/no-shared-state.rule.ts
1537
+ var NoSharedStateRule = class extends ClassRule {
1538
+ meta = {
1539
+ contexts: "every",
1540
+ description: "A static field of the shared kernel that holds state: one without readonly, a collection, or anything but a value.",
1541
+ id: "strategic/no-shared-state",
1542
+ messages: {
1543
+ 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.",
1544
+ 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.",
1545
+ notValue: "{class}.{field} holds {holds} in a static field, which can keep state every context reaches, a channel the context map does not show. A static field of the shared kernel holds a value: a primitive, a value object, an identifier, a class of domainDependencies, or a readonly collection of them."
1546
+ }
1547
+ };
1548
+ findingsFor(codeClass, file, architecture) {
1549
+ if (!architecture.locationOf(file).isInSharedKernel) return [];
1550
+ const findings = [];
1551
+ for (const member of codeClass.members) {
1552
+ const state = this.stateOf(member, architecture);
1553
+ if (state !== void 0) findings.push(this.finding(file, member.line, member.name, state.messageId, {
1554
+ ...state.data,
1555
+ class: codeClass.name,
1556
+ field: member.name
1557
+ }));
1558
+ }
1559
+ return findings;
1560
+ }
1561
+ stateOf(member, architecture) {
1562
+ if (!member.isStatic || member.kind !== "field") return;
1563
+ if (!member.isReadonly) return {
1564
+ data: {},
1565
+ messageId: "mutable"
1566
+ };
1567
+ if (member.holdsCollection) return {
1568
+ data: {},
1569
+ messageId: "collection"
1570
+ };
1571
+ if (member.opaqueValue !== void 0) return {
1572
+ data: { holds: member.opaqueValue },
1573
+ messageId: "notValue"
1574
+ };
1575
+ const service = member.valueTypes.find((type) => !this.isValue(type, architecture));
1576
+ if (service === void 0) return;
1577
+ return {
1578
+ data: { holds: `${/^[AEIOU]/.test(service.name) ? "an" : "a"} ${service.name}` },
1579
+ messageId: "notValue"
1580
+ };
1581
+ }
1582
+ isValue(type, architecture) {
1583
+ if (architecture.is(type, "ValueObject") || architecture.is(type, "Identifier")) return true;
1584
+ const packageName = type.lineage[0]?.packageName;
1585
+ return packageName !== void 0 && architecture.domainDependencies.has(packageName);
1586
+ }
1587
+ };
1588
+ //#endregion
1362
1589
  //#region src/rules/strategic/no-unmapped-context.rule.ts
1363
1590
  var NoUnmappedContextRule = class extends Rule {
1364
1591
  meta = {
1365
1592
  contexts: "every",
1366
- description: "A bounded context consuming one the context map does not allow.",
1593
+ description: "A bounded context consuming one the context map does not allow, through an import or through the wiring.",
1367
1594
  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." }
1595
+ messages: {
1596
+ unmapped: "{from} consumes {to}, which the context map does not allow: reverse the dependency with an integration event that {from} publishes and {to} subscribes to, not with a callback; or if {from} really is downstream of {to}, add {to} to contextMap.{from}.consumes.",
1597
+ unmappedWiring: "{from} receives {expression} from {to} here, which the context map does not allow: reverse the dependency with an integration event that {from} publishes and {to} subscribes to, not with a callback; or if {from} really is downstream of {to}, add {to} to contextMap.{from}.consumes."
1598
+ }
1369
1599
  };
1370
1600
  check(architecture) {
1371
1601
  const findings = [];
1372
1602
  for (const consumption of this.consumptionsIn(architecture)) if (!architecture.contextMap.allows(consumption.from, consumption.to)) findings.push(this.consumptionFinding(consumption));
1603
+ findings.push(...this.unmappedWirings(architecture));
1604
+ return findings;
1605
+ }
1606
+ unmappedWirings(architecture) {
1607
+ const findings = [];
1608
+ 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", {
1609
+ expression: crossing.expression,
1610
+ from: crossing.from,
1611
+ to: crossing.to
1612
+ }));
1373
1613
  return findings;
1374
1614
  }
1375
1615
  consumptionsIn(architecture) {
@@ -1830,6 +2070,7 @@ const ruleIds = [
1830
2070
  "strategic/no-leaky-host-service",
1831
2071
  "strategic/no-unmapped-context",
1832
2072
  "strategic/no-fat-shared-kernel",
2073
+ "strategic/no-shared-state",
1833
2074
  "layers/no-impure-domain",
1834
2075
  "layers/no-outward-import",
1835
2076
  "layers/no-portless-adapter",
@@ -1850,6 +2091,7 @@ var RuleRegistry = class {
1850
2091
  new NoLeakyHostServiceRule(),
1851
2092
  new NoUnmappedContextRule(),
1852
2093
  new NoFatSharedKernelRule(),
2094
+ new NoSharedStateRule(),
1853
2095
  new NoImpureDomainRule(),
1854
2096
  new NoOutwardImportRule(),
1855
2097
  new NoPortlessAdapterRule(),
@@ -2088,6 +2330,7 @@ var Config = class {
2088
2330
  contextMap;
2089
2331
  ignored;
2090
2332
  rules;
2333
+ layout;
2091
2334
  constructor(config, projectDir) {
2092
2335
  this.projectDir = resolve(projectDir);
2093
2336
  this.rootDir = resolve(projectDir, config.root);
@@ -2105,6 +2348,7 @@ var Config = class {
2105
2348
  isSharedKernel: true,
2106
2349
  name: "shared kernel"
2107
2350
  }];
2351
+ this.layout = new Layout(this);
2108
2352
  }
2109
2353
  classifiedContexts(subdomains, boundedContexts) {
2110
2354
  const types = [
@@ -2149,6 +2393,10 @@ var Config = class {
2149
2393
  const projectPath = relative(this.projectDir, path).split(sep).join("/");
2150
2394
  return this.ignored.some((glob) => matchesGlob(projectPath, glob));
2151
2395
  }
2396
+ readsWiring(path) {
2397
+ const location = this.layout.locate(path);
2398
+ return location.isAtRoot || location.isCompositionRoot;
2399
+ }
2152
2400
  severityOf(rule) {
2153
2401
  return this.rules?.[rule] ?? "error";
2154
2402
  }
@@ -2253,7 +2501,11 @@ var ClassReader = class {
2253
2501
  const type = member.getType();
2254
2502
  const [signature] = type.getCallSignatures();
2255
2503
  return [this.member(member, "field", {
2504
+ erasedType: this.types.erasedNameOf(signature === void 0 ? type : signature.getReturnType()),
2505
+ holdsCollection: this.types.holdsCollection(type),
2256
2506
  isCallable: signature !== void 0,
2507
+ opaqueValue: this.types.opaqueValueIn(type),
2508
+ receivesFunction: signature !== void 0 && signature.getParameters().some((parameter) => this.types.holdsFunction(parameter.getTypeAtLocation(member))),
2257
2509
  returns: signature === void 0 ? void 0 : this.types.returnShapeOf(signature.getReturnType()),
2258
2510
  valueTypes: this.types.classTypesIn(type)
2259
2511
  })];
@@ -2261,8 +2513,11 @@ var ClassReader = class {
2261
2513
  if (Node.isMethodDeclaration(member)) {
2262
2514
  const returnType = member.getReturnType();
2263
2515
  const parameterTypes = member.getParameters().flatMap((parameter) => this.types.classTypesIn(parameter.getType()));
2516
+ const receivesFunction = member.getParameters().some((parameter) => this.types.holdsFunction(parameter.getType()));
2264
2517
  return [this.member(member, "method", {
2518
+ erasedType: this.types.erasedNameOf(returnType),
2265
2519
  parameterTypes,
2520
+ receivesFunction,
2266
2521
  returns: this.types.returnShapeOf(returnType),
2267
2522
  valueTypes: this.types.classTypesIn(returnType)
2268
2523
  })];
@@ -2270,6 +2525,7 @@ var ClassReader = class {
2270
2525
  if (Node.isGetAccessorDeclaration(member)) {
2271
2526
  const returnType = member.getReturnType();
2272
2527
  return [this.member(member, "getter", {
2528
+ erasedType: this.types.erasedNameOf(returnType),
2273
2529
  returns: this.types.returnShapeOf(returnType),
2274
2530
  valueTypes: this.types.classTypesIn(returnType)
2275
2531
  })];
@@ -2282,13 +2538,18 @@ var ClassReader = class {
2282
2538
  const isStatic = Node.isStaticable(member) && member.isStatic();
2283
2539
  const scope = Node.isScoped(member) ? member.getScope() : Scope.Public;
2284
2540
  return new Member({
2541
+ erasedType: types.erasedType,
2542
+ holdsCollection: types.holdsCollection ?? false,
2285
2543
  isCallable: types.isCallable ?? false,
2286
2544
  isParameterProperty: false,
2545
+ isReadonly: Node.isReadonlyable(member) && member.isReadonly(),
2287
2546
  isStatic,
2288
2547
  kind,
2289
2548
  line: member.getStartLineNumber(),
2290
2549
  name,
2550
+ opaqueValue: types.opaqueValue,
2291
2551
  parameterTypes: types.parameterTypes ?? [],
2552
+ receivesFunction: types.receivesFunction ?? false,
2292
2553
  returns: types.returns,
2293
2554
  valueTypes: types.valueTypes ?? [],
2294
2555
  visibility: name.startsWith("#") ? "private" : this.visibilityOf(scope)
@@ -2299,13 +2560,18 @@ var ClassReader = class {
2299
2560
  }
2300
2561
  constructorParameter(parameter) {
2301
2562
  return new Member({
2563
+ erasedType: void 0,
2564
+ holdsCollection: this.types.holdsCollection(parameter.getType()),
2302
2565
  isCallable: false,
2303
2566
  isParameterProperty: parameter.isParameterProperty(),
2567
+ isReadonly: parameter.isReadonly(),
2304
2568
  isStatic: false,
2305
2569
  kind: "constructor parameter",
2306
2570
  line: parameter.getStartLineNumber(),
2307
2571
  name: parameter.getName(),
2572
+ opaqueValue: void 0,
2308
2573
  parameterTypes: [],
2574
+ receivesFunction: false,
2309
2575
  returns: void 0,
2310
2576
  valueTypes: this.types.classTypesIn(parameter.getType()),
2311
2577
  visibility: this.visibilityOf(parameter.getScope() ?? Scope.Public)
@@ -2338,11 +2604,101 @@ var ClassReader = class {
2338
2604
  }
2339
2605
  };
2340
2606
  //#endregion
2607
+ //#region src/importer/ts-morph/readers/loader-reader.ts
2608
+ const globalObjects$1 = [
2609
+ "globalThis",
2610
+ "global",
2611
+ "window",
2612
+ "self"
2613
+ ];
2614
+ const globalLoaders = ["eval", "Function"];
2615
+ const loaderMembers = [
2616
+ "eval",
2617
+ "Function",
2618
+ "require",
2619
+ "getBuiltinModule"
2620
+ ];
2621
+ const reflections = [
2622
+ "get",
2623
+ "apply",
2624
+ "construct"
2625
+ ];
2626
+ var LoaderReader = class {
2627
+ read(file) {
2628
+ const loads = [];
2629
+ file.forEachDescendant((node) => {
2630
+ const loader = this.loaderAt(node);
2631
+ if (loader !== void 0) loads.push({
2632
+ line: node.getStartLineNumber(),
2633
+ loader
2634
+ });
2635
+ });
2636
+ return loads;
2637
+ }
2638
+ loaderAt(node) {
2639
+ if (Node.isCallExpression(node) || Node.isNewExpression(node)) return this.calledLoader(node);
2640
+ if (Node.isElementAccessExpression(node)) return this.globalLookup(node.getExpression(), node.getArgumentExpression());
2641
+ }
2642
+ calledLoader(call) {
2643
+ const callee = call.getExpression();
2644
+ const bare = this.unwrapped(callee);
2645
+ if (Node.isElementAccessExpression(bare)) return;
2646
+ const reflected = this.reflectedLoader(bare, call.getArguments());
2647
+ if (reflected !== void 0) return reflected;
2648
+ return this.namedLoader(bare) ?? this.typedLoader(callee) ?? this.typedLoader(bare);
2649
+ }
2650
+ reflectedLoader(callee, args) {
2651
+ if (!Node.isPropertyAccessExpression(callee) || callee.getExpression().getText() !== "Reflect" || !reflections.includes(callee.getName())) return;
2652
+ const [target, key] = args;
2653
+ if (target === void 0) return;
2654
+ if (callee.getName() === "get") return this.globalLookup(target, key);
2655
+ const bare = this.unwrapped(target);
2656
+ return this.namedLoader(bare) ?? this.typedLoader(target) ?? this.typedLoader(bare);
2657
+ }
2658
+ globalLookup(object, key) {
2659
+ const bare = this.unwrapped(object);
2660
+ if (!Node.isIdentifier(bare) || !globalObjects$1.includes(bare.getText()) || !this.isFromLibrary(bare)) return;
2661
+ if (key === void 0 || !Node.isStringLiteral(key)) return `${bare.getText()}[…]`;
2662
+ return globalLoaders.includes(key.getLiteralValue()) ? `${bare.getText()}.${key.getLiteralValue()}` : void 0;
2663
+ }
2664
+ namedLoader(callee) {
2665
+ if (Node.isIdentifier(callee)) return globalLoaders.includes(callee.getText()) && this.isFromLibrary(callee) ? callee.getText() : void 0;
2666
+ if (!Node.isPropertyAccessExpression(callee)) return;
2667
+ if (loaderMembers.includes(callee.getName())) return callee.getText();
2668
+ if (callee.getName() === "constructor") {
2669
+ const owner = callee.getExpression().getType();
2670
+ return owner.isAny() || owner.getCallSignatures().length > 0 ? callee.getText() : void 0;
2671
+ }
2672
+ }
2673
+ typedLoader(callee) {
2674
+ const symbol = callee.getType().getSymbol();
2675
+ if (symbol?.getName() === "FunctionConstructor") return "Function";
2676
+ const declaration = symbol?.getDeclarations()[0];
2677
+ if (symbol?.getName() === "eval" && declaration?.getSourceFile().isDeclarationFile() === true) return "eval";
2678
+ }
2679
+ isFromLibrary(identifier) {
2680
+ const declaration = identifier.getSymbol()?.getDeclarations()[0];
2681
+ return declaration === void 0 || declaration.getSourceFile().isDeclarationFile();
2682
+ }
2683
+ unwrapped(expression) {
2684
+ if (Node.isParenthesizedExpression(expression) || Node.isAsExpression(expression) || Node.isNonNullExpression(expression) || Node.isSatisfiesExpression(expression)) return this.unwrapped(expression.getExpression());
2685
+ if (Node.isBinaryExpression(expression) && expression.getOperatorToken().getText() === ",") return this.unwrapped(expression.getRight());
2686
+ return expression;
2687
+ }
2688
+ };
2689
+ //#endregion
2341
2690
  //#region src/importer/ts-morph/readers/dependency-reader.ts
2342
2691
  const everything = ["*"];
2692
+ const loaderPackages = [
2693
+ "module",
2694
+ "node:module",
2695
+ "vm",
2696
+ "node:vm"
2697
+ ];
2343
2698
  var DependencyReader = class {
2344
2699
  project;
2345
2700
  scope;
2701
+ loaders = new LoaderReader();
2346
2702
  constructor(project, scope) {
2347
2703
  this.project = project;
2348
2704
  this.scope = scope;
@@ -2360,6 +2716,19 @@ var DependencyReader = class {
2360
2716
  for (const declaration of file.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration)) references.push(...this.fromImportEquals(declaration));
2361
2717
  for (const node of file.getDescendantsOfKind(SyntaxKind.ImportType)) references.push(this.fromImportType(node));
2362
2718
  for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) references.push(...this.fromCall(call));
2719
+ for (const load of this.loaders.read(file)) references.push({
2720
+ form: "dynamic load",
2721
+ line: load.line,
2722
+ names: [],
2723
+ specifier: load.loader,
2724
+ text: load.loader
2725
+ });
2726
+ for (const reference of file.getPathReferenceDirectives()) references.push({
2727
+ ...this.fromDirective(reference, file),
2728
+ isPath: true
2729
+ });
2730
+ for (const reference of file.getTypeReferenceDirectives()) references.push(this.fromDirective(reference, file));
2731
+ for (const declaration of file.getDescendantsOfKind(SyntaxKind.ModuleDeclaration)) references.push(...this.fromAugmentation(declaration));
2363
2732
  return references;
2364
2733
  }
2365
2734
  fromDeclaration(declaration, form) {
@@ -2399,6 +2768,22 @@ var DependencyReader = class {
2399
2768
  if (callee.isKind(SyntaxKind.Identifier) && callee.getText() === "require") return [this.reference(call, "require", argument, everything)];
2400
2769
  return [];
2401
2770
  }
2771
+ fromDirective(reference, file) {
2772
+ const line = file.getLineAndColumnAtPos(reference.getPos()).line;
2773
+ const specifier = reference.getFileName();
2774
+ return {
2775
+ form: "reference",
2776
+ line,
2777
+ names: everything,
2778
+ specifier,
2779
+ text: specifier
2780
+ };
2781
+ }
2782
+ fromAugmentation(declaration) {
2783
+ const name = declaration.getNameNode();
2784
+ if (!name.isKind(SyntaxKind.StringLiteral) || name.getLiteralValue().includes("*")) return [];
2785
+ return [this.reference(declaration, "augmentation", name, everything)];
2786
+ }
2402
2787
  reference(node, form, specifierNode, names) {
2403
2788
  const isLiteral = specifierNode.isKind(SyntaxKind.StringLiteral) || specifierNode.isKind(SyntaxKind.NoSubstitutionTemplateLiteral);
2404
2789
  return {
@@ -2410,15 +2795,20 @@ var DependencyReader = class {
2410
2795
  };
2411
2796
  }
2412
2797
  targetOf(reference, file) {
2798
+ if (reference.form === "dynamic load" || loaderPackages.includes(reference.specifier ?? "")) return {
2799
+ kind: "file",
2800
+ path: reference.text,
2801
+ visibility: "dynamic"
2802
+ };
2413
2803
  const directory = file.getDirectoryPath();
2414
2804
  if (reference.specifier === void 0) return {
2415
2805
  kind: "file",
2416
2806
  path: normalize(join(directory, reference.text)),
2417
2807
  visibility: "unresolved"
2418
2808
  };
2419
- const resolved = this.resolve(reference.specifier, file);
2809
+ const resolved = reference.isPath === true ? this.referencedPath(reference.specifier, directory) : this.resolve(reference.specifier, file);
2420
2810
  if (resolved !== void 0 && this.isInsideProject(resolved)) return this.fileTarget(resolved);
2421
- if (reference.specifier.startsWith(".")) return {
2811
+ if (reference.isPath === true || reference.specifier.startsWith(".")) return {
2422
2812
  kind: "file",
2423
2813
  path: normalize(join(directory, reference.specifier)),
2424
2814
  visibility: "unresolved"
@@ -2439,6 +2829,10 @@ var DependencyReader = class {
2439
2829
  const resolved = ts.resolveModuleName(specifier, file.getFilePath(), this.project.getCompilerOptions(), this.project.getModuleResolutionHost()).resolvedModule?.resolvedFileName;
2440
2830
  return resolved === void 0 ? void 0 : normalize(resolved);
2441
2831
  }
2832
+ referencedPath(specifier, directory) {
2833
+ const path = normalize(join(directory, specifier));
2834
+ return this.project.getModuleResolutionHost().fileExists(path) ? path : void 0;
2835
+ }
2442
2836
  packageNameOf(specifier) {
2443
2837
  const segments = specifier.split("/");
2444
2838
  const length = specifier.startsWith("@") ? 2 : 1;
@@ -2464,6 +2858,97 @@ var DisableReader = class {
2464
2858
  }
2465
2859
  };
2466
2860
  //#endregion
2861
+ //#region src/importer/ts-morph/readers/global-channel-reader.ts
2862
+ const globalObjects = [
2863
+ "globalThis",
2864
+ "global",
2865
+ "window",
2866
+ "self"
2867
+ ];
2868
+ const assignments = [
2869
+ SyntaxKind.EqualsToken,
2870
+ SyntaxKind.QuestionQuestionEqualsToken,
2871
+ SyntaxKind.BarBarEqualsToken,
2872
+ SyntaxKind.AmpersandAmpersandEqualsToken,
2873
+ SyntaxKind.PlusEqualsToken
2874
+ ];
2875
+ const reflectionWrites = [
2876
+ "set",
2877
+ "defineProperty",
2878
+ "deleteProperty"
2879
+ ];
2880
+ const objectWrites = [
2881
+ "defineProperty",
2882
+ "defineProperties",
2883
+ "assign"
2884
+ ];
2885
+ var GlobalChannelReader = class {
2886
+ read(file) {
2887
+ const channels = [];
2888
+ file.forEachDescendant((node) => {
2889
+ channels.push(...this.channelsAt(node));
2890
+ });
2891
+ return channels;
2892
+ }
2893
+ channelsAt(node) {
2894
+ if (Node.isPropertyAccessExpression(node)) return this.lookup(node, node.getExpression(), node.getName());
2895
+ if (Node.isElementAccessExpression(node)) {
2896
+ const key = node.getArgumentExpression();
2897
+ return key !== void 0 && Node.isStringLiteral(key) ? this.lookup(node, node.getExpression(), key.getLiteralValue()) : [];
2898
+ }
2899
+ if (Node.isCallExpression(node)) return this.reflected(node);
2900
+ return [];
2901
+ }
2902
+ lookup(access, object, name) {
2903
+ const global = this.globalObject(object);
2904
+ if (global === void 0 || global.getType().getProperty(name) !== void 0) return [];
2905
+ return [new GlobalChannel(access.getStartLineNumber(), `${global.getText()}.${name}`, this.accessOf(access))];
2906
+ }
2907
+ reflected(call) {
2908
+ const callee = call.getExpression();
2909
+ if (!Node.isPropertyAccessExpression(callee)) return [];
2910
+ const owner = callee.getExpression().getText();
2911
+ const method = callee.getName();
2912
+ const [target, key] = call.getArguments();
2913
+ const global = target === void 0 ? void 0 : this.globalObject(target);
2914
+ if (global === void 0) return [];
2915
+ if (owner === "Reflect" && key !== void 0 && Node.isStringLiteral(key)) {
2916
+ const access = reflectionWrites.includes(method) ? "writes" : "reads";
2917
+ return this.undeclared(global, [key.getLiteralValue()], call.getStartLineNumber(), access);
2918
+ }
2919
+ if (owner === "Object" && objectWrites.includes(method) && key !== void 0) {
2920
+ const names = Node.isStringLiteral(key) ? [key.getLiteralValue()] : this.propertyNames(key);
2921
+ return this.undeclared(global, names, call.getStartLineNumber(), "writes");
2922
+ }
2923
+ return [];
2924
+ }
2925
+ undeclared(global, names, line, access) {
2926
+ const type = global.getType();
2927
+ return names.filter((name) => type.getProperty(name) === void 0).map((name) => new GlobalChannel(line, `${global.getText()}.${name}`, access));
2928
+ }
2929
+ propertyNames(node) {
2930
+ if (!Node.isObjectLiteralExpression(node)) return [];
2931
+ const names = [];
2932
+ for (const property of node.getProperties()) if (Node.isPropertyAssignment(property) || Node.isShorthandPropertyAssignment(property) || Node.isMethodDeclaration(property)) names.push(property.getName());
2933
+ return names;
2934
+ }
2935
+ globalObject(node) {
2936
+ const bare = this.unwrapped(node);
2937
+ if (!Node.isIdentifier(bare) || !globalObjects.includes(bare.getText())) return;
2938
+ const declaration = bare.getSymbol()?.getDeclarations()[0];
2939
+ return declaration === void 0 || declaration.getSourceFile().isDeclarationFile() ? bare : void 0;
2940
+ }
2941
+ accessOf(access) {
2942
+ const parent = access.getParent();
2943
+ if (Node.isBinaryExpression(parent) && parent.getLeft() === access && assignments.includes(parent.getOperatorToken().getKind())) return "writes";
2944
+ return Node.isDeleteExpression(parent) ? "writes" : "reads";
2945
+ }
2946
+ unwrapped(node) {
2947
+ if (Node.isParenthesizedExpression(node) || Node.isAsExpression(node) || Node.isNonNullExpression(node) || Node.isSatisfiesExpression(node)) return this.unwrapped(node.getExpression());
2948
+ return node;
2949
+ }
2950
+ };
2951
+ //#endregion
2467
2952
  //#region src/importer/ts-morph/readers/global-reader.ts
2468
2953
  const ecmaScriptLibrary = /^lib\.(es|decorators).*\.d\.ts$/;
2469
2954
  var GlobalReader = class {
@@ -2535,6 +3020,70 @@ var GlobalReader = class {
2535
3020
  }
2536
3021
  };
2537
3022
  //#endregion
3023
+ //#region src/importer/ts-morph/readers/module-reach-reader.ts
3024
+ const candidates = [
3025
+ SyntaxKind.Identifier,
3026
+ SyntaxKind.PropertyAccessExpression,
3027
+ SyntaxKind.ElementAccessExpression,
3028
+ SyntaxKind.CallExpression,
3029
+ SyntaxKind.NewExpression
3030
+ ];
3031
+ var ModuleReachReader = class {
3032
+ types;
3033
+ scope;
3034
+ constructor(types, scope) {
3035
+ this.types = types;
3036
+ this.scope = scope;
3037
+ }
3038
+ read(file) {
3039
+ if (!this.scope.readsWiring(file.getFilePath())) return [];
3040
+ const reaches = [];
3041
+ file.forEachDescendant((node) => {
3042
+ const reach = this.reachAt(node, file);
3043
+ if (reach !== void 0) reaches.push(reach);
3044
+ });
3045
+ return reaches;
3046
+ }
3047
+ reachAt(node, file) {
3048
+ if (!candidates.includes(node.getKind()) || this.isName(node)) return;
3049
+ const type = node.getType();
3050
+ if (!type.isClass()) return;
3051
+ const module = this.types.classTypeOf(type);
3052
+ if (module.declaredIn === void 0 || module.declaredIn === file.getFilePath() || !this.scope.readsWiring(module.declaredIn)) return;
3053
+ const means = this.meansOf(node);
3054
+ return means === void 0 ? void 0 : new ModuleReach(node.getStartLineNumber(), node.getText(), means, module);
3055
+ }
3056
+ meansOf(node) {
3057
+ const { child, parent } = this.usageOf(node);
3058
+ if (parent === void 0) return;
3059
+ if (Node.isElementAccessExpression(parent) && parent.getExpression() === child) return "brackets";
3060
+ if (Node.isSpreadAssignment(parent) || Node.isSpreadElement(parent)) return "a spread";
3061
+ if (Node.isVariableDeclaration(parent) && !Node.isIdentifier(parent.getNameNode())) return "destructuring";
3062
+ if ((Node.isCallExpression(parent) || Node.isNewExpression(parent)) && parent.getArguments().includes(child)) return this.isModuleConstruction(parent) ? void 0 : parent.getExpression().getText();
3063
+ }
3064
+ usageOf(node) {
3065
+ let child = node;
3066
+ let parent = node.getParent();
3067
+ while (parent !== void 0 && (Node.isParenthesizedExpression(parent) || Node.isAsExpression(parent) || Node.isNonNullExpression(parent) || Node.isSatisfiesExpression(parent))) {
3068
+ child = parent;
3069
+ parent = parent.getParent();
3070
+ }
3071
+ return {
3072
+ child,
3073
+ parent
3074
+ };
3075
+ }
3076
+ isModuleConstruction(call) {
3077
+ if (!Node.isNewExpression(call)) return false;
3078
+ const declaredIn = call.getType().getSymbol()?.getDeclarations()[0]?.getSourceFile().getFilePath();
3079
+ return declaredIn !== void 0 && this.scope.readsWiring(declaredIn);
3080
+ }
3081
+ isName(node) {
3082
+ const parent = node.getParent();
3083
+ return parent !== void 0 && Node.hasName(parent) && parent.getNameNode() === node;
3084
+ }
3085
+ };
3086
+ //#endregion
2538
3087
  //#region src/importer/ts-morph/readers/statement-reader.ts
2539
3088
  const declarative = /* @__PURE__ */ new Set([
2540
3089
  SyntaxKind.ImportDeclaration,
@@ -2624,6 +3173,17 @@ var ThrowReader = class {
2624
3173
  };
2625
3174
  //#endregion
2626
3175
  //#region src/importer/ts-morph/readers/type-reader.ts
3176
+ const collections = [
3177
+ "Map",
3178
+ "Set",
3179
+ "WeakMap",
3180
+ "WeakSet"
3181
+ ];
3182
+ const readonlyCollections = [
3183
+ "ReadonlyMap",
3184
+ "ReadonlySet",
3185
+ "ReadonlyArray"
3186
+ ];
2627
3187
  var TypeReader = class {
2628
3188
  packageNames;
2629
3189
  constructor(packageNames) {
@@ -2646,6 +3206,24 @@ var TypeReader = class {
2646
3206
  packageName: void 0
2647
3207
  } : this.namedSymbol(symbol);
2648
3208
  }
3209
+ holdsCollection(type) {
3210
+ if (type.isArray() || type.isTuple()) return !type.isReadonlyArray();
3211
+ if (collections.includes(type.getSymbol()?.getName() ?? "")) return true;
3212
+ if (type.getStringIndexType() !== void 0 || type.getNumberIndexType() !== void 0) return true;
3213
+ return type.isAnonymous() && type.getCallSignatures().length === 0 && type.getConstructSignatures().length === 0;
3214
+ }
3215
+ opaqueValueIn(type) {
3216
+ return this.findsOpaqueValue(type, /* @__PURE__ */ new Set());
3217
+ }
3218
+ holdsFunction(type) {
3219
+ return this.findsFunction(type, /* @__PURE__ */ new Set());
3220
+ }
3221
+ erasedNameOf(type) {
3222
+ const settled = this.settled(type);
3223
+ if (settled.isAny()) return "any";
3224
+ if (settled.isUnknown()) return "unknown";
3225
+ return (settled.getFlags() & ts.TypeFlags.NonPrimitive) === 0 ? void 0 : "object";
3226
+ }
2649
3227
  returnShapeOf(type) {
2650
3228
  if (type.getSymbol()?.getName() === "Promise") {
2651
3229
  const [inner] = type.getTypeArguments();
@@ -2658,6 +3236,52 @@ var TypeReader = class {
2658
3236
  union: members.map((member) => this.namedTypeOf(member))
2659
3237
  };
2660
3238
  }
3239
+ findsOpaqueValue(type, seen) {
3240
+ if (seen.has(type.compilerType)) return;
3241
+ seen.add(type.compilerType);
3242
+ if (type.isAny() || type.isUnknown()) return type.getText();
3243
+ if (type.isUnion()) return this.firstOpaque(type.getUnionTypes(), seen);
3244
+ if (type.isIntersection()) return this.firstOpaque(type.getIntersectionTypes(), seen);
3245
+ if (!type.isObject() || type.isClass()) return;
3246
+ if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) return "a function";
3247
+ if (type.isArray() || type.isTuple()) {
3248
+ if (!type.isReadonlyArray()) return "a mutable array";
3249
+ return this.firstOpaque(type.isTuple() ? type.getTupleElements() : type.getTypeArguments(), seen);
3250
+ }
3251
+ if (readonlyCollections.includes(type.getSymbol()?.getName() ?? "")) return this.firstOpaque(type.getTypeArguments(), seen);
3252
+ return "an object";
3253
+ }
3254
+ firstOpaque(types, seen) {
3255
+ for (const inner of types) {
3256
+ const opaque = this.findsOpaqueValue(inner, seen);
3257
+ if (opaque !== void 0) return opaque;
3258
+ }
3259
+ }
3260
+ findsFunction(type, seen) {
3261
+ if (seen.has(type.compilerType)) return false;
3262
+ seen.add(type.compilerType);
3263
+ if (type.isUnion()) return type.getUnionTypes().some((member) => this.findsFunction(member, seen));
3264
+ if (type.isIntersection()) return type.getIntersectionTypes().some((member) => this.findsFunction(member, seen));
3265
+ if (type.isClass()) return false;
3266
+ if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) return true;
3267
+ return this.dataTypesOf(type).some((inner) => this.findsFunction(inner, seen));
3268
+ }
3269
+ dataTypesOf(type) {
3270
+ const inner = [
3271
+ ...type.getTypeArguments(),
3272
+ ...type.getAliasTypeArguments(),
3273
+ ...type.getTupleElements()
3274
+ ];
3275
+ if (this.isDeclaredByProject(type)) for (const property of type.getProperties()) {
3276
+ const declaration = property.getDeclarations()[0];
3277
+ if (declaration !== void 0) inner.push(declaration.getType());
3278
+ }
3279
+ return inner;
3280
+ }
3281
+ settled(type) {
3282
+ const [inner] = type.getTypeArguments();
3283
+ return type.getSymbol()?.getName() === "Promise" && inner !== void 0 ? this.settled(inner) : type;
3284
+ }
2661
3285
  lineageOf(type) {
2662
3286
  const classType = type.getTargetType() ?? type;
2663
3287
  const symbol = classType.getSymbol();
@@ -2710,17 +3334,99 @@ var TypeReader = class {
2710
3334
  }
2711
3335
  };
2712
3336
  //#endregion
3337
+ //#region src/importer/ts-morph/readers/wiring-reader.ts
3338
+ const references = [SyntaxKind.Identifier, SyntaxKind.PropertyAccessExpression];
3339
+ var WiringReader = class {
3340
+ types;
3341
+ scope;
3342
+ constructor(types, scope) {
3343
+ this.types = types;
3344
+ this.scope = scope;
3345
+ }
3346
+ read(file) {
3347
+ if (!this.scope.readsWiring(file.getFilePath())) return [];
3348
+ const wirings = [];
3349
+ for (const site of this.sitesOf(file)) {
3350
+ const receiver = this.declaringFileOf(site.receiver);
3351
+ if (receiver === void 0) continue;
3352
+ for (const value of site.values) for (const reference of this.referencesIn(value)) wirings.push(new Wiring(reference.getStartLineNumber(), receiver, this.linksOf(reference)));
3353
+ }
3354
+ return wirings;
3355
+ }
3356
+ sitesOf(file) {
3357
+ const sites = [];
3358
+ file.forEachDescendant((node) => {
3359
+ if (Node.isCallExpression(node) || Node.isNewExpression(node)) sites.push({
3360
+ receiver: node.getExpression(),
3361
+ values: node.getArguments()
3362
+ });
3363
+ else if (Node.isBinaryExpression(node) && node.getOperatorToken().isKind(SyntaxKind.EqualsToken)) sites.push({
3364
+ receiver: node.getLeft(),
3365
+ values: [node.getRight()]
3366
+ });
3367
+ else if ((Node.isVariableDeclaration(node) || Node.isPropertyDeclaration(node)) && node.getTypeNode() !== void 0) {
3368
+ const initializer = node.getInitializer();
3369
+ sites.push({
3370
+ receiver: node.getTypeNode(),
3371
+ values: initializer === void 0 ? [] : [initializer]
3372
+ });
3373
+ }
3374
+ });
3375
+ return sites;
3376
+ }
3377
+ referencesIn(value) {
3378
+ const found = [];
3379
+ const visit = (node) => {
3380
+ if (this.isOutermostReference(node)) found.push(node);
3381
+ node.forEachChild(visit);
3382
+ };
3383
+ visit(value);
3384
+ return found;
3385
+ }
3386
+ isOutermostReference(node) {
3387
+ if (!references.includes(node.getKind())) return false;
3388
+ const parent = node.getParent();
3389
+ if (parent === void 0 || Node.isPropertyAccessExpression(parent)) return false;
3390
+ if (Node.isTypeNode(parent) || Node.isQualifiedName(parent) || Node.isExpressionWithTypeArguments(parent)) return false;
3391
+ return Node.isShorthandPropertyAssignment(parent) || !(Node.hasName(parent) && parent.getNameNode() === node);
3392
+ }
3393
+ linksOf(reference) {
3394
+ if (Node.isPropertyAccessExpression(reference)) return [...this.linksOf(reference.getExpression()), this.link(reference, reference.getNameNode())];
3395
+ if (Node.isIdentifier(reference)) return [this.link(reference, reference)];
3396
+ return [];
3397
+ }
3398
+ link(expression, name) {
3399
+ return {
3400
+ declaredIn: this.declaringFileOf(name),
3401
+ text: expression.getText(),
3402
+ types: this.types.classTypesIn(expression.getType())
3403
+ };
3404
+ }
3405
+ declaringFileOf(node) {
3406
+ if (node === void 0) return;
3407
+ const named = Node.isTypeNode(node) ? node.getType().getAliasSymbol() ?? node.getType().getSymbol() : this.nameOf(node).getSymbol();
3408
+ const declaration = (named?.isAlias() === true ? named.getAliasedSymbol() : named)?.getDeclarations()[0];
3409
+ return declaration === void 0 ? void 0 : normalize(declaration.getSourceFile().getFilePath());
3410
+ }
3411
+ nameOf(node) {
3412
+ return Node.isPropertyAccessExpression(node) ? node.getNameNode() : node;
3413
+ }
3414
+ };
3415
+ //#endregion
2713
3416
  //#region src/importer/ts-morph/ts-morph-importer.ts
2714
3417
  var TsMorphImporter = class TsMorphImporter extends Importer {
2715
3418
  sources;
2716
3419
  classes;
3420
+ types;
2717
3421
  statements = new StatementReader();
2718
3422
  disables = new DisableReader();
2719
3423
  throws = new ThrowReader();
3424
+ globalChannels = new GlobalChannelReader();
2720
3425
  constructor(sources) {
2721
3426
  super();
2722
3427
  this.sources = sources;
2723
- this.classes = new ClassReader(new TypeReader(new PackageNames(sources.getFileSystem())));
3428
+ this.types = new TypeReader(new PackageNames(sources.getFileSystem()));
3429
+ this.classes = new ClassReader(this.types);
2724
3430
  }
2725
3431
  static fromTsConfig(tsConfigFilePath) {
2726
3432
  if (!existsSync(tsConfigFilePath)) throw new Error(`No TypeScript configuration found at ${tsConfigFilePath}: pass --tsconfig, or set tsconfig in alveolus.config.ts.`);
@@ -2732,22 +3438,33 @@ var TsMorphImporter = class TsMorphImporter extends Importer {
2732
3438
  }
2733
3439
  read(scope) {
2734
3440
  const dependencies = new DependencyReader(this.sources, scope);
3441
+ const wirings = new WiringReader(this.types, scope);
3442
+ const reaches = new ModuleReachReader(this.types, scope);
2735
3443
  const globals = new GlobalReader(scope.projectDir);
2736
3444
  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));
3445
+ for (const file of this.sources.getSourceFiles()) if (this.isInside(scope.rootDir, file.getFilePath()) && !scope.isIgnored(file.getFilePath())) files.push(this.readFile(file, {
3446
+ dependencies,
3447
+ globals,
3448
+ reaches,
3449
+ wirings
3450
+ }));
2738
3451
  return new Project$1(scope.projectDir, files);
2739
3452
  }
2740
- readFile(file, dependencies, globals) {
3453
+ readFile(file, readers) {
3454
+ const { dependencies, globals, reaches, wirings } = readers;
2741
3455
  const globalReferences = globals.read(file);
2742
3456
  return new SourceFile({
2743
3457
  classes: file.getClasses().map((declaration) => this.classes.read(declaration)),
2744
3458
  dependencies: dependencies.read(file, globalReferences),
2745
3459
  disables: this.disables.read(file),
3460
+ globalChannels: this.globalChannels.read(file),
2746
3461
  globals: this.globalUsesOf(globalReferences),
3462
+ moduleReaches: reaches.read(file),
2747
3463
  path: normalize(file.getFilePath()),
2748
3464
  statements: this.statements.read(file),
2749
3465
  text: file.getFullText(),
2750
- throws: this.throws.read(file)
3466
+ throws: this.throws.read(file),
3467
+ wirings: wirings.read(file)
2751
3468
  });
2752
3469
  }
2753
3470
  globalUsesOf(references) {
@@ -3042,4 +3759,4 @@ var Docs = class Docs {
3042
3759
  //#endregion
3043
3760
  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
3761
 
3045
- //# sourceMappingURL=docs-DcFgskuN.mjs.map
3762
+ //# sourceMappingURL=docs-B6M9DxTM.mjs.map