@codacy/analysis-runner 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -469,8 +469,31 @@ declare function listIgnoredFiles(provider: string, org: string, repo: string):
469
469
  * extensions/filenames against LANGUAGE_CONFIG. When `precomputedFiles` is
470
470
  * provided, uses that list directly instead of running `git ls-files`.
471
471
  * Returns languages sorted alphabetically for deterministic output.
472
+ *
473
+ * `exclude` (optional) drops files matching those globs before detection, so a
474
+ * language whose only files live in excluded paths is not reported. Every caller
475
+ * that turns the result into tool selection must pass the excludes `analyze`
476
+ * will apply — a language detected from a file `analyze` never reads produces a
477
+ * tool that analyzes zero files. Applied to `precomputedFiles` too (filtering is
478
+ * idempotent, so pre-filtered lists are safe to pass).
479
+ */
480
+ declare function detectLanguages(repositoryRoot: string, precomputedFiles?: string[], exclude?: readonly string[]): Promise<Language[]>;
481
+ /**
482
+ * Options shared by the init modes that derive configuration from the local
483
+ * repository (`local`, `default`, `auto`).
472
484
  */
473
- declare function detectLanguages(repositoryRoot: string, precomputedFiles?: string[]): Promise<Language[]>;
485
+ interface InitDiscoveryOptions {
486
+ /**
487
+ * The config currently on disk, when re-generating via `update-config`.
488
+ *
489
+ * Its `exclude` (global and per-tool) is unioned with the freshly-read
490
+ * `.codacy.yaml` to constrain re-discovery, so excludes the user hand-wrote
491
+ * into `codacy.config.json` — or that arrived from Codacy Cloud's
492
+ * ignored-files list — don't get re-populated with tools that analyze zero
493
+ * files. Omit for a fresh `init`, where only `.codacy.yaml` applies.
494
+ */
495
+ existingConfig?: CodacyConfig;
496
+ }
474
497
  /** Stats returned alongside the config from `initLocalConfig`. */
