@codacy/analysis-runner 0.8.0 → 0.9.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
@@ -46,12 +46,17 @@ interface AnalyzeOptions {
46
46
  gitScope?: "staged" | {
47
47
  diff: string | true;
48
48
  } | "pr";
49
+ /** Custom path to the Codacy configuration file. Absolute, or relative to
50
+ * `repositoryRoot`. Defaults to `.codacy/codacy.config.json`. */
51
+ configFile?: string;
49
52
  /** Logger for pipeline progress messages (install status, etc.). Defaults to no-op. */
50
53
  logger?: Logger;
51
54
  /** Called when a tool starts executing. */
52
55
  onToolStart?: (toolId: string, displayName: string) => void;
53
56
  /** Called when a tool finishes executing. */
54
57
  onToolComplete?: (toolId: string, displayName: string, issueCount: number, durationMs: number) => void;
58
+ /** Called when a tool reports mid-execution progress (e.g. "Linting 50/200 files"). */
59
+ onToolProgress?: (toolId: string, displayName: string, message: string) => void;
55
60
  }
56
61
  /**
57
62
  * Runs analysis on the given repository and returns the unified result.
@@ -148,14 +153,39 @@ declare function formatTextSummary(result: AnalysisResult, isInspect?: boolean):
148
153
  * 3. Default (empty patterns, no local config)
149
154
  */
150
155
 
156
+ /**
157
+ * Resolves the absolute path to the Codacy configuration file.
158
+ *
159
+ * A custom `configFile` (absolute, or relative to `repositoryRoot`) overrides the
160
+ * default `.codacy/codacy.config.json` location. This lets callers keep one config
161
+ * (e.g. synced from Codacy Cloud) while testing a variant side-by-side.
162
+ */
163
+ declare function resolveConfigPath(repositoryRoot: string, configFile?: string): string;
151
164
  /**
152
165
  * Reads a CodacyConfig from disk.
153
166
  * Returns null if the file doesn't exist. Throws on parse/permission errors
154
167
  * so callers can distinguish "no config" from "broken config".
168
+ *
169
+ * Pass `configFile` to read from a non-default location.
170
+ */
171
+ declare function readCodacyConfig(repositoryRoot: string, configFile?: string): Promise<CodacyConfig | null>;
172
+ /**
173
+ * Writes a CodacyConfig to disk at the exact resolved path, creating only the
174
+ * parent directory. Unlike {@link writeCodacyConfig}, this has NO `.codacy/`
175
+ * side effects, so it is safe to write to an arbitrary location (e.g. a scratch
176
+ * config a user is experimenting with).
177
+ *
178
+ * Pass `configFile` to write to a non-default location.
179
+ */
180
+ declare function writeCodacyConfigFile(repositoryRoot: string, config: CodacyConfig, configFile?: string): Promise<string>;
181
+ /**
182
+ * Writes a CodacyConfig to disk, creating the target directory if needed.
183
+ *
184
+ * Pass `configFile` to write to a non-default location. Regardless of where the
185
+ * main config lives, `.codacy/.gitignore` is always ensured so generated tool
186
+ * configs (which always land under `.codacy/generated/`) stay untracked.
155
187
  */
156
- declare function readCodacyConfig(repositoryRoot: string): Promise<CodacyConfig | null>;
157
- /** Writes a CodacyConfig to disk, creating the .codacy/ directory if needed. */
158
- declare function writeCodacyConfig(repositoryRoot: string, config: CodacyConfig): Promise<string>;
188
+ declare function writeCodacyConfig(repositoryRoot: string, config: CodacyConfig, configFile?: string): Promise<string>;
159
189
  /**
160
190
  * Assembles a CodacyToolConfig from the global CodacyConfig and a local config check.
161
191
  *
@@ -182,6 +212,61 @@ declare function createCodacyConfig(params?: {
182
212
  autoFilters?: string;
183
213
  }): CodacyConfig;
184
214
 
215
+ /**
216
+ * Set operations on Codacy configuration files.
217
+ *
218
+ * Three operations combine the *tools* and *patterns* of two CodacyConfigs:
219
+ * - `merge` — union of tools, and per-tool union of patterns
220
+ * - `intersect` — tools present in both, and per-tool patterns present in both
221
+ * - `diff` — `dest − source`: tools/patterns in dest that are NOT in source
222
+ *
223
+ * The result is always built from `dest` (its `version`, `metadata`, and
224
+ * top-level `exclude`/`includeFiles` are preserved; only `tools` and
225
+ * `metadata.updatedAt` change), because callers write the result back to the
226
+ * destination file. Inputs are never mutated.
227
+ *
228
+ * Rules that span the operations:
229
+ * - If either side uses a tool's native config file
230
+ * (`useLocalConfigurationFile === true`), the merged/intersected result for
231
+ * that tool also uses a config file (patterns dropped). The path comes from
232
+ * source when source uses the file, otherwise dest.
233
+ * - On `diff`, if the *destination* tool uses a config file and source also has
234
+ * that tool (with or without a config file), the tool is removed entirely — a
235
+ * config file's enabled rules are opaque, so it cannot be subtracted.
236
+ * - When a pattern exists on both sides during merge/intersect, its parameters
237
+ * are taken from source.
238
+ * - On `intersect`/`diff`, a tool left with no patterns and no config file is a
239
+ * no-op and is dropped from the result. `merge` always preserves its inputs.
240
+ */
241
+
242
+ /** The set operation to apply between two configs. */
243
+ type ConfigOperation = "merge" | "intersect" | "diff";
244
+ /**
245
+ * Union of source and dest at the tool and pattern level.
246
+ * Tools present on only one side are carried over as-is; tools on both sides are
247
+ * combined (config-file rule, then per-tool pattern union with source parameters
248
+ * winning). Never drops tools.
249
+ */
250
+ declare function mergeConfigs(source: CodacyConfig, dest: CodacyConfig): CodacyConfig;
251
+ /**
252
+ * Intersection of source and dest: only tools present in both, and per-tool only
253
+ * the patterns present in both (config-file rule applies when either side uses a
254
+ * config file). Tools left empty (no patterns, no config file) are dropped.
255
+ */
256
+ declare function intersectConfigs(source: CodacyConfig, dest: CodacyConfig): CodacyConfig;
257
+ /**
258
+ * `dest − source`: keeps tools/patterns in dest that are not in source.
259
+ * - A tool only in dest is kept whole.
260
+ * - A dest tool that uses a config file and is also present in source is removed
261
+ * entirely (its enabled rules are opaque and cannot be subtracted).
262
+ * - Otherwise dest's inline patterns minus source's patterns are kept (a source
263
+ * tool using a config file enumerates no patterns, so all dest patterns survive).
264
+ * - Tools left empty after subtraction are dropped.
265
+ */
266
+ declare function diffConfigs(source: CodacyConfig, dest: CodacyConfig): CodacyConfig;
267
+ /** Dispatches to the {@link mergeConfigs}, {@link intersectConfigs}, or {@link diffConfigs}. */
268
+ declare function applyConfigOperation(op: ConfigOperation, source: CodacyConfig, dest: CodacyConfig): CodacyConfig;
269
+
185
270
  /**
186
271
  * auto-filters.ts — Parses and applies --auto filter syntax.
187
272
  *
@@ -588,4 +673,4 @@ interface DiscoverOptions {
588
673
  */
