@codacy/analysis-runner 0.10.1 → 0.11.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 +81 -54
  2. package/dist/index.js +532 -162
  3. package/package.json +3 -3
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { OutputFormat, Logger, AnalysisResult, ToolAdapter, AdapterDescriptor, CodacyConfig, ConfigurationFileCheckResult, CodacyToolConfig, IssueSeverity, IssueCategory, Language, DiscoveryResult, Issue, PatternSummary, LogLevel } from '@codacy/tooling';
1
+ import { OutputFormat, Logger, AnalysisResult, ToolAdapter, AdapterDescriptor, CodacyConfig, ConfigurationFileCheckResult, CodacyToolConfig, IssueSeverity, IssueCategory, Language, DiscoveryResult, PatternEntry, Issue, PatternSummary, LogLevel } from '@codacy/tooling';
2
2
  export { AnalysisResult, OutputFormat } from '@codacy/tooling';
3
3
 
4
4
  /**
@@ -94,6 +94,17 @@ declare function registerAdapter(adapter: Readonly<ToolAdapter>): void;
94
94
  * and inspect-mode metadata display without loading the full module.
95
95
  */
96
96
  declare function registerLazyAdapter(descriptor: AdapterDescriptor, factory: AdapterFactory): void;
97
+ /**
98
+ * Register an unsupported (cloud-only) tool by descriptor alone.
99
+ *
100
+ * These tools exist on Codacy Cloud but have no local analyzer in this CLI. They are
101
+ * registered so that `getRegisteredDescriptors()` (and therefore `info`, `init --auto`,
102
+ * and config generation) can see them, but they are intentionally excluded from
103
+ * `getRegisteredAdapters()`/`getAdapter()` since there is nothing to execute.
104
+ *
105
+ * The descriptor should have `unsupported: true`.
106
+ */
107
+ declare function registerUnsupportedTool(descriptor: AdapterDescriptor): void;
97
108
  /**
98
109
  * Returns all registered adapters, optionally filtered by tool ID list.
99
110
  *
@@ -309,6 +320,53 @@ declare function parseAutoFilters(raw: string): {
309
320
  unknown: string[];
310
321
  };
311
322
 
323
+ /**
324
+ * Codacy API client — thin wrappers around the generated OpenAPI client.
325
+ *
326
+ * The generated client (src/api/client/) is produced from the Codacy OpenAPI spec
327
+ * and should never be edited by hand. All API interactions go through this file.
328
+ *
329
+ * Authentication: Codacy uses an `api-token` header (not Bearer). We set it via
330
+ * `OpenAPI.HEADERS` before each call group. `configureApiToken()` must be called
331
+ * before any authenticated endpoint is used.
332
+ *
333
+ * Pagination: all list endpoints use cursor-based pagination. `fetchAllPages()`
334
+ * follows cursors until the response contains no next cursor.
335
+ */
336
+
337
+ /** Result of fetching cloud-ignored files, with pagination cap. */
338
+ interface IgnoredFilesResult {
339
+ /** File paths ignored in Codacy Cloud. */
340
+ files: string[];
341
+ /** Total number of files fetched (same as files.length). */
342
+ totalFetched: number;
343
+ /** Whether the fetch was stopped because it hit the 1000-file cap. */
344
+ capped: boolean;
345
+ }
346
+ /**
347
+ * Configures the API token used for authenticated requests.
348
+ * Must be called before `getRepository`, `listRepositoryTools`, or
349
+ * `listRepositoryToolPatterns`. Public endpoints (listTools, listPatterns)
350
+ * do not require a token.
351
+ */
352
+ declare function configureApiToken(token: string): void;
353
+ /**
354
+ * Validates an API token by calling the authenticated user endpoint.
355
+ * Returns user info on success; caller should catch errors for invalid tokens.
356
+ */
357
+ declare function validateApiToken(token: string): Promise<{
358
+ valid: boolean;
359
+ name?: string;
360
+ email?: string;
361
+ }>;
362
+ /**
363
+ * Fetches files individually ignored in Codacy Cloud for a repository.
364
+ * Uses cursor-based pagination capped at 10 pages (1000 files max) to avoid
365
+ * unbounded fetching for repositories with very large ignore lists.
366
+ * Requires `configureApiToken()` to have been called.
367
+ */
368
+ declare function listIgnoredFiles(provider: string, org: string, repo: string): Promise<IgnoredFilesResult>;
369
+
312
370
  /**
313
371
  * Init mode logic — builds a CodacyConfig from five distinct sources:
314
372
  *
@@ -348,8 +406,10 @@ declare function initLocalConfig(repositoryRoot: string, adapters: Readonly<Tool
348
406
  interface InitDefaultStats {
349
407
  /** Total number of tools listed by the Codacy API. */
350
408
  totalApiTools: number;
351
- /** Number of tool entries added to the config from API defaults. */
409
+ /** Number of supported (local-adapter) tool entries added from API defaults. */
352
410
  addedTools: number;
411
+ /** Number of unsupported (cloud-only) tool entries added from API defaults. */
412
+ addedUnsupported: number;
353
413
  /** Whether a .codacy.yaml file was found and its exclude paths were applied. */
354
414
  codacyYamlApplied: boolean;
355
415
  }
