archtracker-mcp 0.2.1 → 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/dist/cli/index.js CHANGED
@@ -182,12 +182,50 @@ function stripComments(content, style) {
182
182
  return stripRuby(content);
183
183
  case "php":
184
184
  return stripPhp(content);
185
+ default: {
186
+ const _exhaustive = style;
187
+ throw new Error(`Unknown comment style: ${_exhaustive}`);
188
+ }
185
189
  }
186
190
  }
187
191
  function stripCStyle(content) {
188
192
  let result = "";
189
193
  let i = 0;
190
194
  while (i < content.length) {
195
+ if (content[i] === "r" && i + 1 < content.length) {
196
+ let hashes = 0;
197
+ let j = i + 1;
198
+ while (j < content.length && content[j] === "#") {
199
+ hashes++;
200
+ j++;
201
+ }
202
+ if (j < content.length && content[j] === '"') {
203
+ for (let k = i; k <= j; k++) {
204
+ result += " ";
205
+ }
206
+ i = j + 1;
207
+ while (i < content.length) {
208
+ if (content[i] === '"') {
209
+ let matchHashes = 0;
210
+ let m = i + 1;
211
+ while (m < content.length && content[m] === "#" && matchHashes < hashes) {
212
+ matchHashes++;
213
+ m++;
214
+ }
215
+ if (matchHashes === hashes) {
216
+ for (let k = i; k < m; k++) {
217
+ result += " ";
218
+ }
219
+ i = m;
220
+ break;
221
+ }
222
+ }
223
+ result += content[i] === "\n" ? "\n" : " ";
224
+ i++;
225
+ }
226
+ continue;
227
+ }
228
+ }
191
229
  if (content[i] === "/" && content[i + 1] === "/") {
192
230
  while (i < content.length && content[i] !== "\n") {
193
231
  result += " ";
@@ -230,6 +268,24 @@ function stripCStyle(content) {
230
268
  result += content[i];
231
269
  i++;
232
270
  }
271
+ } else if (content[i] === "'") {
272
+ result += content[i];
273
+ i++;
274
+ while (i < content.length && content[i] !== "'") {
275
+ if (content[i] === "\\" && i + 1 < content.length) {
276
+ result += content[i];
277
+ i++;
278
+ result += content[i];
279
+ i++;
280
+ } else {
281
+ result += content[i];
282
+ i++;
283
+ }
284
+ }
285
+ if (i < content.length) {
286
+ result += content[i];
287
+ i++;
288
+ }
233
289
  } else if (content[i] === "`") {
234
290
  result += " ";
235
291
  i++;
@@ -298,31 +354,62 @@ function stripPython(content) {
298
354
  let result = "";
299
355
  let i = 0;
300
356
  while (i < content.length) {
301
- if (content[i] === '"' && content[i + 1] === '"' && content[i + 2] === '"' || content[i] === "'" && content[i + 1] === "'" && content[i + 2] === "'") {
302
- const quote = content[i];
303
- const tripleQuote = quote + quote + quote;
357
+ let prefixLen = 0;
358
+ let isRaw = false;
359
+ if (i < content.length) {
360
+ const c0 = content[i];
361
+ const c1 = i + 1 < content.length ? content[i + 1] : "";
362
+ const c2 = i + 2 < content.length ? content[i + 2] : "";
363
+ if ((c0 === "r" || c0 === "R" || c0 === "b" || c0 === "B" || c0 === "f" || c0 === "F") && (c1 === "r" || c1 === "R" || c1 === "b" || c1 === "B" || c1 === "f" || c1 === "F") && (c2 === '"' || c2 === "'")) {
364
+ const pair = (c0 + c1).toLowerCase();
365
+ if (pair === "rb" || pair === "br" || pair === "fr" || pair === "rf") {
366
+ prefixLen = 2;
367
+ isRaw = pair.includes("r");
368
+ }
369
+ }
370
+ if (prefixLen === 0 && (c0 === "r" || c0 === "R" || c0 === "b" || c0 === "B" || c0 === "f" || c0 === "F") && (c1 === '"' || c1 === "'")) {
371
+ prefixLen = 1;
372
+ isRaw = c0 === "r" || c0 === "R";
373
+ }
374
+ }
375
+ const quoteStart = i + prefixLen;
376
+ if (quoteStart + 2 < content.length && (content[quoteStart] === '"' || content[quoteStart] === "'") && content[quoteStart + 1] === content[quoteStart] && content[quoteStart + 2] === content[quoteStart]) {
377
+ const quote = content[quoteStart];
378
+ for (let k = 0; k < prefixLen; k++) {
379
+ result += " ";
380
+ }
304
381
  result += " ";
305
- i += 3;
382
+ i = quoteStart + 3;
306
383
  while (i < content.length) {
307
384
  if (content[i] === quote && content[i + 1] === quote && content[i + 2] === quote) {
308
385
  result += " ";
309
386
  i += 3;
310
387
  break;
311
388
  }
312
- result += content[i] === "\n" ? "\n" : " ";
313
- i++;
389
+ if (!isRaw && content[i] === "\\" && i + 1 < content.length) {
390
+ result += " ";
391
+ i++;
392
+ result += content[i] === "\n" ? "\n" : " ";
393
+ i++;
394
+ } else {
395
+ result += content[i] === "\n" ? "\n" : " ";
396
+ i++;
397
+ }
314
398
  }
315
- } else if (content[i] === "#") {
399
+ } else if (prefixLen === 0 && content[i] === "#") {
316
400
  while (i < content.length && content[i] !== "\n") {
317
401
  result += " ";
318
402
  i++;
319
403
  }
320
- } else if (content[i] === '"' || content[i] === "'") {
321
- const quote = content[i];
322
- result += content[i];
323
- i++;
404
+ } else if (quoteStart < content.length && (content[quoteStart] === '"' || content[quoteStart] === "'") && (prefixLen > 0 || content[i] === '"' || content[i] === "'")) {
405
+ const quote = content[quoteStart];
406
+ for (let k = i; k < quoteStart; k++) {
407
+ result += content[k];
408
+ }
409
+ result += content[quoteStart];
410
+ i = quoteStart + 1;
324
411
  while (i < content.length && content[i] !== quote) {
325
- if (content[i] === "\\" && i + 1 < content.length) {
412
+ if (!isRaw && content[i] === "\\" && i + 1 < content.length) {
326
413
  result += content[i++];
327
414
  result += content[i++];
328
415
  } else if (content[i] === "\n") {
@@ -349,13 +436,13 @@ function stripRuby(content) {
349
436
  const result = [];
350
437
  let inBlock = false;
351
438
  for (const line of lines) {
352
- if (!inBlock && line.startsWith("=begin")) {
439
+ if (!inBlock && /^=begin(\s|$)/.test(line)) {
353
440
  inBlock = true;
354
441
  result.push(" ".repeat(line.length));
355
442
  continue;
356
443
  }
357
444
  if (inBlock) {
358
- if (line.startsWith("=end")) {
445
+ if (/^=end(\s|$)/.test(line)) {
359
446
  inBlock = false;
360
447
  }
361
448
  result.push(" ".repeat(line.length));
@@ -370,18 +457,8 @@ function stripRuby(content) {
370
457
  } else if (line[i] === '"') {
371
458
  processed += line[i];
372
459
  i++;
373
- while (i < line.length && line[i] !== '"') {
374
- if (line[i] === "\\" && i + 1 < line.length) {
375
- processed += line[i++];
376
- processed += line[i++];
377
- } else {
378
- processed += line[i++];
379
- }
380
- }
381
- if (i < line.length) {
382
- processed += line[i];
383
- i++;
384
- }
460
+ processed = scanRubyDoubleQuotedString(line, i, processed);
461
+ i = scanRubyDoubleQuotedStringIndex(line, i);
385
462
  } else if (line[i] === "'") {
386
463
  processed += line[i];
387
464
  i++;
@@ -406,10 +483,141 @@ function stripRuby(content) {
406
483
  }
407
484
  return result.join("\n");
408
485
  }
486
+ function scanRubyDoubleQuotedString(line, startI, processed) {
487
+ let i = startI;
488
+ while (i < line.length && line[i] !== '"') {
489
+ if (line[i] === "\\" && i + 1 < line.length) {
490
+ processed += line[i++];
491
+ processed += line[i++];
492
+ } else if (line[i] === "#" && i + 1 < line.length && line[i + 1] === "{") {
493
+ processed += line[i++];
494
+ processed += line[i++];
495
+ let depth = 1;
496
+ while (i < line.length && depth > 0) {
497
+ if (line[i] === "{") {
498
+ depth++;
499
+ processed += line[i++];
500
+ } else if (line[i] === "}") {
501
+ depth--;
502
+ processed += line[i++];
503
+ } else if (line[i] === "\\" && i + 1 < line.length) {
504
+ processed += line[i++];
505
+ processed += line[i++];
506
+ } else {
507
+ processed += line[i++];
508
+ }
509
+ }
510
+ } else {
511
+ processed += line[i++];
512
+ }
513
+ }
514
+ if (i < line.length) {
515
+ processed += line[i];
516
+ }
517
+ return processed;
518
+ }
519
+ function scanRubyDoubleQuotedStringIndex(line, startI) {
520
+ let i = startI;
521
+ while (i < line.length && line[i] !== '"') {
522
+ if (line[i] === "\\" && i + 1 < line.length) {
523
+ i += 2;
524
+ } else if (line[i] === "#" && i + 1 < line.length && line[i + 1] === "{") {
525
+ i += 2;
526
+ let depth = 1;
527
+ while (i < line.length && depth > 0) {
528
+ if (line[i] === "{") {
529
+ depth++;
530
+ i++;
531
+ } else if (line[i] === "}") {
532
+ depth--;
533
+ i++;
534
+ } else if (line[i] === "\\" && i + 1 < line.length) {
535
+ i += 2;
536
+ } else {
537
+ i++;
538
+ }
539
+ }
540
+ } else {
541
+ i++;
542
+ }
543
+ }
544
+ if (i < line.length) {
545
+ i++;
546
+ }
547
+ return i;
548
+ }
409
549
  function stripPhp(content) {
410
550
  let result = "";
411
551
  let i = 0;
412
552
  while (i < content.length) {
553
+ if (content[i] === "<" && content[i + 1] === "<" && content[i + 2] === "<") {
554
+ const heredocStart = i;
555
+ let j = i + 3;
556
+ let isNowdoc = false;
557
+ if (j < content.length && content[j] === "'") {
558
+ isNowdoc = true;
559
+ j++;
560
+ }
561
+ const identStart = j;
562
+ while (j < content.length && /[A-Za-z0-9_]/.test(content[j])) {
563
+ j++;
564
+ }
565
+ const identifier = content.slice(identStart, j);
566
+ if (identifier.length > 0) {
567
+ if (isNowdoc && j < content.length && content[j] === "'") {
568
+ j++;
569
+ }
570
+ let validHeredoc = false;
571
+ let lineEnd = j;
572
+ if (lineEnd < content.length && content[lineEnd] === "\n") {
573
+ validHeredoc = true;
574
+ }
575
+ if (validHeredoc) {
576
+ for (let k = heredocStart; k <= lineEnd; k++) {
577
+ if (content[k] === "\n") {
578
+ result += "\n";
579
+ } else {
580
+ result += content[k];
581
+ }
582
+ }
583
+ i = lineEnd + 1;
584
+ let found = false;
585
+ while (i < content.length && !found) {
586
+ const lineStart = i;
587
+ let lineEndIdx = i;
588
+ while (lineEndIdx < content.length && content[lineEndIdx] !== "\n") {
589
+ lineEndIdx++;
590
+ }
591
+ const currentLine = content.slice(lineStart, lineEndIdx);
592
+ const trimmedLine = currentLine.trimStart();
593
+ if (trimmedLine === identifier || trimmedLine === identifier + ";" || trimmedLine === identifier + "," || trimmedLine === identifier + ");" || trimmedLine === identifier + ")" || trimmedLine.startsWith(identifier + ";") || trimmedLine === identifier) {
594
+ for (let k = lineStart; k < lineEndIdx; k++) {
595
+ result += content[k];
596
+ }
597
+ i = lineEndIdx;
598
+ if (i < content.length && content[i] === "\n") {
599
+ result += "\n";
600
+ i++;
601
+ }
602
+ found = true;
603
+ } else {
604
+ for (let k = lineStart; k < lineEndIdx; k++) {
605
+ result += " ";
606
+ }
607
+ i = lineEndIdx;
608
+ if (i < content.length && content[i] === "\n") {
609
+ result += "\n";
610
+ i++;
611
+ }
612
+ }
613
+ }
614
+ continue;
615
+ }
616
+ }
617
+ result += content[i];
618
+ i++;
619
+ continue;
620
+ }
413
621
  if (content[i] === "/" && content[i + 1] === "/" || content[i] === "#") {
414
622
  while (i < content.length && content[i] !== "\n") {
415
623
  result += " ";
@@ -482,6 +690,7 @@ var RegexEngine = class {
482
690
  "\\.archtracker"
483
691
  ].map((p) => new RegExp(p));
484
692
  const projectFiles = await this.collectFiles(
693
+ absRootDir,
485
694
  absRootDir,
486
695
  excludePatterns,
487
696
  options.maxDepth ?? 0
@@ -505,6 +714,7 @@ var RegexEngine = class {
505
714
  try {
506
715
  content = await readFile(filePath, "utf-8");
507
716
  } catch {
717
+ if (files[relSource]) files[relSource].exists = false;
508
718
  continue;
509
719
  }
510
720
  const stripped = stripComments(content, this.config.commentStyle);
@@ -548,7 +758,8 @@ var RegexEngine = class {
548
758
  }
549
759
  const imports = [];
550
760
  for (const pattern of this.config.importPatterns) {
551
- const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
761
+ const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : pattern.regex.flags + "g";
762
+ const regex = new RegExp(pattern.regex.source, flags);
552
763
  let match;
553
764
  while ((match = regex.exec(content)) !== null) {
554
765
  if (match[1]) {
@@ -558,7 +769,7 @@ var RegexEngine = class {
558
769
  }
559
770
  return imports;
560
771
  }
561
- async collectFiles(dir, excludePatterns, maxDepth, currentDepth = 0) {
772
+ async collectFiles(dir, absRootDir, excludePatterns, maxDepth, currentDepth = 0) {
562
773
  if (maxDepth > 0 && currentDepth >= maxDepth) return [];
563
774
  const results = [];
564
775
  let entries;
@@ -569,7 +780,7 @@ var RegexEngine = class {
569
780
  }
570
781
  for (const entry of entries) {
571
782
  const fullPath = join(dir, entry.name);
572
- const relPath = relative(dir, fullPath);
783
+ const relPath = relative(absRootDir, fullPath);
573
784
  if (excludePatterns.some(
574
785
  (p) => p.test(entry.name) || p.test(relPath) || p.test(fullPath)
575
786
  )) {
@@ -579,6 +790,7 @@ var RegexEngine = class {
579
790
  if (entry.name.startsWith(".")) continue;
580
791
  const sub = await this.collectFiles(
581
792
  fullPath,
793
+ absRootDir,
582
794
  excludePatterns,
583
795
  maxDepth,
584
796
  currentDepth + 1
@@ -611,14 +823,21 @@ var MARKERS = [
611
823
  { file: "pom.xml", language: "java" },
612
824
  { file: "build.gradle", language: "java" },
613
825
  { file: "build.gradle.kts", language: "kotlin" },
826
+ { file: "build.sbt", language: "scala" },
827
+ { file: "build.sc", language: "scala" },
614
828
  { file: "Package.swift", language: "swift" },
615
829
  { file: "Gemfile", language: "ruby" },
616
830
  { file: "composer.json", language: "php" },
831
+ { file: "pubspec.yaml", language: "dart" },
617
832
  { file: "CMakeLists.txt", language: "c-cpp" },
618
833
  { file: "Makefile", language: "c-cpp" },
619
834
  { file: "package.json", language: "javascript" },
620
835
  { file: "tsconfig.json", language: "javascript" }
621
836
  ];
837
+ var EXT_MARKERS = [
838
+ [".sln", "c-sharp"],
839
+ [".csproj", "c-sharp"]
840
+ ];
622
841
  var EXT_MAP = {
623
842
  ".ts": "javascript",
624
843
  ".tsx": "javascript",
@@ -640,7 +859,11 @@ var EXT_MAP = {
640
859
  ".php": "php",
641
860
  ".swift": "swift",
642
861
  ".kt": "kotlin",
643
- ".kts": "kotlin"
862
+ ".kts": "kotlin",
863
+ ".cs": "c-sharp",
864
+ ".dart": "dart",
865
+ ".scala": "scala",
866
+ ".sc": "scala"
644
867
  };
645
868
  async function detectLanguage(rootDir) {
646
869
  for (const marker of MARKERS) {
@@ -652,6 +875,16 @@ async function detectLanguage(rootDir) {
652
875
  } catch {
653
876
  }
654
877
  }
878
+ try {
879
+ const topEntries = await readdir2(rootDir, { withFileTypes: true });
880
+ for (const entry of topEntries) {
881
+ if (!entry.isFile()) continue;
882
+ for (const [ext, lang] of EXT_MARKERS) {
883
+ if (entry.name.endsWith(ext)) return lang;
884
+ }
885
+ }
886
+ } catch {
887
+ }
655
888
  const counts = /* @__PURE__ */ new Map();
656
889
  try {
657
890
  await scanExtensions(rootDir, counts, 2, 0);
@@ -704,10 +937,27 @@ var python = {
704
937
  commentStyle: "python",
705
938
  importPatterns: [
706
939
  // from package.module import something
707
- { regex: /^from\s+(\.[\w.]*|\w[\w.]*)\s+import\b/gm },
708
- // import package.module
709
- { regex: /^import\s+([\w.]+)/gm }
940
+ { regex: /^from\s+(\.[\w.]*|\w[\w.]*)\s+import\b/gm }
941
+ // import package.module (handled by extractImports for multi-module case)
710
942
  ],
943
+ // Bug #1 fix: custom extractImports to handle `import a, b, c`
944
+ extractImports(content) {
945
+ const imports = [];
946
+ const fromRegex = /^from\s+(\.[\w.]*|\w[\w.]*)\s+import\b/gm;
947
+ let match;
948
+ while ((match = fromRegex.exec(content)) !== null) {
949
+ imports.push(match[1]);
950
+ }
951
+ const importRegex = /^import\s+([\w.]+(?:\s*,\s*[\w.]+)*)/gm;
952
+ while ((match = importRegex.exec(content)) !== null) {
953
+ const modules = match[1].split(",");
954
+ for (const mod of modules) {
955
+ const trimmed = mod.trim();
956
+ if (trimmed) imports.push(trimmed);
957
+ }
958
+ }
959
+ return imports;
960
+ },
711
961
  resolveImport(importPath, sourceFile, rootDir, projectFiles) {
712
962
  if (importPath.startsWith(".")) {
713
963
  const dots = importPath.match(/^\.+/)?.[0].length ?? 1;
@@ -744,6 +994,16 @@ var rust = {
744
994
  const body = match[1].trim();
745
995
  extractRustUsePaths(body, "", imports);
746
996
  }
997
+ const useSuperRegex = /\buse\s+super::([\s\S]*?);/gm;
998
+ while ((match = useSuperRegex.exec(content)) !== null) {
999
+ const body = match[1].trim();
1000
+ extractRustUsePaths(body, "", imports, "super");
1001
+ }
1002
+ const useSelfRegex = /\buse\s+self::([\s\S]*?);/gm;
1003
+ while ((match = useSelfRegex.exec(content)) !== null) {
1004
+ const body = match[1].trim();
1005
+ extractRustUsePaths(body, "", imports, "self");
1006
+ }
747
1007
  return imports;
748
1008
  },
749
1009
  resolveImport(importPath, sourceFile, rootDir, projectFiles) {
@@ -756,6 +1016,30 @@ var rust = {
756
1016
  if (projectFiles.has(asDir)) return asDir;
757
1017
  return null;
758
1018
  }
1019
+ if (importPath.startsWith("super::")) {
1020
+ const parentDir = dirname(dirname(sourceFile));
1021
+ const segments2 = importPath.slice("super::".length).split("::");
1022
+ for (let i = segments2.length; i > 0; i--) {
1023
+ const path = segments2.slice(0, i).join("/");
1024
+ const asFile = join3(parentDir, path + ".rs");
1025
+ if (projectFiles.has(asFile)) return asFile;
1026
+ const asDir = join3(parentDir, path, "mod.rs");
1027
+ if (projectFiles.has(asDir)) return asDir;
1028
+ }
1029
+ return null;
1030
+ }
1031
+ if (importPath.startsWith("self::")) {
1032
+ const selfDir = dirname(sourceFile);
1033
+ const segments2 = importPath.slice("self::".length).split("::");
1034
+ for (let i = segments2.length; i > 0; i--) {
1035
+ const path = segments2.slice(0, i).join("/");
1036
+ const asFile = join3(selfDir, path + ".rs");
1037
+ if (projectFiles.has(asFile)) return asFile;
1038
+ const asDir = join3(selfDir, path, "mod.rs");
1039
+ if (projectFiles.has(asDir)) return asDir;
1040
+ }
1041
+ return null;
1042
+ }
759
1043
  const segments = importPath.split("::");
760
1044
  for (let i = segments.length; i > 0; i--) {
761
1045
  const path = segments.slice(0, i).join("/");
@@ -768,11 +1052,14 @@ var rust = {
768
1052
  },
769
1053
  defaultExclude: ["target"]
770
1054
  };
771
- function extractRustUsePaths(body, prefix, results) {
1055
+ function extractRustUsePaths(body, prefix, results, rootPrefix) {
772
1056
  const trimmed = body.trim();
773
1057
  const braceStart = trimmed.indexOf("{");
774
1058
  if (braceStart === -1) {
775
- const path = prefix ? `${prefix}::${trimmed}` : trimmed;
1059
+ let path = prefix ? `${prefix}::${trimmed}` : trimmed;
1060
+ if (rootPrefix) {
1061
+ path = `${rootPrefix}::${path}`;
1062
+ }
776
1063
  if (path && !path.includes("{")) {
777
1064
  results.push(path);
778
1065
  }
@@ -801,9 +1088,10 @@ function extractRustUsePaths(body, prefix, results) {
801
1088
  for (const item of items) {
802
1089
  const cleaned = item.trim();
803
1090
  if (cleaned === "self") {
804
- results.push(fullPrefix);
1091
+ const selfPath = rootPrefix ? `${rootPrefix}::${fullPrefix}` : fullPrefix;
1092
+ results.push(selfPath);
805
1093
  } else if (cleaned) {
806
- extractRustUsePaths(cleaned, fullPrefix, results);
1094
+ extractRustUsePaths(cleaned, fullPrefix, results, rootPrefix);
807
1095
  }
808
1096
  }
809
1097
  }
@@ -877,14 +1165,21 @@ var java = {
877
1165
  extensions: [".java"],
878
1166
  commentStyle: "c-style",
879
1167
  importPatterns: [
880
- // import com.example.ClassName;
881
- { regex: /^import\s+(?:static\s+)?([\w.]+);/gm }
1168
+ // Bug #5 fix: import com.example.ClassName; and import com.example.*; (wildcard)
1169
+ // Bug #6: static imports also captured here
1170
+ { regex: /^import\s+(?:static\s+)?([\w.*]+);/gm }
882
1171
  ],
883
1172
  resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
884
- const filePath = importPath.replace(/\./g, "/") + ".java";
885
- for (const srcRoot of ["", "src/main/java/", "src/", "app/src/main/java/"]) {
886
- const full = join3(rootDir, srcRoot, filePath);
887
- if (projectFiles.has(full)) return full;
1173
+ if (importPath.endsWith(".*")) {
1174
+ return null;
1175
+ }
1176
+ const segments = importPath.split(".");
1177
+ for (let i = segments.length; i > 0; i--) {
1178
+ const filePath = segments.slice(0, i).join("/") + ".java";
1179
+ for (const srcRoot of ["", "src/main/java/", "src/", "app/src/main/java/"]) {
1180
+ const full = join3(rootDir, srcRoot, filePath);
1181
+ if (projectFiles.has(full)) return full;
1182
+ }
888
1183
  }
889
1184
  return null;
890
1185
  },
@@ -940,8 +1235,8 @@ var php = {
940
1235
  importPatterns: [
941
1236
  // require/include/require_once/include_once 'path'
942
1237
  { regex: /\b(?:require|include)(?:_once)?\s+['"]([^'"]+)['"]/gm },
943
- // use Namespace\Class (PSR-4 style)
944
- { regex: /^use\s+([\w\\]+)/gm }
1238
+ // Bug #9 fix: use Namespace\Class skip `function` and `const` keywords
1239
+ { regex: /^use\s+(?:function\s+|const\s+)?([\w\\]+)/gm }
945
1240
  ],
946
1241
  resolveImport(importPath, sourceFile, rootDir, projectFiles) {
947
1242
  if (importPath.includes("/") || importPath.endsWith(".php")) {
@@ -966,8 +1261,8 @@ var swift = {
966
1261
  extensions: [".swift"],
967
1262
  commentStyle: "c-style",
968
1263
  importPatterns: [
969
- // import ModuleName (for cross-module dependencies)
970
- { regex: /^import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+|typealias\s+)?(\w+)/gm }
1264
+ // Bug #10 fix: import ModuleName and @testable import ModuleName
1265
+ { regex: /^(?:@testable\s+)?import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+|typealias\s+)?(\w+)/gm }
971
1266
  ],
972
1267
  resolveImport(importPath, sourceFile, rootDir, projectFiles) {
973
1268
  const spmDir = join3(rootDir, "Sources", importPath);
@@ -983,11 +1278,18 @@ var kotlin = {
983
1278
  extensions: [".kt", ".kts"],
984
1279
  commentStyle: "c-style",
985
1280
  importPatterns: [
986
- // import com.example.ClassName
987
- { regex: /^import\s+([\w.]+)/gm }
1281
+ // Bug #7/#8 fix: import com.example.ClassName and import com.example.*
1282
+ { regex: /^import\s+([\w.*]+)/gm }
988
1283
  ],
989
1284
  resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
990
- const filePath = importPath.replace(/\./g, "/");
1285
+ if (importPath.endsWith(".*")) {
1286
+ return null;
1287
+ }
1288
+ let cleanPath = importPath;
1289
+ if (cleanPath.endsWith(".")) {
1290
+ cleanPath = cleanPath.slice(0, -1);
1291
+ }
1292
+ const filePath = cleanPath.replace(/\./g, "/");
991
1293
  for (const ext of [".kt", ".kts"]) {
992
1294
  for (const srcRoot of [
993
1295
  "",
@@ -1005,6 +1307,124 @@ var kotlin = {
1005
1307
  },
1006
1308
  defaultExclude: ["build", "\\.gradle", "\\.idea"]
1007
1309
  };
1310
+ var cSharp = {
1311
+ id: "c-sharp",
1312
+ extensions: [".cs"],
1313
+ commentStyle: "c-style",
1314
+ importPatterns: [
1315
+ // using Namespace; and using Namespace.SubNamespace;
1316
+ // using static Namespace.Class;
1317
+ // Skip: using Alias = Namespace.Class; (captured but resolved same way)
1318
+ { regex: /^using\s+(?:static\s+)?([\w.]+)\s*;/gm }
1319
+ ],
1320
+ resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
1321
+ const segments = importPath.split(".");
1322
+ for (let i = segments.length; i > 0; i--) {
1323
+ const filePath = segments.slice(0, i).join("/") + ".cs";
1324
+ for (const srcRoot of ["", "src/", "lib/"]) {
1325
+ const full = join3(rootDir, srcRoot, filePath);
1326
+ if (projectFiles.has(full)) return full;
1327
+ }
1328
+ }
1329
+ for (let start = 0; start < segments.length; start++) {
1330
+ const dirPath = segments.slice(start).join("/");
1331
+ for (const srcRoot of ["", "src/", "lib/"]) {
1332
+ const prefix = join3(rootDir, srcRoot, dirPath) + "/";
1333
+ for (const f of projectFiles) {
1334
+ if (f.startsWith(prefix) && f.endsWith(".cs")) return f;
1335
+ }
1336
+ }
1337
+ }
1338
+ return null;
1339
+ },
1340
+ defaultExclude: ["bin", "obj", "\\.vs", "packages", "TestResults"]
1341
+ };
1342
+ var dart = {
1343
+ id: "dart",
1344
+ extensions: [".dart"],
1345
+ commentStyle: "c-style",
1346
+ importPatterns: [
1347
+ // import 'package:pkg/file.dart'; or import 'relative/path.dart';
1348
+ { regex: /^import\s+['"]([^'"]+)['"]/gm },
1349
+ // export 'file.dart'; (re-exports create dependencies too)
1350
+ { regex: /^export\s+['"]([^'"]+)['"]/gm }
1351
+ ],
1352
+ resolveImport(importPath, sourceFile, rootDir, projectFiles) {
1353
+ if (importPath.startsWith("dart:")) return null;
1354
+ if (importPath.startsWith("package:")) {
1355
+ const ownPackage = dartPackageName(rootDir);
1356
+ if (!ownPackage) return null;
1357
+ const prefix = `package:${ownPackage}/`;
1358
+ if (!importPath.startsWith(prefix)) return null;
1359
+ const relPath = importPath.slice(prefix.length);
1360
+ const full = join3(rootDir, "lib", relPath);
1361
+ if (projectFiles.has(full)) return full;
1362
+ return null;
1363
+ }
1364
+ const resolved = resolve3(dirname(sourceFile), importPath);
1365
+ if (projectFiles.has(resolved)) return resolved;
1366
+ return null;
1367
+ },
1368
+ defaultExclude: ["\\.dart_tool", "build", "\\.packages"]
1369
+ };
1370
+ var dartPackageCache = /* @__PURE__ */ new Map();
1371
+ function dartPackageName(rootDir) {
1372
+ if (dartPackageCache.has(rootDir)) return dartPackageCache.get(rootDir);
1373
+ try {
1374
+ const content = readFileSync(join3(rootDir, "pubspec.yaml"), "utf-8");
1375
+ const match = content.match(/^name:\s*(\S+)/m);
1376
+ const name = match ? match[1] : null;
1377
+ dartPackageCache.set(rootDir, name);
1378
+ return name;
1379
+ } catch {
1380
+ dartPackageCache.set(rootDir, null);
1381
+ return null;
1382
+ }
1383
+ }
1384
+ var scala = {
1385
+ id: "scala",
1386
+ extensions: [".scala", ".sc"],
1387
+ commentStyle: "c-style",
1388
+ importPatterns: [],
1389
+ // handled by extractImports for grouped syntax
1390
+ extractImports(content) {
1391
+ const imports = [];
1392
+ const importRegex = /\bimport\s+([\w.]+(?:\.\{[^}]+\}|\.\w+|\._))/gm;
1393
+ let match;
1394
+ while ((match = importRegex.exec(content)) !== null) {
1395
+ const full = match[1];
1396
+ const braceMatch = full.match(/^([\w.]+)\.\{([^}]+)\}$/);
1397
+ if (braceMatch) {
1398
+ const prefix = braceMatch[1];
1399
+ const items = braceMatch[2].split(",");
1400
+ for (const item of items) {
1401
+ const trimmed = item.trim().split(/\s+/)[0];
1402
+ if (trimmed === "_") continue;
1403
+ imports.push(`${prefix}.${trimmed}`);
1404
+ }
1405
+ } else if (full.endsWith("._")) {
1406
+ continue;
1407
+ } else {
1408
+ imports.push(full);
1409
+ }
1410
+ }
1411
+ return imports;
1412
+ },
1413
+ resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
1414
+ const segments = importPath.split(".");
1415
+ for (let i = segments.length; i > 0; i--) {
1416
+ const filePath = segments.slice(0, i).join("/");
1417
+ for (const ext of [".scala", ".sc"]) {
1418
+ for (const srcRoot of ["", "src/main/scala/", "src/", "app/"]) {
1419
+ const full = join3(rootDir, srcRoot, filePath + ext);
1420
+ if (projectFiles.has(full)) return full;
1421
+ }
1422
+ }
1423
+ }
1424
+ return null;
1425
+ },
1426
+ defaultExclude: ["target", "\\.bsp", "\\.metals", "\\.bloop"]
1427
+ };
1008
1428
  var LANGUAGE_CONFIGS = {
1009
1429
  javascript: null,
1010
1430
  // handled by DependencyCruiserEngine
@@ -1013,10 +1433,13 @@ var LANGUAGE_CONFIGS = {
1013
1433
  go,
1014
1434
  java,
1015
1435
  "c-cpp": cCpp,
1436
+ "c-sharp": cSharp,
1016
1437
  ruby,
1017
1438
  php,
1018
1439
  swift,
1019
- kotlin
1440
+ kotlin,
1441
+ dart,
1442
+ scala
1020
1443
  };
1021
1444
  function getLanguageConfig(id) {
1022
1445
  return LANGUAGE_CONFIGS[id] ?? null;