589
674
  declare function discover(repositoryRoot: string, options?: DiscoverOptions): Promise<DiscoveryResult>;
590
675
 
591
- export { type AdapterFactory, type AnalyzeOptions, type AutoFilters, type CategoryFilter, type CodacyYamlConfig, type CodacyYamlEngineConfig, type DiscoverOptions, type ExecutionMode, type IgnoredFilesResult, type InitAutoStats, type InitDefaultStats, type InitLocalStats, type InitRemoteStats, type LoggerOptions, type PatternMetadataLookup, type SeverityFilter, analyze, applyCodacyYamlExcludes, buildToolConfig, clearAdapters, computePatternSummary, configureApiToken, createCodacyConfig, createLogger, detectLanguages, discover, formatOutput, formatTextSummary, getAdapter, getRegisteredAdapters, getRegisteredDescriptors, initAutoConfig, initContainerConfig, initDefaultConfig, initLocalConfig, initRemoteConfig, listDiffFiles, listIgnoredFiles, listPrFiles, listStagedFiles, parseAutoFilters, readCodacyConfig, readCodacyYaml, registerAdapter, registerLazyAdapter, resolveEngineToAdapter, rotateLogFiles, validateApiToken, writeCodacyConfig };
676
+ export { type AdapterFactory, type AnalyzeOptions, type AutoFilters, type CategoryFilter, type CodacyYamlConfig, type CodacyYamlEngineConfig, type ConfigOperation, type DiscoverOptions, type ExecutionMode, type IgnoredFilesResult, type InitAutoStats, type InitDefaultStats, type InitLocalStats, type InitRemoteStats, type LoggerOptions, type PatternMetadataLookup, type SeverityFilter, analyze, applyCodacyYamlExcludes, applyConfigOperation, buildToolConfig, clearAdapters, computePatternSummary, configureApiToken, createCodacyConfig, createLogger, detectLanguages, diffConfigs, discover, formatOutput, formatTextSummary, getAdapter, getRegisteredAdapters, getRegisteredDescriptors, initAutoConfig, initContainerConfig, initDefaultConfig, initLocalConfig, initRemoteConfig, intersectConfigs, listDiffFiles, listIgnoredFiles, listPrFiles, listStagedFiles, mergeConfigs, parseAutoFilters, readCodacyConfig, readCodacyYaml, registerAdapter, registerLazyAdapter, resolveConfigPath, resolveEngineToAdapter, rotateLogFiles, validateApiToken, writeCodacyConfig, writeCodacyConfigFile };
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  analyze: () => analyze,
34
34
  applyCodacyYamlExcludes: () => applyCodacyYamlExcludes,
35
+ applyConfigOperation: () => applyConfigOperation,
35
36
  buildToolConfig: () => buildToolConfig,
36
37
  clearAdapters: () => clearAdapters,
37
38
  computePatternSummary: () => computePatternSummary,