@@ -373,6 +433,8 @@ interface InitRemoteStats {
373
433
  totalRemoteTools: number;
374
434
  /** Number of those tools that have a registered local adapter. */
375
435
  matchedAdapters: number;
436
+ /** Number of those tools included as unsupported (cloud-only) entries. */
437
+ unsupportedTools: number;
376
438
  /** Whether a .codacy.yaml file was found and its exclude paths were applied. */
377
439
  codacyYamlApplied: boolean;
378
440
  /** Number of cloud-ignored files fetched from the Codacy API. */
@@ -383,8 +445,10 @@ interface InitRemoteStats {
383
445
  /**
384
446
  * `--remote <provider> <org> <repo>` mode.
385
447
  * Fetches the full tool/pattern configuration from the repository's Codacy settings.
386
- * The remote config is treated as the source of truth — only tools with a registered
387
- * adapter are included.
448
+ * The remote config is treated as the source of truth: tools with a registered adapter
449
+ * are included as supported tools, and enabled tools without a local adapter are tracked
450
+ * as unsupported (cloud-only) entries (subject to the trackability filter — deprecated and
451
+ * client-side/standalone-only tools are skipped).
388
452
  *
389
453
  * @param onRemoteToolsFound - Optional callback invoked once after the list of enabled
390
454
  * remote tools is fetched. Receives the total count of enabled tools and the count
@@ -418,8 +482,10 @@ interface InitAutoStats {
418
482
  detectedFrameworks: number;
419
483
  /** Number of libraries detected by discover. */
420
484
  detectedLibraries: number;
421
- /** Number of tool entries added to the config. */
485
+ /** Number of tool entries added to the config (supported + unsupported). */
422
486
  selectedTools: number;
487
+ /** Number of those entries that are unsupported (cloud-only) tools. */
488
+ selectedUnsupported: number;
423
489
  /** Whether a .codacy.yaml file was found and its exclude paths were applied. */
424
490
  codacyYamlApplied: boolean;
425
491
  }
@@ -440,8 +506,16 @@ interface InitAutoStats {
440
506
  * ESLint core rules and their `@typescript-eslint` equivalents may both be
441
507
  * enabled, potentially causing duplicate findings. This will be addressed in v2
442
508
  * via conflict-resolution metadata in the mapping files.
509
+ *
510
+ * Unsupported (cloud-only) tools — descriptors with `unsupported: true` — are
511
+ * also filled when their languages overlap the detected stack. Their patterns are
512
+ * loaded lazily via `loadUnsupportedPatterns`; when it is not provided they are
513
+ * skipped. Tools that ship framework-specific patterns (e.g. PHPCS, dartanalyzer,
514
+ * SonarCSharp, CodeNarc) carry curated `frameworkMappings` on their descriptor, so
515
+ * those patterns are gated on detected frameworks exactly like supported tools;
516
+ * tools without mappings have every committed pattern treated as universal.
443
517
  */
444
- declare function initAutoConfig(repositoryRoot: string, adapters: Readonly<ToolAdapter>[], descriptors: AdapterDescriptor[], filters?: AutoFilters, onDiscoveryComplete?: (result: DiscoveryResult) => void, rawFilterString?: string): Promise<{
518
+ declare function initAutoConfig(repositoryRoot: string, adapters: Readonly<ToolAdapter>[], descriptors: AdapterDescriptor[], filters?: AutoFilters, onDiscoveryComplete?: (result: DiscoveryResult) => void, rawFilterString?: string, loadUnsupportedPatterns?: (toolId: string) => readonly PatternEntry[]): Promise<{
445
519
  config: CodacyConfig;
446
520
  stats: InitAutoStats;
447
521
  }>;
@@ -552,53 +626,6 @@ declare function listDiffFiles(repositoryRoot: string, base?: string): string[];
552
626
  */
553
627
  declare function listPrFiles(repositoryRoot: string): string[];
554
628
 
555
- /**
556
- * Codacy API client — thin wrappers around the generated OpenAPI client.
557
- *
558
- * The generated client (src/api/client/) is produced from the Codacy OpenAPI spec
559
- * and should never be edited by hand. All API interactions go through this file.
560
- *
561
- * Authentication: Codacy uses an `api-token` header (not Bearer). We set it via
562
- * `OpenAPI.HEADERS` before each call group. `configureApiToken()` must be called
563
- * before any authenticated endpoint is used.
564
- *
565
- * Pagination: all list endpoints use cursor-based pagination. `fetchAllPages()`
566
- * follows cursors until the response contains no next cursor.
567
- */
568
-
569
- /** Result of fetching cloud-ignored files, with pagination cap. */
570
- interface IgnoredFilesResult {
571
- /** File paths ignored in Codacy Cloud. */
572
- files: string[];
573
- /** Total number of files fetched (same as files.length). */
574
- totalFetched: number;
575
- /** Whether the fetch was stopped because it hit the 1000-file cap. */
576
- capped: boolean;
577
- }
578
- /**
579
- * Configures the API token used for authenticated requests.
580
- * Must be called before `getRepository`, `listRepositoryTools`, or
581
- * `listRepositoryToolPatterns`. Public endpoints (listTools, listPatterns)
582
- * do not require a token.
583
- */
584
- declare function configureApiToken(token: string): void;
585
- /**
586
- * Validates an API token by calling the authenticated user endpoint.
587
- * Returns user info on success; caller should catch errors for invalid tokens.
588
- */
589
- declare function validateApiToken(token: string): Promise<{
590
- valid: boolean;
591
- name?: string;
592
- email?: string;
593
- }>;
594
- /**
595
- * Fetches files individually ignored in Codacy Cloud for a repository.
596
- * Uses cursor-based pagination capped at 10 pages (1000 files max) to avoid
597
- * unbounded fetching for repositories with very large ignore lists.
598
- * Requires `configureApiToken()` to have been called.
599
- */
600
- declare function listIgnoredFiles(provider: string, org: string, repo: string): Promise<IgnoredFilesResult>;
601
-
602
629
  /**
603
630
  * Structured file + stderr logger for the analysis pipeline.
604
631
  *
@@ -673,4 +700,4 @@ interface DiscoverOptions {
673
700
  */
674
701
  declare function discover(repositoryRoot: string, options?: DiscoverOptions): Promise<DiscoveryResult>;
675
702
 
676
- export { type AdapterFactory, type AnalyzeOptions, type AutoFilters, type CategoryFilter, type CodacyYamlConfig, type CodacyYamlEngineConfig, type ConfigOperation, type DiscoverOptions, type ExecutionMode, type IgnoredFilesResult, type InitAutoStats, type InitDefaultStats, type InitLocalStats, type InitRemoteStats, type LoggerOptions, type PatternMetadataLookup, type SeverityFilter, analyze, applyCodacyYamlExcludes, applyConfigOperation, buildToolConfig, clearAdapters, computePatternSummary, configureApiToken, createCodacyConfig, createLogger, detectLanguages, diffConfigs, discover, formatOutput, formatTextSummary, getAdapter, getRegisteredAdapters, getRegisteredDescriptors, initAutoConfig, initContainerConfig, initDefaultConfig, initLocalConfig, initRemoteConfig, intersectConfigs, listDiffFiles, listIgnoredFiles, listPrFiles, listStagedFiles, mergeConfigs, parseAutoFilters, readCodacyConfig, readCodacyYaml, registerAdapter, registerLazyAdapter, resolveConfigPath, resolveEngineToAdapter, rotateLogFiles, validateApiToken, writeCodacyConfig, writeCodacyConfigFile };
703
+ export { type AdapterFactory, type AnalyzeOptions, type AutoFilters, type CategoryFilter, type CodacyYamlConfig, type CodacyYamlEngineConfig, type ConfigOperation, type DiscoverOptions, type ExecutionMode, type IgnoredFilesResult, type InitAutoStats, type InitDefaultStats, type InitLocalStats, type InitRemoteStats, type LoggerOptions, type PatternMetadataLookup, type SeverityFilter, analyze, applyCodacyYamlExcludes, applyConfigOperation, buildToolConfig, clearAdapters, computePatternSummary, configureApiToken, createCodacyConfig, createLogger, detectLanguages, diffConfigs, discover, formatOutput, formatTextSummary, getAdapter, getRegisteredAdapters, getRegisteredDescriptors, initAutoConfig, initContainerConfig, initDefaultConfig, initLocalConfig, initRemoteConfig, intersectConfigs, listDiffFiles, listIgnoredFiles, listPrFiles, listStagedFiles, mergeConfigs, parseAutoFilters, readCodacyConfig, readCodacyYaml, registerAdapter, registerLazyAdapter, registerUnsupportedTool, resolveConfigPath, resolveEngineToAdapter, rotateLogFiles, validateApiToken, writeCodacyConfig, writeCodacyConfigFile };
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,
@@ -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() {
@@ -231,6 +243,88 @@ var import_fs = __toESM(require("fs"));
231
243
  var import_path2 = __toESM(require("path"));
232
244
  var import_minimatch = require("minimatch");
233
245
  var MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
246
+ var PURE_EXTENSION_PATTERN = /^\*\*\/\*(\.[^*/.]+)$/;
247
+ function pureExtensionOf(pattern) {
248
+ const m = PURE_EXTENSION_PATTERN.exec(pattern);
249
+ return m ? m[1] : null;
250
+ }
251
+ var CompiledMatcher = class {
252
+ /** True if any pattern is the bare `"*"` wildcard (matches every file). */
253
+ matchAll;
254
+ literalPaths;
255
+ literalNames;
256
+ exts;
257
+ // Residual globs are split by whether they contain a path separator so the
258
+ // hot loop never runs a full-path match for a slash-less pattern (which can
259
+ // only ever match via the basename) — that call is always wasted on nested
260
+ // files. Slash patterns match against the full path; slash-less ones against
261
+ // the basename, which together reproduce the old dual check exactly.
262
+ residualPath;
263
+ residualName;
264
+ constructor(patterns) {
265
+ let matchAll = false;
266
+ const literalPaths = /* @__PURE__ */ new Set();
267
+ const literalNames = /* @__PURE__ */ new Set();
268
+ const exts = /* @__PURE__ */ new Set();
269
+ const residualPath = [];
270
+ const residualName = [];
271
+ for (const pattern of patterns) {
272
+ if (pattern === "*") {
273
+ matchAll = true;
274
+ continue;
275
+ }
276
+ const ext = pureExtensionOf(pattern);
277
+ if (ext !== null) {
278
+ exts.add(ext);
279
+ continue;
280
+ }
281
+ const mm = new import_minimatch.Minimatch(pattern, { dot: true });
282
+ if (mm.hasMagic()) {
283
+ (pattern.includes("/") ? residualPath : residualName).push(mm);
284
+ } else if (pattern.includes("/")) {
285
+ literalPaths.add(pattern);
286
+ } else {
287
+ literalNames.add(pattern);
288
+ }
289
+ }
290
+ this.matchAll = matchAll;
291
+ this.literalPaths = literalPaths;
292
+ this.literalNames = literalNames;
293
+ this.exts = exts;
294
+ this.residualPath = residualPath;
295
+ this.residualName = residualName;
296
+ }
297
+ /** True when this matcher holds no patterns at all (matches nothing). */
298
+ get isEmpty() {
299
+ return !this.matchAll && this.literalPaths.size === 0 && this.literalNames.size === 0 && this.exts.size === 0 && this.residualPath.length === 0 && this.residualName.length === 0;
300
+ }
301
+ /**
302
+ * Returns true if `file` matches any pattern. `basename` must be
303
+ * `path.basename(file)` — passed in so callers compute it once per file.
304
+ *
305
+ * Mirrors the historical dual check: a pattern matches if it matches the full
306
+ * relative path, or (for patterns with no path separator) the basename. For
307
+ * globstar/slash patterns the full-path check already subsumes the basename
308
+ * check, so gating the basename check on `!includes("/")` is equivalent to
309
+ * the old unconditional basename check used by `routeFilesToTool`.
310
+ */
311
+ matches(file, basename) {
312
+ if (this.matchAll) return true;
313
+ if (this.literalPaths.size > 0 && this.literalPaths.has(file)) return true;
314
+ if (this.literalNames.size > 0 && this.literalNames.has(basename)) return true;
315
+ if (this.exts.size > 0) {
316
+ const dot = basename.lastIndexOf(".");
317
+ if (dot >= 0 && this.exts.has(basename.slice(dot))) return true;
318
+ }
319
+ for (const m of this.residualPath) {
320
+ if (m.match(file)) return true;
321
+ }
322
+ for (const m of this.residualName) {
323
+ if (m.match(basename)) return true;
324
+ }
325
+ return false;
326
+ }
327
+ };
234
328
  function listRepoFiles(repositoryRoot) {
235
329
  try {
236
330
  const tracked = (0, import_child_process.execSync)("git ls-files", {
@@ -280,29 +374,19 @@ function filterOversizedFiles(files, repositoryRoot, logger) {
280
374
  return result;
281
375
  }
282
376
  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
- });
377
+ if (globalExclude.length === 0 && toolExclude.length === 0) return files;
378
+ const matcher = new CompiledMatcher([...globalExclude, ...toolExclude]);
379
+ if (matcher.isEmpty) return files;
380
+ return files.filter((file) => !matcher.matches(file, import_path2.default.basename(file)));
293
381
  }
294
382
  function routeFilesToTool(files, adapter, toolConfig) {
295
383
  const patterns = [...adapter.filePatterns];
296
384
  if (toolConfig?.extraFilePatterns) {
297
385
  patterns.push(...toolConfig.extraFilePatterns);
298
386
  }
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
- });
387
+ const matcher = new CompiledMatcher(patterns);
388
+ if (matcher.matchAll) return files;
389
+ return files.filter((file) => matcher.matches(file, import_path2.default.basename(file)));
306
390
  }
307
391
  function listStagedFiles(repositoryRoot) {
308
392
  try {
@@ -392,14 +476,15 @@ function detectDefaultBranch(repositoryRoot) {
392
476
  function filterByTargetFiles(allFiles, targetPatterns) {
393
477
  if (targetPatterns.length === 0) return allFiles;
394
478
  const normalized = targetPatterns.map((p) => p.replace(/^\.\//, ""));
479
+ const exact = new Set(normalized);
480
+ const globs = normalized.map((p) => new import_minimatch.Minimatch(p, { dot: true, matchBase: false })).filter((m) => m.hasMagic());
481
+ const dirPrefixes = normalized.filter((p) => p.endsWith("/"));
395
482
  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
- )
483
+ (f) => (
484
+ // Exact match (fast path)
485
+ exact.has(f) || // Glob match
486
+ globs.some((m) => m.match(f)) || // Directory prefix (e.g. "src/subdir/" matches all files under it)
487
+ dirPrefixes.some((p) => f.startsWith(p))
403
488
  )
404
489
  );
405
490
  }
@@ -634,6 +719,20 @@ var nullLogger = {
634
719
  error: () => {
635
720
  }
636
721
  };
722
+ var CHECK_CONCURRENCY = 12;
723
+ async function mapWithConcurrency(items, concurrency, task) {
724
+ const results = new Array(items.length);
725
+ let next = 0;
726
+ async function worker() {
727
+ while (next < items.length) {
728
+ const index = next++;
729
+ results[index] = await task(items[index], index);
730
+ }
731
+ }
732
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
733
+ await Promise.all(workers);
734
+ return results;
735
+ }
637
736
  async function analyze(options) {
638
737
  const startedAt = /* @__PURE__ */ new Date();
639
738
  const mode = options.mode || "standard";
@@ -727,23 +826,25 @@ async function analyze(options) {
727
826
  const unavailable = [];
728
827
  const availabilityTimeout = 6e4;
729
828
  const availabilityStartMs = Date.now();
730
- for (const adapter of adapters) {
829
+ const checkAvailabilityOf = async (adapter) => {
731
830
  const ctx = makeToolContext(baseCtx, adapter, []);
732
831
  const checkStart = Date.now();
733
832
  logger.debug("Checking availability", { toolId: adapter.id });
833
+ let timer;
734
834
  try {
735
835
  const availability = await Promise.race([
736
836
  adapter.checkAvailability(ctx),
737
- new Promise(
738
- (_, reject) => setTimeout(
837
+ new Promise((_, reject) => {
838
+ timer = setTimeout(
739
839
  () => reject(
740
840
  new Error(
741
841
  `checkAvailability timed out after ${(0, import_tooling3.formatDuration)(availabilityTimeout)}`
742
842
  )
743
843
  ),
744
844
  availabilityTimeout
745
- )
746
- )
845
+ );
846
+ timer.unref?.();
847
+ })
747
848
  ]);
748
849
  if (availability.available) {
749
850
  logger.debug("Tool available", {
@@ -752,38 +853,57 @@ async function analyze(options) {
752
853
  installation: availability.installation,
753
854
  durationMs: Date.now() - checkStart
754
855
  });
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({
856
+ return {
857
+ kind: "ready",
858
+ status: {
859
+ toolId: adapter.id,
860
+ displayName: adapter.displayName,
861
+ version: availability.version ?? adapter.preferredVersion,
862
+ installation: availability.installation ?? "global",
863
+ runner: "local"
864
+ }
865
+ };
866
+ }
867
+ logger.warn("Tool unavailable", {
868
+ toolId: adapter.id,
869
+ reason: availability.reason,
870
+ durationMs: Date.now() - checkStart
871
+ });
872
+ return {
873
+ kind: "unavailable",
874
+ status: {
769
875
  toolId: adapter.id,
770
876
  displayName: adapter.displayName,
771
877
  reason: availability.reason || "Tool is not available",
772
878
  ...availability.installHint ? { remediation: availability.installHint } : {}
773
- });
774
- }
879
+ }
880
+ };
775
881
  } catch (err) {
776
882
  logger.warn("Availability check failed", {
777
883
  toolId: adapter.id,
778
884
  error: err instanceof Error ? err.message : String(err),
779
885
  durationMs: Date.now() - checkStart
780
886
  });
781
- unavailable.push({
782
- toolId: adapter.id,
783
- displayName: adapter.displayName,
784
- reason: `Availability check failed: ${err instanceof Error ? err.message : String(err)}`
785
- });
887
+ return {
888
+ kind: "unavailable",
889
+ status: {
890
+ toolId: adapter.id,
891
+ displayName: adapter.displayName,
892
+ reason: `Availability check failed: ${err instanceof Error ? err.message : String(err)}`
893
+ }
894
+ };
895
+ } finally {
896
+ if (timer) clearTimeout(timer);
786
897
  }
898
+ };
899
+ const availabilityOutcomes = await mapWithConcurrency(
900
+ adapters,
901
+ CHECK_CONCURRENCY,
902
+ checkAvailabilityOf
903
+ );
904
+ for (const outcome of availabilityOutcomes) {
905
+ if (outcome.kind === "ready") ready.push(outcome.status);
906
+ else unavailable.push(outcome.status);
787
907
  }
788
908
  phaseTimings.availabilityMs = Date.now() - availabilityStartMs;
789
909
  const capability = {
@@ -800,6 +920,31 @@ async function analyze(options) {
800
920
  readyAfter: capability.ready.length,
801
921
  configuredToolCount: configuredIds.size
802
922
  });
923
+ const representedIds = /* @__PURE__ */ new Set([
924
+ ...capability.ready.map((t) => t.toolId),
925
+ ...capability.unavailable.map((t) => t.toolId)
926
+ ]);
927
+ const requestedLower = options.tools?.map((t) => t.toLowerCase());
928
+ const inScope = (toolId) => {
929
+ if (!requestedLower) return true;
930
+ const id = toolId.toLowerCase();
931
+ return requestedLower.some((r) => id.includes(r) || r.includes(id));
932
+ };
933
+ const descriptorsById = new Map(getRegisteredDescriptors().map((d) => [d.id, d]));
934
+ for (const toolConfig of codacyConfig.tools) {
935
+ if (!toolConfig?.toolId) continue;
936
+ if (representedIds.has(toolConfig.toolId)) continue;
937
+ if (!inScope(toolConfig.toolId)) continue;
938
+ const descriptor = descriptorsById.get(toolConfig.toolId);
939
+ if (!descriptor?.unsupported) continue;
940
+ capability.unavailable.push({
941
+ toolId: toolConfig.toolId,
942
+ displayName: descriptor.displayName ?? toolConfig.toolId,
943
+ reason: "No local analyzer (configured on Codacy Cloud)",
944
+ availableVia: "cloud"
945
+ });
946
+ representedIds.add(toolConfig.toolId);
947
+ }
803
948
  if (mode === "auto-install" && capability.unavailable.length > 0) {
804
949
  const installStartMs = Date.now();
805
950
  const installErrors = [];
@@ -908,8 +1053,9 @@ async function analyze(options) {
908
1053
  }
909
1054
  return buildResult(startedAt, options.repositoryRoot, mode, capability, [], [], []);
910
1055
  }
911
- if (mode === "strict" && capability.unavailable.length > 0) {
912
- const missingTools = capability.unavailable.map((u) => u.toolId).join(", ");
1056
+ const strictMissing = capability.unavailable.filter((u) => u.availableVia !== "cloud");
1057
+ if (mode === "strict" && strictMissing.length > 0) {
1058
+ const missingTools = strictMissing.map((u) => u.toolId).join(", ");
913
1059
  return buildResult(
914
1060
  startedAt,
915
1061
  options.repositoryRoot,
@@ -953,22 +1099,47 @@ async function analyze(options) {
953
1099
  allFiles = filterOversizedFiles(allFiles, options.repositoryRoot, logger);
954
1100
  phaseTimings.fileDiscoveryMs = Date.now() - fileDiscoveryStartMs;
955
1101
  const fileRoutingStartMs = Date.now();
956
- const readyAdapters = adapters.filter((a) => capability.ready.some((r) => r.toolId === a.id));
1102
+ const readyById = new Map(capability.ready.map((r) => [r.toolId, r]));
1103
+ const readyAdapters = adapters.filter((a) => readyById.has(a.id));
1104
+ const filesAfterGlobalExclude = applyExclusions(allFiles, globalExclude, []);
957
1105
  const toolRuns = [];
1106
+ const candidates = [];
958
1107
  for (const adapter of readyAdapters) {
959
1108
  const toolConfig = getToolConfig(codacyConfig, adapter.id);
960
1109
  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;
1110
+ const toolExclude = toolConfig.exclude || [];
1111
+ const filesForTool = toolExclude.length > 0 ? applyExclusions(filesAfterGlobalExclude, toolExclude, []) : filesAfterGlobalExclude;
1112
+ const toolFiles = routeFilesToTool(filesForTool, adapter, toolConfig);
1113
+ const readyEntry = readyById.get(adapter.id);
1114
+ if (readyEntry) {
1115
+ readyEntry.filesRouted = toolFiles.length;
967
1116
  }
968
- const configCtx = makeToolContext(baseCtx, adapter, toolFiles);
969
- const configCheck = await adapter.checkLocalConfigurationFile(configCtx);
1117
+ candidates.push({
1118
+ adapter,
1119
+ toolFiles,
1120
+ configCtx: makeToolContext(baseCtx, adapter, toolFiles),
1121
+ readyEntry
1122
+ });
1123
+ }
1124
+ const configChecks = await mapWithConcurrency(
1125
+ candidates,
1126
+ CHECK_CONCURRENCY,
1127
+ async (c) => {
1128
+ try {
1129
+ return await c.adapter.checkLocalConfigurationFile(c.configCtx);
1130
+ } catch (err) {
1131
+ logger.debug("Local config check failed, assuming none", {
1132
+ toolId: c.adapter.id,
1133
+ error: err instanceof Error ? err.message : String(err)
1134
+ });
1135
+ return { found: false };
1136
+ }
1137
+ }
1138
+ );
1139
+ for (let i = 0; i < candidates.length; i++) {
1140
+ const { adapter, toolFiles, configCtx, readyEntry } = candidates[i];
1141
+ const configCheck = configChecks[i];
970
1142
  const codacyToolConfig = buildToolConfig(codacyConfig, adapter, configCheck);
971
- const readyEntry = capability.ready.find((r) => r.toolId === adapter.id);
972
1143
  if (readyEntry) {
973
1144
  if (configCheck.found && codacyToolConfig.useLocalConfigurationFile) {
974
1145
  readyEntry.configSource = "local";
@@ -1301,13 +1472,14 @@ function formatTextSummary(result, isInspect) {
1301
1472
  ]);
1302
1473
  }
1303
1474
  for (const tool of unavailableTools) {
1475
+ const isUnsupported = tool.availableVia === "cloud";
1304
1476
  table.push([
1305
- import_ansis.default.dim.red(tool.displayName ?? tool.toolId),
1306
- "-",
1477
+ isUnsupported ? import_ansis.default.dim(tool.displayName ?? tool.toolId) : import_ansis.default.dim.red(tool.displayName ?? tool.toolId),
1307
1478
  "-",
1479
+ isUnsupported ? import_ansis.default.dim("Unsupported") : "-",
1308
1480
  "-",
1309
1481
  "-",
1310
- tool.reason
1482
+ isUnsupported ? import_ansis.default.dim("No local analyzer (Codacy Cloud)") : tool.reason
1311
1483
  ]);
1312
1484
  }
1313
1485
  lines.push(table.toString());
@@ -1332,13 +1504,14 @@ function formatTextSummary(result, isInspect) {
1332
1504
  ]);
1333
1505
  }
1334
1506
  for (const tool of unavailableTools) {
1507
+ const isUnsupported = tool.availableVia === "cloud";
1335
1508
  table.push([
1336
- import_ansis.default.dim.red(tool.displayName ?? tool.toolId),
1509
+ isUnsupported ? import_ansis.default.dim(tool.displayName ?? tool.toolId) : import_ansis.default.dim.red(tool.displayName ?? tool.toolId),
1337
1510
  "-",
1511
+ isUnsupported ? import_ansis.default.dim("Unsupported") : "-",
1338
1512
  "-",
1339
1513
  "-",
1340
- "-",
1341
- import_ansis.default.dim("missing binary"),
1514
+ isUnsupported ? import_ansis.default.dim("cloud-only") : import_ansis.default.dim("missing binary"),
1342
1515
  "-"
1343
1516
  ]);
1344
1517
  }
@@ -1954,6 +2127,12 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
1954
2127
  ["gson", { displayName: "Gson", kind: "library" }],
1955
2128
  ["okhttp", { displayName: "OkHttp", kind: "library" }],
1956
2129
  ["retrofit", { displayName: "Retrofit", kind: "library" }],
2130
+ ["spock-core", { displayName: "Spock", kind: "library" }],
2131
+ // --- Groovy — Frameworks ---
2132
+ // Grails/GORM artifact IDs vary (grails-core, grails-web, gorm-hibernate5, …);
2133
+ // the `grails-`/`gorm-` prefix rules below catch the rest.
2134
+ ["grails-core", { displayName: "Grails", kind: "framework" }],
2135
+ ["gorm-core", { displayName: "GORM", kind: "library" }],
1957
2136
  // --- Go — Frameworks ---
1958
2137
  ["github.com/gin-gonic/gin", { displayName: "Gin", kind: "framework" }],
1959
2138
  ["github.com/labstack/echo", { displayName: "Echo", kind: "framework" }],
@@ -1995,9 +2174,20 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
1995
2174
  // --- PHP — Frameworks ---
1996
2175
  ["laravel/framework", { displayName: "Laravel", kind: "framework" }],
1997
2176
  ["symfony/framework-bundle", { displayName: "Symfony", kind: "framework" }],
2177
+ ["symfony/symfony", { displayName: "Symfony", kind: "framework" }],
1998
2178
  ["codeigniter4/framework", { displayName: "CodeIgniter", kind: "framework" }],
1999
2179
  ["slim/slim", { displayName: "Slim", kind: "framework" }],
2000
2180
  ["cakephp/cakephp", { displayName: "CakePHP", kind: "framework" }],
2181
+ // WordPress is rarely a real Composer dependency; these are the common
2182
+ // "WordPress as a dependency" packages used by Bedrock/Composer-based sites.
2183
+ ["johnpbloch/wordpress-core", { displayName: "WordPress", kind: "framework" }],
2184
+ ["johnpbloch/wordpress", { displayName: "WordPress", kind: "framework" }],
2185
+ ["roots/wordpress", { displayName: "WordPress", kind: "framework" }],
2186
+ ["drupal/core", { displayName: "Drupal", kind: "framework" }],
2187
+ ["drupal/core-recommended", { displayName: "Drupal", kind: "framework" }],
2188
+ ["magento/product-community-edition", { displayName: "Magento", kind: "framework" }],
2189
+ ["magento/product-enterprise-edition", { displayName: "Magento", kind: "framework" }],
2190
+ ["magento/framework", { displayName: "Magento", kind: "framework" }],
2001
2191
  // --- PHP — Libraries ---
2002
2192
  ["phpunit/phpunit", { displayName: "PHPUnit", kind: "library" }],
2003
2193
  ["guzzlehttp/guzzle", { displayName: "Guzzle", kind: "library" }],
@@ -2027,7 +2217,32 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2027
2217
  ["scalatest", { displayName: "ScalaTest", kind: "library" }],
2028
2218
  ["specs2-core", { displayName: "Specs2", kind: "library" }],
2029
2219
  ["cats-core", { displayName: "Cats", kind: "library" }],
2030
- ["circe-core", { displayName: "Circe", kind: "library" }]
2220
+ ["circe-core", { displayName: "Circe", kind: "library" }],
2221
+ // --- Dart — Frameworks ---
2222
+ // `flutter` and `angular` appear as plain keys in pubspec.yaml.
2223
+ ["flutter", { displayName: "Flutter", kind: "framework" }],
2224
+ ["flutter_test", { displayName: "Flutter", kind: "framework" }],
2225
+ ["angular", { displayName: "Angular", kind: "framework" }],
2226
+ // --- Dart — Libraries ---
2227
+ // Kept to Dart-specific names with negligible cross-ecosystem collision risk
2228
+ // (generic names like "get"/"provider" are intentionally omitted).
2229
+ ["riverpod", { displayName: "Riverpod", kind: "library" }],
2230
+ ["flutter_bloc", { displayName: "Bloc", kind: "library" }],
2231
+ ["dio", { displayName: "Dio", kind: "library" }],
2232
+ // --- .NET (C# / VB) — Libraries ---
2233
+ // NuGet package IDs. Microsoft.* families are also covered by prefix rules below.
2234
+ ["EntityFramework", { displayName: "Entity Framework", kind: "library" }],
2235
+ ["NUnit", { displayName: "NUnit", kind: "library" }],
2236
+ ["Moq", { displayName: "Moq", kind: "library" }],
2237
+ ["NSubstitute", { displayName: "NSubstitute", kind: "library" }],
2238
+ ["FluentAssertions", { displayName: "FluentAssertions", kind: "library" }],
2239
+ ["Shouldly", { displayName: "Shouldly", kind: "library" }],
2240
+ ["NLog", { displayName: "NLog", kind: "library" }],
2241
+ ["log4net", { displayName: "log4net", kind: "library" }],
2242
+ ["Newtonsoft.Json", { displayName: "Json.NET", kind: "library" }],
2243
+ ["AutoMapper", { displayName: "AutoMapper", kind: "library" }],
2244
+ ["Dapper", { displayName: "Dapper", kind: "library" }],
2245
+ ["MediatR", { displayName: "MediatR", kind: "library" }]
2031
2246
  ]);
2032
2247
  var DEPENDENCY_PREFIX_RULES = [
2033
2248
  { prefix: "@angular/", classification: { displayName: "Angular", kind: "framework" } },
@@ -2057,7 +2272,35 @@ var DEPENDENCY_PREFIX_RULES = [
2057
2272
  { prefix: "@shopify/", classification: { displayName: "Shopify", kind: "platform" } },
2058
2273
  { prefix: "@aws-sdk/", classification: { displayName: "AWS SDK", kind: "library" } },
2059
2274
  { prefix: "@google-cloud/", classification: { displayName: "Google Cloud", kind: "library" } },
2060
- { prefix: "@azure/", classification: { displayName: "Azure SDK", kind: "library" } }
2275
+ { prefix: "@azure/", classification: { displayName: "Azure SDK", kind: "library" } },
2276
+ // Groovy — Grails/GORM artifact families (build.gradle artifactIds).
2277
+ { prefix: "grails-", classification: { displayName: "Grails", kind: "framework" } },
2278
+ { prefix: "gorm-", classification: { displayName: "GORM", kind: "library" } },
2279
+ // .NET — Microsoft package families and common test/logging libraries. More
2280
+ // specific prefixes (Blazor) must precede broader ones (ASP.NET Core).
2281
+ {
2282
+ prefix: "Microsoft.AspNetCore.Components.WebAssembly",
2283
+ classification: { displayName: "Blazor", kind: "framework" }
2284
+ },
2285
+ {
2286
+ prefix: "Microsoft.AspNetCore.",
2287
+ classification: { displayName: "ASP.NET Core", kind: "framework" }
2288
+ },
2289
+ {
2290
+ prefix: "Microsoft.EntityFrameworkCore",
2291
+ classification: { displayName: "Entity Framework", kind: "library" }
2292
+ },
2293
+ {
2294
+ prefix: "Microsoft.Azure.Functions",
2295
+ classification: { displayName: "Azure Functions", kind: "framework" }
2296
+ },
2297
+ {
2298
+ prefix: "Microsoft.Azure.WebJobs",
2299
+ classification: { displayName: "Azure Functions", kind: "framework" }
2300
+ },
2301
+ { prefix: "xunit", classification: { displayName: "xUnit", kind: "library" } },
2302
+ { prefix: "MSTest.", classification: { displayName: "MSTest", kind: "library" } },
2303
+ { prefix: "Serilog", classification: { displayName: "Serilog", kind: "library" } }
2061
2304
  ];
2062
2305
  function classifyDependency(name) {
2063
2306
  const exact = DEPENDENCY_CATALOG.get(name);
@@ -2086,7 +2329,12 @@ var NOTABLE_ANY_DEPTH = /* @__PURE__ */ new Set([
2086
2329
  "Package.swift",
2087
2330
  "Jenkinsfile",
2088
2331
  "Vagrantfile",
2089
- "Procfile"
2332
+ "Procfile",
2333
+ "pubspec.yaml",
2334
+ "mix.exs",
2335
+ "shard.yml",
2336
+ "packages.config",
2337
+ "Directory.Packages.props"
2090
2338
  ]);
2091
2339
  var NOTABLE_ROOT_ONLY = /* @__PURE__ */ new Set([
2092
2340
  ".dockerignore",
@@ -2146,6 +2394,7 @@ function isNotableFile(filePath) {
2146
2394
  const isRoot = !filePath.includes("/");
2147
2395
  if (NOTABLE_ANY_DEPTH.has(basename)) return true;
2148
2396
  if (isRoot && NOTABLE_ROOT_ONLY.has(basename)) return true;
2397
+ if (/\.(csproj|vbproj|fsproj|sln)$/i.test(basename)) return true;
2149
2398
  for (const prefix of NOTABLE_PATH_PREFIXES) {
2150
2399
  if (filePath.startsWith(prefix)) return true;
2151
2400
  }
@@ -2352,6 +2601,51 @@ function parsePackageSwift(content) {
2352
2601
  }
2353
2602
  return deps;
2354
2603
  }
2604
+ function parsePubspecYaml(content) {
2605
+ const deps = [];
2606
+ let inSection = false;
2607
+ let childIndent = -1;
2608
+ for (const rawLine of content.split("\n")) {
2609
+ const trimmed = rawLine.trim();
2610
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
2611
+ const indent = rawLine.length - rawLine.trimStart().length;
2612
+ if (indent === 0) {
2613
+ inSection = /^(dependencies|dev_dependencies|dependency_overrides):/.test(trimmed);
2614
+ childIndent = -1;
2615
+ continue;
2616
+ }
2617
+ if (!inSection) continue;
2618
+ if (childIndent === -1) childIndent = indent;
2619
+ if (indent !== childIndent) continue;
2620
+ const m = trimmed.match(/^([a-zA-Z0-9_.]+)\s*:(?:\s*["']?\^?([0-9][^"'#\s]*))?/);
2621
+ if (m) deps.push({ name: m[1], version: m[2] });
2622
+ }
2623
+ return deps;
2624
+ }
2625
+ function parseXmlDependencyElements(content, elementRegex, nameAttr) {
2626
+ const deps = [];
2627
+ const nameRegex = new RegExp(`\\b${nameAttr}\\s*=\\s*["']([^"']+)["']`, "i");
2628
+ const versionRegex = /\bversion\s*=\s*["']([^"']+)["']/i;
2629
+ let match;
2630
+ while ((match = elementRegex.exec(content)) !== null) {
2631
+ const attrs = match[1];
2632
+ const nameMatch = attrs.match(nameRegex);
2633
+ if (!nameMatch) continue;
2634
+ const versionMatch = attrs.match(versionRegex);
2635
+ deps.push({ name: nameMatch[1].trim(), version: versionMatch?.[1]?.trim() });
2636
+ }
2637
+ return deps;
2638
+ }
2639
+ function parseCsproj(content) {
2640
+ return parseXmlDependencyElements(
2641
+ content,
2642
+ /<Package(?:Reference|Version)\s+([^>]+?)\/?>/gi,
2643
+ "Include"
2644
+ );
2645
+ }
2646
+ function parsePackagesConfig(content) {
2647
+ return parseXmlDependencyElements(content, /<package\s+([^>]+?)\/?>/gi, "id");
2648
+ }
2355
2649
  var MANIFEST_PARSERS = /* @__PURE__ */ new Map([
2356
2650
  ["package.json", parsePackageJson],
2357
2651
  ["requirements.txt", parseRequirementsTxt],
@@ -2365,7 +2659,15 @@ var MANIFEST_PARSERS = /* @__PURE__ */ new Map([
2365
2659
  ["Cargo.toml", parseCargoToml],
2366
2660
  ["composer.json", parseComposerJson],
2367
2661
  ["Podfile", parsePodfile],
2368
- ["Package.swift", parsePackageSwift]
2662
+ ["Package.swift", parsePackageSwift],
2663
+ ["pubspec.yaml", parsePubspecYaml],
2664
+ ["packages.config", parsePackagesConfig],
2665
+ ["Directory.Packages.props", parseCsproj]
2666
+ ]);
2667
+ var MANIFEST_SUFFIX_PARSERS = /* @__PURE__ */ new Map([
2668
+ [".csproj", parseCsproj],
2669
+ [".vbproj", parseCsproj],
2670
+ [".fsproj", parseCsproj]
2369
2671
  ]);
2370
2672
 
2371
2673
  // src/discover.ts
@@ -2415,39 +2717,39 @@ function findNotableFiles(files) {
2415
2717
  }
2416
2718
  return Array.from(notable).sort();
2417
2719
  }
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
- }
2720
+ function resolveManifestParser(filePath) {
2721
+ const basename = filePath.split("/").pop() ?? filePath;
2722
+ const byName = MANIFEST_PARSERS.get(basename);
2723
+ if (byName) return byName;
2724
+ const lowerBasename = basename.toLowerCase();
2725
+ for (const [suffix, parser] of MANIFEST_SUFFIX_PARSERS) {
2726
+ if (lowerBasename.endsWith(suffix)) return parser;
2427
2727
  }
2728
+ return void 0;
2729
+ }
2730
+ async function extractDependencies(repositoryRoot, files) {
2428
2731
  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
- }
2732
+ for (const filePath of files) {
2733
+ const parser = resolveManifestParser(filePath);
2734
+ if (!parser) continue;
2735
+ let content;
2736
+ try {
2737
+ content = await import_promises2.default.readFile(import_path5.default.join(repositoryRoot, filePath), "utf-8");
2738
+ } catch {
2739
+ continue;
2740
+ }
2741
+ const rawDeps = parser(content);
2742
+ for (const raw of rawDeps) {
2743
+ const classification = classifyDependency(raw.name);
2744
+ if (classification) {
2745
+ allDeps.push({
2746
+ name: raw.name,
2747
+ displayName: classification.displayName,
2748
+ kind: classification.kind,
2749
+ source: "manifest",
2750
+ manifestFile: filePath,
2751
+ version: raw.version
2752
+ });
2451
2753
  }
2452
2754
  }
2453
2755
  }
@@ -6083,6 +6385,37 @@ async function listIgnoredFiles(provider, org, repo) {
6083
6385
  }
6084
6386
 
6085
6387
  // src/init.ts
6388
+ function deriveApiToolId(tool) {
6389
+ return tool.prefix ? tool.prefix.replace(/_$/, "") : tool.shortName;
6390
+ }
6391
+ function isTrackableTool(tool) {
6392
+ if (tool.clientSide) return false;
6393
+ if (tool.standalone) return false;
6394
+ if (/\bdeprecated\b/i.test(tool.name ?? "")) return false;
6395
+ return true;
6396
+ }
6397
+ function buildAdapterByUuid(adapters, apiTools) {
6398
+ const byKey = /* @__PURE__ */ new Map();
6399
+ for (const adapter of adapters) {
6400
+ byKey.set(adapter.id.toLowerCase(), adapter);
6401
+ byKey.set(adapter.displayName.toLowerCase(), adapter);
6402
+ }
6403
+ const result = /* @__PURE__ */ new Map();
6404
+ for (const tool of apiTools) {
6405
+ const candidates = [];
6406
+ if (tool.prefix) candidates.push(tool.prefix.replace(/_$/, "").toLowerCase());
6407
+ if (tool.shortName) candidates.push(tool.shortName.toLowerCase());
6408
+ if (tool.name) candidates.push(tool.name.toLowerCase());
6409
+ for (const candidate of candidates) {
6410
+ const adapter = byKey.get(candidate);
6411
+ if (adapter) {
6412
+ result.set(tool.uuid, adapter);
6413
+ break;
6414
+ }
6415
+ }
6416
+ }
6417
+ return result;
6418
+ }
6086
6419
  async function detectLanguages(repositoryRoot, precomputedFiles) {
6087
6420
  let files;
6088
6421
  if (precomputedFiles) {
@@ -6185,34 +6518,32 @@ async function initDefaultConfig(repositoryRoot, adapters) {
6185
6518
  );
6186
6519
  const apiTools = await listTools();
6187
6520
  const repoLanguages = new Set(base.metadata.languages);
6521
+ const adapterByUuid = buildAdapterByUuid(adapters, apiTools);
6188
6522
  const updatedTools = [...base.tools];
6189
6523
  const coveredToolIds = new Set(updatedTools.map((t) => t.toolId));
6190
6524
  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;
6525
+ let addedUnsupported = 0;
6526
+ for (const apiTool of apiTools) {
6527
+ const adapter = adapterByUuid.get(apiTool.uuid);
6528
+ if (adapter && toolsWithLocalConfig.has(adapter.id)) continue;
6529
+ if (!adapter && !isTrackableTool(apiTool)) continue;
6197
6530
  if (!apiTool.enabledByDefault) continue;
6198
6531
  if (apiTool.languages.length > 0 && repoLanguages.size > 0) {
6199
6532
  const hasOverlap = apiTool.languages.some((lang) => repoLanguages.has(lang));
6200
6533
  if (!hasOverlap) continue;
6201
6534
  }
6535
+ const toolId = adapter ? adapter.id : deriveApiToolId(apiTool);
6536
+ if (coveredToolIds.has(toolId)) continue;
6202
6537
  let patterns = [];
6203
6538
  try {
6204
6539
  const apiPatterns = await listPatterns(apiTool.uuid);
6205
6540
  patterns = apiPatterns.filter((p) => p.enabled).map((p) => ({ patternId: p.id }));
6206
6541
  } catch {
6207
6542
  }
6208
- if (!coveredToolIds.has(adapter.id)) {
6209
- updatedTools.push({
6210
- toolId: adapter.id,
6211
- patterns
6212
- });
6213
- coveredToolIds.add(adapter.id);
6214
- addedTools++;
6215
- }
6543
+ updatedTools.push({ toolId, patterns });
6544
+ coveredToolIds.add(toolId);
6545
+ if (adapter) addedTools++;
6546
+ else addedUnsupported++;
6216
6547
  }
6217
6548
  const baseConfig = createCodacyConfig({
6218
6549
  source: "default",
@@ -6224,7 +6555,10 @@ async function initDefaultConfig(repositoryRoot, adapters) {
6224
6555
  repositoryRoot,
6225
6556
  adapters
6226
6557
  );
6227
- return { config, stats: { totalApiTools: apiTools.length, addedTools, codacyYamlApplied } };
6558
+ return {
6559
+ config,
6560
+ stats: { totalApiTools: apiTools.length, addedTools, addedUnsupported, codacyYamlApplied }
6561
+ };
6228
6562
  }
6229
6563
  async function initRemoteConfig(repositoryRoot, provider, organization, repositoryName, adapters, apiToken, onRemoteToolsFound, onToolProcessed, onIgnoredFilesFetched) {
6230
6564
  configureApiToken(apiToken);
@@ -6236,56 +6570,61 @@ async function initRemoteConfig(repositoryRoot, provider, organization, reposito
6236
6570
  } catch {
6237
6571
  }
6238
6572
  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
6573
  const localConfigs = await detectLocalConfigFiles(repositoryRoot, adapters);
6245
6574
  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
- }
6575
+ const adapterByUuid = buildAdapterByUuid(adapters, allApiTools);
6576
+ const apiToolByUuid = new Map(allApiTools.map((t) => [t.uuid, t]));
6253
6577
  const matchedAdapterCount = remoteTools.filter((t) => adapterByUuid.has(t.uuid)).length;
6254
6578
  onRemoteToolsFound?.(remoteTools.length, matchedAdapterCount);
6579
+ const fetchPatterns = async (uuid) => {
6580
+ try {
6581
+ const configuredPatterns = await listRepositoryToolPatterns(
6582
+ provider,
6583
+ organization,
6584
+ repositoryName,
6585
+ uuid
6586
+ );
6587
+ return configuredPatterns.map((cp) => {
6588
+ const params = cp.parameters.length > 0 ? Object.fromEntries(cp.parameters.map((p) => [p.name, p.value])) : void 0;
6589
+ return {
6590
+ patternId: cp.patternDefinition.id,
6591
+ ...params ? { parameters: params } : {}
6592
+ };
6593
+ });
6594
+ } catch {
6595
+ return [];
6596
+ }
6597
+ };
6255
6598
  const tools = [];
6256
6599
  let matchedAdapters = 0;
6600
+ let unsupportedTools = 0;
6257
6601
  for (const remoteTool of remoteTools) {
6258
- const adapter = adapterByUuid.get(remoteTool.uuid);
6259
- if (!adapter) continue;
6260
- matchedAdapters++;
6261
6602
  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
- }
6603
+ const adapter = adapterByUuid.get(remoteTool.uuid);
6604
+ if (adapter) {
6605
+ matchedAdapters++;
6606
+ const localConfigPath = localConfigs.get(adapter.id);
6607
+ const patterns2 = usesLocalConfig ? [] : await fetchPatterns(remoteTool.uuid);
6608
+ onToolProcessed?.(adapter.displayName, patterns2.length, usesLocalConfig);
6609
+ tools.push({
6610
+ toolId: adapter.id,
6611
+ ...localConfigPath ? { localConfigurationFile: localConfigPath } : {},
6612
+ // Honour local config when the API says usesConfigurationFile, OR when a
6613
+ // local config exists and no patterns were configured (backwards-compat).
6614
+ ...usesLocalConfig || localConfigPath && patterns2.length === 0 ? { useLocalConfigurationFile: true } : {},
6615
+ patterns: patterns2
6616
+ });
6617
+ continue;
6281
6618
  }
6282
- onToolProcessed?.(adapter.displayName, patterns.length, usesLocalConfig);
6619
+ const apiTool = apiToolByUuid.get(remoteTool.uuid);
6620
+ if (!apiTool || !isTrackableTool(apiTool)) continue;
6621
+ const toolId = deriveApiToolId(apiTool);
6622
+ const patterns = usesLocalConfig ? [] : await fetchPatterns(remoteTool.uuid);
6623
+ unsupportedTools++;
6624
+ onToolProcessed?.(apiTool.name ?? toolId, patterns.length, usesLocalConfig);
6283
6625
  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 } : {},
6626
+ toolId,
6627
+ ...usesLocalConfig ? { useLocalConfigurationFile: true } : {},
6289
6628
  patterns
6290
6629
  });
6291
6630
  }
@@ -6311,6 +6650,7 @@ async function initRemoteConfig(repositoryRoot, provider, organization, reposito
6311
6650
  stats: {
6312
6651
  totalRemoteTools: remoteTools.length,
6313
6652
  matchedAdapters,
6653
+ unsupportedTools,
6314
6654
  codacyYamlApplied,
6315
6655
  ignoredFilesCount: ignoredFilesResult.totalFetched,
6316
6656
  ignoredFilesCapped: ignoredFilesResult.capped
@@ -6372,7 +6712,7 @@ async function initContainerConfig(repositoryRoot, adapters) {
6372
6712
  }
6373
6713
  return createCodacyConfig({ source: "container", tools, includeFiles });
6374
6714
  }
6375
- async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, onDiscoveryComplete, rawFilterString) {
6715
+ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, onDiscoveryComplete, rawFilterString, loadUnsupportedPatterns) {
6376
6716
  const discoveryResult = await discover(repositoryRoot);
6377
6717
  onDiscoveryComplete?.(discoveryResult);
6378
6718
  const detectedKeys = /* @__PURE__ */ new Set();
@@ -6421,6 +6761,34 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
6421
6761
  });
6422
6762
  }
6423
6763
  }
6764
+ let selectedUnsupported = 0;
6765
+ const selectedFamilies = new Set(tools.map((t) => toolFamily(t.toolId)));
6766
+ for (const desc of descriptors) {
6767
+ if (!desc.unsupported) continue;
6768
+ if (!loadUnsupportedPatterns) break;
6769
+ if (!desc.languages.includes("*") && desc.languages.length > 0 && repoLanguages.size > 0 && !desc.languages.some((lang) => repoLanguages.has(lang))) {
6770
+ continue;
6771
+ }
6772
+ const family = toolFamily(desc.id);
6773
+ if (selectedFamilies.has(family)) continue;
6774
+ const patternsMetadata = loadUnsupportedPatterns(desc.id);
6775
+ if (patternsMetadata.length === 0) continue;
6776
+ const selectedPatterns = filterAutoPatterns(
6777
+ patternsMetadata,
6778
+ desc.frameworkMappings,
6779
+ detectedKeys,
6780
+ repoLanguages,
6781
+ filters
6782
+ );
6783
+ if (selectedPatterns.length > 0) {
6784
+ tools.push({
6785
+ toolId: desc.id,
6786
+ patterns: selectedPatterns.map((p) => ({ patternId: p.id }))
6787
+ });
6788
+ selectedFamilies.add(family);
6789
+ selectedUnsupported++;
6790
+ }
6791
+ }
6424
6792
  const baseConfig = createCodacyConfig({
6425
6793
  source: "auto",
6426
6794
  languages: discoveryResult.languages,
@@ -6439,6 +6807,7 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
6439
6807
  detectedFrameworks: discoveryResult.frameworks.length,
6440
6808
  detectedLibraries: discoveryResult.libraries.length,
6441
6809
  selectedTools: tools.length,
6810
+ selectedUnsupported,
6442
6811
  codacyYamlApplied
6443
6812
  }
6444
6813
  };
@@ -6760,6 +7129,7 @@ function createLogger(options = {}) {
6760
7129
  readCodacyYaml,
6761
7130
  registerAdapter,
6762
7131
  registerLazyAdapter,
7132
+ registerUnsupportedTool,
6763
7133
  resolveConfigPath,
6764
7134
  resolveEngineToAdapter,
6765
7135
  rotateLogFiles,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/analysis-runner",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Core orchestration engine for Codacy analysis",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,11 +12,11 @@
12
12
  "cli-table3": "^0.6",
13
13
  "minimatch": "10.2.4",
14
14
  "yaml": "^2.6.0",
15
- "@codacy/tooling": "0.10.1"
15
+ "@codacy/tooling": "0.11.0"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@codacy/openapi-typescript-codegen": "0.0.9",
19
- "@codacy/tools-dummy-1": "0.10.1"
19
+ "@codacy/tools-dummy-1": "0.11.0"
20
20
  },
21
21
  "license": "MIT",
22
22
  "author": "Codacy <support@codacy.com> (https://www.codacy.com)",