@codacy/analysis-runner 0.17.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
  }>;
@@ -604,7 +627,7 @@ interface InitAutoStats {
604
627
  * those patterns are gated on detected frameworks exactly like supported tools;
605
628
  * tools without mappings have every committed pattern treated as universal.
606
629
  */
607
- 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<{
608
631
  config: CodacyConfig;
609
632
  stats: InitAutoStats;
610
633
  }>;
@@ -666,6 +689,41 @@ declare function resolveEngineToAdapter(engineName: string, adapters: Readonly<T
666
689
  * Returns a new config object (does not mutate the input).
667
690
  */
668
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>;
669
727
 
670
728
  /**
671
729
  * Computes pattern-level summary statistics for analysis results.
@@ -834,6 +892,18 @@ declare function resolveApiToken(): Promise<string>;
834
892
  interface DiscoverOptions {
835
893
  /** Skip dependency extraction from manifests (only detect files and languages). */
836
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[];
837
907
  }
838
908
  /**
839
909
  * Scans a repository and returns a structured report of languages, file patterns,
@@ -902,6 +972,10 @@ declare function resolveHeadCommit(repositoryRoot: string): string;
902
972
  * Codacy patterns). We normalize either format into a flat issue list and build the
903
973
  * payload directly.
904
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
+ *
905
979
  * ## Where it uploads
906
980
  *
907
981
  * The **legacy v2 API** (`https://api.codacy.com/2.0`), matching the Go CLI: one
@@ -933,6 +1007,12 @@ interface NormalizedIssue {
933
1007
  message: string;
934
1008
  /** Codacy level string (Info/Warning/High/Error for json; Info/Warning/Error for sarif). */
935
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;
936
1016
  }
937
1017
  /** Authentication + routing for the upload. */
938
1018
  type UploadAuth = {
@@ -995,6 +1075,8 @@ interface PayloadIssue {
995
1075
  line: number;
996
1076
  };
997
1077
  };
1078
+ /** Present only for tools that need it; see {@link buildPayloads}. */
1079
+ sourceId?: string;
998
1080
  }
999
1081
  /**
1000
1082
  * Resolves the base URL for the legacy v2 results API. Defaults to Codacy SaaS;
@@ -1012,8 +1094,17 @@ declare function parseReport(fileContents: string): NormalizedIssue[];
1012
1094
  * Builds one v2 payload per tool, grouping issues by file. `resolveShortName` maps
1013
1095
  * our internal tool id to the Codacy tool short-name expected in the `tool` field
1014
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.
1015
1106
  */
1016
- 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[];
1017
1108
  /**
1018
1109
  * Uploads an analysis report to Codacy. Reads and parses the report, resolves tool
1019
1110
  * short-names, POSTs one `issuesRemoteResults` per tool, then a single `resultsFinal`.
@@ -1028,4 +1119,4 @@ declare function buildPayloads(issues: NormalizedIssue[], resolveShortName: (too
1028
1119
  */
1029
1120
  declare function uploadReport(options: UploadOptions): Promise<UploadResult>;
1030
1121
 
1031
- 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
  }
@@ -2402,6 +2410,8 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2402
2410
  ["graphql", { displayName: "GraphQL", kind: "library" }],
2403
2411
  ["@apollo/client", { displayName: "Apollo Client", kind: "library" }],
2404
2412
  ["socket.io", { displayName: "Socket.IO", kind: "library" }],
2413
+ ["jquery", { displayName: "jQuery", kind: "library" }],
2414
+ ["mongodb", { displayName: "MongoDB", kind: "library" }],
2405
2415
  ["three", { displayName: "Three.js", kind: "library" }],
2406
2416
  ["d3", { displayName: "D3.js", kind: "library" }],
2407
2417
  ["chart.js", { displayName: "Chart.js", kind: "library" }],
@@ -2484,6 +2494,10 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2484
2494
  ["matplotlib", { displayName: "Matplotlib", kind: "library" }],
2485
2495
  ["seaborn", { displayName: "Seaborn", kind: "library" }],
2486
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" }],
2487
2501
  // --- Python — Tools ---
2488
2502
  ["black", { displayName: "Black", kind: "tool" }],
2489
2503
  ["ruff", { displayName: "Ruff", kind: "tool" }],