@@ -39,6 +40,7 @@ __export(index_exports, {
39
40
  createCodacyConfig: () => createCodacyConfig,
40
41
  createLogger: () => createLogger,
41
42
  detectLanguages: () => detectLanguages,
43
+ diffConfigs: () => diffConfigs,
42
44
  discover: () => discover,
43
45
  formatOutput: () => formatOutput,
44
46
  formatTextSummary: () => formatTextSummary,
@@ -50,19 +52,23 @@ __export(index_exports, {
50
52
  initDefaultConfig: () => initDefaultConfig,
51
53
  initLocalConfig: () => initLocalConfig,
52
54
  initRemoteConfig: () => initRemoteConfig,
55
+ intersectConfigs: () => intersectConfigs,
53
56
  listDiffFiles: () => listDiffFiles,
54
57
  listIgnoredFiles: () => listIgnoredFiles,
55
58
  listPrFiles: () => listPrFiles,
56
59
  listStagedFiles: () => listStagedFiles,
60
+ mergeConfigs: () => mergeConfigs,
57
61
  parseAutoFilters: () => parseAutoFilters,
58
62
  readCodacyConfig: () => readCodacyConfig,
59
63
  readCodacyYaml: () => readCodacyYaml,
60
64
  registerAdapter: () => registerAdapter,
61
65
  registerLazyAdapter: () => registerLazyAdapter,
66
+ resolveConfigPath: () => resolveConfigPath,
62
67
  resolveEngineToAdapter: () => resolveEngineToAdapter,
63
68
  rotateLogFiles: () => rotateLogFiles,
64
69
  validateApiToken: () => validateApiToken,
65
- writeCodacyConfig: () => writeCodacyConfig
70
+ writeCodacyConfig: () => writeCodacyConfig,
71
+ writeCodacyConfigFile: () => writeCodacyConfigFile
66
72
  });
67
73
  module.exports = __toCommonJS(index_exports);
68
74
 
@@ -127,8 +133,11 @@ var import_promises = __toESM(require("fs/promises"));
127
133
  var import_path = __toESM(require("path"));
128
134
  var CODACY_DIR = ".codacy";
129
135
  var CONFIG_FILE = "codacy.config.json";
130
- async function readCodacyConfig(repositoryRoot) {
131
- const configPath = import_path.default.join(repositoryRoot, CODACY_DIR, CONFIG_FILE);
136
+ function resolveConfigPath(repositoryRoot, configFile) {
137
+ return configFile ? import_path.default.resolve(repositoryRoot, configFile) : import_path.default.join(repositoryRoot, CODACY_DIR, CONFIG_FILE);
138
+ }
139
+ async function readCodacyConfig(repositoryRoot, configFile) {
140
+ const configPath = resolveConfigPath(repositoryRoot, configFile);
132
141
  let raw;
133
142
  try {
134
143
  raw = await import_promises.default.readFile(configPath, "utf-8");
@@ -154,13 +163,17 @@ function coerceParameterValues(config) {
154
163
  }
155
164
  }
156
165
  }
157
- async function writeCodacyConfig(repositoryRoot, config) {
166
+ async function writeCodacyConfigFile(repositoryRoot, config, configFile) {
167
+ const configPath = resolveConfigPath(repositoryRoot, configFile);
168
+ await import_promises.default.mkdir(import_path.default.dirname(configPath), { recursive: true });
169
+ await import_promises.default.writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
170
+ return configPath;
171
+ }
172
+ async function writeCodacyConfig(repositoryRoot, config, configFile) {
158
173
  const codacyDir = import_path.default.join(repositoryRoot, CODACY_DIR);
159
174
  await import_promises.default.mkdir(codacyDir, { recursive: true });
160
175
  await ensureGitignore(codacyDir);
161
- const configPath = import_path.default.join(codacyDir, CONFIG_FILE);
162
- await import_promises.default.writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
163
- return configPath;
176
+ return writeCodacyConfigFile(repositoryRoot, config, configFile);
164
177
  }
165
178
  async function ensureGitignore(codacyDir) {
166
179
  const gitignorePath = import_path.default.join(codacyDir, ".gitignore");
@@ -214,8 +227,10 @@ function createCodacyConfig(params = {}) {
214
227
 
215
228
  // src/files.ts
216
229
  var import_child_process = require("child_process");
230
+ var import_fs = __toESM(require("fs"));
217
231
  var import_path2 = __toESM(require("path"));
218
232
  var import_minimatch = require("minimatch");
233
+ var MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
219
234
  function listRepoFiles(repositoryRoot) {
220
235
  try {
221
236
  const tracked = (0, import_child_process.execSync)("git ls-files", {
@@ -240,6 +255,30 @@ function listRepoFiles(repositoryRoot) {
240
255
  return [];
241
256
  }
242
257
  }
258
+ function filterOversizedFiles(files, repositoryRoot, logger) {
259
+ const result = [];
260
+ let skipped = 0;
261
+ for (const f of files) {
262
+ try {
263
+ const stat = import_fs.default.statSync(import_path2.default.join(repositoryRoot, f));
264
+ if (stat.size > MAX_FILE_SIZE_BYTES) {
265
+ skipped++;
266
+ logger?.debug("Skipping oversized file", {
267
+ file: f,
268
+ sizeKB: Math.round(stat.size / 1024),
269
+ limitKB: MAX_FILE_SIZE_BYTES / 1024
270
+ });
271
+ continue;
272
+ }
273
+ } catch {
274
+ }
275
+ result.push(f);
276
+ }
277
+ if (skipped > 0) {
278
+ logger?.info("Filtered oversized files", { skipped, remaining: result.length });
279
+ }
280
+ return result;
281
+ }
243
282
  function applyExclusions(files, globalExclude, toolExclude) {
244
283
  const allExclusions = [...globalExclude, ...toolExclude];
245
284
  if (allExclusions.length === 0) return files;
@@ -431,6 +470,10 @@ async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
431
470
  async function runToolsSequential(tools, timeoutMs, callbacks) {
432
471
  const results = [];
433
472
  for (const { adapter, ctx, codacyToolConfig } of tools) {
473
+ if (callbacks?.onToolProgress) {
474
+ const cb = callbacks.onToolProgress;
475
+ ctx.onProgress = (message) => cb(adapter.id, adapter.displayName, message);
476
+ }
434
477
  callbacks?.onToolStart?.(adapter.id, adapter.displayName);
435
478
  const result = await runTool(adapter, ctx, codacyToolConfig, timeoutMs);
436
479
  callbacks?.onToolComplete?.(
@@ -450,6 +493,10 @@ async function runToolsParallel(tools, timeoutMs, concurrency, callbacks) {
450
493
  while (nextIndex < tools.length) {
451
494
  const index = nextIndex++;
452
495
  const { adapter, ctx, codacyToolConfig } = tools[index];
496
+ if (callbacks?.onToolProgress) {
497
+ const cb = callbacks.onToolProgress;
498
+ ctx.onProgress = (message) => cb(adapter.id, adapter.displayName, message);
499
+ }
453
500
  callbacks?.onToolStart?.(adapter.id, adapter.displayName);
454
501
  results[index] = await runTool(adapter, ctx, codacyToolConfig, timeoutMs);
455
502
  callbacks?.onToolComplete?.(
@@ -593,6 +640,13 @@ async function analyze(options) {
593
640
  const toolTimeout = options.toolTimeout || 6e5;
594
641
  const parallelTools = options.parallelTools || 1;
595
642
  const logger = options.logger ?? nullLogger;
643
+ const phaseTimings = {
644
+ availabilityMs: 0,
645
+ fileDiscoveryMs: 0,
646
+ fileRoutingMs: 0,
647
+ toolExecutionMs: 0,
648
+ installMs: 0
649
+ };
596
650
  const emptyCapability = {
597
651
  ready: [],
598
652
  unavailable: [],
@@ -616,9 +670,10 @@ async function analyze(options) {
616
670
  []
617
671
  );
618
672
  }
673
+ const configLabel = options.configFile ?? ".codacy/codacy.config.json";
619
674
  let codacyConfig;
620
675
  try {
621
- codacyConfig = await readCodacyConfig(options.repositoryRoot);
676
+ codacyConfig = await readCodacyConfig(options.repositoryRoot, options.configFile);
622
677
  } catch (err) {
623
678
  return buildResult(
624
679
  startedAt,
@@ -631,7 +686,7 @@ async function analyze(options) {
631
686
  toolId: "runner",
632
687
  phase: "toolConfig",
633
688
  kind: "InvalidConfig",
634
- message: `.codacy/codacy.config.json is invalid: ${err instanceof Error ? err.message : String(err)}`,
689
+ message: `${configLabel} is invalid: ${err instanceof Error ? err.message : String(err)}`,
635
690
  level: "error"
636
691
  }
637
692
  ],
@@ -650,7 +705,7 @@ async function analyze(options) {
650
705
  toolId: "runner",
651
706
  phase: "toolConfig",
652
707
  kind: "MissingConfig",
653
- message: "No .codacy/codacy.config.json found. Run `codacy-analysis init` to initialize the repository.",
708
+ message: `No ${configLabel} found. ` + (options.configFile ? `Run \`codacy-analysis init --config-file ${options.configFile}\` to initialize the repository.` : "Run `codacy-analysis init` to initialize the repository."),
654
709
  level: "error"
655
710
  }
656
711
  ],
@@ -671,6 +726,7 @@ async function analyze(options) {
671
726
  const ready = [];
672
727
  const unavailable = [];
673
728
  const availabilityTimeout = 6e4;
729
+ const availabilityStartMs = Date.now();
674
730
  for (const adapter of adapters) {
675
731
  const ctx = makeToolContext(baseCtx, adapter, []);
676
732
  const checkStart = Date.now();
@@ -729,6 +785,7 @@ async function analyze(options) {
729
785
  });
730
786
  }
731
787
  }
788
+ phaseTimings.availabilityMs = Date.now() - availabilityStartMs;
732
789
  const capability = {
733
790
  ready,
734
791
  unavailable,
@@ -744,6 +801,7 @@ async function analyze(options) {
744
801
  configuredToolCount: configuredIds.size
745
802
  });
746
803
  if (mode === "auto-install" && capability.unavailable.length > 0) {
804
+ const installStartMs = Date.now();
747
805
  const installErrors = [];
748
806
  for (let i = capability.unavailable.length - 1; i >= 0; i--) {
749
807
  const status = capability.unavailable[i];
@@ -807,6 +865,7 @@ async function analyze(options) {
807
865
  });
808
866
  }
809
867
  }
868
+ phaseTimings.installMs = Date.now() - installStartMs;
810
869
  if (installErrors.length > 0) {
811
870
  return buildResult(
812
871
  startedAt,
@@ -815,7 +874,9 @@ async function analyze(options) {
815
874
  capability,
816
875
  [],
817
876
  installErrors,
818
- []
877
+ [],
878
+ void 0,
879
+ phaseTimings
819
880
  );
820
881
  }
821
882
  }
@@ -867,6 +928,7 @@ async function analyze(options) {
867
928
  []
868
929
  );
869
930
  }
931
+ const fileDiscoveryStartMs = Date.now();
870
932
  const globalExclude = codacyConfig.exclude || [];
871
933
  let allFiles = listRepoFiles(options.repositoryRoot);
872
934
  logger.debug("File discovery", { totalFiles: allFiles.length });
@@ -888,6 +950,9 @@ async function analyze(options) {
888
950
  allFiles = filterByTargetFiles(allFiles, options.files);
889
951
  logger.debug("Filtered by target files", { afterFilter: allFiles.length });
890
952
  }
953
+ allFiles = filterOversizedFiles(allFiles, options.repositoryRoot, logger);
954
+ phaseTimings.fileDiscoveryMs = Date.now() - fileDiscoveryStartMs;
955
+ const fileRoutingStartMs = Date.now();
891
956
  const readyAdapters = adapters.filter((a) => capability.ready.some((r) => r.toolId === a.id));
892
957
  const toolRuns = [];
893
958
  for (const adapter of readyAdapters) {
@@ -928,6 +993,8 @@ async function analyze(options) {
928
993
  });
929
994
  toolRuns.push({ adapter, ctx: configCtx, codacyToolConfig });
930
995
  }
996
+ phaseTimings.fileRoutingMs = Date.now() - fileRoutingStartMs;
997
+ const toolExecStartMs = Date.now();
931
998
  logger.info("Starting tool execution", {
932
999
  toolCount: toolRuns.length,
933
1000
  parallel: parallelTools > 1,
@@ -935,7 +1002,8 @@ async function analyze(options) {
935
1002
  });
936
1003
  const callbacks = {
937
1004
  onToolStart: options.onToolStart,
938
- onToolComplete: options.onToolComplete
1005
+ onToolComplete: options.onToolComplete,
1006
+ onToolProgress: options.onToolProgress
939
1007
  };
940
1008
  let results;
941
1009
  if (parallelTools > 1 && toolRuns.length > 1) {
@@ -943,6 +1011,7 @@ async function analyze(options) {
943
1011
  } else {
944
1012
  results = await runToolsSequential(toolRuns, toolTimeout, callbacks);
945
1013
  }
1014
+ phaseTimings.toolExecutionMs = Date.now() - toolExecStartMs;
946
1015
  const allIssues = normalizeIssues(results.flatMap((r) => r.issues));
947
1016
  const allErrors = results.flatMap((r) => r.errors);
948
1017
  const allSummaries = results.map((r) => r.summary);
@@ -961,7 +1030,8 @@ async function analyze(options) {
961
1030
  logger.info("Pipeline complete", {
962
1031
  totalIssues: allIssues.length,
963
1032
  totalErrors: allErrors.length,
964
- durationMs: Date.now() - startedAt.getTime()
1033
+ durationMs: Date.now() - startedAt.getTime(),
1034
+ phaseTimings
965
1035
  });
966
1036
  return buildResult(
967
1037
  startedAt,
@@ -971,7 +1041,8 @@ async function analyze(options) {
971
1041
  allIssues,
972
1042
  allErrors,
973
1043
  allSummaries,
974
- patternSummary
1044
+ patternSummary,
1045
+ phaseTimings
975
1046
  );
976
1047
  }
977
1048
  function makeToolContext(base, adapter, targetFiles) {
@@ -997,7 +1068,7 @@ function resolveGitScope(repositoryRoot, scope) {
997
1068
  const base = scope.diff === true ? void 0 : scope.diff;
998
1069
  return listDiffFiles(repositoryRoot, base);
999
1070
  }
1000
- function buildResult(startedAt, repositoryRoot, mode, capability, issues, errors, toolResults, patternSummary) {
1071
+ function buildResult(startedAt, repositoryRoot, mode, capability, issues, errors, toolResults, patternSummary, phaseTimings) {
1001
1072
  const completedAt = /* @__PURE__ */ new Date();
1002
1073
  return {
1003
1074
  metadata: {
@@ -1005,7 +1076,8 @@ function buildResult(startedAt, repositoryRoot, mode, capability, issues, errors
1005
1076
  completedAt: completedAt.toISOString(),
1006
1077
  durationMs: completedAt.getTime() - startedAt.getTime(),
1007
1078
  repositoryRoot,
1008
- executionMode: mode
1079
+ executionMode: mode,
1080
+ ...phaseTimings ? { phaseTimings } : {}
1009
1081
  },
1010
1082
  capability,
1011
1083
  issues,
@@ -1174,11 +1246,7 @@ function formatText(result) {
1174
1246
  if (result.errors.length > 0) {
1175
1247
  lines.push("");
1176
1248
  lines.push("Errors:");
1177
- for (const error of result.errors) {
1178
- const prefix = `[${error.toolId}/${error.phase}]`;
1179
- const location = error.filePath ? ` ${error.filePath}:` : "";
1180
- lines.push(import_ansis.default.red(` ${prefix}${location} ${error.message}`));
1181
- }
1249
+ lines.push(...formatErrorSection(result));
1182
1250
  lines.push("");
1183
1251
  }
1184
1252
  lines.push(formatTextSummary(result, isInspect));
@@ -1277,6 +1345,33 @@ function formatTextSummary(result, isInspect) {
1277
1345
  lines.push(table.toString());
1278
1346
  }
1279
1347
  }
1348
+ if (!inspect && result.metadata.phaseTimings) {
1349
+ const pt = result.metadata.phaseTimings;
1350
+ const totalMs = result.metadata.durationMs;
1351
+ const toolSumMs = pt.toolExecutionMs;
1352
+ const overheadMs = totalMs - toolSumMs;
1353
+ if (overheadMs > 2e3 && overheadMs / totalMs > 0.2) {
1354
+ lines.push("");
1355
+ lines.push(import_ansis.default.bold("Pipeline timing:"));
1356
+ const entries2 = [
1357
+ ["Availability checks", pt.availabilityMs],
1358
+ ["File discovery", pt.fileDiscoveryMs],
1359
+ ["File routing & config", pt.fileRoutingMs]
1360
+ ];
1361
+ if (pt.installMs > 0) {
1362
+ entries2.push(["Install", pt.installMs]);
1363
+ }
1364
+ entries2.push(["Tool execution", pt.toolExecutionMs]);
1365
+ const accounted = entries2.reduce((sum, [, ms]) => sum + ms, 0);
1366
+ const other = totalMs - accounted;
1367
+ if (other > 500) {
1368
+ entries2.push(["Other", other]);
1369
+ }
1370
+ for (const [label, ms] of entries2) {
1371
+ lines.push(` ${label.padEnd(22)} ${(0, import_tooling4.formatDuration)(ms)}`);
1372
+ }
1373
+ }
1374
+ }
1280
1375
  const toolsWithHints = result.capability.unavailable.filter((t) => t.remediation);
1281
1376
  if (toolsWithHints.length > 0) {
1282
1377
  lines.push("");
@@ -1295,6 +1390,51 @@ function formatTextSummary(result, isInspect) {
1295
1390
  }
1296
1391
  return lines.join("\n");
1297
1392
  }
1393
+ var MAX_ERRORS_PER_GROUP = 5;
1394
+ function formatErrorSection(result) {
1395
+ const lines = [];
1396
+ const displayNameMap = /* @__PURE__ */ new Map();
1397
+ for (const t of result.capability.ready) {
1398
+ displayNameMap.set(t.toolId, t.displayName ?? t.toolId);
1399
+ }
1400
+ for (const t of result.capability.unavailable) {
1401
+ displayNameMap.set(t.toolId, t.displayName ?? t.toolId);
1402
+ }
1403
+ const displayName = (toolId) => displayNameMap.get(toolId) ?? toolId;
1404
+ const groups = /* @__PURE__ */ new Map();
1405
+ for (const error of result.errors) {
1406
+ const key = `${displayName(error.toolId)}/${error.phase}`;
1407
+ const list = groups.get(key) ?? [];
1408
+ list.push(error);
1409
+ groups.set(key, list);
1410
+ }
1411
+ for (const [groupKey, groupErrors] of groups) {
1412
+ const deduped = [];
1413
+ const seen = /* @__PURE__ */ new Map();
1414
+ for (const err of groupErrors) {
1415
+ const dedupKey = `${err.filePath ?? ""}|||${err.message}`;
1416
+ const idx = seen.get(dedupKey);
1417
+ if (idx !== void 0) {
1418
+ deduped[idx].count++;
1419
+ } else {
1420
+ seen.set(dedupKey, deduped.length);
1421
+ deduped.push({ filePath: err.filePath, message: err.message, count: 1 });
1422
+ }
1423
+ }
1424
+ const shown = deduped.slice(0, MAX_ERRORS_PER_GROUP);
1425
+ const overflowCount = deduped.length - shown.length;
1426
+ for (const entry of shown) {
1427
+ const prefix = `[${groupKey}]`;
1428
+ const location = entry.filePath ? ` ${entry.filePath}:` : "";
1429
+ const suffix = entry.count > 1 ? ` (x${entry.count})` : "";
1430
+ lines.push(import_ansis.default.red(` ${prefix}${location} ${entry.message}${suffix}`));
1431
+ }
1432
+ if (overflowCount > 0) {
1433
+ lines.push(import_ansis.default.dim(` ... and ${overflowCount} more ${groupKey} errors`));
1434
+ }
1435
+ }
1436
+ return lines;
1437
+ }
1298
1438
  var MAX_LINE_LENGTH = 100;
1299
1439
  var MAX_MULTILINE_DISPLAY = 10;
1300
1440
  function formatMultilineIssue(output, issue) {
@@ -1437,6 +1577,171 @@ function formatPatternSummary(summary) {
1437
1577
  return lines.join("\n");
1438
1578
  }
1439
1579
 
1580
+ // src/config-ops.ts
1581
+ function usesConfigFile(tool) {
1582
+ return tool.useLocalConfigurationFile === true;
1583
+ }
1584
+ function isEmptyTool(tool) {
1585
+ return !usesConfigFile(tool) && tool.patterns.length === 0;
1586
+ }
1587
+ function indexByToolId(tools) {
1588
+ return new Map(tools.map((t) => [t.toolId, t]));
1589
+ }
1590
+ function indexByPatternId(patterns) {
1591
+ return new Map(patterns.map((p) => [p.patternId, p]));
1592
+ }
1593
+ function unionList(dest, source) {
1594
+ const result = [.../* @__PURE__ */ new Set([...dest ?? [], ...source ?? []])];
1595
+ return result.length > 0 ? result : void 0;
1596
+ }
1597
+ function intersectList(dest, source) {
1598
+ if (!dest || !source) return void 0;
1599
+ const sourceSet = new Set(source);
1600
+ const result = dest.filter((x) => sourceSet.has(x));
1601
+ return result.length > 0 ? result : void 0;
1602
+ }
1603
+ function clonePattern(pattern) {
1604
+ const result = { patternId: pattern.patternId };
1605
+ if (pattern.parameters !== void 0) result.parameters = { ...pattern.parameters };
1606
+ return result;
1607
+ }
1608
+ function cloneTool(tool) {
1609
+ return structuredClone(tool);
1610
+ }
1611
+ function applyToolLevelLists(tool, source, dest, listFn) {
1612
+ const exclude = listFn(dest.exclude, source.exclude);
1613
+ if (exclude) tool.exclude = exclude;
1614
+ const extraFilePatterns = listFn(dest.extraFilePatterns, source.extraFilePatterns);
1615
+ if (extraFilePatterns) tool.extraFilePatterns = extraFilePatterns;
1616
+ }
1617
+ function buildConfigFileTool(toolId, source, dest, listFn) {
1618
+ const localConfigurationFile = (usesConfigFile(source) ? source.localConfigurationFile : void 0) ?? dest.localConfigurationFile;
1619
+ const tool = {
1620
+ toolId,
1621
+ ...localConfigurationFile !== void 0 ? { localConfigurationFile } : {},
1622
+ useLocalConfigurationFile: true,
1623
+ patterns: []
1624
+ };
1625
+ applyToolLevelLists(tool, source, dest, listFn);
1626
+ return tool;
1627
+ }
1628
+ function mergePatterns(source, dest) {
1629
+ const sourceMap = indexByPatternId(source);
1630
+ const seen = /* @__PURE__ */ new Set();
1631
+ const result = [];
1632
+ for (const dp of dest) {
1633
+ const sp = sourceMap.get(dp.patternId);
1634
+ result.push(clonePattern(sp ?? dp));
1635
+ seen.add(dp.patternId);
1636
+ }
1637
+ for (const sp of source) {
1638
+ if (!seen.has(sp.patternId)) result.push(clonePattern(sp));
1639
+ }
1640
+ return result;
1641
+ }
1642
+ function intersectPatterns(source, dest) {
1643
+ const sourceMap = indexByPatternId(source);
1644
+ const result = [];
1645
+ for (const dp of dest) {
1646
+ const sp = sourceMap.get(dp.patternId);
1647
+ if (sp) result.push(clonePattern(sp));
1648
+ }
1649
+ return result;
1650
+ }
1651
+ function diffPatterns(source, dest) {
1652
+ const sourceIds = new Set(source.map((p) => p.patternId));
1653
+ return dest.filter((dp) => !sourceIds.has(dp.patternId)).map(clonePattern);
1654
+ }
1655
+ function combineTool(op, toolId, source, dest) {
1656
+ const listFn = op === "merge" ? unionList : intersectList;
1657
+ if (usesConfigFile(source) || usesConfigFile(dest)) {
1658
+ return buildConfigFileTool(toolId, source, dest, listFn);
1659
+ }
1660
+ const patterns = op === "merge" ? mergePatterns(source.patterns, dest.patterns) : intersectPatterns(source.patterns, dest.patterns);
1661
+ const tool = { toolId, patterns };
1662
+ if (dest.useLocalConfigurationFile !== void 0) {
1663
+ tool.useLocalConfigurationFile = dest.useLocalConfigurationFile;
1664
+ } else if (source.useLocalConfigurationFile !== void 0) {
1665
+ tool.useLocalConfigurationFile = source.useLocalConfigurationFile;
1666
+ }
1667
+ applyToolLevelLists(tool, source, dest, listFn);
1668
+ return tool;
1669
+ }
1670
+ function buildResultConfig(dest, tools) {
1671
+ const result = {
1672
+ version: dest.version,
1673
+ metadata: { ...structuredClone(dest.metadata), updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
1674
+ tools
1675
+ };
1676
+ if (dest.exclude !== void 0) result.exclude = structuredClone(dest.exclude);
1677
+ if (dest.includeFiles !== void 0) result.includeFiles = structuredClone(dest.includeFiles);
1678
+ return result;
1679
+ }
1680
+ function mergeConfigs(source, dest) {
1681
+ const sourceMap = indexByToolId(source.tools);
1682
+ const seen = /* @__PURE__ */ new Set();
1683
+ const tools = [];
1684
+ for (const d of dest.tools) {
1685
+ const s = sourceMap.get(d.toolId);
1686
+ tools.push(s ? combineTool("merge", d.toolId, s, d) : cloneTool(d));
1687
+ seen.add(d.toolId);
1688
+ }
1689
+ for (const s of source.tools) {
1690
+ if (!seen.has(s.toolId)) tools.push(cloneTool(s));
1691
+ }
1692
+ return buildResultConfig(dest, tools);
1693
+ }
1694
+ function intersectConfigs(source, dest) {
1695
+ const sourceMap = indexByToolId(source.tools);
1696
+ const tools = [];
1697
+ for (const d of dest.tools) {
1698
+ const s = sourceMap.get(d.toolId);
1699
+ if (!s) continue;
1700
+ const tool = combineTool("intersect", d.toolId, s, d);
1701
+ if (isEmptyTool(tool)) continue;
1702
+ tools.push(tool);
1703
+ }
1704
+ return buildResultConfig(dest, tools);
1705
+ }
1706
+ function diffConfigs(source, dest) {
1707
+ const sourceMap = indexByToolId(source.tools);
1708
+ const tools = [];
1709
+ for (const d of dest.tools) {
1710
+ const s = sourceMap.get(d.toolId);
1711
+ if (!s) {
1712
+ tools.push(cloneTool(d));
1713
+ continue;
1714
+ }
1715
+ if (usesConfigFile(d)) continue;
1716
+ const tool = {
1717
+ toolId: d.toolId,
1718
+ patterns: diffPatterns(s.patterns, d.patterns)
1719
+ };
1720
+ if (d.useLocalConfigurationFile !== void 0) {
1721
+ tool.useLocalConfigurationFile = d.useLocalConfigurationFile;
1722
+ }
1723
+ if (d.exclude !== void 0) tool.exclude = [...d.exclude];
1724
+ if (d.extraFilePatterns !== void 0) tool.extraFilePatterns = [...d.extraFilePatterns];
1725
+ if (isEmptyTool(tool)) continue;
1726
+ tools.push(tool);
1727
+ }
1728
+ return buildResultConfig(dest, tools);
1729
+ }
1730
+ function applyConfigOperation(op, source, dest) {
1731
+ switch (op) {
1732
+ case "merge":
1733
+ return mergeConfigs(source, dest);
1734
+ case "intersect":
1735
+ return intersectConfigs(source, dest);
1736
+ case "diff":
1737
+ return diffConfigs(source, dest);
1738
+ default: {
1739
+ const exhaustive = op;
1740
+ throw new Error(`Unknown config operation: ${String(exhaustive)}`);
1741
+ }
1742
+ }
1743
+ }
1744
+
1440
1745
  // src/init.ts
1441
1746
  var import_promises4 = __toESM(require("fs/promises"));
1442
1747
  var import_path7 = __toESM(require("path"));
@@ -6315,7 +6620,7 @@ function parseAutoFilters(raw) {
6315
6620
  }
6316
6621
 
6317
6622
  // src/logger.ts
6318
- var import_fs = __toESM(require("fs"));
6623
+ var import_fs2 = __toESM(require("fs"));
6319
6624
  var import_path8 = __toESM(require("path"));
6320
6625
  var import_os2 = __toESM(require("os"));
6321
6626
  var LEVEL_ORDER = {
@@ -6327,7 +6632,7 @@ var LEVEL_ORDER = {
6327
6632
  function rotateLogFiles(logsDir, maxFiles) {
6328
6633
  let entries2;
6329
6634
  try {
6330
- entries2 = import_fs.default.readdirSync(logsDir).filter((f) => f.startsWith("analysis-") && f.endsWith(".log"));
6635
+ entries2 = import_fs2.default.readdirSync(logsDir).filter((f) => f.startsWith("analysis-") && f.endsWith(".log"));
6331
6636
  } catch {
6332
6637
  return;
6333
6638
  }
@@ -6336,7 +6641,7 @@ function rotateLogFiles(logsDir, maxFiles) {
6336
6641
  const toRemove = entries2.slice(0, entries2.length - maxFiles);
6337
6642
  for (const f of toRemove) {
6338
6643
  try {
6339
- import_fs.default.unlinkSync(import_path8.default.join(logsDir, f));
6644
+ import_fs2.default.unlinkSync(import_path8.default.join(logsDir, f));
6340
6645
  } catch {
6341
6646
  }
6342
6647
  }
@@ -6378,10 +6683,10 @@ function createLogger(options = {}) {
6378
6683
  let logFd = null;
6379
6684
  let bytesWritten = 0;
6380
6685
  if (fileEnabled) {
6381
- import_fs.default.mkdirSync(logsDir, { recursive: true });
6686
+ import_fs2.default.mkdirSync(logsDir, { recursive: true });
6382
6687
  rotateLogFiles(logsDir, maxFiles);
6383
6688
  const logFilePath = import_path8.default.join(logsDir, `analysis-${fileTimestamp()}.log`);
6384
- logFd = import_fs.default.openSync(logFilePath, "a");
6689
+ logFd = import_fs2.default.openSync(logFilePath, "a");
6385
6690
  }
6386
6691
  function log(level, message, fields) {
6387
6692
  const numLevel = LEVEL_ORDER[level];
@@ -6394,13 +6699,13 @@ function createLogger(options = {}) {
6394
6699
  ...fields
6395
6700
  });
6396
6701
  const buf = Buffer.from(entry + "\n", "utf-8");
6397
- import_fs.default.writeSync(logFd, buf);
6702
+ import_fs2.default.writeSync(logFd, buf);
6398
6703
  bytesWritten += buf.length;
6399
6704
  if (bytesWritten >= maxFileSize) {
6400
- import_fs.default.closeSync(logFd);
6705
+ import_fs2.default.closeSync(logFd);
6401
6706
  rotateLogFiles(logsDir, maxFiles);
6402
6707
  const newPath = import_path8.default.join(logsDir, `analysis-${fileTimestamp()}.log`);
6403
- logFd = import_fs.default.openSync(newPath, "a");
6708
+ logFd = import_fs2.default.openSync(newPath, "a");
6404
6709
  bytesWritten = 0;
6405
6710
  }
6406
6711
  }
@@ -6423,6 +6728,7 @@ function createLogger(options = {}) {
6423
6728
  0 && (module.exports = {
6424
6729
  analyze,
6425
6730
  applyCodacyYamlExcludes,
6731
+ applyConfigOperation,
6426
6732
  buildToolConfig,
6427
6733
  clearAdapters,
6428
6734
  computePatternSummary,
@@ -6430,6 +6736,7 @@ function createLogger(options = {}) {
6430
6736
  createCodacyConfig,
6431
6737
  createLogger,
6432
6738
  detectLanguages,
6739
+ diffConfigs,
6433
6740
  discover,
6434
6741
  formatOutput,
6435
6742
  formatTextSummary,
@@ -6441,17 +6748,21 @@ function createLogger(options = {}) {
6441
6748
  initDefaultConfig,
6442
6749
  initLocalConfig,
6443
6750
  initRemoteConfig,
6751
+ intersectConfigs,
6444
6752
  listDiffFiles,
6445
6753
  listIgnoredFiles,
6446
6754
  listPrFiles,
6447
6755
  listStagedFiles,
6756
+ mergeConfigs,
6448
6757
  parseAutoFilters,
6449
6758
  readCodacyConfig,
6450
6759
  readCodacyYaml,
6451
6760
  registerAdapter,
6452
6761
  registerLazyAdapter,
6762
+ resolveConfigPath,
6453
6763
  resolveEngineToAdapter,
6454
6764
  rotateLogFiles,
6455
6765
  validateApiToken,
6456
- writeCodacyConfig
6766
+ writeCodacyConfig,
6767
+ writeCodacyConfigFile
6457
6768
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/analysis-runner",
3
- "version": "0.8.0",
3
+ "version": "0.9.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.8.0"
15
+ "@codacy/tooling": "0.9.0"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@codacy/openapi-typescript-codegen": "0.0.9",
19
- "@codacy/tools-dummy-1": "0.8.0"
19
+ "@codacy/tools-dummy-1": "0.9.0"
20
20
  },
21
21
  "license": "MIT",
22
22
  "author": "Codacy <support@codacy.com> (https://www.codacy.com)",