475
498
  interface InitLocalStats {
476
499
  /** Whether a .codacy.yaml file was found and its exclude paths were applied. */
@@ -482,7 +505,7 @@ interface InitLocalStats {
482
505
  * that have a local configuration file present in the repository.
483
506
  * If a .codacy.yaml file is present, its exclude paths are merged into the config.
484
507
  */
485
- declare function initLocalConfig(repositoryRoot: string, adapters: Readonly<ToolAdapter>[]): Promise<{
508
+ declare function initLocalConfig(repositoryRoot: string, adapters: Readonly<ToolAdapter>[], options?: InitDiscoveryOptions): Promise<{
486
509
  config: CodacyConfig;
487
510
  stats: InitLocalStats;
488
511
  }>;
@@ -507,7 +530,7 @@ interface InitDefaultStats {
507
530
  *
508
531
  * Returns both the config and stats so the caller can report what happened.
509
532
  */
510
- declare function initDefaultConfig(repositoryRoot: string, adapters: Readonly<ToolAdapter>[]): Promise<{
533
+ declare function initDefaultConfig(repositoryRoot: string, adapters: Readonly<ToolAdapter>[], options?: InitDiscoveryOptions): Promise<{
511
534
  config: CodacyConfig;
512
535
  stats: InitDefaultStats;
513
536
  }>;
@@ -586,7 +609,12 @@ interface InitAutoStats {
586
609
  *
587
610
  * Severity and category filters are ANDed across dimensions, ORed within.
588
611
  *
589
- * **Known limitation (v1):** When both TypeScript and JavaScript are detected,
612
+ * Tool-level overlap (two *different* tools that lint the same stack, e.g.
613
+ * ESLint and Biome) is resolved by `deduplicateToolFamilies` via each
614
+ * descriptor's `alternativeGroup` — only one member of a group is selected.
615
+ *
616
+ * **Known limitation (v1):** *Pattern*-level overlap within a single tool is
617
+ * not yet resolved — e.g. when both TypeScript and JavaScript are detected,
590
618
  * ESLint core rules and their `@typescript-eslint` equivalents may both be
591
619
  * enabled, potentially causing duplicate findings. This will be addressed in v2
592
620
  * via conflict-resolution metadata in the mapping files.
@@ -599,7 +627,7 @@ interface InitAutoStats {
599
627
  * those patterns are gated on detected frameworks exactly like supported tools;
600
628
  * tools without mappings have every committed pattern treated as universal.
601
629
  */
602
- declare function initAutoConfig(repositoryRoot: string, adapters: Readonly<ToolAdapter>[], descriptors: AdapterDescriptor[], filters?: AutoFilters, onDiscoveryComplete?: (result: DiscoveryResult) => void, rawFilterString?: string, loadUnsupportedPatterns?: (toolId: string) => readonly PatternEntry[]): Promise<{
630
+ declare function initAutoConfig(repositoryRoot: string, adapters: Readonly<ToolAdapter>[], descriptors: AdapterDescriptor[], filters?: AutoFilters, onDiscoveryComplete?: (result: DiscoveryResult) => void, rawFilterString?: string, loadUnsupportedPatterns?: (toolId: string) => readonly PatternEntry[], options?: InitDiscoveryOptions): Promise<{
603
631
  config: CodacyConfig;
604
632
  stats: InitAutoStats;
605
633
  }>;
@@ -661,6 +689,41 @@ declare function resolveEngineToAdapter(engineName: string, adapters: Readonly<T
661
689
  * Returns a new config object (does not mutate the input).
662
690
  */
663
691
  declare function applyCodacyYamlExcludes(config: CodacyConfig, yamlConfig: CodacyYamlConfig, adapters: Readonly<ToolAdapter>[], logger?: Logger): CodacyConfig;
692
+ /**
693
+ * The exclude patterns repository discovery must honor, resolved from every
694
+ * source that will also constrain `analyze`.
695
+ */
696
+ interface DiscoveryExcludes {
697
+ /** Parsed `.codacy.yaml`, or null when the repository has none. Reused by `applyCodacyYamlExcludes` so the file is read once per init. */
698
+ yamlConfig: CodacyYamlConfig | null;
699
+ /** Global patterns — applied to every file-derived decision (languages, frameworks, tool selection). */
700
+ global: string[];
701
+ /** Per-adapter-ID patterns, applied on top of `global` when deciding whether that specific tool has files. */
702
+ perTool: Map<string, string[]>;
703
+ }
704
+ /**
705
+ * Resolves the exclude patterns that repository discovery must honor so that
706
+ * tool/language selection sees the same file set `analyze` will.
707
+ *
708
+ * Without this, discovery reads the raw `git ls-files` output while `analyze`
709
+ * reads that output minus `exclude` — so `init --auto` enables tools whose only
710
+ * evidence sits in excluded paths (e.g. language fixtures under
711
+ * `**\/test/fixtures/**`), and every one of them then receives zero files.
712
+ *
713
+ * Sources, unioned:
714
+ * - `.codacy.yaml` global `exclude_paths` → `global`
715
+ * - `.codacy.yaml` per-engine `exclude_paths` → `perTool` (engine names resolved
716
+ * via {@link resolveEngineToAdapter}; unresolvable names are skipped)
717
+ * - `existingConfig.exclude` / `existingConfig.tools[].exclude` → both, for
718
+ * `update-config`, so excludes the user hand-wrote into `codacy.config.json`
719
+ * (or that arrived from Codacy Cloud's ignored-files list) also constrain
720
+ * re-discovery
721
+ *
722
+ * `.codacy.yaml` is always re-read rather than trusted from the existing config,
723
+ * since it is the user-editable source of truth and may have changed since the
724
+ * config was generated.
725
+ */
726
+ declare function resolveDiscoveryExcludes(repositoryRoot: string, adapters: Readonly<ToolAdapter>[], existingConfig?: CodacyConfig): Promise<DiscoveryExcludes>;
664
727
 
665
728
  /**
666
729
  * Computes pattern-level summary statistics for analysis results.
@@ -829,6 +892,18 @@ declare function resolveApiToken(): Promise<string>;
829
892
  interface DiscoverOptions {
830
893
  /** Skip dependency extraction from manifests (only detect files and languages). */
831
894
  skipDependencies?: boolean;
895
+ /**
896
+ * Glob patterns to exclude from the scan, on top of `.gitignore`.
897
+ *
898
+ * Callers that feed discovery into configuration decisions **must** pass the
899
+ * same excludes `analyze` will apply (see `resolveDiscoveryExcludes` in
900
+ * `./codacy-yaml`). Otherwise discovery reports languages, frameworks, and
901
+ * libraries that only exist in excluded paths — e.g. a tool-orchestration
902
+ * monorepo whose per-language sample files all live under
903
+ * `**\/test/fixtures/**` looks like a 20-language polyglot repo, and every
904
+ * tool selected from those languages then analyzes zero files.
905
+ */
906
+ exclude?: readonly string[];
832
907
  }
833
908
  /**
834
909
  * Scans a repository and returns a structured report of languages, file patterns,
@@ -897,6 +972,10 @@ declare function resolveHeadCommit(repositoryRoot: string): string;
897
972
  * Codacy patterns). We normalize either format into a flat issue list and build the
898
973
  * payload directly.
899
974
  *
975
+ * Issues from tools that group many findings under one pattern additionally carry
976
+ * `sourceId` (e.g. Trivy's CVE id), gated on the adapter descriptor's
977
+ * `needsSourceIdUpload` — see {@link buildPayloads}.
978
+ *
900
979
  * ## Where it uploads
901
980
  *
902
981
  * The **legacy v2 API** (`https://api.codacy.com/2.0`), matching the Go CLI: one
@@ -928,6 +1007,12 @@ interface NormalizedIssue {
928
1007
  message: string;
929
1008
  /** Codacy level string (Info/Warning/High/Error for json; Info/Warning/Error for sarif). */
930
1009
  level: string;
1010
+ /**
1011
+ * The tool's own finding id (e.g. a CVE), when the report carries one. Only
1012
+ * forwarded to the payload for tools whose descriptor sets
1013
+ * `needsSourceIdUpload` — see {@link buildPayloads}.
1014
+ */
1015
+ sourceId?: string;
931
1016
  }
932
1017
  /** Authentication + routing for the upload. */
933
1018
  type UploadAuth = {
@@ -990,6 +1075,8 @@ interface PayloadIssue {
990
1075
  line: number;
991
1076
  };
992
1077
  };
1078
+ /** Present only for tools that need it; see {@link buildPayloads}. */
1079
+ sourceId?: string;
993
1080
  }
994
1081
  /**
995
1082
  * Resolves the base URL for the legacy v2 results API. Defaults to Codacy SaaS;
@@ -1007,8 +1094,17 @@ declare function parseReport(fileContents: string): NormalizedIssue[];
1007
1094
  * Builds one v2 payload per tool, grouping issues by file. `resolveShortName` maps
1008
1095
  * our internal tool id to the Codacy tool short-name expected in the `tool` field
1009
1096
  * (which becomes an S3 key segment consumed downstream).
1097
+ *
1098
+ * `needsSourceId` decides, per tool, whether `sourceId` is included on each issue —
1099
+ * mirroring the Go CLI, which gates it on the tool's `needs_source_id_upload`
1100
+ * manifest flag. This is not cosmetic: `sourceId` is one of the four inputs to
1101
+ * Codacy's deterministic result UUID, so sending it for a tool the cloud engine
1102
+ * doesn't send it for would give the same finding a different UUID. Tools that
1103
+ * don't need it omit the field entirely rather than sending an empty string.
1104
+ * Defaults to never including it, so a caller that hasn't opted in can't change
1105
+ * UUIDs by accident.
1010
1106
  */
1011
- declare function buildPayloads(issues: NormalizedIssue[], resolveShortName: (toolId: string) => string): ToolPayload[];
1107
+ declare function buildPayloads(issues: NormalizedIssue[], resolveShortName: (toolId: string) => string, needsSourceId?: (toolId: string) => boolean): ToolPayload[];
1012
1108
  /**
1013
1109
  * Uploads an analysis report to Codacy. Reads and parses the report, resolves tool
1014
1110
  * short-names, POSTs one `issuesRemoteResults` per tool, then a single `resultsFinal`.
@@ -1023,4 +1119,4 @@ declare function buildPayloads(issues: NormalizedIssue[], resolveShortName: (too
1023
1119
  */
1024
1120
  declare function uploadReport(options: UploadOptions): Promise<UploadResult>;
1025
1121
 
1026
- 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 MergeOptions, type NormalizedIssue, type PatternMetadataLookup, type SeverityFilter, type UploadAuth, type UploadOptions, type UploadResult, analyze, applyCodacyYamlExcludes, applyConfigOperation, buildPayloads, buildToolConfig, clearAdapters, computePatternSummary, configureApiToken, createCodacyConfig, createLogger, createNullLogger, deleteCredentials, detectLanguages, diffConfigs, discover, formatOutput, formatTextSummary, getAdapter, getKnownDisplayNames, getRegisteredAdapters, getRegisteredDescriptors, initAutoConfig, initContainerConfig, initDefaultConfig, initLocalConfig, initRemoteConfig, intersectConfigs, listDiffFiles, listIgnoredFiles, listPrFiles, listStagedFiles, loadCredentials, mergeConfigs, parseAutoFilters, parseReport, pruneEmptyTools, readBaselineConfig, readCodacyConfig, readCodacyYaml, registerAdapter, registerLazyAdapter, registerUnsupportedTool, resolveApiToken, resolveBaselinePath, resolveConfigPath, resolveEngineToAdapter, resolveHeadCommit, resolveRepositoryRoot, resolveResultsApiBaseUrl, rotateLogFiles, saveCredentials, updateConfigIncremental, uploadReport, validateApiToken, writeBaselineConfig, writeCodacyConfig, writeCodacyConfigFile };
1122
+ export { type AdapterFactory, type AnalyzeOptions, type AutoFilters, type CategoryFilter, type CodacyYamlConfig, type CodacyYamlEngineConfig, type ConfigOperation, type DiscoverOptions, type DiscoveryExcludes, type ExecutionMode, type IgnoredFilesResult, type InitAutoStats, type InitDefaultStats, type InitDiscoveryOptions, type InitLocalStats, type InitRemoteStats, type LoggerOptions, type MergeOptions, type NormalizedIssue, type PatternMetadataLookup, type SeverityFilter, type UploadAuth, type UploadOptions, type UploadResult, analyze, applyCodacyYamlExcludes, applyConfigOperation, buildPayloads, buildToolConfig, clearAdapters, computePatternSummary, configureApiToken, createCodacyConfig, createLogger, createNullLogger, deleteCredentials, detectLanguages, diffConfigs, discover, formatOutput, formatTextSummary, getAdapter, getKnownDisplayNames, getRegisteredAdapters, getRegisteredDescriptors, initAutoConfig, initContainerConfig, initDefaultConfig, initLocalConfig, initRemoteConfig, intersectConfigs, listDiffFiles, listIgnoredFiles, listPrFiles, listStagedFiles, loadCredentials, mergeConfigs, parseAutoFilters, parseReport, pruneEmptyTools, readBaselineConfig, readCodacyConfig, readCodacyYaml, registerAdapter, registerLazyAdapter, registerUnsupportedTool, resolveApiToken, resolveBaselinePath, resolveConfigPath, resolveDiscoveryExcludes, resolveEngineToAdapter, resolveHeadCommit, resolveRepositoryRoot, resolveResultsApiBaseUrl, rotateLogFiles, saveCredentials, updateConfigIncremental, uploadReport, validateApiToken, writeBaselineConfig, writeCodacyConfig, writeCodacyConfigFile };
package/dist/index.js CHANGED
@@ -76,6 +76,7 @@ __export(index_exports, {
76
76
  resolveApiToken: () => resolveApiToken,
77
77
  resolveBaselinePath: () => resolveBaselinePath,
78
78
  resolveConfigPath: () => resolveConfigPath,
79
+ resolveDiscoveryExcludes: () => resolveDiscoveryExcludes,
79
80
  resolveEngineToAdapter: () => resolveEngineToAdapter,
80
81
  resolveHeadCommit: () => resolveHeadCommit,
81
82
  resolveProxyConfig: () => import_tooling6.resolveProxyConfig,
@@ -515,7 +516,7 @@ function isDependencyManifest(relativePath) {
515
516
  if (DEPENDENCY_MANIFEST_FILES.has(base)) return true;
516
517
  return DEPENDENCY_MANIFEST_SUFFIXES.some((suffix) => base.endsWith(suffix));
517
518
  }
518
- function listRepoFiles(repositoryRoot) {
519
+ function listRepoFiles(repositoryRoot, exclude) {
519
520
  try {
520
521
  const tracked = (0, import_child_process2.execSync)("git ls-files", {
521
522
  cwd: repositoryRoot,
@@ -534,7 +535,8 @@ function listRepoFiles(repositoryRoot) {
534
535
  for (const f of untracked.split("\n")) {
535
536
  if (f) files.add(f);
536
537
  }
537
- return Array.from(files).sort();
538
+ const all = Array.from(files).sort();
539
+ return exclude && exclude.length > 0 ? applyExclusions(all, [...exclude], []) : all;
538
540
  } catch {
539
541
  return [];
540
542
  }
@@ -1652,6 +1654,11 @@ function formatSarif(result) {
1652
1654
  ruleIndex: ruleIndex.get(issue.patternId) ?? 0,
1653
1655
  level: mapSeverityToSarif(issue.severity),
1654
1656
  message: { text: issue.message },
1657
+ // SARIF has no first-class field for a tool's own finding id, so it rides in
1658
+ // the result property bag. `upload` reads it back from here — without it the
1659
+ // SARIF path would silently drop `sourceId` and produce different Codacy
1660
+ // result UUIDs than the JSON path for the same analysis.
1661
+ ...issue.sourceId ? { properties: { sourceId: issue.sourceId } } : {},
1655
1662
  locations: [
1656
1663
  {
1657
1664
  physicalLocation: {
@@ -1716,7 +1723,8 @@ function formatContainer(result) {
1716
1723
  patternId: issue.patternId,
1717
1724
  line: issue.line,
1718
1725
  message: issue.message,
1719
- suggestion: issue.suggestion
1726
+ suggestion: issue.suggestion,
1727
+ sourceId: issue.sourceId
1720
1728
  })
1721
1729
  ).join("\n");
1722
1730
  }
@@ -2320,6 +2328,7 @@ function updateConfigIncremental(base, current, next) {
2320
2328
 
2321
2329
  // src/init.ts
2322
2330
  var import_promises4 = __toESM(require("fs/promises"));
2331
+ var import_os3 = __toESM(require("os"));
2323
2332
  var import_path8 = __toESM(require("path"));
2324
2333
  var import_child_process4 = require("child_process");
2325
2334
  var import_tooling5 = require("@codacy/tooling");
@@ -2401,6 +2410,8 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2401
2410
  ["graphql", { displayName: "GraphQL", kind: "library" }],
2402
2411
  ["@apollo/client", { displayName: "Apollo Client", kind: "library" }],
2403
2412
  ["socket.io", { displayName: "Socket.IO", kind: "library" }],
2413
+ ["jquery", { displayName: "jQuery", kind: "library" }],
2414
+ ["mongodb", { displayName: "MongoDB", kind: "library" }],
2404
2415
  ["three", { displayName: "Three.js", kind: "library" }],
2405
2416
  ["d3", { displayName: "D3.js", kind: "library" }],
2406
2417
  ["chart.js", { displayName: "Chart.js", kind: "library" }],
@@ -2483,6 +2494,10 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2483
2494
  ["matplotlib", { displayName: "Matplotlib", kind: "library" }],
2484
2495
  ["seaborn", { displayName: "Seaborn", kind: "library" }],
2485
2496
  ["plotly", { displayName: "Plotly", kind: "library" }],
2497
+ // MongoDB drivers, one entry per ecosystem — they all gate the same
2498
+ // MongoDB-specific patterns (Opengrep's `java.mongodb.`/`python.pymongo.`).
2499
+ ["pymongo", { displayName: "MongoDB", kind: "library" }],
2500
+ ["motor", { displayName: "MongoDB", kind: "library" }],
2486
2501
  // --- Python — Tools ---
2487
2502
  ["black", { displayName: "Black", kind: "tool" }],
2488
2503
  ["ruff", { displayName: "Ruff", kind: "tool" }],
@@ -2513,9 +2528,16 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2513
2528
  ["minitest", { displayName: "Minitest", kind: "library" }],
2514
2529
  ["factory_bot", { displayName: "FactoryBot", kind: "library" }],
2515
2530
  ["dry-validation", { displayName: "dry-validation", kind: "library" }],
2531
+ // NOTE: `faker` and `rake` are Gemfile-scoped (MANIFEST_SCOPED_CATALOG), not
2532
+ // global — they are bare words that also name npm/PyPI packages.
2516
2533
  // --- Ruby — Tools ---
2517
2534
  ["rubocop", { displayName: "RuboCop", kind: "tool" }],
2518
2535
  ["bundler", { displayName: "Bundler", kind: "tool" }],
2536
+ // Sorbet ships as several gems; any of them means the project is Sorbet-typed.
2537
+ ["sorbet", { displayName: "Sorbet", kind: "tool" }],
2538
+ ["sorbet-runtime", { displayName: "Sorbet", kind: "tool" }],
2539
+ ["sorbet-static", { displayName: "Sorbet", kind: "tool" }],
2540
+ ["sorbet-static-and-runtime", { displayName: "Sorbet", kind: "tool" }],
2519
2541
  ["solargraph", { displayName: "Solargraph", kind: "tool" }],
2520
2542
  // --- Java / Kotlin — Frameworks ---
2521
2543
  ["spring-boot-starter", { displayName: "Spring Boot", kind: "framework" }],
@@ -2546,6 +2568,10 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2546
2568
  ["okhttp", { displayName: "OkHttp", kind: "library" }],
2547
2569
  ["retrofit", { displayName: "Retrofit", kind: "library" }],
2548
2570
  ["spock-core", { displayName: "Spock", kind: "library" }],
2571
+ ["mongodb-driver-sync", { displayName: "MongoDB", kind: "library" }],
2572
+ ["mongodb-driver-core", { displayName: "MongoDB", kind: "library" }],
2573
+ ["mongodb-driver-reactivestreams", { displayName: "MongoDB", kind: "library" }],
2574
+ ["mongo-java-driver", { displayName: "MongoDB", kind: "library" }],
2549
2575
  // --- Groovy — Frameworks ---
2550
2576
  // Grails/GORM artifact IDs vary (grails-core, grails-web, gorm-hibernate5, …);
2551
2577
  // the `grails-`/`gorm-` prefix rules below catch the rest.
@@ -2629,7 +2655,19 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2629
2655
  ["RxSwift", { displayName: "RxSwift", kind: "library" }],
2630
2656
  ["RealmSwift", { displayName: "Realm", kind: "library" }],
2631
2657
  // --- Scala — Frameworks ---
2658
+ // Play is declared as an sbt *plugin*, so `group:artifact` (from
2659
+ // `addSbtPlugin`) is the reliable signal; the library artifacts below only
2660
+ // appear in some builds. `play-json` is deliberately absent — it is a
2661
+ // standalone JSON library used widely outside Play.
2662
+ ["com.typesafe.play:sbt-plugin", { displayName: "Play Framework", kind: "framework" }],
2663
+ ["org.playframework:sbt-plugin", { displayName: "Play Framework", kind: "framework" }],
2664
+ ["play", { displayName: "Play Framework", kind: "framework" }],
2632
2665
  ["play-server", { displayName: "Play Framework", kind: "framework" }],
2666
+ ["play-ws", { displayName: "Play Framework", kind: "framework" }],
2667
+ ["play-test", { displayName: "Play Framework", kind: "framework" }],
2668
+ ["play-slick", { displayName: "Play Framework", kind: "framework" }],
2669
+ ["twirl-api", { displayName: "Play Framework", kind: "framework" }],
2670
+ ["scalatestplus-play", { displayName: "Play Framework", kind: "framework" }],
2633
2671
  ["akka-actor", { displayName: "Akka", kind: "framework" }],
2634
2672
  ["spray-can", { displayName: "Spray", kind: "framework" }],
2635
2673
  // --- Scala — Libraries ---
@@ -2637,6 +2675,22 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2637
2675
  ["specs2-core", { displayName: "Specs2", kind: "library" }],
2638
2676
  ["cats-core", { displayName: "Cats", kind: "library" }],
2639
2677
  ["circe-core", { displayName: "Circe", kind: "library" }],
2678
+ // Slick gates Opengrep's `scala.slick.` SQL-injection patterns.
2679
+ ["slick", { displayName: "Slick", kind: "library" }],
2680
+ ["slick-hikaricp", { displayName: "Slick", kind: "library" }],
2681
+ // jwt-scala publishes one artifact per JSON backend; all gate the same
2682
+ // hardcoded-secret patterns.
2683
+ ["jwt-core", { displayName: "jwt-scala", kind: "library" }],
2684
+ ["jwt-circe", { displayName: "jwt-scala", kind: "library" }],
2685
+ ["jwt-play-json", { displayName: "jwt-scala", kind: "library" }],
2686
+ ["jwt-json4s", { displayName: "jwt-scala", kind: "library" }],
2687
+ ["jwt-spray-json", { displayName: "jwt-scala", kind: "library" }],
2688
+ ["jwt-upickle", { displayName: "jwt-scala", kind: "library" }],
2689
+ ["jwt-argonaut", { displayName: "jwt-scala", kind: "library" }],
2690
+ ["munit", { displayName: "MUnit", kind: "library" }],
2691
+ ["scalacheck", { displayName: "ScalaCheck", kind: "library" }],
2692
+ ["scala-logging", { displayName: "scala-logging", kind: "library" }],
2693
+ ["chimney", { displayName: "Chimney", kind: "library" }],
2640
2694
  // --- Elixir — Frameworks ---
2641
2695
  ["phoenix", { displayName: "Phoenix", kind: "framework" }],
2642
2696
  ["plug", { displayName: "Plug", kind: "framework" }],
@@ -2712,6 +2766,25 @@ var DEPENDENCY_PREFIX_RULES = [
2712
2766
  // Groovy — Grails/GORM artifact families (build.gradle artifactIds).
2713
2767
  { prefix: "grails-", classification: { displayName: "Grails", kind: "framework" } },
2714
2768
  { prefix: "gorm-", classification: { displayName: "GORM", kind: "library" } },
2769
+ // Scala — artifact families published one module per feature (sbt artifactIds).
2770
+ // Enumerating members would go stale on every release; the prefixes are
2771
+ // distinctive enough not to collide with other ecosystems.
2772
+ { prefix: "http4s-", classification: { displayName: "http4s", kind: "framework" } },
2773
+ { prefix: "akka-", classification: { displayName: "Akka", kind: "framework" } },
2774
+ { prefix: "pekko-", classification: { displayName: "Pekko", kind: "framework" } },
2775
+ { prefix: "zio-", classification: { displayName: "ZIO", kind: "library" } },
2776
+ // Must precede any broader `cats-` rule: cats-effect is its own project.
2777
+ { prefix: "cats-effect", classification: { displayName: "Cats Effect", kind: "library" } },
2778
+ { prefix: "doobie-", classification: { displayName: "Doobie", kind: "library" } },
2779
+ { prefix: "fs2-", classification: { displayName: "FS2", kind: "library" } },
2780
+ { prefix: "circe-", classification: { displayName: "Circe", kind: "library" } },
2781
+ { prefix: "pureconfig", classification: { displayName: "PureConfig", kind: "library" } },
2782
+ { prefix: "kamon-", classification: { displayName: "Kamon", kind: "library" } },
2783
+ { prefix: "flyway-", classification: { displayName: "Flyway", kind: "library" } },
2784
+ {
2785
+ prefix: "testcontainers-scala",
2786
+ classification: { displayName: "Testcontainers", kind: "library" }
2787
+ },
2715
2788
  // .NET — Microsoft package families and common test/logging libraries. More
2716
2789
  // specific prefixes (Blazor) must precede broader ones (ASP.NET Core).
2717
2790
  {
@@ -2739,7 +2812,14 @@ var DEPENDENCY_PREFIX_RULES = [
2739
2812
  { prefix: "Serilog", classification: { displayName: "Serilog", kind: "library" } }
2740
2813
  ];
2741
2814
  var MANIFEST_SCOPED_CATALOG = /* @__PURE__ */ new Map([
2742
- ["package.json", /* @__PURE__ */ new Map([["angular", { displayName: "AngularJS", kind: "framework" }]])]
2815
+ ["package.json", /* @__PURE__ */ new Map([["angular", { displayName: "AngularJS", kind: "framework" }]])],
2816
+ [
2817
+ "Gemfile",
2818
+ /* @__PURE__ */ new Map([
2819
+ ["faker", { displayName: "Faker", kind: "library" }],
2820
+ ["rake", { displayName: "Rake", kind: "tool" }]
2821
+ ])
2822
+ ]
2743
2823
  ]);
2744
2824
  function classifyDependency(name, manifestBasename) {
2745
2825
  if (manifestBasename) {
@@ -2811,6 +2891,43 @@ var FILE_SIGNAL_RULES = [
2811
2891
  name: "azure-pipelines.yml",
2812
2892
  matches: (f) => f === "azure-pipelines.yml" || f === "azure-pipelines.yaml",
2813
2893
  classification: { displayName: "Azure Pipelines", kind: "framework" }
2894
+ },
2895
+ {
2896
+ // Jenkins pipelines are Groovy scripts with no manifest at all — the
2897
+ // Jenkinsfile *is* the declaration. Gates CodeNarc's `jenkins`/
2898
+ // `jenkinspipeline` tagged patterns.
2899
+ name: "Jenkinsfile",
2900
+ matches: (f) => {
2901
+ const base = pathBasename(f);
2902
+ return base === "Jenkinsfile" || base.startsWith("Jenkinsfile.");
2903
+ },
2904
+ classification: { displayName: "Jenkins", kind: "tool" }
2905
+ },
2906
+ {
2907
+ // A `sorbet/` directory (typically `sorbet/config` + RBI files) marks a
2908
+ // Sorbet-typed Ruby project even when the gem is pulled in transitively.
2909
+ name: "sorbet/config",
2910
+ matches: (f) => f === "sorbet/config" || f.endsWith("/sorbet/config"),
2911
+ classification: { displayName: "Sorbet", kind: "tool" }
2912
+ },
2913
+ {
2914
+ name: "Rakefile",
2915
+ matches: (f) => {
2916
+ const base = pathBasename(f);
2917
+ return base === "Rakefile" || base === "rakefile" || base.endsWith(".rake");
2918
+ },
2919
+ classification: { displayName: "Rake", kind: "tool" }
2920
+ },
2921
+ {
2922
+ // Play's routes file — `conf/routes`, plus the `conf/*.routes` sub-files a
2923
+ // multi-module app includes from it (Codacy's own website uses
2924
+ // `app/conf/website.routes`, `modules/legacy-api/conf/api.routes`). Nothing
2925
+ // but Play uses this layout, and it is version-independent, unlike the
2926
+ // artifact names. Note `conf/application.conf` is NOT a signal: plain
2927
+ // Typesafe Config uses the same filename without any Play involvement.
2928
+ name: "conf/routes",
2929
+ matches: (f) => /(^|\/)conf\/([^/]+\.)?routes$/.test(f),
2930
+ classification: { displayName: "Play Framework", kind: "framework" }
2814
2931
  }
2815
2932
  ];
2816
2933
  function detectFileSignals(files) {
@@ -2975,18 +3092,76 @@ function parsePackageJson(content) {
2975
3092
  return [];
2976
3093
  }
2977
3094
  }
3095
+ function parseRequirementLine(rawLine) {
3096
+ const line = rawLine.trim();
3097
+ if (!line || line.startsWith("#") || line.startsWith("-")) return void 0;
3098
+ const noComment = line.split("#")[0].trim();
3099
+ if (!noComment) return void 0;
3100
+ const match = noComment.match(/^([a-zA-Z0-9_.-]+(?:\[[^\]]*\])?)\s*([<>=!~@;].*)?$/);
3101
+ if (!match) return void 0;
3102
+ const rawName = match[1].replace(/\[.*\]$/, "");
3103
+ return { name: rawName.toLowerCase().replace(/_/g, "-"), version: match[2]?.trim() };
3104
+ }
2978
3105
  function parseRequirementsTxt(content) {
2979
3106
  const deps = [];
2980
3107
  for (const rawLine of content.split("\n")) {
2981
- const line = rawLine.trim();
2982
- if (!line || line.startsWith("#") || line.startsWith("-")) continue;
2983
- const noComment = line.split("#")[0].trim();
2984
- if (!noComment) continue;
2985
- const match = noComment.match(/^([a-zA-Z0-9_.-]+(?:\[[^\]]*\])?)\s*([<>=!~@;].*)?$/);
2986
- if (match) {
2987
- const rawName = match[1].replace(/\[.*\]$/, "");
2988
- const normalized = rawName.toLowerCase().replace(/_/g, "-");
2989
- deps.push({ name: normalized, version: match[2]?.trim() });
3108
+ const dep = parseRequirementLine(rawLine);
3109
+ if (dep) deps.push(dep);
3110
+ }
3111
+ return deps;
3112
+ }
3113
+ function sliceBalanced(content, fromIndex, open, close) {
3114
+ const start = content.indexOf(open, fromIndex);
3115
+ if (start === -1) return void 0;
3116
+ let depth = 0;
3117
+ let quote;
3118
+ for (let i = start; i < content.length; i++) {
3119
+ const ch = content[i];
3120
+ if (quote) {
3121
+ if (ch === "\\") i++;
3122
+ else if (ch === quote) quote = void 0;
3123
+ continue;
3124
+ }
3125
+ if (ch === '"' || ch === "'") {
3126
+ quote = ch;
3127
+ continue;
3128
+ }
3129
+ if (ch === open) depth++;
3130
+ else if (ch === close && --depth === 0) {
3131
+ return { body: content.slice(start + 1, i), endIndex: i };
3132
+ }
3133
+ }
3134
+ return void 0;
3135
+ }
3136
+ function parseSetupPy(content) {
3137
+ const deps = [];
3138
+ const collectQuoted = (block) => {
3139
+ for (const quoted of block.matchAll(/["']([^"']+)["']/g)) {
3140
+ const dep = parseRequirementLine(quoted[1]);
3141
+ if (dep) deps.push(dep);
3142
+ }
3143
+ };
3144
+ const valueStartOf = (key) => {
3145
+ const match = new RegExp(`\\b${key}\\s*=\\s*`).exec(content);
3146
+ return match ? match.index + match[0].length : -1;
3147
+ };
3148
+ for (const key of ["install_requires", "tests_require", "setup_requires"]) {
3149
+ const start = valueStartOf(key);
3150
+ if (start === -1 || content[start] !== "[") continue;
3151
+ const list = sliceBalanced(content, start, "[", "]");
3152
+ if (list) collectQuoted(list.body);
3153
+ }
3154
+ const extrasStart = valueStartOf("extras_require");
3155
+ if (extrasStart !== -1 && content[extrasStart] === "{") {
3156
+ const dict = sliceBalanced(content, extrasStart, "{", "}");
3157
+ if (dict) {
3158
+ let cursor = 0;
3159
+ while (cursor < dict.body.length) {
3160
+ const list = sliceBalanced(dict.body, cursor, "[", "]");
3161
+ if (!list) break;
3162
+ collectQuoted(list.body);
3163
+ cursor = list.endIndex + 1;
3164
+ }
2990
3165
  }
2991
3166
  }
2992
3167
  return deps;
@@ -3104,15 +3279,52 @@ function parseBuildGradle(content) {
3104
3279
  }
3105
3280
  return deps;
3106
3281
  }
3282
+ function parseVersionCatalogToml(content) {
3283
+ const deps = [];
3284
+ let inLibraries = false;
3285
+ for (const rawLine of content.split("\n")) {
3286
+ const line = rawLine.trim();
3287
+ if (line === "" || line.startsWith("#")) continue;
3288
+ if (line.startsWith("[")) {
3289
+ inLibraries = /^\[\s*libraries\s*\]/.test(line);
3290
+ continue;
3291
+ }
3292
+ if (!inLibraries) continue;
3293
+ const moduleMatch = line.match(/\bmodule\s*=\s*["'][^"':]+:([^"']+)["']/);
3294
+ const nameMatch = line.match(/\bname\s*=\s*["']([^"']+)["']/);
3295
+ const versionMatch = line.match(/\bversion\s*=\s*["']([^"']+)["']/);
3296
+ if (moduleMatch || nameMatch) {
3297
+ deps.push({
3298
+ name: (moduleMatch?.[1] ?? nameMatch?.[1] ?? "").trim(),
3299
+ version: versionMatch?.[1]?.trim()
3300
+ });
3301
+ continue;
3302
+ }
3303
+ const shorthand = line.match(/^[A-Za-z0-9_.-]+\s*=\s*["'][^"':]+:([^"':]+)(?::([^"']+))?["']/);
3304
+ if (shorthand) {
3305
+ deps.push({ name: shorthand[1].trim(), version: shorthand[2]?.trim() });
3306
+ }
3307
+ }
3308
+ return deps;
3309
+ }
3107
3310
  function parseBuildSbt(content) {
3108
3311
  const deps = [];
3109
- const depRegex = /"([^"]+)"\s*%%?\s*"([^"]+)"\s*%\s*"([^"]+)"/g;
3312
+ const depRegex = /"([^"]+)"\s*%%?\s*"([^"]+)"\s*%(?!%)\s*(?:"([^"]+)")?/g;
3110
3313
  let match;
3111
3314
  while ((match = depRegex.exec(content)) !== null) {
3112
3315
  deps.push({ name: match[2], version: match[3] });
3113
3316
  }
3114
3317
  return deps;
3115
3318
  }
3319
+ function parseSbtBuildDefinition(content) {
3320
+ const deps = parseBuildSbt(content);
3321
+ const pluginRegex = /addSbtPlugin\s*\(\s*"([^"]+)"\s*%%?\s*"([^"]+)"/g;
3322
+ let match;
3323
+ while ((match = pluginRegex.exec(content)) !== null) {
3324
+ deps.push({ name: `${match[1]}:${match[2]}` });
3325
+ }
3326
+ return deps;
3327
+ }
3116
3328
  function parseCargoToml(content) {
3117
3329
  const deps = [];
3118
3330
  let inDepsSection = false;
@@ -3262,6 +3474,8 @@ var MANIFEST_PARSERS = /* @__PURE__ */ new Map([
3262
3474
  ["pom.xml", parsePomXml],
3263
3475
  ["build.gradle", parseBuildGradle],
3264
3476
  ["build.gradle.kts", parseBuildGradle],
3477
+ ["libs.versions.toml", parseVersionCatalogToml],
3478
+ ["setup.py", parseSetupPy],
3265
3479
  ["build.sbt", parseBuildSbt],
3266
3480
  ["Cargo.toml", parseCargoToml],
3267
3481
  ["composer.json", parseComposerJson],
@@ -3278,10 +3492,32 @@ var MANIFEST_SUFFIX_PARSERS = /* @__PURE__ */ new Map([
3278
3492
  [".vbproj", parseCsproj],
3279
3493
  [".fsproj", parseCsproj]
3280
3494
  ]);
3495
+ var MANIFEST_PATH_PARSERS = [
3496
+ {
3497
+ matches: (filePath, basename) => /^requirements[a-zA-Z0-9_.-]*\.(txt|in)$/i.test(basename) || // Any .txt/.in directly inside a `requirements/` directory: base.txt,
3498
+ // dev.txt, prod.in — the other common split-layout convention.
3499
+ /(^|\/)requirements\/[^/]+\.(txt|in)$/i.test(filePath),
3500
+ parser: parseRequirementsTxt
3501
+ },
3502
+ {
3503
+ // sbt build definition: `project/Dependencies.scala`, `project/plugins.sbt`
3504
+ // and the meta-build's own `project/project/*`. Matching by directory rather
3505
+ // than basename is what this rule exists for — the Scala file can be called
3506
+ // anything, and `*.scala` on its own is source code, not a manifest.
3507
+ //
3508
+ // Case-sensitive deliberately: sbt's directory is always lowercase `project`,
3509
+ // while `Project` is an ordinary domain-package name, and reading
3510
+ // `…/domain/Project/Project.scala` as a manifest would be wrong.
3511
+ matches: (filePath) => /(^|\/)project\/[^/]+\.(scala|sbt)$/.test(filePath),
3512
+ parser: parseSbtBuildDefinition
3513
+ }
3514
+ ];
3281
3515
 
3282
3516
  // src/discover.ts
3283
3517
  async function discover(repositoryRoot, options) {
3284
- const files = listRepoFiles(repositoryRoot);
3518
+ const unfiltered = listRepoFiles(repositoryRoot);
3519
+ const files = applyExclusions(unfiltered, [...options?.exclude ?? []], []);
3520
+ const excludedFiles = unfiltered.length - files.length;
3285
3521
  const filePatterns = extractFilePatterns(files);
3286
3522
  const notableFiles = findNotableFiles(files);
3287
3523
  const languages = await detectLanguages(repositoryRoot, files);
@@ -3304,7 +3540,10 @@ async function discover(repositoryRoot, options) {
3304
3540
  metadata: {
3305
3541
  repositoryRoot: import_path6.default.resolve(repositoryRoot),
3306
3542
  totalFiles: files.length,
3307
- scannedAt: (/* @__PURE__ */ new Date()).toISOString()
3543
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
3544
+ // Surfaced so a scan narrowed by excludes is never mistaken for a repo
3545
+ // that genuinely lacks those languages.
3546
+ ...excludedFiles > 0 ? { excludedFiles } : {}
3308
3547
  }
3309
3548
  };
3310
3549
  }
@@ -3335,6 +3574,9 @@ function resolveManifestParser(filePath) {
3335
3574
  for (const [suffix, parser] of MANIFEST_SUFFIX_PARSERS) {
3336
3575
  if (lowerBasename.endsWith(suffix)) return parser;
3337
3576
  }
3577
+ for (const rule of MANIFEST_PATH_PARSERS) {
3578
+ if (rule.matches(filePath, basename)) return rule.parser;
3579
+ }
3338
3580
  return void 0;
3339
3581
  }
3340
3582
  async function extractDependencies(repositoryRoot, files) {
@@ -3468,6 +3710,28 @@ function applyCodacyYamlExcludes(config, yamlConfig, adapters, logger) {
3468
3710
  function deduplicateStrings(arr) {
3469
3711
  return [...new Set(arr)];
3470
3712
  }
3713
+ async function resolveDiscoveryExcludes(repositoryRoot, adapters, existingConfig) {
3714
+ const yamlConfig = await readCodacyYaml(repositoryRoot);
3715
+ const global = deduplicateStrings([
3716
+ ...existingConfig?.exclude ?? [],
3717
+ ...yamlConfig?.excludePaths ?? []
3718
+ ]);
3719
+ const perTool = /* @__PURE__ */ new Map();
3720
+ const addToolExcludes = (toolId, patterns) => {
3721
+ if (patterns.length === 0) return;
3722
+ perTool.set(toolId, deduplicateStrings([...perTool.get(toolId) ?? [], ...patterns]));
3723
+ };
3724
+ for (const tool of existingConfig?.tools ?? []) {
3725
+ addToolExcludes(tool.toolId, tool.exclude ?? []);
3726
+ }
3727
+ for (const [engineName, engineConfig] of yamlConfig?.engines ?? []) {
3728
+ if (engineConfig.excludePaths.length === 0) continue;
3729
+ const adapterId = resolveEngineToAdapter(engineName, adapters);
3730
+ if (!adapterId) continue;
3731
+ addToolExcludes(adapterId, engineConfig.excludePaths);
3732
+ }
3733
+ return { yamlConfig, global, perTool };
3734
+ }
3471
3735
 
3472
3736
  // src/api/client/core/OpenAPI.ts
3473
3737
  var OpenAPI = {
@@ -7032,7 +7296,7 @@ function buildAdapterByUuid(adapters, apiTools) {
7032
7296
  }
7033
7297
  return result;
7034
7298
  }
7035
- async function detectLanguages(repositoryRoot, precomputedFiles) {
7299
+ async function detectLanguages(repositoryRoot, precomputedFiles, exclude) {
7036
7300
  let files;
7037
7301
  if (precomputedFiles) {
7038
7302
  files = precomputedFiles;
@@ -7048,6 +7312,9 @@ async function detectLanguages(repositoryRoot, precomputedFiles) {
7048
7312
  return [];
7049
7313
  }
7050
7314
  }
7315
+ if (exclude && exclude.length > 0) {
7316
+ files = applyExclusions(files, [...exclude], []);
7317
+ }
7051
7318
  const detected = /* @__PURE__ */ new Set();
7052
7319
  for (const file of files) {
7053
7320
  const basename = import_path8.default.basename(file);
@@ -7086,16 +7353,18 @@ async function detectLocalConfigFiles(repositoryRoot, adapters) {
7086
7353
  }
7087
7354
  return found;
7088
7355
  }
7089
- async function enrichWithCodacyYaml(config, repositoryRoot, adapters) {
7090
- const yamlConfig = await readCodacyYaml(repositoryRoot);
7091
- if (!yamlConfig) return { config, codacyYamlApplied: false };
7356
+ function enrichWithCodacyYaml(config, adapters, excludes) {
7357
+ if (!excludes.yamlConfig) return { config, codacyYamlApplied: false };
7092
7358
  return {
7093
- config: applyCodacyYamlExcludes(config, yamlConfig, adapters),
7359
+ config: applyCodacyYamlExcludes(config, excludes.yamlConfig, adapters),
7094
7360
  codacyYamlApplied: true
7095
7361
  };
7096
7362
  }
7097
- async function initLocalConfigBase(repositoryRoot, adapters) {
7098
- const languages = await detectLanguages(repositoryRoot);
7363
+ async function initLocalConfigBase(repositoryRoot, adapters, excludes) {
7364
+ const languages = await detectLanguages(
7365
+ repositoryRoot,
7366
+ listRepoFiles(repositoryRoot, excludes.global)
7367
+ );
7099
7368
  const localConfigs = await detectLocalConfigFiles(repositoryRoot, adapters);
7100
7369
  const tools = [];
7101
7370
  for (const [toolId, configPath] of localConfigs.entries()) {
@@ -7108,17 +7377,23 @@ async function initLocalConfigBase(repositoryRoot, adapters) {
7108
7377
  }
7109
7378
  return createCodacyConfig({ source: "local", languages, tools });
7110
7379
  }
7111
- async function initLocalConfig(repositoryRoot, adapters) {
7112
- const baseConfig = await initLocalConfigBase(repositoryRoot, adapters);
7113
- const { config, codacyYamlApplied } = await enrichWithCodacyYaml(
7114
- baseConfig,
7380
+ async function initLocalConfig(repositoryRoot, adapters, options) {
7381
+ const excludes = await resolveDiscoveryExcludes(
7115
7382
  repositoryRoot,
7116
- adapters
7383
+ adapters,
7384
+ options?.existingConfig
7117
7385
  );
7386
+ const baseConfig = await initLocalConfigBase(repositoryRoot, adapters, excludes);
7387
+ const { config, codacyYamlApplied } = enrichWithCodacyYaml(baseConfig, adapters, excludes);
7118
7388
  return { config, stats: { codacyYamlApplied } };
7119
7389
  }
7120
- async function initDefaultConfig(repositoryRoot, adapters) {
7121
- const base = await initLocalConfigBase(repositoryRoot, adapters);
7390
+ async function initDefaultConfig(repositoryRoot, adapters, options) {
7391
+ const excludes = await resolveDiscoveryExcludes(
7392
+ repositoryRoot,
7393
+ adapters,
7394
+ options?.existingConfig
7395
+ );
7396
+ const base = await initLocalConfigBase(repositoryRoot, adapters, excludes);
7122
7397
  const toolsWithLocalConfig = new Set(
7123
7398
  base.tools.filter((t) => t.useLocalConfigurationFile).map((t) => t.toolId)
7124
7399
  );
@@ -7156,11 +7431,7 @@ async function initDefaultConfig(repositoryRoot, adapters) {
7156
7431
  languages: base.metadata.languages,
7157
7432
  tools: updatedTools
7158
7433
  });
7159
- const { config, codacyYamlApplied } = await enrichWithCodacyYaml(
7160
- baseConfig,
7161
- repositoryRoot,
7162
- adapters
7163
- );
7434
+ const { config, codacyYamlApplied } = enrichWithCodacyYaml(baseConfig, adapters, excludes);
7164
7435
  return {
7165
7436
  config,
7166
7437
  stats: { totalApiTools: apiTools.length, addedTools, addedUnsupported, codacyYamlApplied }
@@ -7246,10 +7517,10 @@ async function initRemoteConfig(repositoryRoot, provider, organization, reposito
7246
7517
  if (ignoredFilesResult.files.length > 0) {
7247
7518
  baseConfig.exclude = [.../* @__PURE__ */ new Set([...baseConfig.exclude ?? [], ...ignoredFilesResult.files])];
7248
7519
  }
7249
- const { config, codacyYamlApplied } = await enrichWithCodacyYaml(
7520
+ const { config, codacyYamlApplied } = enrichWithCodacyYaml(
7250
7521
  baseConfig,
7251
- repositoryRoot,
7252
- adapters
7522
+ adapters,
7523
+ await resolveDiscoveryExcludes(repositoryRoot, adapters)
7253
7524
  );
7254
7525
  return {
7255
7526
  config,
@@ -7323,8 +7594,13 @@ function hasMatchingFiles(files, filePatterns) {
7323
7594
  if (matcher.matchAll) return files.length > 0;
7324
7595
  return files.some((file) => matcher.matches(file, file.split("/").pop() ?? ""));
7325
7596
  }
7326
- async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, onDiscoveryComplete, rawFilterString, loadUnsupportedPatterns) {
7327
- const discoveryResult = await discover(repositoryRoot);
7597
+ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, onDiscoveryComplete, rawFilterString, loadUnsupportedPatterns, options) {
7598
+ const excludes = await resolveDiscoveryExcludes(
7599
+ repositoryRoot,
7600
+ adapters,
7601
+ options?.existingConfig
7602
+ );
7603
+ const discoveryResult = await discover(repositoryRoot, { exclude: excludes.global });
7328
7604
  onDiscoveryComplete?.(discoveryResult);
7329
7605
  const detectedKeys = /* @__PURE__ */ new Set();
7330
7606
  for (const lang of discoveryResult.languages) detectedKeys.add(lang);
@@ -7332,15 +7608,26 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
7332
7608
  for (const lib of discoveryResult.libraries) detectedKeys.add(lib.displayName);
7333
7609
  const repoLanguages = new Set(discoveryResult.languages);
7334
7610
  const localConfigs = await detectLocalConfigFiles(repositoryRoot, adapters);
7335
- const repoFiles = listRepoFiles(repositoryRoot);
7611
+ const repoFiles = listRepoFiles(repositoryRoot, excludes.global);
7336
7612
  const descriptorMap = new Map(descriptors.map((d) => [d.id, d]));
7613
+ const perToolFiles = /* @__PURE__ */ new Map();
7614
+ const filesVisibleTo = (toolId) => {
7615
+ const toolExclude = excludes.perTool.get(toolId);
7616
+ if (!toolExclude || toolExclude.length === 0) return repoFiles;
7617
+ let files = perToolFiles.get(toolId);
7618
+ if (!files) {
7619
+ files = applyExclusions(repoFiles, [], [...toolExclude]);
7620
+ perToolFiles.set(toolId, files);
7621
+ }
7622
+ return files;
7623
+ };
7337
7624
  const candidates = [];
7338
7625
  for (const adapter of adapters) {
7339
7626
  if (!adapter.languages.includes("*") && adapter.languages.length > 0 && repoLanguages.size > 0) {
7340
7627
  const hasOverlap = adapter.languages.some((lang) => repoLanguages.has(lang));
7341
7628
  if (!hasOverlap) continue;
7342
7629
  }
7343
- if (!hasMatchingFiles(repoFiles, adapter.filePatterns)) continue;
7630
+ if (!hasMatchingFiles(filesVisibleTo(adapter.id), adapter.filePatterns)) continue;
7344
7631
  candidates.push(adapter);
7345
7632
  }
7346
7633
  const deduped = deduplicateToolFamilies(candidates, localConfigs);
@@ -7368,23 +7655,32 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
7368
7655
  filters
7369
7656
  );
7370
7657
  if (selectedPatterns.length > 0) {
7371
- tools.push({
7658
+ const toolEntry = {
7372
7659
  toolId: adapter.id,
7373
7660
  patterns: selectedPatterns.map((p) => ({ patternId: p.id }))
7374
- });
7661
+ };
7662
+ tools.push(toolEntry);
7663
+ await applyAdapterParameterSuggestions(
7664
+ adapter,
7665
+ toolEntry,
7666
+ repositoryRoot,
7667
+ filesVisibleTo(adapter.id)
7668
+ );
7375
7669
  }
7376
7670
  }
7377
7671
  let selectedUnsupported = 0;
7378
- const selectedFamilies = new Set(tools.map((t) => toolFamily(t.toolId)));
7672
+ const selectedFamilies = new Set(
7673
+ tools.map((t) => toolGroupKey(t.toolId, descriptorMap.get(t.toolId)?.alternativeGroup))
7674
+ );
7379
7675
  for (const desc of descriptors) {
7380
7676
  if (!desc.unsupported) continue;
7381
7677
  if (!loadUnsupportedPatterns) break;
7382
7678
  if (!desc.languages.includes("*") && desc.languages.length > 0 && repoLanguages.size > 0 && !desc.languages.some((lang) => repoLanguages.has(lang))) {
7383
7679
  continue;
7384
7680
  }
7385
- const family = toolFamily(desc.id);
7681
+ const family = toolGroupKey(desc.id, desc.alternativeGroup);
7386
7682
  if (selectedFamilies.has(family)) continue;
7387
- if (!hasMatchingFiles(repoFiles, desc.filePatterns)) continue;
7683
+ if (!hasMatchingFiles(filesVisibleTo(desc.id), desc.filePatterns)) continue;
7388
7684
  const patternsMetadata = loadUnsupportedPatterns(desc.id);
7389
7685
  if (patternsMetadata.length === 0) continue;
7390
7686
  const selectedPatterns = filterAutoPatterns(
@@ -7409,11 +7705,7 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
7409
7705
  tools,
7410
7706
  autoFilters: rawFilterString || void 0
7411
7707
  });
7412
- const { config, codacyYamlApplied } = await enrichWithCodacyYaml(
7413
- baseConfig,
7414
- repositoryRoot,
7415
- adapters
7416
- );
7708
+ const { config, codacyYamlApplied } = enrichWithCodacyYaml(baseConfig, adapters, excludes);
7417
7709
  return {
7418
7710
  config,
7419
7711
  stats: {
@@ -7426,16 +7718,58 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
7426
7718
  }
7427
7719
  };
7428
7720
  }
7721
+ async function applyAdapterParameterSuggestions(adapter, toolEntry, repositoryRoot, repoFiles) {
7722
+ if (!adapter.suggestParameters) return;
7723
+ const logger = {
7724
+ debug() {
7725
+ },
7726
+ info() {
7727
+ },
7728
+ warn(message, fields) {
7729
+ console.error(`[init --auto] ${adapter.id}: ${message}`, fields ?? "");
7730
+ },
7731
+ error(message, fields) {
7732
+ console.error(`[init --auto] ${adapter.id}: ${message}`, fields ?? "");
7733
+ }
7734
+ };
7735
+ try {
7736
+ const globalDir = import_path8.default.join(import_os3.default.homedir(), ".codacy");
7737
+ const ctx = {
7738
+ repositoryRoot,
7739
+ generatedConfigDir: import_path8.default.join(repositoryRoot, ".codacy", "generated", adapter.id),
7740
+ globalDir,
7741
+ toolInstallDir: import_path8.default.join(globalDir, "tools", adapter.id),
7742
+ targetFiles: routeFilesToTool(repoFiles, adapter, toolEntry),
7743
+ platform: { os: process.platform, arch: process.arch },
7744
+ logger,
7745
+ runner: "local"
7746
+ };
7747
+ const suggestions = await adapter.suggestParameters(ctx);
7748
+ if (!suggestions) return;
7749
+ for (const pattern of toolEntry.patterns) {
7750
+ const params = suggestions[pattern.patternId];
7751
+ if (!params) continue;
7752
+ pattern.parameters = { ...pattern.parameters, ...params };
7753
+ }
7754
+ } catch (err) {
7755
+ logger.error("suggestParameters failed", {
7756
+ error: err instanceof Error ? err.message : String(err)
7757
+ });
7758
+ }
7759
+ }
7429
7760
  function toolFamily(id) {
7430
7761
  return id.replace(/\d+$/, "");
7431
7762
  }
7763
+ function toolGroupKey(id, alternativeGroup) {
7764
+ return alternativeGroup ?? toolFamily(id);
7765
+ }
7432
7766
  function deduplicateToolFamilies(candidates, localConfigs) {
7433
7767
  const families = /* @__PURE__ */ new Map();
7434
7768
  for (const adapter of candidates) {
7435
- const family = toolFamily(adapter.id);
7436
- const group = families.get(family) ?? [];
7769
+ const key = toolGroupKey(adapter.id, adapter.alternativeGroup);
7770
+ const group = families.get(key) ?? [];
7437
7771
  group.push(adapter);
7438
- families.set(family, group);
7772
+ families.set(key, group);
7439
7773
  }
7440
7774
  const result = [];
7441
7775
  for (const group of families.values()) {
@@ -7444,16 +7778,28 @@ function deduplicateToolFamilies(candidates, localConfigs) {
7444
7778
  continue;
7445
7779
  }
7446
7780
  const withConfig = group.filter((a) => localConfigs.has(a.id));
7447
- if (withConfig.length === 1) {
7448
- result.push(withConfig[0]);
7781
+ const pool = withConfig.length > 0 ? withConfig : group;
7782
+ if (pool.length === 1) {
7783
+ result.push(pool[0]);
7449
7784
  continue;
7450
7785
  }
7451
- group.sort((a, b) => {
7452
- const majorA = parseInt(a.preferredVersion.split(".")[0], 10) || 0;
7453
- const majorB = parseInt(b.preferredVersion.split(".")[0], 10) || 0;
7454
- return majorB - majorA;
7455
- });
7456
- result.push(group[0]);
7786
+ const preferred = pool.filter((a) => a.alternativeGroupDefault);
7787
+ if (preferred.length === 1) {
7788
+ result.push(preferred[0]);
7789
+ continue;
7790
+ }
7791
+ const defaultFamilies = new Set(
7792
+ group.filter((a) => a.alternativeGroupDefault).map((a) => toolFamily(a.id))
7793
+ );
7794
+ const defaultFamilyMembers = defaultFamilies.size === 1 ? pool.filter((a) => defaultFamilies.has(toolFamily(a.id))) : [];
7795
+ const ranked = [...defaultFamilyMembers.length > 0 ? defaultFamilyMembers : pool].sort(
7796
+ (a, b) => {
7797
+ const majorA = parseInt(a.preferredVersion.split(".")[0], 10) || 0;
7798
+ const majorB = parseInt(b.preferredVersion.split(".")[0], 10) || 0;
7799
+ return majorB - majorA;
7800
+ }
7801
+ );
7802
+ result.push(ranked[0]);
7457
7803
  }
7458
7804
  return result;
7459
7805
  }
@@ -7609,14 +7955,14 @@ var import_tooling6 = require("@codacy/tooling");
7609
7955
  // src/credentials.ts
7610
7956
  var import_crypto = __toESM(require("crypto"));
7611
7957
  var import_promises5 = __toESM(require("fs/promises"));
7612
- var import_os3 = __toESM(require("os"));
7958
+ var import_os4 = __toESM(require("os"));
7613
7959
  var import_path9 = __toESM(require("path"));
7614
7960
  function credentialsPath() {
7615
- return import_path9.default.join(import_os3.default.homedir(), ".codacy", "credentials");
7961
+ return import_path9.default.join(import_os4.default.homedir(), ".codacy", "credentials");
7616
7962
  }
7617
7963
  function deriveKey(salt) {
7618
- const info = import_os3.default.userInfo();
7619
- const machineKey = [import_os3.default.hostname(), info.username, import_os3.default.homedir(), import_os3.default.platform()].join("|");
7964
+ const info = import_os4.default.userInfo();
7965
+ const machineKey = [import_os4.default.hostname(), info.username, import_os4.default.homedir(), import_os4.default.platform()].join("|");
7620
7966
  return import_crypto.default.pbkdf2Sync(machineKey, salt, 1e5, 32, "sha512");
7621
7967
  }
7622
7968
  async function saveCredentials(token) {
@@ -7750,11 +8096,12 @@ function parseSarif(runs) {
7750
8096
  if (!patternId) continue;
7751
8097
  const level = mapSarifLevelToCodacy(result.level);
7752
8098
  const message = result.message?.text ?? "";
8099
+ const sourceId = result.properties?.sourceId;
7753
8100
  for (const loc of result.locations ?? []) {
7754
8101
  const filePath = loc.physicalLocation?.artifactLocation?.uri;
7755
8102
  const line = loc.physicalLocation?.region?.startLine;
7756
8103
  if (!filePath || !line) continue;
7757
- issues.push({ toolId, patternId, filePath, line, message, level });
8104
+ issues.push({ toolId, patternId, filePath, line, message, level, sourceId });
7758
8105
  }
7759
8106
  }
7760
8107
  }
@@ -7768,10 +8115,11 @@ function parseAnalysisResult(result) {
7768
8115
  line: issue.line,
7769
8116
  message: issue.message,
7770
8117
  // JSON carries the authoritative Codacy severity; pass it through unchanged.
7771
- level: issue.severity
8118
+ level: issue.severity,
8119
+ sourceId: issue.sourceId
7772
8120
  }));
7773
8121
  }
7774
- function buildPayloads(issues, resolveShortName) {
8122
+ function buildPayloads(issues, resolveShortName, needsSourceId = () => false) {
7775
8123
  const byTool = /* @__PURE__ */ new Map();
7776
8124
  for (const issue of issues) {
7777
8125
  const list = byTool.get(issue.toolId);
@@ -7780,6 +8128,7 @@ function buildPayloads(issues, resolveShortName) {
7780
8128
  }
7781
8129
  const payloads = [];
7782
8130
  for (const [toolId, toolIssues] of byTool) {
8131
+ const withSourceId = needsSourceId(toolId);
7783
8132
  const byFile = /* @__PURE__ */ new Map();
7784
8133
  for (const issue of toolIssues) {
7785
8134
  const entry = {
@@ -7788,7 +8137,8 @@ function buildPayloads(issues, resolveShortName) {
7788
8137
  filename: issue.filePath,
7789
8138
  message: { text: issue.message },
7790
8139
  level: issue.level,
7791
- location: { LineLocation: { line: issue.line } }
8140
+ location: { LineLocation: { line: issue.line } },
8141
+ ...withSourceId && issue.sourceId ? { sourceId: issue.sourceId } : {}
7792
8142
  }
7793
8143
  };
7794
8144
  const list = byFile.get(issue.filePath);
@@ -7820,6 +8170,13 @@ async function buildShortNameResolver(onProgress) {
7820
8170
  }
7821
8171
  return (toolId) => byName.get(toolId.toLowerCase()) ?? toolId.toLowerCase();
7822
8172
  }
8173
+ function buildSourceIdResolver() {
8174
+ const needsIt = /* @__PURE__ */ new Set();
8175
+ for (const descriptor of getRegisteredDescriptors()) {
8176
+ if (descriptor.needsSourceIdUpload) needsIt.add(descriptor.id.toLowerCase());
8177
+ }
8178
+ return (toolId) => needsIt.has(toolId.toLowerCase());
8179
+ }
7823
8180
  var COORDINATE_PATTERN = /^[A-Za-z0-9._-]+$/;
7824
8181
  function assertValidCoordinate(kind, value) {
7825
8182
  if (!COORDINATE_PATTERN.test(value)) {
@@ -7859,7 +8216,7 @@ async function uploadReport(options) {
7859
8216
  onProgress?.("No issues found in the report; nothing to upload.");
7860
8217
  }
7861
8218
  const resolveShortName = await buildShortNameResolver(onProgress);
7862
- const payloads = buildPayloads(issues, resolveShortName);
8219
+ const payloads = buildPayloads(issues, resolveShortName, buildSourceIdResolver());
7863
8220
  const base = resolveResultsApiBaseUrl();
7864
8221
  const urls = buildUrls(base, auth, commitUuid);
7865
8222
  const headers = { "content-type": "application/json", ...authHeader(auth) };
@@ -7960,6 +8317,7 @@ async function safeReadBody(res) {
7960
8317
  resolveApiToken,
7961
8318
  resolveBaselinePath,
7962
8319
  resolveConfigPath,
8320
+ resolveDiscoveryExcludes,
7963
8321
  resolveEngineToAdapter,
7964
8322
  resolveHeadCommit,
7965
8323
  resolveProxyConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/analysis-runner",
3
- "version": "0.16.0",
3
+ "version": "0.18.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.16.0"
15
+ "@codacy/tooling": "0.18.0"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@codacy/openapi-typescript-codegen": "0.0.9",
19
- "@codacy/tools-dummy-1": "0.16.0"
19
+ "@codacy/tools-dummy-1": "0.18.0"
20
20
  },
21
21
  "license": "MIT",
22
22
  "author": "Codacy <support@codacy.com> (https://www.codacy.com)",