@@ -2514,9 +2528,16 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2514
2528
  ["minitest", { displayName: "Minitest", kind: "library" }],
2515
2529
  ["factory_bot", { displayName: "FactoryBot", kind: "library" }],
2516
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.
2517
2533
  // --- Ruby — Tools ---
2518
2534
  ["rubocop", { displayName: "RuboCop", kind: "tool" }],
2519
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" }],
2520
2541
  ["solargraph", { displayName: "Solargraph", kind: "tool" }],
2521
2542
  // --- Java / Kotlin — Frameworks ---
2522
2543
  ["spring-boot-starter", { displayName: "Spring Boot", kind: "framework" }],
@@ -2547,6 +2568,10 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2547
2568
  ["okhttp", { displayName: "OkHttp", kind: "library" }],
2548
2569
  ["retrofit", { displayName: "Retrofit", kind: "library" }],
2549
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" }],
2550
2575
  // --- Groovy — Frameworks ---
2551
2576
  // Grails/GORM artifact IDs vary (grails-core, grails-web, gorm-hibernate5, …);
2552
2577
  // the `grails-`/`gorm-` prefix rules below catch the rest.
@@ -2630,7 +2655,19 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2630
2655
  ["RxSwift", { displayName: "RxSwift", kind: "library" }],
2631
2656
  ["RealmSwift", { displayName: "Realm", kind: "library" }],
2632
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" }],
2633
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" }],
2634
2671
  ["akka-actor", { displayName: "Akka", kind: "framework" }],
2635
2672
  ["spray-can", { displayName: "Spray", kind: "framework" }],
2636
2673
  // --- Scala — Libraries ---
