@codacy/analysis-runner 0.10.1 → 0.12.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +85 -59
  2. package/dist/index.js +653 -227
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -63,6 +63,7 @@ __export(index_exports, {
63
63
  readCodacyYaml: () => readCodacyYaml,
64
64
  registerAdapter: () => registerAdapter,
65
65
  registerLazyAdapter: () => registerLazyAdapter,
66
+ registerUnsupportedTool: () => registerUnsupportedTool,
66
67
  resolveConfigPath: () => resolveConfigPath,
67
68
  resolveEngineToAdapter: () => resolveEngineToAdapter,
68
69
  rotateLogFiles: () => rotateLogFiles,
@@ -75,7 +76,7 @@ module.exports = __toCommonJS(index_exports);
75
76
  // src/analyze.ts
76
77
  var import_os = __toESM(require("os"));
77
78
  var import_path3 = __toESM(require("path"));
78
- var import_child_process2 = require("child_process");
79
+ var import_child_process3 = require("child_process");
79
80
  var import_util = require("util");
80
81
  var import_tooling3 = require("@codacy/tooling");
81
82
 
@@ -92,6 +93,9 @@ function registerAdapter(adapter) {
92
93
  function registerLazyAdapter(descriptor, factory) {
93
94
  entries.set(descriptor.id, { descriptor, factory });
94
95
  }
96
+ function registerUnsupportedTool(descriptor) {
97
+ entries.set(descriptor.id, { descriptor });
98
+ }
95
99
  function resolveEntry(input) {
96
100
  const exact = entries.get(input);
97
101
  if (exact) return exact;
@@ -103,15 +107,23 @@ function resolveEntry(input) {
103
107
  }
104
108
  async function loadAdapter(entry) {
105
109
  if (entry.resolved) return entry.resolved;
110
+ if (!entry.factory) {
111
+ throw new Error(
112
+ `Tool "${entry.descriptor.id}" is unsupported and has no local adapter to load`
113
+ );
114
+ }
106
115
  entry.resolved = await entry.factory();
107
116
  return entry.resolved;
108
117
  }
118
+ function isLoadable(entry) {
119
+ return entry.factory !== void 0;
120
+ }
109
121
  async function getRegisteredAdapters(toolIds) {
110
122
  if (toolIds && toolIds.length > 0) {
111
- const matched = toolIds.map((id) => resolveEntry(id)).filter((e) => e !== void 0);
123
+ const matched = toolIds.map((id) => resolveEntry(id)).filter((e) => e !== void 0).filter(isLoadable);
112
124
  return Promise.all(matched.map(loadAdapter));
113
125
  }
114
- return Promise.all(Array.from(entries.values()).map(loadAdapter));
126
+ return Promise.all(Array.from(entries.values()).filter(isLoadable).map(loadAdapter));
115
127
  }
116
128
  function getRegisteredDescriptors(toolIds) {
117
129
  if (toolIds && toolIds.length > 0) {
@@ -121,7 +133,7 @@ function getRegisteredDescriptors(toolIds) {
121
133
  }
122
134
  async function getAdapter(toolId) {
123
135
  const entry = entries.get(toolId);
124
- if (!entry) return void 0;
136
+ if (!entry || !isLoadable(entry)) return void 0;
125
137
  return loadAdapter(entry);
126
138
  }
127
139
  function clearAdapters() {
@@ -226,84 +238,98 @@ function createCodacyConfig(params = {}) {
226
238
  }
227
239
 
228
240
  // src/files.ts
229
- var import_child_process = require("child_process");
241
+ var import_child_process2 = require("child_process");
230
242
  var import_fs = __toESM(require("fs"));
231
243
  var import_path2 = __toESM(require("path"));
244
+ var import_minimatch2 = require("minimatch");
245
+
246
+ // src/glob-matcher.ts
232
247
  var import_minimatch = require("minimatch");
233
- var MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
234
- function listRepoFiles(repositoryRoot) {
235
- try {
236
- const tracked = (0, import_child_process.execSync)("git ls-files", {
237
- cwd: repositoryRoot,
238
- encoding: "utf-8",
239
- maxBuffer: 50 * 1024 * 1024
240
- }).trim();
241
- const untracked = (0, import_child_process.execSync)("git ls-files --others --exclude-standard", {
242
- cwd: repositoryRoot,
243
- encoding: "utf-8",
244
- maxBuffer: 50 * 1024 * 1024
245
- }).trim();
246
- const files = /* @__PURE__ */ new Set();
247
- for (const f of tracked.split("\n")) {
248
- if (f) files.add(f);
249
- }
250
- for (const f of untracked.split("\n")) {
251
- if (f) files.add(f);
252
- }
253
- return Array.from(files).sort();
254
- } catch {
255
- return [];
256
- }
248
+ var PURE_EXTENSION_PATTERN = /^\*\*\/\*(\.[^*/.]+)$/;
249
+ function pureExtensionOf(pattern) {
250
+ const m = PURE_EXTENSION_PATTERN.exec(pattern);
251
+ return m ? m[1] : null;
257
252
  }
258
- function filterOversizedFiles(files, repositoryRoot, logger) {
259
- const result = [];
260
- let skipped = 0;
261
- for (const f of files) {
262
- try {
263
- const stat = import_fs.default.statSync(import_path2.default.join(repositoryRoot, f));
264
- if (stat.size > MAX_FILE_SIZE_BYTES) {
265
- skipped++;
266
- logger?.debug("Skipping oversized file", {
267
- file: f,
268
- sizeKB: Math.round(stat.size / 1024),
269
- limitKB: MAX_FILE_SIZE_BYTES / 1024
270
- });
253
+ var CompiledMatcher = class {
254
+ /** True if any pattern is the bare `"*"` wildcard (matches every file). */
255
+ matchAll;
256
+ literalPaths;
257
+ literalNames;
258
+ exts;
259
+ // Residual globs are split by whether they contain a path separator so the
260
+ // hot loop never runs a full-path match for a slash-less pattern (which can
261
+ // only ever match via the basename) — that call is always wasted on nested
262
+ // files. Slash patterns match against the full path; slash-less ones against
263
+ // the basename, which together reproduce the old dual check exactly.
264
+ residualPath;
265
+ residualName;
266
+ constructor(patterns) {
267
+ let matchAll = false;
268
+ const literalPaths = /* @__PURE__ */ new Set();
269
+ const literalNames = /* @__PURE__ */ new Set();
270
+ const exts = /* @__PURE__ */ new Set();
271
+ const residualPath = [];
272
+ const residualName = [];
273
+ for (const pattern of patterns) {
274
+ if (pattern === "*") {
275
+ matchAll = true;
271
276
  continue;
272
277
  }
273
- } catch {
278
+ const ext = pureExtensionOf(pattern);
279
+ if (ext !== null) {
280
+ exts.add(ext);
281
+ continue;
282
+ }
283
+ const mm = new import_minimatch.Minimatch(pattern, { dot: true });
284
+ if (mm.hasMagic()) {
285
+ (pattern.includes("/") ? residualPath : residualName).push(mm);
286
+ } else if (pattern.includes("/")) {
287
+ literalPaths.add(pattern);
288
+ } else {
289
+ literalNames.add(pattern);
290
+ }
274
291
  }
275
- result.push(f);
292
+ this.matchAll = matchAll;
293
+ this.literalPaths = literalPaths;
294
+ this.literalNames = literalNames;
295
+ this.exts = exts;
296
+ this.residualPath = residualPath;
297
+ this.residualName = residualName;
276
298
  }
277
- if (skipped > 0) {
278
- logger?.info("Filtered oversized files", { skipped, remaining: result.length });
299
+ /** True when this matcher holds no patterns at all (matches nothing). */
300
+ get isEmpty() {
301
+ return !this.matchAll && this.literalPaths.size === 0 && this.literalNames.size === 0 && this.exts.size === 0 && this.residualPath.length === 0 && this.residualName.length === 0;
279
302
  }
280
- return result;
281
- }
282
- function applyExclusions(files, globalExclude, toolExclude) {
283
- const allExclusions = [...globalExclude, ...toolExclude];
284
- if (allExclusions.length === 0) return files;
285
- return files.filter((file) => {
286
- const basename = import_path2.default.basename(file);
287
- return !allExclusions.some((pattern) => {
288
- if ((0, import_minimatch.minimatch)(file, pattern, { dot: true })) return true;
289
- if (!pattern.includes("/") && (0, import_minimatch.minimatch)(basename, pattern, { dot: true })) return true;
290
- return false;
291
- });
292
- });
293
- }
294
- function routeFilesToTool(files, adapter, toolConfig) {
295
- const patterns = [...adapter.filePatterns];
296
- if (toolConfig?.extraFilePatterns) {
297
- patterns.push(...toolConfig.extraFilePatterns);
303
+ /**
304
+ * Returns true if `file` matches any pattern. `basename` must be
305
+ * `path.basename(file)` — passed in so callers compute it once per file.
306
+ *
307
+ * Mirrors the historical dual check: a pattern matches if it matches the full
308
+ * relative path, or (for patterns with no path separator) the basename. For
309
+ * globstar/slash patterns the full-path check already subsumes the basename
310
+ * check, so gating the basename check on `!includes("/")` is equivalent to
311
+ * the old unconditional basename check used by `routeFilesToTool`.
312
+ */
313
+ matches(file, basename) {
314
+ if (this.matchAll) return true;
315
+ if (this.literalPaths.size > 0 && this.literalPaths.has(file)) return true;
316
+ if (this.literalNames.size > 0 && this.literalNames.has(basename)) return true;
317
+ if (this.exts.size > 0) {
318
+ const dot = basename.lastIndexOf(".");
319
+ if (dot >= 0 && this.exts.has(basename.slice(dot))) return true;
320
+ }
321
+ for (const m of this.residualPath) {
322
+ if (m.match(file)) return true;
323
+ }
324
+ for (const m of this.residualName) {
325
+ if (m.match(basename)) return true;
326
+ }
327
+ return false;
298
328
  }
299
- if (patterns.includes("*")) return files;
300
- return files.filter((file) => {
301
- const basename = import_path2.default.basename(file);
302
- return patterns.some(
303
- (pattern) => (0, import_minimatch.minimatch)(basename, pattern, { dot: true }) || (0, import_minimatch.minimatch)(file, pattern, { dot: true })
304
- );
305
- });
306
- }
329
+ };
330
+
331
+ // src/git-scope.ts
332
+ var import_child_process = require("child_process");
307
333
  function listStagedFiles(repositoryRoot) {
308
334
  try {
309
335
  const output = (0, import_child_process.execSync)("git diff --cached --name-only --diff-filter=d", {
@@ -333,7 +359,7 @@ function listDiffFiles(repositoryRoot, base) {
333
359
  }
334
360
  function listPrFiles(repositoryRoot) {
335
361
  try {
336
- const output = (0, import_child_process.execSync)("gh pr view --json files --jq '.files[].path'", {
362
+ const output = (0, import_child_process.execSync)('gh pr view --json files --jq ".files[].path"', {
337
363
  cwd: repositoryRoot,
338
364
  encoding: "utf-8",
339
365
  maxBuffer: 10 * 1024 * 1024,
@@ -389,23 +415,95 @@ function detectDefaultBranch(repositoryRoot) {
389
415
  "Cannot auto-detect default branch: no origin/HEAD, origin/main, or origin/master found. Specify a base explicitly with --diff <base>."
390
416
  );
391
417
  }
418
+
419
+ // src/files.ts
420
+ var MAX_FILE_SIZE_BYTES = 150 * 1024;
421
+ function listRepoFiles(repositoryRoot) {
422
+ try {
423
+ const tracked = (0, import_child_process2.execSync)("git ls-files", {
424
+ cwd: repositoryRoot,
425
+ encoding: "utf-8",
426
+ maxBuffer: 50 * 1024 * 1024
427
+ }).trim();
428
+ const untracked = (0, import_child_process2.execSync)("git ls-files --others --exclude-standard", {
429
+ cwd: repositoryRoot,
430
+ encoding: "utf-8",
431
+ maxBuffer: 50 * 1024 * 1024
432
+ }).trim();
433
+ const files = /* @__PURE__ */ new Set();
434
+ for (const f of tracked.split("\n")) {
435
+ if (f) files.add(f);
436
+ }
437
+ for (const f of untracked.split("\n")) {
438
+ if (f) files.add(f);
439
+ }
440
+ return Array.from(files).sort();
441
+ } catch {
442
+ return [];
443
+ }
444
+ }
445
+ function filterOversizedFiles(files, repositoryRoot, logger) {
446
+ const result = [];
447
+ const skipped = [];
448
+ for (const f of files) {
449
+ try {
450
+ const stat = import_fs.default.statSync(import_path2.default.join(repositoryRoot, f));
451
+ if (stat.size > MAX_FILE_SIZE_BYTES) {
452
+ skipped.push({ filePath: f, sizeBytes: stat.size });
453
+ logger?.debug("Skipping oversized file", {
454
+ file: f,
455
+ sizeKB: Math.round(stat.size / 1024),
456
+ limitKB: MAX_FILE_SIZE_BYTES / 1024
457
+ });
458
+ continue;
459
+ }
460
+ } catch {
461
+ }
462
+ result.push(f);
463
+ }
464
+ if (skipped.length > 0) {
465
+ logger?.info("Filtered oversized files", {
466
+ skipped: skipped.length,
467
+ remaining: result.length,
468
+ limitKB: MAX_FILE_SIZE_BYTES / 1024
469
+ });
470
+ }
471
+ return { files: result, skipped };
472
+ }
473
+ function applyExclusions(files, globalExclude, toolExclude) {
474
+ if (globalExclude.length === 0 && toolExclude.length === 0) return files;
475
+ const matcher = new CompiledMatcher([...globalExclude, ...toolExclude]);
476
+ if (matcher.isEmpty) return files;
477
+ return files.filter((file) => !matcher.matches(file, import_path2.default.basename(file)));
478
+ }
479
+ function routeFilesToTool(files, adapter, toolConfig) {
480
+ const patterns = [...adapter.filePatterns];
481
+ if (toolConfig?.extraFilePatterns) {
482
+ patterns.push(...toolConfig.extraFilePatterns);
483
+ }
484
+ const matcher = new CompiledMatcher(patterns);
485
+ if (matcher.matchAll) return files;
486
+ return files.filter((file) => matcher.matches(file, import_path2.default.basename(file)));
487
+ }
392
488
  function filterByTargetFiles(allFiles, targetPatterns) {
393
489
  if (targetPatterns.length === 0) return allFiles;
394
490
  const normalized = targetPatterns.map((p) => p.replace(/^\.\//, ""));
491
+ const exact = new Set(normalized);
492
+ const globs = normalized.map((p) => new import_minimatch2.Minimatch(p, { dot: true, matchBase: false })).filter((m) => m.hasMagic());
493
+ const dirPrefixes = normalized.filter((p) => p.endsWith("/"));
395
494
  return allFiles.filter(
396
- (f) => normalized.some(
397
- (pattern) => (
398
- // Exact match (fast path)
399
- f === pattern || // Glob match
400
- (0, import_minimatch.minimatch)(f, pattern, { dot: true, matchBase: false }) || // Directory prefix (e.g. "src/subdir/" matches all files under it)
401
- pattern.endsWith("/") && f.startsWith(pattern)
402
- )
495
+ (f) => (
496
+ // Exact match (fast path)
497
+ exact.has(f) || // Glob match
498
+ globs.some((m) => m.match(f)) || // Directory prefix (e.g. "src/subdir/" matches all files under it)
499
+ dirPrefixes.some((p) => f.startsWith(p))
403
500
  )
404
501
  );
405
502
  }
406
503
 
407
504
  // src/executor.ts
408
505
  var import_tooling2 = require("@codacy/tooling");
506
+ var HARD_TIMEOUT_GRACE_MS = 2e3;
409
507
  async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
410
508
  const startTime = Date.now();
411
509
  ctx.logger.debug("runTool started", {
@@ -413,12 +511,18 @@ async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
413
511
  fileCount: ctx.targetFiles.length,
414
512
  timeoutMs
415
513
  });
514
+ const abortController = new AbortController();
515
+ ctx.signal = abortController.signal;
516
+ const abortTimer = setTimeout(() => abortController.abort(), timeoutMs);
517
+ abortTimer.unref?.();
518
+ let hardTimer;
416
519
  try {
417
520
  const timeoutPromise = new Promise((_resolve, reject) => {
418
- setTimeout(
521
+ hardTimer = setTimeout(
419
522
  () => reject(new Error(`TIMEOUT: Tool ${adapter.id} exceeded ${(0, import_tooling2.formatDuration)(timeoutMs)}`)),
420
- timeoutMs
523
+ timeoutMs + HARD_TIMEOUT_GRACE_MS
421
524
  );
525
+ hardTimer.unref?.();
422
526
  });
423
527
  const result = await Promise.race([adapter.analyze(ctx, codacyToolConfig), timeoutPromise]);
424
528
  ctx.logger.debug("runTool completed", {
@@ -465,6 +569,9 @@ async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
465
569
  filesAnalyzed: ctx.targetFiles.length
466
570
  }
467
571
  };
572
+ } finally {
573
+ clearTimeout(abortTimer);
574
+ if (hardTimer) clearTimeout(hardTimer);
468
575
  }
469
576
  }
470
577
  async function runToolsSequential(tools, timeoutMs, callbacks) {
@@ -623,7 +730,7 @@ function computePatternSummary(issues, lookup) {
623
730
  }
624
731
 
625
732
  // src/analyze.ts
626
- var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
733
+ var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
627
734
  var nullLogger = {
628
735
  debug: () => {
629
736
  },
@@ -634,10 +741,24 @@ var nullLogger = {
634
741
  error: () => {
635
742
  }
636
743
  };
744
+ var CHECK_CONCURRENCY = 12;
745
+ async function mapWithConcurrency(items, concurrency, task) {
746
+ const results = new Array(items.length);
747
+ let next = 0;
748
+ async function worker() {
749
+ while (next < items.length) {
750
+ const index = next++;
751
+ results[index] = await task(items[index], index);
752
+ }
753
+ }
754
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
755
+ await Promise.all(workers);
756
+ return results;
757
+ }
637
758
  async function analyze(options) {
638
759
  const startedAt = /* @__PURE__ */ new Date();
639
760
  const mode = options.mode || "standard";
640
- const toolTimeout = options.toolTimeout || 6e5;
761
+ const toolTimeout = options.toolTimeout || 9e5;
641
762
  const parallelTools = options.parallelTools || 1;
642
763
  const logger = options.logger ?? nullLogger;
643
764
  const phaseTimings = {
@@ -727,23 +848,25 @@ async function analyze(options) {
727
848
  const unavailable = [];
728
849
  const availabilityTimeout = 6e4;
729
850
  const availabilityStartMs = Date.now();
730
- for (const adapter of adapters) {
851
+ const checkAvailabilityOf = async (adapter) => {
731
852
  const ctx = makeToolContext(baseCtx, adapter, []);
732
853
  const checkStart = Date.now();
733
854
  logger.debug("Checking availability", { toolId: adapter.id });
855
+ let timer;
734
856
  try {
735
857
  const availability = await Promise.race([
736
858
  adapter.checkAvailability(ctx),
737
- new Promise(
738
- (_, reject) => setTimeout(
859
+ new Promise((_, reject) => {
860
+ timer = setTimeout(
739
861
  () => reject(
740
862
  new Error(
741
863
  `checkAvailability timed out after ${(0, import_tooling3.formatDuration)(availabilityTimeout)}`
742
864
  )
743
865
  ),
744
866
  availabilityTimeout
745
- )
746
- )
867
+ );
868
+ timer.unref?.();
869
+ })
747
870
  ]);
748
871
  if (availability.available) {
749
872
  logger.debug("Tool available", {
@@ -752,38 +875,57 @@ async function analyze(options) {
752
875
  installation: availability.installation,
753
876
  durationMs: Date.now() - checkStart
754
877
  });
755
- ready.push({
756
- toolId: adapter.id,
757
- displayName: adapter.displayName,
758
- version: availability.version ?? adapter.preferredVersion,
759
- installation: availability.installation ?? "global",
760
- runner: "local"
761
- });
762
- } else {
763
- logger.warn("Tool unavailable", {
764
- toolId: adapter.id,
765
- reason: availability.reason,
766
- durationMs: Date.now() - checkStart
767
- });
768
- unavailable.push({
878
+ return {
879
+ kind: "ready",
880
+ status: {
881
+ toolId: adapter.id,
882
+ displayName: adapter.displayName,
883
+ version: availability.version ?? adapter.preferredVersion,
884
+ installation: availability.installation ?? "global",
885
+ runner: "local"
886
+ }
887
+ };
888
+ }
889
+ logger.warn("Tool unavailable", {
890
+ toolId: adapter.id,
891
+ reason: availability.reason,
892
+ durationMs: Date.now() - checkStart
893
+ });
894
+ return {
895
+ kind: "unavailable",
896
+ status: {
769
897
  toolId: adapter.id,
770
898
  displayName: adapter.displayName,
771
899
  reason: availability.reason || "Tool is not available",
772
900
  ...availability.installHint ? { remediation: availability.installHint } : {}
773
- });
774
- }
901
+ }
902
+ };
775
903
  } catch (err) {
776
904
  logger.warn("Availability check failed", {
777
905
  toolId: adapter.id,
778
906
  error: err instanceof Error ? err.message : String(err),
779
907
  durationMs: Date.now() - checkStart
780
908
  });
781
- unavailable.push({
782
- toolId: adapter.id,
783
- displayName: adapter.displayName,
784
- reason: `Availability check failed: ${err instanceof Error ? err.message : String(err)}`
785
- });
909
+ return {
910
+ kind: "unavailable",
911
+ status: {
912
+ toolId: adapter.id,
913
+ displayName: adapter.displayName,
914
+ reason: `Availability check failed: ${err instanceof Error ? err.message : String(err)}`
915
+ }
916
+ };
917
+ } finally {
918
+ if (timer) clearTimeout(timer);
786
919
  }
920
+ };
921
+ const availabilityOutcomes = await mapWithConcurrency(
922
+ adapters,
923
+ CHECK_CONCURRENCY,
924
+ checkAvailabilityOf
925
+ );
926
+ for (const outcome of availabilityOutcomes) {
927
+ if (outcome.kind === "ready") ready.push(outcome.status);
928
+ else unavailable.push(outcome.status);
787
929
  }
788
930
  phaseTimings.availabilityMs = Date.now() - availabilityStartMs;
789
931
  const capability = {
@@ -800,6 +942,31 @@ async function analyze(options) {
800
942
  readyAfter: capability.ready.length,
801
943
  configuredToolCount: configuredIds.size
802
944
  });
945
+ const representedIds = /* @__PURE__ */ new Set([
946
+ ...capability.ready.map((t) => t.toolId),
947
+ ...capability.unavailable.map((t) => t.toolId)
948
+ ]);
949
+ const requestedLower = options.tools?.map((t) => t.toLowerCase());
950
+ const inScope = (toolId) => {
951
+ if (!requestedLower) return true;
952
+ const id = toolId.toLowerCase();
953
+ return requestedLower.some((r) => id.includes(r) || r.includes(id));
954
+ };
955
+ const descriptorsById = new Map(getRegisteredDescriptors().map((d) => [d.id, d]));
956
+ for (const toolConfig of codacyConfig.tools) {
957
+ if (!toolConfig?.toolId) continue;
958
+ if (representedIds.has(toolConfig.toolId)) continue;
959
+ if (!inScope(toolConfig.toolId)) continue;
960
+ const descriptor = descriptorsById.get(toolConfig.toolId);
961
+ if (!descriptor?.unsupported) continue;
962
+ capability.unavailable.push({
963
+ toolId: toolConfig.toolId,
964
+ displayName: descriptor.displayName ?? toolConfig.toolId,
965
+ reason: "No local analyzer (configured on Codacy Cloud)",
966
+ availableVia: "cloud"
967
+ });
968
+ representedIds.add(toolConfig.toolId);
969
+ }
803
970
  if (mode === "auto-install" && capability.unavailable.length > 0) {
804
971
  const installStartMs = Date.now();
805
972
  const installErrors = [];
@@ -908,8 +1075,9 @@ async function analyze(options) {
908
1075
  }
909
1076
  return buildResult(startedAt, options.repositoryRoot, mode, capability, [], [], []);
910
1077
  }
911
- if (mode === "strict" && capability.unavailable.length > 0) {
912
- const missingTools = capability.unavailable.map((u) => u.toolId).join(", ");
1078
+ const strictMissing = capability.unavailable.filter((u) => u.availableVia !== "cloud");
1079
+ if (mode === "strict" && strictMissing.length > 0) {
1080
+ const missingTools = strictMissing.map((u) => u.toolId).join(", ");
913
1081
  return buildResult(
914
1082
  startedAt,
915
1083
  options.repositoryRoot,
@@ -950,25 +1118,52 @@ async function analyze(options) {
950
1118
  allFiles = filterByTargetFiles(allFiles, options.files);
951
1119
  logger.debug("Filtered by target files", { afterFilter: allFiles.length });
952
1120
  }
953
- allFiles = filterOversizedFiles(allFiles, options.repositoryRoot, logger);
1121
+ const oversize = filterOversizedFiles(allFiles, options.repositoryRoot, logger);
1122
+ allFiles = oversize.files;
1123
+ const skippedFiles = oversize.skipped;
954
1124
  phaseTimings.fileDiscoveryMs = Date.now() - fileDiscoveryStartMs;
955
1125
  const fileRoutingStartMs = Date.now();
956
- const readyAdapters = adapters.filter((a) => capability.ready.some((r) => r.toolId === a.id));
1126
+ const readyById = new Map(capability.ready.map((r) => [r.toolId, r]));
1127
+ const readyAdapters = adapters.filter((a) => readyById.has(a.id));
1128
+ const filesAfterGlobalExclude = applyExclusions(allFiles, globalExclude, []);
957
1129
  const toolRuns = [];
1130
+ const candidates = [];
958
1131
  for (const adapter of readyAdapters) {
959
1132
  const toolConfig = getToolConfig(codacyConfig, adapter.id);
960
1133
  if (!toolConfig) continue;
961
- const toolExclude = toolConfig?.exclude || [];
962
- const filesAfterExclusion = applyExclusions(allFiles, globalExclude, toolExclude);
963
- const toolFiles = routeFilesToTool(filesAfterExclusion, adapter, toolConfig);
964
- const readyEntryForFiles = capability.ready.find((r) => r.toolId === adapter.id);
965
- if (readyEntryForFiles) {
966
- readyEntryForFiles.filesRouted = toolFiles.length;
1134
+ const toolExclude = toolConfig.exclude || [];
1135
+ const filesForTool = toolExclude.length > 0 ? applyExclusions(filesAfterGlobalExclude, toolExclude, []) : filesAfterGlobalExclude;
1136
+ const toolFiles = routeFilesToTool(filesForTool, adapter, toolConfig);
1137
+ const readyEntry = readyById.get(adapter.id);
1138
+ if (readyEntry) {
1139
+ readyEntry.filesRouted = toolFiles.length;
1140
+ }
1141
+ candidates.push({
1142
+ adapter,
1143
+ toolFiles,
1144
+ configCtx: makeToolContext(baseCtx, adapter, toolFiles),
1145
+ readyEntry
1146
+ });
1147
+ }
1148
+ const configChecks = await mapWithConcurrency(
1149
+ candidates,
1150
+ CHECK_CONCURRENCY,
1151
+ async (c) => {
1152
+ try {
1153
+ return await c.adapter.checkLocalConfigurationFile(c.configCtx);
1154
+ } catch (err) {
1155
+ logger.debug("Local config check failed, assuming none", {
1156
+ toolId: c.adapter.id,
1157
+ error: err instanceof Error ? err.message : String(err)
1158
+ });
1159
+ return { found: false };
1160
+ }
967
1161
  }
968
- const configCtx = makeToolContext(baseCtx, adapter, toolFiles);
969
- const configCheck = await adapter.checkLocalConfigurationFile(configCtx);
1162
+ );
1163
+ for (let i = 0; i < candidates.length; i++) {
1164
+ const { adapter, toolFiles, configCtx, readyEntry } = candidates[i];
1165
+ const configCheck = configChecks[i];
970
1166
  const codacyToolConfig = buildToolConfig(codacyConfig, adapter, configCheck);
971
- const readyEntry = capability.ready.find((r) => r.toolId === adapter.id);
972
1167
  if (readyEntry) {
973
1168
  if (configCheck.found && codacyToolConfig.useLocalConfigurationFile) {
974
1169
  readyEntry.configSource = "local";
@@ -1042,7 +1237,8 @@ async function analyze(options) {
1042
1237
  allErrors,
1043
1238
  allSummaries,
1044
1239
  patternSummary,
1045
- phaseTimings
1240
+ phaseTimings,
1241
+ skippedFiles
1046
1242
  );
1047
1243
  }
1048
1244
  function makeToolContext(base, adapter, targetFiles) {
@@ -1068,7 +1264,7 @@ function resolveGitScope(repositoryRoot, scope) {
1068
1264
  const base = scope.diff === true ? void 0 : scope.diff;
1069
1265
  return listDiffFiles(repositoryRoot, base);
1070
1266
  }
1071
- function buildResult(startedAt, repositoryRoot, mode, capability, issues, errors, toolResults, patternSummary, phaseTimings) {
1267
+ function buildResult(startedAt, repositoryRoot, mode, capability, issues, errors, toolResults, patternSummary, phaseTimings, skippedFiles) {
1072
1268
  const completedAt = /* @__PURE__ */ new Date();
1073
1269
  return {
1074
1270
  metadata: {
@@ -1083,7 +1279,8 @@ function buildResult(startedAt, repositoryRoot, mode, capability, issues, errors
1083
1279
  issues,
1084
1280
  errors,
1085
1281
  toolResults,
1086
- ...patternSummary ? { patternSummary } : {}
1282
+ ...patternSummary ? { patternSummary } : {},
1283
+ ...skippedFiles && skippedFiles.length > 0 ? { skippedFiles } : {}
1087
1284
  };
1088
1285
  }
1089
1286
 
@@ -1092,6 +1289,7 @@ var import_path4 = __toESM(require("path"));
1092
1289
  var import_tooling4 = require("@codacy/tooling");
1093
1290
  var import_ansis = __toESM(require("ansis"));
1094
1291
  var import_cli_table3 = __toESM(require("cli-table3"));
1292
+ var MAX_SKIPPED_FILES_SHOWN = 5;
1095
1293
  function formatOutput(result, format) {
1096
1294
  switch (format) {
1097
1295
  case "json":
@@ -1249,9 +1447,30 @@ function formatText(result) {
1249
1447
  lines.push(...formatErrorSection(result));
1250
1448
  lines.push("");
1251
1449
  }
1450
+ if (result.skippedFiles && result.skippedFiles.length > 0) {
1451
+ lines.push(...formatSkippedFilesSection(result.skippedFiles));
1452
+ lines.push("");
1453
+ }
1252
1454
  lines.push(formatTextSummary(result, isInspect));
1253
1455
  return lines.join("\n");
1254
1456
  }
1457
+ function formatSkippedFilesSection(skipped) {
1458
+ const limitKB = Math.round(MAX_FILE_SIZE_BYTES / 1024);
1459
+ const out = [""];
1460
+ out.push(
1461
+ import_ansis.default.yellow(
1462
+ `\u26A0 ${skipped.length} file${skipped.length !== 1 ? "s" : ""} skipped (exceeding the ${limitKB} KB size limit):`
1463
+ )
1464
+ );
1465
+ for (const f of skipped.slice(0, MAX_SKIPPED_FILES_SHOWN)) {
1466
+ out.push(import_ansis.default.dim(` - ${f.filePath} (${Math.round(f.sizeBytes / 1024)} KB)`));
1467
+ }
1468
+ const remaining = skipped.length - MAX_SKIPPED_FILES_SHOWN;
1469
+ if (remaining > 0) {
1470
+ out.push(import_ansis.default.dim(` \u2026 and ${remaining} more`));
1471
+ }
1472
+ return out;
1473
+ }
1255
1474
  function formatTextSummary(result, isInspect) {
1256
1475
  const lines = [];
1257
1476
  const inspect = isInspect ?? result.metadata.executionMode === "inspect";
@@ -1301,13 +1520,14 @@ function formatTextSummary(result, isInspect) {
1301
1520
  ]);
1302
1521
  }
1303
1522
  for (const tool of unavailableTools) {
1523
+ const isUnsupported = tool.availableVia === "cloud";
1304
1524
  table.push([
1305
- import_ansis.default.dim.red(tool.displayName ?? tool.toolId),
1525
+ isUnsupported ? import_ansis.default.dim(tool.displayName ?? tool.toolId) : import_ansis.default.dim.red(tool.displayName ?? tool.toolId),
1306
1526
  "-",
1527
+ isUnsupported ? import_ansis.default.dim("Unsupported") : "-",
1307
1528
  "-",
1308
1529
  "-",
1309
- "-",
1310
- tool.reason
1530
+ isUnsupported ? import_ansis.default.dim("No local analyzer (Codacy Cloud)") : tool.reason
1311
1531
  ]);
1312
1532
  }
1313
1533
  lines.push(table.toString());
@@ -1332,13 +1552,14 @@ function formatTextSummary(result, isInspect) {
1332
1552
  ]);
1333
1553
  }
1334
1554
  for (const tool of unavailableTools) {
1555
+ const isUnsupported = tool.availableVia === "cloud";
1335
1556
  table.push([
1336
- import_ansis.default.dim.red(tool.displayName ?? tool.toolId),
1337
- "-",
1557
+ isUnsupported ? import_ansis.default.dim(tool.displayName ?? tool.toolId) : import_ansis.default.dim.red(tool.displayName ?? tool.toolId),
1338
1558
  "-",
1559
+ isUnsupported ? import_ansis.default.dim("Unsupported") : "-",
1339
1560
  "-",
1340
1561
  "-",
1341
- import_ansis.default.dim("missing binary"),
1562
+ isUnsupported ? import_ansis.default.dim("cloud-only") : import_ansis.default.dim("missing binary"),
1342
1563
  "-"
1343
1564
  ]);
1344
1565
  }
@@ -1391,6 +1612,13 @@ function formatTextSummary(result, isInspect) {
1391
1612
  return lines.join("\n");
1392
1613
  }
1393
1614
  var MAX_ERRORS_PER_GROUP = 5;
1615
+ var MAX_ERROR_MESSAGE_LENGTH = 240;
1616
+ function flattenErrorMessage(message) {
1617
+ return message.replace(/\s+/g, " ").trim();
1618
+ }
1619
+ function truncateErrorMessage(message) {
1620
+ return message.length > MAX_ERROR_MESSAGE_LENGTH ? `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}\u2026` : message;
1621
+ }
1394
1622
  function formatErrorSection(result) {
1395
1623
  const lines = [];
1396
1624
  const displayNameMap = /* @__PURE__ */ new Map();
@@ -1412,13 +1640,14 @@ function formatErrorSection(result) {
1412
1640
  const deduped = [];
1413
1641
  const seen = /* @__PURE__ */ new Map();
1414
1642
  for (const err of groupErrors) {
1415
- const dedupKey = `${err.filePath ?? ""}|||${err.message}`;
1643
+ const flat = flattenErrorMessage(err.message);
1644
+ const dedupKey = `${err.filePath ?? ""}|||${flat}`;
1416
1645
  const idx = seen.get(dedupKey);
1417
1646
  if (idx !== void 0) {
1418
1647
  deduped[idx].count++;
1419
1648
  } else {
1420
1649
  seen.set(dedupKey, deduped.length);
1421
- deduped.push({ filePath: err.filePath, message: err.message, count: 1 });
1650
+ deduped.push({ filePath: err.filePath, message: truncateErrorMessage(flat), count: 1 });
1422
1651
  }
1423
1652
  }
1424
1653
  const shown = deduped.slice(0, MAX_ERRORS_PER_GROUP);
@@ -1745,7 +1974,7 @@ function applyConfigOperation(op, source, dest) {
1745
1974
  // src/init.ts
1746
1975
  var import_promises4 = __toESM(require("fs/promises"));
1747
1976
  var import_path7 = __toESM(require("path"));
1748
- var import_child_process3 = require("child_process");
1977
+ var import_child_process4 = require("child_process");
1749
1978
  var import_tooling5 = require("@codacy/tooling");
1750
1979
 
1751
1980
  // src/discover.ts
@@ -1954,6 +2183,12 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
1954
2183
  ["gson", { displayName: "Gson", kind: "library" }],
1955
2184
  ["okhttp", { displayName: "OkHttp", kind: "library" }],
1956
2185
  ["retrofit", { displayName: "Retrofit", kind: "library" }],
2186
+ ["spock-core", { displayName: "Spock", kind: "library" }],
2187
+ // --- Groovy — Frameworks ---
2188
+ // Grails/GORM artifact IDs vary (grails-core, grails-web, gorm-hibernate5, …);
2189
+ // the `grails-`/`gorm-` prefix rules below catch the rest.
2190
+ ["grails-core", { displayName: "Grails", kind: "framework" }],
2191
+ ["gorm-core", { displayName: "GORM", kind: "library" }],
1957
2192
  // --- Go — Frameworks ---
1958
2193
  ["github.com/gin-gonic/gin", { displayName: "Gin", kind: "framework" }],
1959
2194
  ["github.com/labstack/echo", { displayName: "Echo", kind: "framework" }],
@@ -1995,9 +2230,20 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
1995
2230
  // --- PHP — Frameworks ---
1996
2231
  ["laravel/framework", { displayName: "Laravel", kind: "framework" }],
1997
2232
  ["symfony/framework-bundle", { displayName: "Symfony", kind: "framework" }],
2233
+ ["symfony/symfony", { displayName: "Symfony", kind: "framework" }],
1998
2234
  ["codeigniter4/framework", { displayName: "CodeIgniter", kind: "framework" }],
1999
2235
  ["slim/slim", { displayName: "Slim", kind: "framework" }],
2000
2236
  ["cakephp/cakephp", { displayName: "CakePHP", kind: "framework" }],
2237
+ // WordPress is rarely a real Composer dependency; these are the common
2238
+ // "WordPress as a dependency" packages used by Bedrock/Composer-based sites.
2239
+ ["johnpbloch/wordpress-core", { displayName: "WordPress", kind: "framework" }],
2240
+ ["johnpbloch/wordpress", { displayName: "WordPress", kind: "framework" }],
2241
+ ["roots/wordpress", { displayName: "WordPress", kind: "framework" }],
2242
+ ["drupal/core", { displayName: "Drupal", kind: "framework" }],
2243
+ ["drupal/core-recommended", { displayName: "Drupal", kind: "framework" }],
2244
+ ["magento/product-community-edition", { displayName: "Magento", kind: "framework" }],
2245
+ ["magento/product-enterprise-edition", { displayName: "Magento", kind: "framework" }],
2246
+ ["magento/framework", { displayName: "Magento", kind: "framework" }],
2001
2247
  // --- PHP — Libraries ---
2002
2248
  ["phpunit/phpunit", { displayName: "PHPUnit", kind: "library" }],
2003
2249
  ["guzzlehttp/guzzle", { displayName: "Guzzle", kind: "library" }],
@@ -2027,7 +2273,32 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2027
2273
  ["scalatest", { displayName: "ScalaTest", kind: "library" }],
2028
2274
  ["specs2-core", { displayName: "Specs2", kind: "library" }],
2029
2275
  ["cats-core", { displayName: "Cats", kind: "library" }],
2030
- ["circe-core", { displayName: "Circe", kind: "library" }]
2276
+ ["circe-core", { displayName: "Circe", kind: "library" }],
2277
+ // --- Dart — Frameworks ---
2278
+ // `flutter` and `angular` appear as plain keys in pubspec.yaml.
2279
+ ["flutter", { displayName: "Flutter", kind: "framework" }],
2280
+ ["flutter_test", { displayName: "Flutter", kind: "framework" }],
2281
+ ["angular", { displayName: "Angular", kind: "framework" }],
2282
+ // --- Dart — Libraries ---
2283
+ // Kept to Dart-specific names with negligible cross-ecosystem collision risk
2284
+ // (generic names like "get"/"provider" are intentionally omitted).
2285
+ ["riverpod", { displayName: "Riverpod", kind: "library" }],
2286
+ ["flutter_bloc", { displayName: "Bloc", kind: "library" }],
2287
+ ["dio", { displayName: "Dio", kind: "library" }],
2288
+ // --- .NET (C# / VB) — Libraries ---
2289
+ // NuGet package IDs. Microsoft.* families are also covered by prefix rules below.
2290
+ ["EntityFramework", { displayName: "Entity Framework", kind: "library" }],
2291
+ ["NUnit", { displayName: "NUnit", kind: "library" }],
2292
+ ["Moq", { displayName: "Moq", kind: "library" }],
2293
+ ["NSubstitute", { displayName: "NSubstitute", kind: "library" }],
2294
+ ["FluentAssertions", { displayName: "FluentAssertions", kind: "library" }],
2295
+ ["Shouldly", { displayName: "Shouldly", kind: "library" }],
2296
+ ["NLog", { displayName: "NLog", kind: "library" }],
2297
+ ["log4net", { displayName: "log4net", kind: "library" }],
2298
+ ["Newtonsoft.Json", { displayName: "Json.NET", kind: "library" }],
2299
+ ["AutoMapper", { displayName: "AutoMapper", kind: "library" }],
2300
+ ["Dapper", { displayName: "Dapper", kind: "library" }],
2301
+ ["MediatR", { displayName: "MediatR", kind: "library" }]
2031
2302
  ]);
2032
2303
  var DEPENDENCY_PREFIX_RULES = [
2033
2304
  { prefix: "@angular/", classification: { displayName: "Angular", kind: "framework" } },
@@ -2057,7 +2328,35 @@ var DEPENDENCY_PREFIX_RULES = [
2057
2328
  { prefix: "@shopify/", classification: { displayName: "Shopify", kind: "platform" } },
2058
2329
  { prefix: "@aws-sdk/", classification: { displayName: "AWS SDK", kind: "library" } },
2059
2330
  { prefix: "@google-cloud/", classification: { displayName: "Google Cloud", kind: "library" } },
2060
- { prefix: "@azure/", classification: { displayName: "Azure SDK", kind: "library" } }
2331
+ { prefix: "@azure/", classification: { displayName: "Azure SDK", kind: "library" } },
2332
+ // Groovy — Grails/GORM artifact families (build.gradle artifactIds).
2333
+ { prefix: "grails-", classification: { displayName: "Grails", kind: "framework" } },
2334
+ { prefix: "gorm-", classification: { displayName: "GORM", kind: "library" } },
2335
+ // .NET — Microsoft package families and common test/logging libraries. More
2336
+ // specific prefixes (Blazor) must precede broader ones (ASP.NET Core).
2337
+ {
2338
+ prefix: "Microsoft.AspNetCore.Components.WebAssembly",
2339
+ classification: { displayName: "Blazor", kind: "framework" }
2340
+ },
2341
+ {
2342
+ prefix: "Microsoft.AspNetCore.",
2343
+ classification: { displayName: "ASP.NET Core", kind: "framework" }
2344
+ },
2345
+ {
2346
+ prefix: "Microsoft.EntityFrameworkCore",
2347
+ classification: { displayName: "Entity Framework", kind: "library" }
2348
+ },
2349
+ {
2350
+ prefix: "Microsoft.Azure.Functions",
2351
+ classification: { displayName: "Azure Functions", kind: "framework" }
2352
+ },
2353
+ {
2354
+ prefix: "Microsoft.Azure.WebJobs",
2355
+ classification: { displayName: "Azure Functions", kind: "framework" }
2356
+ },
2357
+ { prefix: "xunit", classification: { displayName: "xUnit", kind: "library" } },
2358
+ { prefix: "MSTest.", classification: { displayName: "MSTest", kind: "library" } },
2359
+ { prefix: "Serilog", classification: { displayName: "Serilog", kind: "library" } }
2061
2360
  ];
2062
2361
  function classifyDependency(name) {
2063
2362
  const exact = DEPENDENCY_CATALOG.get(name);
@@ -2086,7 +2385,12 @@ var NOTABLE_ANY_DEPTH = /* @__PURE__ */ new Set([
2086
2385
  "Package.swift",
2087
2386
  "Jenkinsfile",
2088
2387
  "Vagrantfile",
2089
- "Procfile"
2388
+ "Procfile",
2389
+ "pubspec.yaml",
2390
+ "mix.exs",
2391
+ "shard.yml",
2392
+ "packages.config",
2393
+ "Directory.Packages.props"
2090
2394
  ]);
2091
2395
  var NOTABLE_ROOT_ONLY = /* @__PURE__ */ new Set([
2092
2396
  ".dockerignore",
@@ -2146,6 +2450,7 @@ function isNotableFile(filePath) {
2146
2450
  const isRoot = !filePath.includes("/");
2147
2451
  if (NOTABLE_ANY_DEPTH.has(basename)) return true;
2148
2452
  if (isRoot && NOTABLE_ROOT_ONLY.has(basename)) return true;
2453
+ if (/\.(csproj|vbproj|fsproj|sln)$/i.test(basename)) return true;
2149
2454
  for (const prefix of NOTABLE_PATH_PREFIXES) {
2150
2455
  if (filePath.startsWith(prefix)) return true;
2151
2456
  }
@@ -2352,6 +2657,51 @@ function parsePackageSwift(content) {
2352
2657
  }
2353
2658
  return deps;
2354
2659
  }
2660
+ function parsePubspecYaml(content) {
2661
+ const deps = [];
2662
+ let inSection = false;
2663
+ let childIndent = -1;
2664
+ for (const rawLine of content.split("\n")) {
2665
+ const trimmed = rawLine.trim();
2666
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
2667
+ const indent = rawLine.length - rawLine.trimStart().length;
2668
+ if (indent === 0) {
2669
+ inSection = /^(dependencies|dev_dependencies|dependency_overrides):/.test(trimmed);
2670
+ childIndent = -1;
2671
+ continue;
2672
+ }
2673
+ if (!inSection) continue;
2674
+ if (childIndent === -1) childIndent = indent;
2675
+ if (indent !== childIndent) continue;
2676
+ const m = trimmed.match(/^([a-zA-Z0-9_.]+)\s*:(?:\s*["']?\^?([0-9][^"'#\s]*))?/);
2677
+ if (m) deps.push({ name: m[1], version: m[2] });
2678
+ }
2679
+ return deps;
2680
+ }
2681
+ function parseXmlDependencyElements(content, elementRegex, nameAttr) {
2682
+ const deps = [];
2683
+ const nameRegex = new RegExp(`\\b${nameAttr}\\s*=\\s*["']([^"']+)["']`, "i");
2684
+ const versionRegex = /\bversion\s*=\s*["']([^"']+)["']/i;
2685
+ let match;
2686
+ while ((match = elementRegex.exec(content)) !== null) {
2687
+ const attrs = match[1];
2688
+ const nameMatch = attrs.match(nameRegex);
2689
+ if (!nameMatch) continue;
2690
+ const versionMatch = attrs.match(versionRegex);
2691
+ deps.push({ name: nameMatch[1].trim(), version: versionMatch?.[1]?.trim() });
2692
+ }
2693
+ return deps;
2694
+ }
2695
+ function parseCsproj(content) {
2696
+ return parseXmlDependencyElements(
2697
+ content,
2698
+ /<Package(?:Reference|Version)\s+([^>]+?)\/?>/gi,
2699
+ "Include"
2700
+ );
2701
+ }
2702
+ function parsePackagesConfig(content) {
2703
+ return parseXmlDependencyElements(content, /<package\s+([^>]+?)\/?>/gi, "id");
2704
+ }
2355
2705
  var MANIFEST_PARSERS = /* @__PURE__ */ new Map([
2356
2706
  ["package.json", parsePackageJson],
2357
2707
  ["requirements.txt", parseRequirementsTxt],
@@ -2365,7 +2715,15 @@ var MANIFEST_PARSERS = /* @__PURE__ */ new Map([
2365
2715
  ["Cargo.toml", parseCargoToml],
2366
2716
  ["composer.json", parseComposerJson],
2367
2717
  ["Podfile", parsePodfile],
2368
- ["Package.swift", parsePackageSwift]
2718
+ ["Package.swift", parsePackageSwift],
2719
+ ["pubspec.yaml", parsePubspecYaml],
2720
+ ["packages.config", parsePackagesConfig],
2721
+ ["Directory.Packages.props", parseCsproj]
2722
+ ]);
2723
+ var MANIFEST_SUFFIX_PARSERS = /* @__PURE__ */ new Map([
2724
+ [".csproj", parseCsproj],
2725
+ [".vbproj", parseCsproj],
2726
+ [".fsproj", parseCsproj]
2369
2727
  ]);
2370
2728
 
2371
2729
  // src/discover.ts
@@ -2415,39 +2773,39 @@ function findNotableFiles(files) {
2415
2773
  }
2416
2774
  return Array.from(notable).sort();
2417
2775
  }
2418
- async function extractDependencies(repositoryRoot, files) {
2419
- const manifestFiles = /* @__PURE__ */ new Map();
2420
- for (const file of files) {
2421
- const basename = file.split("/").pop() ?? file;
2422
- if (MANIFEST_PARSERS.has(basename)) {
2423
- const existing = manifestFiles.get(basename) ?? [];
2424
- existing.push(file);
2425
- manifestFiles.set(basename, existing);
2426
- }
2776
+ function resolveManifestParser(filePath) {
2777
+ const basename = filePath.split("/").pop() ?? filePath;
2778
+ const byName = MANIFEST_PARSERS.get(basename);
2779
+ if (byName) return byName;
2780
+ const lowerBasename = basename.toLowerCase();
2781
+ for (const [suffix, parser] of MANIFEST_SUFFIX_PARSERS) {
2782
+ if (lowerBasename.endsWith(suffix)) return parser;
2427
2783
  }
2784
+ return void 0;
2785
+ }
2786
+ async function extractDependencies(repositoryRoot, files) {
2428
2787
  const allDeps = [];
2429
- for (const [manifestName, paths] of manifestFiles) {
2430
- const parser = MANIFEST_PARSERS.get(manifestName);
2431
- for (const filePath of paths) {
2432
- let content;
2433
- try {
2434
- content = await import_promises2.default.readFile(import_path5.default.join(repositoryRoot, filePath), "utf-8");
2435
- } catch {
2436
- continue;
2437
- }
2438
- const rawDeps = parser(content);
2439
- for (const raw of rawDeps) {
2440
- const classification = classifyDependency(raw.name);
2441
- if (classification) {
2442
- allDeps.push({
2443
- name: raw.name,
2444
- displayName: classification.displayName,
2445
- kind: classification.kind,
2446
- source: "manifest",
2447
- manifestFile: filePath,
2448
- version: raw.version
2449
- });
2450
- }
2788
+ for (const filePath of files) {
2789
+ const parser = resolveManifestParser(filePath);
2790
+ if (!parser) continue;
2791
+ let content;
2792
+ try {
2793
+ content = await import_promises2.default.readFile(import_path5.default.join(repositoryRoot, filePath), "utf-8");
2794
+ } catch {
2795
+ continue;
2796
+ }
2797
+ const rawDeps = parser(content);
2798
+ for (const raw of rawDeps) {
2799
+ const classification = classifyDependency(raw.name);
2800
+ if (classification) {
2801
+ allDeps.push({
2802
+ name: raw.name,
2803
+ displayName: classification.displayName,
2804
+ kind: classification.kind,
2805
+ source: "manifest",
2806
+ manifestFile: filePath,
2807
+ version: raw.version
2808
+ });
2451
2809
  }
2452
2810
  }
2453
2811
  }
@@ -6083,13 +6441,44 @@ async function listIgnoredFiles(provider, org, repo) {
6083
6441
  }
6084
6442
 
6085
6443
  // src/init.ts
6444
+ function deriveApiToolId(tool) {
6445
+ return tool.prefix ? tool.prefix.replace(/_$/, "") : tool.shortName;
6446
+ }
6447
+ function isTrackableTool(tool) {
6448
+ if (tool.clientSide) return false;
6449
+ if (tool.standalone) return false;
6450
+ if (/\bdeprecated\b/i.test(tool.name ?? "")) return false;
6451
+ return true;
6452
+ }
6453
+ function buildAdapterByUuid(adapters, apiTools) {
6454
+ const byKey = /* @__PURE__ */ new Map();
6455
+ for (const adapter of adapters) {
6456
+ byKey.set(adapter.id.toLowerCase(), adapter);
6457
+ byKey.set(adapter.displayName.toLowerCase(), adapter);
6458
+ }
6459
+ const result = /* @__PURE__ */ new Map();
6460
+ for (const tool of apiTools) {
6461
+ const candidates = [];
6462
+ if (tool.prefix) candidates.push(tool.prefix.replace(/_$/, "").toLowerCase());
6463
+ if (tool.shortName) candidates.push(tool.shortName.toLowerCase());
6464
+ if (tool.name) candidates.push(tool.name.toLowerCase());
6465
+ for (const candidate of candidates) {
6466
+ const adapter = byKey.get(candidate);
6467
+ if (adapter) {
6468
+ result.set(tool.uuid, adapter);
6469
+ break;
6470
+ }
6471
+ }
6472
+ }
6473
+ return result;
6474
+ }
6086
6475
  async function detectLanguages(repositoryRoot, precomputedFiles) {
6087
6476
  let files;
6088
6477
  if (precomputedFiles) {
6089
6478
  files = precomputedFiles;
6090
6479
  } else {
6091
6480
  try {
6092
- const output = (0, import_child_process3.execSync)("git ls-files", {
6481
+ const output = (0, import_child_process4.execSync)("git ls-files", {
6093
6482
  cwd: repositoryRoot,
6094
6483
  encoding: "utf-8",
6095
6484
  maxBuffer: 50 * 1024 * 1024
@@ -6185,34 +6574,32 @@ async function initDefaultConfig(repositoryRoot, adapters) {
6185
6574
  );
6186
6575
  const apiTools = await listTools();
6187
6576
  const repoLanguages = new Set(base.metadata.languages);
6577
+ const adapterByUuid = buildAdapterByUuid(adapters, apiTools);
6188
6578
  const updatedTools = [...base.tools];
6189
6579
  const coveredToolIds = new Set(updatedTools.map((t) => t.toolId));
6190
6580
  let addedTools = 0;
6191
- for (const adapter of adapters) {
6192
- if (toolsWithLocalConfig.has(adapter.id)) continue;
6193
- const apiTool = apiTools.find(
6194
- (t) => t.prefix ? t.prefix.replace(/_$/, "") === adapter.id : t.shortName === adapter.id
6195
- );
6196
- if (!apiTool) continue;
6581
+ let addedUnsupported = 0;
6582
+ for (const apiTool of apiTools) {
6583
+ const adapter = adapterByUuid.get(apiTool.uuid);
6584
+ if (adapter && toolsWithLocalConfig.has(adapter.id)) continue;
6585
+ if (!adapter && !isTrackableTool(apiTool)) continue;
6197
6586
  if (!apiTool.enabledByDefault) continue;
6198
6587
  if (apiTool.languages.length > 0 && repoLanguages.size > 0) {
6199
6588
  const hasOverlap = apiTool.languages.some((lang) => repoLanguages.has(lang));
6200
6589
  if (!hasOverlap) continue;
6201
6590
  }
6591
+ const toolId = adapter ? adapter.id : deriveApiToolId(apiTool);
6592
+ if (coveredToolIds.has(toolId)) continue;
6202
6593
  let patterns = [];
6203
6594
  try {
6204
6595
  const apiPatterns = await listPatterns(apiTool.uuid);
6205
6596
  patterns = apiPatterns.filter((p) => p.enabled).map((p) => ({ patternId: p.id }));
6206
6597
  } catch {
6207
6598
  }
6208
- if (!coveredToolIds.has(adapter.id)) {
6209
- updatedTools.push({
6210
- toolId: adapter.id,
6211
- patterns
6212
- });
6213
- coveredToolIds.add(adapter.id);
6214
- addedTools++;
6215
- }
6599
+ updatedTools.push({ toolId, patterns });
6600
+ coveredToolIds.add(toolId);
6601
+ if (adapter) addedTools++;
6602
+ else addedUnsupported++;
6216
6603
  }
6217
6604
  const baseConfig = createCodacyConfig({
6218
6605
  source: "default",
@@ -6224,7 +6611,10 @@ async function initDefaultConfig(repositoryRoot, adapters) {
6224
6611
  repositoryRoot,
6225
6612
  adapters
6226
6613
  );
6227
- return { config, stats: { totalApiTools: apiTools.length, addedTools, codacyYamlApplied } };
6614
+ return {
6615
+ config,
6616
+ stats: { totalApiTools: apiTools.length, addedTools, addedUnsupported, codacyYamlApplied }
6617
+ };
6228
6618
  }
6229
6619
  async function initRemoteConfig(repositoryRoot, provider, organization, repositoryName, adapters, apiToken, onRemoteToolsFound, onToolProcessed, onIgnoredFilesFetched) {
6230
6620
  configureApiToken(apiToken);
@@ -6236,56 +6626,61 @@ async function initRemoteConfig(repositoryRoot, provider, organization, reposito
6236
6626
  } catch {
6237
6627
  }
6238
6628
  onIgnoredFilesFetched?.(ignoredFilesResult.totalFetched, ignoredFilesResult.capped);
6239
- const adapterByName = /* @__PURE__ */ new Map();
6240
- for (const adapter of adapters) {
6241
- adapterByName.set(adapter.id, adapter);
6242
- adapterByName.set(adapter.displayName.toLowerCase(), adapter);
6243
- }
6244
6629
  const localConfigs = await detectLocalConfigFiles(repositoryRoot, adapters);
6245
6630
  const allApiTools = await listTools();
6246
- const adapterByUuid = /* @__PURE__ */ new Map();
6247
- for (const apiTool of allApiTools) {
6248
- const adapter = apiTool.prefix ? adapterByName.get(apiTool.prefix.replace(/_$/, "")) : adapterByName.get(apiTool.name);
6249
- if (adapter) {
6250
- adapterByUuid.set(apiTool.uuid, adapter);
6251
- }
6252
- }
6631
+ const adapterByUuid = buildAdapterByUuid(adapters, allApiTools);
6632
+ const apiToolByUuid = new Map(allApiTools.map((t) => [t.uuid, t]));
6253
6633
  const matchedAdapterCount = remoteTools.filter((t) => adapterByUuid.has(t.uuid)).length;
6254
6634
  onRemoteToolsFound?.(remoteTools.length, matchedAdapterCount);
6635
+ const fetchPatterns = async (uuid) => {
6636
+ try {
6637
+ const configuredPatterns = await listRepositoryToolPatterns(
6638
+ provider,
6639
+ organization,
6640
+ repositoryName,
6641
+ uuid
6642
+ );
6643
+ return configuredPatterns.map((cp) => {
6644
+ const params = cp.parameters.length > 0 ? Object.fromEntries(cp.parameters.map((p) => [p.name, p.value])) : void 0;
6645
+ return {
6646
+ patternId: cp.patternDefinition.id,
6647
+ ...params ? { parameters: params } : {}
6648
+ };
6649
+ });
6650
+ } catch {
6651
+ return [];
6652
+ }
6653
+ };
6255
6654
  const tools = [];
6256
6655
  let matchedAdapters = 0;
6656
+ let unsupportedTools = 0;
6257
6657
  for (const remoteTool of remoteTools) {
6258
- const adapter = adapterByUuid.get(remoteTool.uuid);
6259
- if (!adapter) continue;
6260
- matchedAdapters++;
6261
6658
  const usesLocalConfig = remoteTool.settings.usesConfigurationFile;
6262
- const localConfigPath = localConfigs.get(adapter.id);
6263
- let patterns = [];
6264
- if (!usesLocalConfig) {
6265
- try {
6266
- const configuredPatterns = await listRepositoryToolPatterns(
6267
- provider,
6268
- organization,
6269
- repositoryName,
6270
- remoteTool.uuid
6271
- );
6272
- patterns = configuredPatterns.map((cp) => {
6273
- const params = cp.parameters.length > 0 ? Object.fromEntries(cp.parameters.map((p) => [p.name, p.value])) : void 0;
6274
- return {
6275
- patternId: cp.patternDefinition.id,
6276
- ...params ? { parameters: params } : {}
6277
- };
6278
- });
6279
- } catch {
6280
- }
6659
+ const adapter = adapterByUuid.get(remoteTool.uuid);
6660
+ if (adapter) {
6661
+ matchedAdapters++;
6662
+ const localConfigPath = localConfigs.get(adapter.id);
6663
+ const patterns2 = usesLocalConfig ? [] : await fetchPatterns(remoteTool.uuid);
6664
+ onToolProcessed?.(adapter.displayName, patterns2.length, usesLocalConfig);
6665
+ tools.push({
6666
+ toolId: adapter.id,
6667
+ ...localConfigPath ? { localConfigurationFile: localConfigPath } : {},
6668
+ // Honour local config when the API says usesConfigurationFile, OR when a
6669
+ // local config exists and no patterns were configured (backwards-compat).
6670
+ ...usesLocalConfig || localConfigPath && patterns2.length === 0 ? { useLocalConfigurationFile: true } : {},
6671
+ patterns: patterns2
6672
+ });
6673
+ continue;
6281
6674
  }
6282
- onToolProcessed?.(adapter.displayName, patterns.length, usesLocalConfig);
6675
+ const apiTool = apiToolByUuid.get(remoteTool.uuid);
6676
+ if (!apiTool || !isTrackableTool(apiTool)) continue;
6677
+ const toolId = deriveApiToolId(apiTool);
6678
+ const patterns = usesLocalConfig ? [] : await fetchPatterns(remoteTool.uuid);
6679
+ unsupportedTools++;
6680
+ onToolProcessed?.(apiTool.name ?? toolId, patterns.length, usesLocalConfig);
6283
6681
  tools.push({
6284
- toolId: adapter.id,
6285
- ...localConfigPath ? { localConfigurationFile: localConfigPath } : {},
6286
- // Honour local config when the API says usesConfigurationFile, OR when a
6287
- // local config exists and no patterns were configured (backwards-compat).
6288
- ...usesLocalConfig || localConfigPath && patterns.length === 0 ? { useLocalConfigurationFile: true } : {},
6682
+ toolId,
6683
+ ...usesLocalConfig ? { useLocalConfigurationFile: true } : {},
6289
6684
  patterns
6290
6685
  });
6291
6686
  }
@@ -6311,6 +6706,7 @@ async function initRemoteConfig(repositoryRoot, provider, organization, reposito
6311
6706
  stats: {
6312
6707
  totalRemoteTools: remoteTools.length,
6313
6708
  matchedAdapters,
6709
+ unsupportedTools,
6314
6710
  codacyYamlApplied,
6315
6711
  ignoredFilesCount: ignoredFilesResult.totalFetched,
6316
6712
  ignoredFilesCapped: ignoredFilesResult.capped
@@ -6372,7 +6768,7 @@ async function initContainerConfig(repositoryRoot, adapters) {
6372
6768
  }
6373
6769
  return createCodacyConfig({ source: "container", tools, includeFiles });
6374
6770
  }
6375
- async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, onDiscoveryComplete, rawFilterString) {
6771
+ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, onDiscoveryComplete, rawFilterString, loadUnsupportedPatterns) {
6376
6772
  const discoveryResult = await discover(repositoryRoot);
6377
6773
  onDiscoveryComplete?.(discoveryResult);
6378
6774
  const detectedKeys = /* @__PURE__ */ new Set();
@@ -6421,6 +6817,34 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
6421
6817
  });
6422
6818
  }
6423
6819
  }
6820
+ let selectedUnsupported = 0;
6821
+ const selectedFamilies = new Set(tools.map((t) => toolFamily(t.toolId)));
6822
+ for (const desc of descriptors) {
6823
+ if (!desc.unsupported) continue;
6824
+ if (!loadUnsupportedPatterns) break;
6825
+ if (!desc.languages.includes("*") && desc.languages.length > 0 && repoLanguages.size > 0 && !desc.languages.some((lang) => repoLanguages.has(lang))) {
6826
+ continue;
6827
+ }
6828
+ const family = toolFamily(desc.id);
6829
+ if (selectedFamilies.has(family)) continue;
6830
+ const patternsMetadata = loadUnsupportedPatterns(desc.id);
6831
+ if (patternsMetadata.length === 0) continue;
6832
+ const selectedPatterns = filterAutoPatterns(
6833
+ patternsMetadata,
6834
+ desc.frameworkMappings,
6835
+ detectedKeys,
6836
+ repoLanguages,
6837
+ filters
6838
+ );
6839
+ if (selectedPatterns.length > 0) {
6840
+ tools.push({
6841
+ toolId: desc.id,
6842
+ patterns: selectedPatterns.map((p) => ({ patternId: p.id }))
6843
+ });
6844
+ selectedFamilies.add(family);
6845
+ selectedUnsupported++;
6846
+ }
6847
+ }
6424
6848
  const baseConfig = createCodacyConfig({
6425
6849
  source: "auto",
6426
6850
  languages: discoveryResult.languages,
@@ -6439,6 +6863,7 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
6439
6863
  detectedFrameworks: discoveryResult.frameworks.length,
6440
6864
  detectedLibraries: discoveryResult.libraries.length,
6441
6865
  selectedTools: tools.length,
6866
+ selectedUnsupported,
6442
6867
  codacyYamlApplied
6443
6868
  }
6444
6869
  };
@@ -6760,6 +7185,7 @@ function createLogger(options = {}) {
6760
7185
  readCodacyYaml,
6761
7186
  registerAdapter,
6762
7187
  registerLazyAdapter,
7188
+ registerUnsupportedTool,
6763
7189
  resolveConfigPath,
6764
7190
  resolveEngineToAdapter,
6765
7191
  rotateLogFiles,