@codacy/analysis-runner 0.8.0 → 0.8.1
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 +2 -0
- package/dist/index.js +145 -19
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ interface AnalyzeOptions {
|
|
|
52
52
|
onToolStart?: (toolId: string, displayName: string) => void;
|
|
53
53
|
/** Called when a tool finishes executing. */
|
|
54
54
|
onToolComplete?: (toolId: string, displayName: string, issueCount: number, durationMs: number) => void;
|
|
55
|
+
/** Called when a tool reports mid-execution progress (e.g. "Linting 50/200 files"). */
|
|
56
|
+
onToolProgress?: (toolId: string, displayName: string, message: string) => void;
|
|
55
57
|
}
|
|
56
58
|
/**
|
|
57
59
|
* Runs analysis on the given repository and returns the unified result.
|
package/dist/index.js
CHANGED
|
@@ -214,8 +214,10 @@ function createCodacyConfig(params = {}) {
|
|
|
214
214
|
|
|
215
215
|
// src/files.ts
|
|
216
216
|
var import_child_process = require("child_process");
|
|
217
|
+
var import_fs = __toESM(require("fs"));
|
|
217
218
|
var import_path2 = __toESM(require("path"));
|
|
218
219
|
var import_minimatch = require("minimatch");
|
|
220
|
+
var MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
219
221
|
function listRepoFiles(repositoryRoot) {
|
|
220
222
|
try {
|
|
221
223
|
const tracked = (0, import_child_process.execSync)("git ls-files", {
|
|
@@ -240,6 +242,30 @@ function listRepoFiles(repositoryRoot) {
|
|
|
240
242
|
return [];
|
|
241
243
|
}
|
|
242
244
|
}
|
|
245
|
+
function filterOversizedFiles(files, repositoryRoot, logger) {
|
|
246
|
+
const result = [];
|
|
247
|
+
let skipped = 0;
|
|
248
|
+
for (const f of files) {
|
|
249
|
+
try {
|
|
250
|
+
const stat = import_fs.default.statSync(import_path2.default.join(repositoryRoot, f));
|
|
251
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) {
|
|
252
|
+
skipped++;
|
|
253
|
+
logger?.debug("Skipping oversized file", {
|
|
254
|
+
file: f,
|
|
255
|
+
sizeKB: Math.round(stat.size / 1024),
|
|
256
|
+
limitKB: MAX_FILE_SIZE_BYTES / 1024
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
result.push(f);
|
|
263
|
+
}
|
|
264
|
+
if (skipped > 0) {
|
|
265
|
+
logger?.info("Filtered oversized files", { skipped, remaining: result.length });
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
243
269
|
function applyExclusions(files, globalExclude, toolExclude) {
|
|
244
270
|
const allExclusions = [...globalExclude, ...toolExclude];
|
|
245
271
|
if (allExclusions.length === 0) return files;
|
|
@@ -431,6 +457,10 @@ async function runTool(adapter, ctx, codacyToolConfig, timeoutMs) {
|
|
|
431
457
|
async function runToolsSequential(tools, timeoutMs, callbacks) {
|
|
432
458
|
const results = [];
|
|
433
459
|
for (const { adapter, ctx, codacyToolConfig } of tools) {
|
|
460
|
+
if (callbacks?.onToolProgress) {
|
|
461
|
+
const cb = callbacks.onToolProgress;
|
|
462
|
+
ctx.onProgress = (message) => cb(adapter.id, adapter.displayName, message);
|
|
463
|
+
}
|
|
434
464
|
callbacks?.onToolStart?.(adapter.id, adapter.displayName);
|
|
435
465
|
const result = await runTool(adapter, ctx, codacyToolConfig, timeoutMs);
|
|
436
466
|
callbacks?.onToolComplete?.(
|
|
@@ -450,6 +480,10 @@ async function runToolsParallel(tools, timeoutMs, concurrency, callbacks) {
|
|
|
450
480
|
while (nextIndex < tools.length) {
|
|
451
481
|
const index = nextIndex++;
|
|
452
482
|
const { adapter, ctx, codacyToolConfig } = tools[index];
|
|
483
|
+
if (callbacks?.onToolProgress) {
|
|
484
|
+
const cb = callbacks.onToolProgress;
|
|
485
|
+
ctx.onProgress = (message) => cb(adapter.id, adapter.displayName, message);
|
|
486
|
+
}
|
|
453
487
|
callbacks?.onToolStart?.(adapter.id, adapter.displayName);
|
|
454
488
|
results[index] = await runTool(adapter, ctx, codacyToolConfig, timeoutMs);
|
|
455
489
|
callbacks?.onToolComplete?.(
|
|
@@ -593,6 +627,13 @@ async function analyze(options) {
|
|
|
593
627
|
const toolTimeout = options.toolTimeout || 6e5;
|
|
594
628
|
const parallelTools = options.parallelTools || 1;
|
|
595
629
|
const logger = options.logger ?? nullLogger;
|
|
630
|
+
const phaseTimings = {
|
|
631
|
+
availabilityMs: 0,
|
|
632
|
+
fileDiscoveryMs: 0,
|
|
633
|
+
fileRoutingMs: 0,
|
|
634
|
+
toolExecutionMs: 0,
|
|
635
|
+
installMs: 0
|
|
636
|
+
};
|
|
596
637
|
const emptyCapability = {
|
|
597
638
|
ready: [],
|
|
598
639
|
unavailable: [],
|
|
@@ -671,6 +712,7 @@ async function analyze(options) {
|
|
|
671
712
|
const ready = [];
|
|
672
713
|
const unavailable = [];
|
|
673
714
|
const availabilityTimeout = 6e4;
|
|
715
|
+
const availabilityStartMs = Date.now();
|
|
674
716
|
for (const adapter of adapters) {
|
|
675
717
|
const ctx = makeToolContext(baseCtx, adapter, []);
|
|
676
718
|
const checkStart = Date.now();
|
|
@@ -729,6 +771,7 @@ async function analyze(options) {
|
|
|
729
771
|
});
|
|
730
772
|
}
|
|
731
773
|
}
|
|
774
|
+
phaseTimings.availabilityMs = Date.now() - availabilityStartMs;
|
|
732
775
|
const capability = {
|
|
733
776
|
ready,
|
|
734
777
|
unavailable,
|
|
@@ -744,6 +787,7 @@ async function analyze(options) {
|
|
|
744
787
|
configuredToolCount: configuredIds.size
|
|
745
788
|
});
|
|
746
789
|
if (mode === "auto-install" && capability.unavailable.length > 0) {
|
|
790
|
+
const installStartMs = Date.now();
|
|
747
791
|
const installErrors = [];
|
|
748
792
|
for (let i = capability.unavailable.length - 1; i >= 0; i--) {
|
|
749
793
|
const status = capability.unavailable[i];
|
|
@@ -807,6 +851,7 @@ async function analyze(options) {
|
|
|
807
851
|
});
|
|
808
852
|
}
|
|
809
853
|
}
|
|
854
|
+
phaseTimings.installMs = Date.now() - installStartMs;
|
|
810
855
|
if (installErrors.length > 0) {
|
|
811
856
|
return buildResult(
|
|
812
857
|
startedAt,
|
|
@@ -815,7 +860,9 @@ async function analyze(options) {
|
|
|
815
860
|
capability,
|
|
816
861
|
[],
|
|
817
862
|
installErrors,
|
|
818
|
-
[]
|
|
863
|
+
[],
|
|
864
|
+
void 0,
|
|
865
|
+
phaseTimings
|
|
819
866
|
);
|
|
820
867
|
}
|
|
821
868
|
}
|
|
@@ -867,6 +914,7 @@ async function analyze(options) {
|
|
|
867
914
|
[]
|
|
868
915
|
);
|
|
869
916
|
}
|
|
917
|
+
const fileDiscoveryStartMs = Date.now();
|
|
870
918
|
const globalExclude = codacyConfig.exclude || [];
|
|
871
919
|
let allFiles = listRepoFiles(options.repositoryRoot);
|
|
872
920
|
logger.debug("File discovery", { totalFiles: allFiles.length });
|
|
@@ -888,6 +936,9 @@ async function analyze(options) {
|
|
|
888
936
|
allFiles = filterByTargetFiles(allFiles, options.files);
|
|
889
937
|
logger.debug("Filtered by target files", { afterFilter: allFiles.length });
|
|
890
938
|
}
|
|
939
|
+
allFiles = filterOversizedFiles(allFiles, options.repositoryRoot, logger);
|
|
940
|
+
phaseTimings.fileDiscoveryMs = Date.now() - fileDiscoveryStartMs;
|
|
941
|
+
const fileRoutingStartMs = Date.now();
|
|
891
942
|
const readyAdapters = adapters.filter((a) => capability.ready.some((r) => r.toolId === a.id));
|
|
892
943
|
const toolRuns = [];
|
|
893
944
|
for (const adapter of readyAdapters) {
|
|
@@ -928,6 +979,8 @@ async function analyze(options) {
|
|
|
928
979
|
});
|
|
929
980
|
toolRuns.push({ adapter, ctx: configCtx, codacyToolConfig });
|
|
930
981
|
}
|
|
982
|
+
phaseTimings.fileRoutingMs = Date.now() - fileRoutingStartMs;
|
|
983
|
+
const toolExecStartMs = Date.now();
|
|
931
984
|
logger.info("Starting tool execution", {
|
|
932
985
|
toolCount: toolRuns.length,
|
|
933
986
|
parallel: parallelTools > 1,
|
|
@@ -935,7 +988,8 @@ async function analyze(options) {
|
|
|
935
988
|
});
|
|
936
989
|
const callbacks = {
|
|
937
990
|
onToolStart: options.onToolStart,
|
|
938
|
-
onToolComplete: options.onToolComplete
|
|
991
|
+
onToolComplete: options.onToolComplete,
|
|
992
|
+
onToolProgress: options.onToolProgress
|
|
939
993
|
};
|
|
940
994
|
let results;
|
|
941
995
|
if (parallelTools > 1 && toolRuns.length > 1) {
|
|
@@ -943,6 +997,7 @@ async function analyze(options) {
|
|
|
943
997
|
} else {
|
|
944
998
|
results = await runToolsSequential(toolRuns, toolTimeout, callbacks);
|
|
945
999
|
}
|
|
1000
|
+
phaseTimings.toolExecutionMs = Date.now() - toolExecStartMs;
|
|
946
1001
|
const allIssues = normalizeIssues(results.flatMap((r) => r.issues));
|
|
947
1002
|
const allErrors = results.flatMap((r) => r.errors);
|
|
948
1003
|
const allSummaries = results.map((r) => r.summary);
|
|
@@ -961,7 +1016,8 @@ async function analyze(options) {
|
|
|
961
1016
|
logger.info("Pipeline complete", {
|
|
962
1017
|
totalIssues: allIssues.length,
|
|
963
1018
|
totalErrors: allErrors.length,
|
|
964
|
-
durationMs: Date.now() - startedAt.getTime()
|
|
1019
|
+
durationMs: Date.now() - startedAt.getTime(),
|
|
1020
|
+
phaseTimings
|
|
965
1021
|
});
|
|
966
1022
|
return buildResult(
|
|
967
1023
|
startedAt,
|
|
@@ -971,7 +1027,8 @@ async function analyze(options) {
|
|
|
971
1027
|
allIssues,
|
|
972
1028
|
allErrors,
|
|
973
1029
|
allSummaries,
|
|
974
|
-
patternSummary
|
|
1030
|
+
patternSummary,
|
|
1031
|
+
phaseTimings
|
|
975
1032
|
);
|
|
976
1033
|
}
|
|
977
1034
|
function makeToolContext(base, adapter, targetFiles) {
|
|
@@ -997,7 +1054,7 @@ function resolveGitScope(repositoryRoot, scope) {
|
|
|
997
1054
|
const base = scope.diff === true ? void 0 : scope.diff;
|
|
998
1055
|
return listDiffFiles(repositoryRoot, base);
|
|
999
1056
|
}
|
|
1000
|
-
function buildResult(startedAt, repositoryRoot, mode, capability, issues, errors, toolResults, patternSummary) {
|
|
1057
|
+
function buildResult(startedAt, repositoryRoot, mode, capability, issues, errors, toolResults, patternSummary, phaseTimings) {
|
|
1001
1058
|
const completedAt = /* @__PURE__ */ new Date();
|
|
1002
1059
|
return {
|
|
1003
1060
|
metadata: {
|
|
@@ -1005,7 +1062,8 @@ function buildResult(startedAt, repositoryRoot, mode, capability, issues, errors
|
|
|
1005
1062
|
completedAt: completedAt.toISOString(),
|
|
1006
1063
|
durationMs: completedAt.getTime() - startedAt.getTime(),
|
|
1007
1064
|
repositoryRoot,
|
|
1008
|
-
executionMode: mode
|
|
1065
|
+
executionMode: mode,
|
|
1066
|
+
...phaseTimings ? { phaseTimings } : {}
|
|
1009
1067
|
},
|
|
1010
1068
|
capability,
|
|
1011
1069
|
issues,
|
|
@@ -1174,11 +1232,7 @@ function formatText(result) {
|
|
|
1174
1232
|
if (result.errors.length > 0) {
|
|
1175
1233
|
lines.push("");
|
|
1176
1234
|
lines.push("Errors:");
|
|
1177
|
-
|
|
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
|
-
}
|
|
1235
|
+
lines.push(...formatErrorSection(result));
|
|
1182
1236
|
lines.push("");
|
|
1183
1237
|
}
|
|
1184
1238
|
lines.push(formatTextSummary(result, isInspect));
|
|
@@ -1277,6 +1331,33 @@ function formatTextSummary(result, isInspect) {
|
|
|
1277
1331
|
lines.push(table.toString());
|
|
1278
1332
|
}
|
|
1279
1333
|
}
|
|
1334
|
+
if (!inspect && result.metadata.phaseTimings) {
|
|
1335
|
+
const pt = result.metadata.phaseTimings;
|
|
1336
|
+
const totalMs = result.metadata.durationMs;
|
|
1337
|
+
const toolSumMs = pt.toolExecutionMs;
|
|
1338
|
+
const overheadMs = totalMs - toolSumMs;
|
|
1339
|
+
if (overheadMs > 2e3 && overheadMs / totalMs > 0.2) {
|
|
1340
|
+
lines.push("");
|
|
1341
|
+
lines.push(import_ansis.default.bold("Pipeline timing:"));
|
|
1342
|
+
const entries2 = [
|
|
1343
|
+
["Availability checks", pt.availabilityMs],
|
|
1344
|
+
["File discovery", pt.fileDiscoveryMs],
|
|
1345
|
+
["File routing & config", pt.fileRoutingMs]
|
|
1346
|
+
];
|
|
1347
|
+
if (pt.installMs > 0) {
|
|
1348
|
+
entries2.push(["Install", pt.installMs]);
|
|
1349
|
+
}
|
|
1350
|
+
entries2.push(["Tool execution", pt.toolExecutionMs]);
|
|
1351
|
+
const accounted = entries2.reduce((sum, [, ms]) => sum + ms, 0);
|
|
1352
|
+
const other = totalMs - accounted;
|
|
1353
|
+
if (other > 500) {
|
|
1354
|
+
entries2.push(["Other", other]);
|
|
1355
|
+
}
|
|
1356
|
+
for (const [label, ms] of entries2) {
|
|
1357
|
+
lines.push(` ${label.padEnd(22)} ${(0, import_tooling4.formatDuration)(ms)}`);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1280
1361
|
const toolsWithHints = result.capability.unavailable.filter((t) => t.remediation);
|
|
1281
1362
|
if (toolsWithHints.length > 0) {
|
|
1282
1363
|
lines.push("");
|
|
@@ -1295,6 +1376,51 @@ function formatTextSummary(result, isInspect) {
|
|
|
1295
1376
|
}
|
|
1296
1377
|
return lines.join("\n");
|
|
1297
1378
|
}
|
|
1379
|
+
var MAX_ERRORS_PER_GROUP = 5;
|
|
1380
|
+
function formatErrorSection(result) {
|
|
1381
|
+
const lines = [];
|
|
1382
|
+
const displayNameMap = /* @__PURE__ */ new Map();
|
|
1383
|
+
for (const t of result.capability.ready) {
|
|
1384
|
+
displayNameMap.set(t.toolId, t.displayName ?? t.toolId);
|
|
1385
|
+
}
|
|
1386
|
+
for (const t of result.capability.unavailable) {
|
|
1387
|
+
displayNameMap.set(t.toolId, t.displayName ?? t.toolId);
|
|
1388
|
+
}
|
|
1389
|
+
const displayName = (toolId) => displayNameMap.get(toolId) ?? toolId;
|
|
1390
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1391
|
+
for (const error of result.errors) {
|
|
1392
|
+
const key = `${displayName(error.toolId)}/${error.phase}`;
|
|
1393
|
+
const list = groups.get(key) ?? [];
|
|
1394
|
+
list.push(error);
|
|
1395
|
+
groups.set(key, list);
|
|
1396
|
+
}
|
|
1397
|
+
for (const [groupKey, groupErrors] of groups) {
|
|
1398
|
+
const deduped = [];
|
|
1399
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1400
|
+
for (const err of groupErrors) {
|
|
1401
|
+
const dedupKey = `${err.filePath ?? ""}|||${err.message}`;
|
|
1402
|
+
const idx = seen.get(dedupKey);
|
|
1403
|
+
if (idx !== void 0) {
|
|
1404
|
+
deduped[idx].count++;
|
|
1405
|
+
} else {
|
|
1406
|
+
seen.set(dedupKey, deduped.length);
|
|
1407
|
+
deduped.push({ filePath: err.filePath, message: err.message, count: 1 });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
const shown = deduped.slice(0, MAX_ERRORS_PER_GROUP);
|
|
1411
|
+
const overflowCount = deduped.length - shown.length;
|
|
1412
|
+
for (const entry of shown) {
|
|
1413
|
+
const prefix = `[${groupKey}]`;
|
|
1414
|
+
const location = entry.filePath ? ` ${entry.filePath}:` : "";
|
|
1415
|
+
const suffix = entry.count > 1 ? ` (x${entry.count})` : "";
|
|
1416
|
+
lines.push(import_ansis.default.red(` ${prefix}${location} ${entry.message}${suffix}`));
|
|
1417
|
+
}
|
|
1418
|
+
if (overflowCount > 0) {
|
|
1419
|
+
lines.push(import_ansis.default.dim(` ... and ${overflowCount} more ${groupKey} errors`));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return lines;
|
|
1423
|
+
}
|
|
1298
1424
|
var MAX_LINE_LENGTH = 100;
|
|
1299
1425
|
var MAX_MULTILINE_DISPLAY = 10;
|
|
1300
1426
|
function formatMultilineIssue(output, issue) {
|
|
@@ -6315,7 +6441,7 @@ function parseAutoFilters(raw) {
|
|
|
6315
6441
|
}
|
|
6316
6442
|
|
|
6317
6443
|
// src/logger.ts
|
|
6318
|
-
var
|
|
6444
|
+
var import_fs2 = __toESM(require("fs"));
|
|
6319
6445
|
var import_path8 = __toESM(require("path"));
|
|
6320
6446
|
var import_os2 = __toESM(require("os"));
|
|
6321
6447
|
var LEVEL_ORDER = {
|
|
@@ -6327,7 +6453,7 @@ var LEVEL_ORDER = {
|
|
|
6327
6453
|
function rotateLogFiles(logsDir, maxFiles) {
|
|
6328
6454
|
let entries2;
|
|
6329
6455
|
try {
|
|
6330
|
-
entries2 =
|
|
6456
|
+
entries2 = import_fs2.default.readdirSync(logsDir).filter((f) => f.startsWith("analysis-") && f.endsWith(".log"));
|
|
6331
6457
|
} catch {
|
|
6332
6458
|
return;
|
|
6333
6459
|
}
|
|
@@ -6336,7 +6462,7 @@ function rotateLogFiles(logsDir, maxFiles) {
|
|
|
6336
6462
|
const toRemove = entries2.slice(0, entries2.length - maxFiles);
|
|
6337
6463
|
for (const f of toRemove) {
|
|
6338
6464
|
try {
|
|
6339
|
-
|
|
6465
|
+
import_fs2.default.unlinkSync(import_path8.default.join(logsDir, f));
|
|
6340
6466
|
} catch {
|
|
6341
6467
|
}
|
|
6342
6468
|
}
|
|
@@ -6378,10 +6504,10 @@ function createLogger(options = {}) {
|
|
|
6378
6504
|
let logFd = null;
|
|
6379
6505
|
let bytesWritten = 0;
|
|
6380
6506
|
if (fileEnabled) {
|
|
6381
|
-
|
|
6507
|
+
import_fs2.default.mkdirSync(logsDir, { recursive: true });
|
|
6382
6508
|
rotateLogFiles(logsDir, maxFiles);
|
|
6383
6509
|
const logFilePath = import_path8.default.join(logsDir, `analysis-${fileTimestamp()}.log`);
|
|
6384
|
-
logFd =
|
|
6510
|
+
logFd = import_fs2.default.openSync(logFilePath, "a");
|
|
6385
6511
|
}
|
|
6386
6512
|
function log(level, message, fields) {
|
|
6387
6513
|
const numLevel = LEVEL_ORDER[level];
|
|
@@ -6394,13 +6520,13 @@ function createLogger(options = {}) {
|
|
|
6394
6520
|
...fields
|
|
6395
6521
|
});
|
|
6396
6522
|
const buf = Buffer.from(entry + "\n", "utf-8");
|
|
6397
|
-
|
|
6523
|
+
import_fs2.default.writeSync(logFd, buf);
|
|
6398
6524
|
bytesWritten += buf.length;
|
|
6399
6525
|
if (bytesWritten >= maxFileSize) {
|
|
6400
|
-
|
|
6526
|
+
import_fs2.default.closeSync(logFd);
|
|
6401
6527
|
rotateLogFiles(logsDir, maxFiles);
|
|
6402
6528
|
const newPath = import_path8.default.join(logsDir, `analysis-${fileTimestamp()}.log`);
|
|
6403
|
-
logFd =
|
|
6529
|
+
logFd = import_fs2.default.openSync(newPath, "a");
|
|
6404
6530
|
bytesWritten = 0;
|
|
6405
6531
|
}
|
|
6406
6532
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codacy/analysis-runner",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
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.
|
|
15
|
+
"@codacy/tooling": "0.8.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@codacy/openapi-typescript-codegen": "0.0.9",
|
|
19
|
-
"@codacy/tools-dummy-1": "0.8.
|
|
19
|
+
"@codacy/tools-dummy-1": "0.8.1"
|
|
20
20
|
},
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "Codacy <support@codacy.com> (https://www.codacy.com)",
|