@@ -2638,6 +2675,22 @@ var DEPENDENCY_CATALOG = /* @__PURE__ */ new Map([
2638
2675
  ["specs2-core", { displayName: "Specs2", kind: "library" }],
2639
2676
  ["cats-core", { displayName: "Cats", kind: "library" }],
2640
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" }],
2641
2694
  // --- Elixir — Frameworks ---
2642
2695
  ["phoenix", { displayName: "Phoenix", kind: "framework" }],
2643
2696
  ["plug", { displayName: "Plug", kind: "framework" }],
@@ -2713,6 +2766,25 @@ var DEPENDENCY_PREFIX_RULES = [
2713
2766
  // Groovy — Grails/GORM artifact families (build.gradle artifactIds).
2714
2767
  { prefix: "grails-", classification: { displayName: "Grails", kind: "framework" } },
2715
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
+ },
2716
2788
  // .NET — Microsoft package families and common test/logging libraries. More
2717
2789
  // specific prefixes (Blazor) must precede broader ones (ASP.NET Core).
2718
2790
  {
@@ -2740,7 +2812,14 @@ var DEPENDENCY_PREFIX_RULES = [
2740
2812
  { prefix: "Serilog", classification: { displayName: "Serilog", kind: "library" } }
2741
2813
  ];
2742
2814
  var MANIFEST_SCOPED_CATALOG = /* @__PURE__ */ new Map([
2743
- ["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
+ ]
2744
2823
  ]);
2745
2824
  function classifyDependency(name, manifestBasename) {
2746
2825
  if (manifestBasename) {
@@ -2812,6 +2891,43 @@ var FILE_SIGNAL_RULES = [
2812
2891
  name: "azure-pipelines.yml",
2813
2892
  matches: (f) => f === "azure-pipelines.yml" || f === "azure-pipelines.yaml",
2814
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" }
2815
2931
  }
2816
2932
  ];
2817
2933
  function detectFileSignals(files) {
@@ -2976,18 +3092,76 @@ function parsePackageJson(content) {
2976
3092
  return [];
2977
3093
  }
2978
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
+ }
2979
3105
  function parseRequirementsTxt(content) {
2980
3106
  const deps = [];
2981
3107
  for (const rawLine of content.split("\n")) {
2982
- const line = rawLine.trim();
2983
- if (!line || line.startsWith("#") || line.startsWith("-")) continue;
2984
- const noComment = line.split("#")[0].trim();
2985
- if (!noComment) continue;
2986
- const match = noComment.match(/^([a-zA-Z0-9_.-]+(?:\[[^\]]*\])?)\s*([<>=!~@;].*)?$/);
2987
- if (match) {
2988
- const rawName = match[1].replace(/\[.*\]$/, "");
2989
- const normalized = rawName.toLowerCase().replace(/_/g, "-");
2990
- 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
+ }
2991
3165
  }
2992
3166
  }
2993
3167
  return deps;
@@ -3105,15 +3279,52 @@ function parseBuildGradle(content) {
3105
3279
  }
3106
3280
  return deps;
3107
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
+ }
3108
3310
  function parseBuildSbt(content) {
3109
3311
  const deps = [];
3110
- const depRegex = /"([^"]+)"\s*%%?\s*"([^"]+)"\s*%\s*"([^"]+)"/g;
3312
+ const depRegex = /"([^"]+)"\s*%%?\s*"([^"]+)"\s*%(?!%)\s*(?:"([^"]+)")?/g;
3111
3313
  let match;
3112
3314
  while ((match = depRegex.exec(content)) !== null) {
3113
3315
  deps.push({ name: match[2], version: match[3] });
3114
3316
  }
3115
3317
  return deps;
3116
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
+ }
3117
3328
  function parseCargoToml(content) {
3118
3329
  const deps = [];
3119
3330
  let inDepsSection = false;
@@ -3263,6 +3474,8 @@ var MANIFEST_PARSERS = /* @__PURE__ */ new Map([
3263
3474
  ["pom.xml", parsePomXml],
3264
3475
  ["build.gradle", parseBuildGradle],
3265
3476
  ["build.gradle.kts", parseBuildGradle],
3477
+ ["libs.versions.toml", parseVersionCatalogToml],
3478
+ ["setup.py", parseSetupPy],
3266
3479
  ["build.sbt", parseBuildSbt],
3267
3480
  ["Cargo.toml", parseCargoToml],
3268
3481
  ["composer.json", parseComposerJson],
@@ -3279,10 +3492,32 @@ var MANIFEST_SUFFIX_PARSERS = /* @__PURE__ */ new Map([
3279
3492
  [".vbproj", parseCsproj],
3280
3493
  [".fsproj", parseCsproj]
3281
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
+ ];
3282
3515
 
3283
3516
  // src/discover.ts
3284
3517
  async function discover(repositoryRoot, options) {
3285
- const files = listRepoFiles(repositoryRoot);
3518
+ const unfiltered = listRepoFiles(repositoryRoot);
3519
+ const files = applyExclusions(unfiltered, [...options?.exclude ?? []], []);
3520
+ const excludedFiles = unfiltered.length - files.length;
3286
3521
  const filePatterns = extractFilePatterns(files);
3287
3522
  const notableFiles = findNotableFiles(files);
3288
3523
  const languages = await detectLanguages(repositoryRoot, files);
@@ -3305,7 +3540,10 @@ async function discover(repositoryRoot, options) {
3305
3540
  metadata: {
3306
3541
  repositoryRoot: import_path6.default.resolve(repositoryRoot),
3307
3542
  totalFiles: files.length,
3308
- 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 } : {}
3309
3547
  }
3310
3548
  };
3311
3549
  }
@@ -3336,6 +3574,9 @@ function resolveManifestParser(filePath) {
3336
3574
  for (const [suffix, parser] of MANIFEST_SUFFIX_PARSERS) {
3337
3575
  if (lowerBasename.endsWith(suffix)) return parser;
3338
3576
  }
3577
+ for (const rule of MANIFEST_PATH_PARSERS) {
3578
+ if (rule.matches(filePath, basename)) return rule.parser;
3579
+ }
3339
3580
  return void 0;
3340
3581
  }
3341
3582
  async function extractDependencies(repositoryRoot, files) {
@@ -3469,6 +3710,28 @@ function applyCodacyYamlExcludes(config, yamlConfig, adapters, logger) {
3469
3710
  function deduplicateStrings(arr) {
3470
3711
  return [...new Set(arr)];
3471
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
+ }
3472
3735
 
3473
3736
  // src/api/client/core/OpenAPI.ts
3474
3737
  var OpenAPI = {
@@ -7033,7 +7296,7 @@ function buildAdapterByUuid(adapters, apiTools) {
7033
7296
  }
7034
7297
  return result;
7035
7298
  }
7036
- async function detectLanguages(repositoryRoot, precomputedFiles) {
7299
+ async function detectLanguages(repositoryRoot, precomputedFiles, exclude) {
7037
7300
  let files;
7038
7301
  if (precomputedFiles) {
7039
7302
  files = precomputedFiles;
@@ -7049,6 +7312,9 @@ async function detectLanguages(repositoryRoot, precomputedFiles) {
7049
7312
  return [];
7050
7313
  }
7051
7314
  }
7315
+ if (exclude && exclude.length > 0) {
7316
+ files = applyExclusions(files, [...exclude], []);
7317
+ }
7052
7318
  const detected = /* @__PURE__ */ new Set();
7053
7319
  for (const file of files) {
7054
7320
  const basename = import_path8.default.basename(file);
@@ -7087,16 +7353,18 @@ async function detectLocalConfigFiles(repositoryRoot, adapters) {
7087
7353
  }
7088
7354
  return found;
7089
7355
  }
7090
- async function enrichWithCodacyYaml(config, repositoryRoot, adapters) {
7091
- const yamlConfig = await readCodacyYaml(repositoryRoot);
7092
- if (!yamlConfig) return { config, codacyYamlApplied: false };
7356
+ function enrichWithCodacyYaml(config, adapters, excludes) {
7357
+ if (!excludes.yamlConfig) return { config, codacyYamlApplied: false };
7093
7358
  return {
7094
- config: applyCodacyYamlExcludes(config, yamlConfig, adapters),
7359
+ config: applyCodacyYamlExcludes(config, excludes.yamlConfig, adapters),
7095
7360
  codacyYamlApplied: true
7096
7361
  };
7097
7362
  }
7098
- async function initLocalConfigBase(repositoryRoot, adapters) {
7099
- 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
+ );
7100
7368
  const localConfigs = await detectLocalConfigFiles(repositoryRoot, adapters);
7101
7369
  const tools = [];
7102
7370
  for (const [toolId, configPath] of localConfigs.entries()) {
@@ -7109,17 +7377,23 @@ async function initLocalConfigBase(repositoryRoot, adapters) {
7109
7377
  }
7110
7378
  return createCodacyConfig({ source: "local", languages, tools });
7111
7379
  }
7112
- async function initLocalConfig(repositoryRoot, adapters) {
7113
- const baseConfig = await initLocalConfigBase(repositoryRoot, adapters);
7114
- const { config, codacyYamlApplied } = await enrichWithCodacyYaml(
7115
- baseConfig,
7380
+ async function initLocalConfig(repositoryRoot, adapters, options) {
7381
+ const excludes = await resolveDiscoveryExcludes(
7116
7382
  repositoryRoot,
7117
- adapters
7383
+ adapters,
7384
+ options?.existingConfig
7118
7385
  );
7386
+ const baseConfig = await initLocalConfigBase(repositoryRoot, adapters, excludes);
7387
+ const { config, codacyYamlApplied } = enrichWithCodacyYaml(baseConfig, adapters, excludes);
7119
7388
  return { config, stats: { codacyYamlApplied } };
7120
7389
  }
7121
- async function initDefaultConfig(repositoryRoot, adapters) {
7122
- 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);
7123
7397
  const toolsWithLocalConfig = new Set(
7124
7398
  base.tools.filter((t) => t.useLocalConfigurationFile).map((t) => t.toolId)
7125
7399
  );
@@ -7157,11 +7431,7 @@ async function initDefaultConfig(repositoryRoot, adapters) {
7157
7431
  languages: base.metadata.languages,
7158
7432
  tools: updatedTools
7159
7433
  });
7160
- const { config, codacyYamlApplied } = await enrichWithCodacyYaml(
7161
- baseConfig,
7162
- repositoryRoot,
7163
- adapters
7164
- );
7434
+ const { config, codacyYamlApplied } = enrichWithCodacyYaml(baseConfig, adapters, excludes);
7165
7435
  return {
7166
7436
  config,
7167
7437
  stats: { totalApiTools: apiTools.length, addedTools, addedUnsupported, codacyYamlApplied }
@@ -7247,10 +7517,10 @@ async function initRemoteConfig(repositoryRoot, provider, organization, reposito
7247
7517
  if (ignoredFilesResult.files.length > 0) {
7248
7518
  baseConfig.exclude = [.../* @__PURE__ */ new Set([...baseConfig.exclude ?? [], ...ignoredFilesResult.files])];
7249
7519
  }
7250
- const { config, codacyYamlApplied } = await enrichWithCodacyYaml(
7520
+ const { config, codacyYamlApplied } = enrichWithCodacyYaml(
7251
7521
  baseConfig,
7252
- repositoryRoot,
7253
- adapters
7522
+ adapters,
7523
+ await resolveDiscoveryExcludes(repositoryRoot, adapters)
7254
7524
  );
7255
7525
  return {
7256
7526
  config,
@@ -7324,8 +7594,13 @@ function hasMatchingFiles(files, filePatterns) {
7324
7594
  if (matcher.matchAll) return files.length > 0;
7325
7595
  return files.some((file) => matcher.matches(file, file.split("/").pop() ?? ""));
7326
7596
  }
7327
- async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, onDiscoveryComplete, rawFilterString, loadUnsupportedPatterns) {
7328
- 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 });
7329
7604
  onDiscoveryComplete?.(discoveryResult);
7330
7605
  const detectedKeys = /* @__PURE__ */ new Set();
7331
7606
  for (const lang of discoveryResult.languages) detectedKeys.add(lang);
@@ -7333,15 +7608,26 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
7333
7608
  for (const lib of discoveryResult.libraries) detectedKeys.add(lib.displayName);
7334
7609
  const repoLanguages = new Set(discoveryResult.languages);
7335
7610
  const localConfigs = await detectLocalConfigFiles(repositoryRoot, adapters);
7336
- const repoFiles = listRepoFiles(repositoryRoot);
7611
+ const repoFiles = listRepoFiles(repositoryRoot, excludes.global);
7337
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
+ };
7338
7624
  const candidates = [];
7339
7625
  for (const adapter of adapters) {
7340
7626
  if (!adapter.languages.includes("*") && adapter.languages.length > 0 && repoLanguages.size > 0) {
7341
7627
  const hasOverlap = adapter.languages.some((lang) => repoLanguages.has(lang));
7342
7628
  if (!hasOverlap) continue;
7343
7629
  }
7344
- if (!hasMatchingFiles(repoFiles, adapter.filePatterns)) continue;
7630
+ if (!hasMatchingFiles(filesVisibleTo(adapter.id), adapter.filePatterns)) continue;
7345
7631
  candidates.push(adapter);
7346
7632
  }
7347
7633
  const deduped = deduplicateToolFamilies(candidates, localConfigs);
@@ -7374,7 +7660,12 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
7374
7660
  patterns: selectedPatterns.map((p) => ({ patternId: p.id }))
7375
7661
  };
7376
7662
  tools.push(toolEntry);
7377
- await applyAdapterParameterSuggestions(adapter, toolEntry, repositoryRoot, repoFiles);
7663
+ await applyAdapterParameterSuggestions(
7664
+ adapter,
7665
+ toolEntry,
7666
+ repositoryRoot,
7667
+ filesVisibleTo(adapter.id)
7668
+ );
7378
7669
  }
7379
7670
  }
7380
7671
  let selectedUnsupported = 0;
@@ -7389,7 +7680,7 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
7389
7680
  }
7390
7681
  const family = toolGroupKey(desc.id, desc.alternativeGroup);
7391
7682
  if (selectedFamilies.has(family)) continue;
7392
- if (!hasMatchingFiles(repoFiles, desc.filePatterns)) continue;
7683
+ if (!hasMatchingFiles(filesVisibleTo(desc.id), desc.filePatterns)) continue;
7393
7684
  const patternsMetadata = loadUnsupportedPatterns(desc.id);
7394
7685
  if (patternsMetadata.length === 0) continue;
7395
7686
  const selectedPatterns = filterAutoPatterns(
@@ -7414,11 +7705,7 @@ async function initAutoConfig(repositoryRoot, adapters, descriptors, filters, on
7414
7705
  tools,
7415
7706
  autoFilters: rawFilterString || void 0
7416
7707
  });
7417
- const { config, codacyYamlApplied } = await enrichWithCodacyYaml(
7418
- baseConfig,
7419
- repositoryRoot,
7420
- adapters
7421
- );
7708
+ const { config, codacyYamlApplied } = enrichWithCodacyYaml(baseConfig, adapters, excludes);
7422
7709
  return {
7423
7710
  config,
7424
7711
  stats: {
@@ -7809,11 +8096,12 @@ function parseSarif(runs) {
7809
8096
  if (!patternId) continue;
7810
8097
  const level = mapSarifLevelToCodacy(result.level);
7811
8098
  const message = result.message?.text ?? "";
8099
+ const sourceId = result.properties?.sourceId;
7812
8100
  for (const loc of result.locations ?? []) {
7813
8101
  const filePath = loc.physicalLocation?.artifactLocation?.uri;
7814
8102
  const line = loc.physicalLocation?.region?.startLine;
7815
8103
  if (!filePath || !line) continue;
7816
- issues.push({ toolId, patternId, filePath, line, message, level });
8104
+ issues.push({ toolId, patternId, filePath, line, message, level, sourceId });
7817
8105
  }
7818
8106
  }
7819
8107
  }
@@ -7827,10 +8115,11 @@ function parseAnalysisResult(result) {
7827
8115
  line: issue.line,
7828
8116
  message: issue.message,
7829
8117
  // JSON carries the authoritative Codacy severity; pass it through unchanged.
7830
- level: issue.severity
8118
+ level: issue.severity,
8119
+ sourceId: issue.sourceId
7831
8120
  }));
7832
8121
  }
7833
- function buildPayloads(issues, resolveShortName) {
8122
+ function buildPayloads(issues, resolveShortName, needsSourceId = () => false) {
7834
8123
  const byTool = /* @__PURE__ */ new Map();
7835
8124
  for (const issue of issues) {
7836
8125
  const list = byTool.get(issue.toolId);
@@ -7839,6 +8128,7 @@ function buildPayloads(issues, resolveShortName) {
7839
8128
  }
7840
8129
  const payloads = [];
7841
8130
  for (const [toolId, toolIssues] of byTool) {
8131
+ const withSourceId = needsSourceId(toolId);
7842
8132
  const byFile = /* @__PURE__ */ new Map();
7843
8133
  for (const issue of toolIssues) {
7844
8134
  const entry = {
@@ -7847,7 +8137,8 @@ function buildPayloads(issues, resolveShortName) {
7847
8137
  filename: issue.filePath,
7848
8138
  message: { text: issue.message },
7849
8139
  level: issue.level,
7850
- location: { LineLocation: { line: issue.line } }
8140
+ location: { LineLocation: { line: issue.line } },
8141
+ ...withSourceId && issue.sourceId ? { sourceId: issue.sourceId } : {}
7851
8142
  }
7852
8143
  };
7853
8144
  const list = byFile.get(issue.filePath);
@@ -7879,6 +8170,13 @@ async function buildShortNameResolver(onProgress) {
7879
8170
  }
7880
8171
  return (toolId) => byName.get(toolId.toLowerCase()) ?? toolId.toLowerCase();
7881
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
+ }
7882
8180
  var COORDINATE_PATTERN = /^[A-Za-z0-9._-]+$/;
7883
8181
  function assertValidCoordinate(kind, value) {
7884
8182
  if (!COORDINATE_PATTERN.test(value)) {
@@ -7918,7 +8216,7 @@ async function uploadReport(options) {
7918
8216
  onProgress?.("No issues found in the report; nothing to upload.");
7919
8217
  }
7920
8218
  const resolveShortName = await buildShortNameResolver(onProgress);
7921
- const payloads = buildPayloads(issues, resolveShortName);
8219
+ const payloads = buildPayloads(issues, resolveShortName, buildSourceIdResolver());
7922
8220
  const base = resolveResultsApiBaseUrl();
7923
8221
  const urls = buildUrls(base, auth, commitUuid);
7924
8222
  const headers = { "content-type": "application/json", ...authHeader(auth) };
@@ -8019,6 +8317,7 @@ async function safeReadBody(res) {
8019
8317
  resolveApiToken,
8020
8318
  resolveBaselinePath,
8021
8319
  resolveConfigPath,
8320
+ resolveDiscoveryExcludes,
8022
8321
  resolveEngineToAdapter,
8023
8322
  resolveHeadCommit,
8024
8323
  resolveProxyConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/analysis-runner",
3
- "version": "0.17.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.17.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.17.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)",