@syntopica/db-quality 0.2.1 → 0.3.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/README.md +67 -19
- package/dist/cli.js +612 -215
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -546,6 +546,52 @@ var postgrestSectionFrom = (raw) => {
|
|
|
546
546
|
return { roots };
|
|
547
547
|
};
|
|
548
548
|
|
|
549
|
+
// src/config/SQLITE_QUERY_MIN_ROWS.ts
|
|
550
|
+
var SQLITE_QUERY_MIN_ROWS = 1e4;
|
|
551
|
+
|
|
552
|
+
// src/config/sqliteQueriesSectionFrom.ts
|
|
553
|
+
var sqliteQueriesSectionFrom = (raw) => {
|
|
554
|
+
const paths = raw["paths"];
|
|
555
|
+
const database = raw["database"];
|
|
556
|
+
const minRows = raw["minRows"] ?? SQLITE_QUERY_MIN_ROWS;
|
|
557
|
+
expectConfig(
|
|
558
|
+
isStringList(paths),
|
|
559
|
+
"sqlite.queries.paths must be a list of strings"
|
|
560
|
+
);
|
|
561
|
+
expectConfig(
|
|
562
|
+
paths.length > 0,
|
|
563
|
+
"sqlite.queries.paths must name at least one directory"
|
|
564
|
+
);
|
|
565
|
+
expectConfig(
|
|
566
|
+
database === void 0 || typeof database === "string",
|
|
567
|
+
"sqlite.queries.database must be a string"
|
|
568
|
+
);
|
|
569
|
+
expectConfig(
|
|
570
|
+
isPositiveInteger(minRows),
|
|
571
|
+
"sqlite.queries.minRows must be a positive integer"
|
|
572
|
+
);
|
|
573
|
+
return {
|
|
574
|
+
paths,
|
|
575
|
+
...database === void 0 ? {} : { database },
|
|
576
|
+
minRows
|
|
577
|
+
};
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// src/config/sqliteSectionFrom.ts
|
|
581
|
+
var sqliteSectionFrom = (raw) => {
|
|
582
|
+
const files = raw["files"];
|
|
583
|
+
if (!isStringList(files))
|
|
584
|
+
throw new ConfigError("sqlite.files must be a list of strings");
|
|
585
|
+
const queries = raw["queries"];
|
|
586
|
+
if (queries === void 0) return { files };
|
|
587
|
+
if (typeof queries !== "object" || queries === null || Array.isArray(queries))
|
|
588
|
+
throw new ConfigError("sqlite.queries must be an object");
|
|
589
|
+
return {
|
|
590
|
+
files,
|
|
591
|
+
queries: sqliteQueriesSectionFrom(queries)
|
|
592
|
+
};
|
|
593
|
+
};
|
|
594
|
+
|
|
549
595
|
// src/config/stackSectionsFrom.ts
|
|
550
596
|
var stackSectionsFrom = (raw) => {
|
|
551
597
|
const stacks = {};
|
|
@@ -566,12 +612,7 @@ var stackSectionsFrom = (raw) => {
|
|
|
566
612
|
const drizzle = configSection(raw, "drizzle");
|
|
567
613
|
if (drizzle) stacks.drizzle = drizzleSectionFrom(drizzle);
|
|
568
614
|
const sqlite = configSection(raw, "sqlite");
|
|
569
|
-
if (sqlite)
|
|
570
|
-
const files = sqlite["files"];
|
|
571
|
-
if (!isStringList(files))
|
|
572
|
-
throw new ConfigError("sqlite.files must be a list of strings");
|
|
573
|
-
stacks.sqlite = { files };
|
|
574
|
-
}
|
|
615
|
+
if (sqlite) stacks.sqlite = sqliteSectionFrom(sqlite);
|
|
575
616
|
const postgrest = configSection(raw, "postgrest");
|
|
576
617
|
if (postgrest) stacks.postgrest = postgrestSectionFrom(postgrest);
|
|
577
618
|
return stacks;
|
|
@@ -653,8 +694,8 @@ var auditCommand = (argv2, io) => {
|
|
|
653
694
|
};
|
|
654
695
|
|
|
655
696
|
// src/commands/baselineCommand.ts
|
|
656
|
-
import { existsSync as
|
|
657
|
-
import { join as
|
|
697
|
+
import { existsSync as existsSync7 } from "fs";
|
|
698
|
+
import { join as join12 } from "path";
|
|
658
699
|
|
|
659
700
|
// src/baseline/BASELINE_FILENAME.ts
|
|
660
701
|
var BASELINE_FILENAME = ".codeality-db-baseline.json";
|
|
@@ -855,7 +896,7 @@ var sqliteFinding = (input, disabled) => {
|
|
|
855
896
|
code: input.code,
|
|
856
897
|
severity: input.severity,
|
|
857
898
|
path: input.file,
|
|
858
|
-
line: 0,
|
|
899
|
+
line: input.line ?? 0,
|
|
859
900
|
message: input.message,
|
|
860
901
|
subject: input.subject
|
|
861
902
|
};
|
|
@@ -962,9 +1003,434 @@ var runSqliteChecks = (runner, root, files, disabled) => files.flatMap((file) =>
|
|
|
962
1003
|
...primaryKeyFindings(runner, root, file, disabled)
|
|
963
1004
|
]);
|
|
964
1005
|
|
|
1006
|
+
// src/adapters/sqlite/probeQueryDatabase.ts
|
|
1007
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1008
|
+
|
|
1009
|
+
// src/adapters/sqlite/attemptSqlite.ts
|
|
1010
|
+
var attemptSqlite = (call) => {
|
|
1011
|
+
try {
|
|
1012
|
+
return call();
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
if (error instanceof ToolMissingError) throw error;
|
|
1015
|
+
return void 0;
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// src/adapters/sqlite/resolveQueryDatabase.ts
|
|
1020
|
+
import { homedir } from "os";
|
|
1021
|
+
import { join as join6, resolve } from "path";
|
|
1022
|
+
var resolveQueryDatabase = (root, database) => database.startsWith("~/") ? join6(homedir(), database.slice(2)) : resolve(root, database);
|
|
1023
|
+
|
|
1024
|
+
// src/adapters/sqlite/userTables.ts
|
|
1025
|
+
var userTables = (runner, root, database) => new Map(
|
|
1026
|
+
sqliteRows(
|
|
1027
|
+
runner,
|
|
1028
|
+
root,
|
|
1029
|
+
database,
|
|
1030
|
+
"select name from sqlite_master where type = 'table'"
|
|
1031
|
+
).map((row) => [row.name.toLowerCase(), row.name])
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
// src/adapters/sqlite/probeQueryDatabase.ts
|
|
1035
|
+
var probeQueryDatabase = (runner, root, database) => {
|
|
1036
|
+
const path = resolveQueryDatabase(root, database);
|
|
1037
|
+
if (!existsSync5(path)) return { path, unavailable: "does not exist" };
|
|
1038
|
+
const tables = attemptSqlite(() => userTables(runner, root, path));
|
|
1039
|
+
return tables ? { path, tables } : { path, unavailable: "is not a readable SQLite database" };
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
// src/adapters/sqlite/fullScanMessage.ts
|
|
1043
|
+
var fullScanMessage = (scan, rows, sorts) => {
|
|
1044
|
+
const what = scan.kind === "table" ? "full table scan" : "full index scan";
|
|
1045
|
+
const sorted = sorts ? ", then USE TEMP B-TREE FOR ORDER BY" : "";
|
|
1046
|
+
const hint = scan.kind === "table" ? "give the filter an index it can use" : "the whole index is read; filter on its leading column or change the join order";
|
|
1047
|
+
return `${what} of ${scan.table} (${String(rows)} rows) in the plan the application prepares: ${scan.detail}${sorted}; ${hint}`;
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
// src/adapters/sqlite/planDetails.ts
|
|
1051
|
+
var planDetails = (output) => {
|
|
1052
|
+
const text = output.trim();
|
|
1053
|
+
if (text.startsWith("["))
|
|
1054
|
+
return JSON.parse(text).map((row) => row.detail);
|
|
1055
|
+
return text.split("\n").map((line) => line.replace(/^[\s|`-]*/, "").trim()).filter((line) => line !== "" && line !== "QUERY PLAN");
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
// src/adapters/sqlite/queryPlan.ts
|
|
1059
|
+
var queryPlan = (runner, root, database, statement) => planDetails(
|
|
1060
|
+
sqliteQuery(runner, root, database, `EXPLAIN QUERY PLAN ${statement}`)
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
// src/adapters/sqlite/cteNames.ts
|
|
1064
|
+
var cteNames = (text) => new Set(
|
|
1065
|
+
[
|
|
1066
|
+
...text.matchAll(
|
|
1067
|
+
/(?:\bwith\s+(?:recursive\s+)?|,\s*)"?(?<name>\w+)"?(?:\s*\([^()]*\))?\s+as\s*(?:not\s+)?(?:materialized\s*)?\(/gi
|
|
1068
|
+
)
|
|
1069
|
+
].map((match) => (match.groups?.["name"] ?? "").toLowerCase())
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
// src/adapters/sqlite/scanOfPlanRow.ts
|
|
1073
|
+
var scanOfPlanRow = (detail) => {
|
|
1074
|
+
const match = /^SCAN (?:TABLE )?(?<name>[^\s(]\S*)(?: AS \S+)?(?<rest>(?: .*)?)$/.exec(
|
|
1075
|
+
detail
|
|
1076
|
+
);
|
|
1077
|
+
const name = match?.groups?.["name"];
|
|
1078
|
+
const rest = match?.groups?.["rest"] ?? "";
|
|
1079
|
+
if (name === void 0 || detail === "SCAN CONSTANT ROW") return void 0;
|
|
1080
|
+
if (rest.includes("VIRTUAL TABLE")) return void 0;
|
|
1081
|
+
return {
|
|
1082
|
+
name,
|
|
1083
|
+
kind: /\bUSING (?:COVERING )?INDEX\b/.test(rest) ? "index" : "table",
|
|
1084
|
+
detail
|
|
1085
|
+
};
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
// src/adapters/sqlite/ALIAS_KEYWORDS.ts
|
|
1089
|
+
var ALIAS_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1090
|
+
"and",
|
|
1091
|
+
"as",
|
|
1092
|
+
"cross",
|
|
1093
|
+
"default",
|
|
1094
|
+
"except",
|
|
1095
|
+
"from",
|
|
1096
|
+
"full",
|
|
1097
|
+
"group",
|
|
1098
|
+
"having",
|
|
1099
|
+
"indexed",
|
|
1100
|
+
"inner",
|
|
1101
|
+
"intersect",
|
|
1102
|
+
"join",
|
|
1103
|
+
"left",
|
|
1104
|
+
"limit",
|
|
1105
|
+
"natural",
|
|
1106
|
+
"not",
|
|
1107
|
+
"offset",
|
|
1108
|
+
"on",
|
|
1109
|
+
"or",
|
|
1110
|
+
"order",
|
|
1111
|
+
"outer",
|
|
1112
|
+
"returning",
|
|
1113
|
+
"right",
|
|
1114
|
+
"select",
|
|
1115
|
+
"set",
|
|
1116
|
+
"union",
|
|
1117
|
+
"using",
|
|
1118
|
+
"values",
|
|
1119
|
+
"where",
|
|
1120
|
+
"window"
|
|
1121
|
+
]);
|
|
1122
|
+
|
|
1123
|
+
// src/adapters/sqlite/statementTableAliases.ts
|
|
1124
|
+
var statementTableAliases = (text, tables) => {
|
|
1125
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1126
|
+
const pattern = /(?<=\b(?:from|join|update|into)\s+|,\s*)"?(?<table>\w+)"?(?:\s+as)?\s+"?(?<alias>\w+)"?/gi;
|
|
1127
|
+
for (const match of text.matchAll(pattern)) {
|
|
1128
|
+
const table = tables.get(match.groups?.["table"]?.toLowerCase() ?? "");
|
|
1129
|
+
const alias = match.groups?.["alias"]?.toLowerCase() ?? "";
|
|
1130
|
+
if (table !== void 0 && !ALIAS_KEYWORDS.has(alias))
|
|
1131
|
+
aliases.set(alias, table);
|
|
1132
|
+
}
|
|
1133
|
+
return aliases;
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
// src/adapters/sqlite/scannedTables.ts
|
|
1137
|
+
var scannedTables = (statement, details, tables) => {
|
|
1138
|
+
const ctes = cteNames(statement);
|
|
1139
|
+
const aliases = statementTableAliases(statement, tables);
|
|
1140
|
+
return details.flatMap((detail) => {
|
|
1141
|
+
const scan = scanOfPlanRow(detail);
|
|
1142
|
+
if (!scan) return [];
|
|
1143
|
+
const name = scan.name.toLowerCase();
|
|
1144
|
+
if (ctes.has(name)) return [];
|
|
1145
|
+
const table = aliases.get(name) ?? tables.get(name);
|
|
1146
|
+
return table === void 0 ? [] : [{ table, kind: scan.kind, detail: scan.detail }];
|
|
1147
|
+
});
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
// src/adapters/sqlite/tableRowCount.ts
|
|
1151
|
+
var tableRowCount = (runner, root, database, table) => sqliteRows(
|
|
1152
|
+
runner,
|
|
1153
|
+
root,
|
|
1154
|
+
database,
|
|
1155
|
+
`select count(*) as n from "${table.replaceAll('"', '""')}"`
|
|
1156
|
+
)[0]?.n ?? 0;
|
|
1157
|
+
|
|
1158
|
+
// src/adapters/sqlite/queryPlanFindings.ts
|
|
1159
|
+
var queryPlanFindings = (context, files) => {
|
|
1160
|
+
if (isDisabled("BDB406", context.disabled)) return [];
|
|
1161
|
+
const { runner, root, database } = context;
|
|
1162
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1163
|
+
const rowsOf = (table) => {
|
|
1164
|
+
const known = counts.get(table);
|
|
1165
|
+
if (known !== void 0) return known;
|
|
1166
|
+
const rows = attemptSqlite(() => tableRowCount(runner, root, database, table)) ?? 0;
|
|
1167
|
+
counts.set(table, rows);
|
|
1168
|
+
return rows;
|
|
1169
|
+
};
|
|
1170
|
+
return files.flatMap((file) => {
|
|
1171
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1172
|
+
return file.statements.flatMap((statement) => {
|
|
1173
|
+
const details = attemptSqlite(
|
|
1174
|
+
() => queryPlan(runner, root, database, statement.text)
|
|
1175
|
+
);
|
|
1176
|
+
if (!details) return [];
|
|
1177
|
+
const sorts = details.includes("USE TEMP B-TREE FOR ORDER BY");
|
|
1178
|
+
return scannedTables(statement.text, details, context.tables).flatMap(
|
|
1179
|
+
(scan) => {
|
|
1180
|
+
const key = `${scan.table}:${scan.kind}`;
|
|
1181
|
+
const rows = rowsOf(scan.table);
|
|
1182
|
+
if (reported.has(key) || rows < context.minRows) return [];
|
|
1183
|
+
reported.add(key);
|
|
1184
|
+
return sqliteFinding(
|
|
1185
|
+
{
|
|
1186
|
+
code: "BDB406",
|
|
1187
|
+
severity: "warn",
|
|
1188
|
+
file: file.path,
|
|
1189
|
+
line: statement.line,
|
|
1190
|
+
subject: scan.table,
|
|
1191
|
+
message: fullScanMessage(scan, rows, sorts),
|
|
1192
|
+
context: key
|
|
1193
|
+
},
|
|
1194
|
+
context.disabled
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
);
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
// src/adapters/sqlite/readQueryFiles.ts
|
|
1203
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1204
|
+
import { join as join8 } from "path";
|
|
1205
|
+
|
|
1206
|
+
// src/adapters/sqlite/isQueryStatement.ts
|
|
1207
|
+
var isQueryStatement = (statement) => /^(?:select|with|update|delete|insert|replace)\b/i.test(statement.text);
|
|
1208
|
+
|
|
1209
|
+
// src/adapters/sqlite/queryFilesUnder.ts
|
|
1210
|
+
import { readdirSync } from "fs";
|
|
1211
|
+
import { join as join7, relative as relative2 } from "path";
|
|
1212
|
+
|
|
1213
|
+
// src/config/isDirectory.ts
|
|
1214
|
+
import { existsSync as existsSync6, statSync } from "fs";
|
|
1215
|
+
var isDirectory = (path) => existsSync6(path) && statSync(path).isDirectory();
|
|
1216
|
+
|
|
1217
|
+
// src/postgrest/SKIPPED_SOURCE_DIRS.ts
|
|
1218
|
+
var SKIPPED_SOURCE_DIRS = /* @__PURE__ */ new Set([
|
|
1219
|
+
"node_modules",
|
|
1220
|
+
"dist",
|
|
1221
|
+
"build",
|
|
1222
|
+
".next",
|
|
1223
|
+
"coverage",
|
|
1224
|
+
"__tests__",
|
|
1225
|
+
"__mocks__"
|
|
1226
|
+
]);
|
|
1227
|
+
|
|
1228
|
+
// src/adapters/sqlite/queryFilesUnder.ts
|
|
1229
|
+
var queryFilesUnder = (root, paths) => {
|
|
1230
|
+
for (const base of paths) {
|
|
1231
|
+
if (!isDirectory(join7(root, base)))
|
|
1232
|
+
throw new ConfigError(
|
|
1233
|
+
`sqlite.queries.paths: "${base}" is not a directory`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
const found = /* @__PURE__ */ new Set();
|
|
1237
|
+
const walk = (dir) => {
|
|
1238
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1239
|
+
if (entry.name.startsWith(".")) continue;
|
|
1240
|
+
const path = join7(dir, entry.name);
|
|
1241
|
+
if (entry.isDirectory()) {
|
|
1242
|
+
if (!SKIPPED_SOURCE_DIRS.has(entry.name)) walk(path);
|
|
1243
|
+
} else if (entry.isFile() && entry.name.endsWith(".sql")) {
|
|
1244
|
+
found.add(relative2(root, path).replaceAll("\\", "/"));
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
for (const base of paths) walk(join7(root, base));
|
|
1249
|
+
return [...found].sort();
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// src/sql/quotedSpanEnd.ts
|
|
1253
|
+
var quotedSpanEnd = (sql, index) => {
|
|
1254
|
+
const dollar = /^\$[a-z_]*\$/i.exec(sql.slice(index, index + 64));
|
|
1255
|
+
if (dollar) {
|
|
1256
|
+
const end2 = sql.indexOf(dollar[0], index + dollar[0].length);
|
|
1257
|
+
return end2 === -1 ? sql.length : end2 + dollar[0].length;
|
|
1258
|
+
}
|
|
1259
|
+
const quote = sql[index];
|
|
1260
|
+
if (quote !== "'" && quote !== '"') return void 0;
|
|
1261
|
+
const end = sql.indexOf(quote, index + 1);
|
|
1262
|
+
return end === -1 ? sql.length : end + 1;
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// src/sql/stripSqlComments.ts
|
|
1266
|
+
var stripSqlComments = (sql) => {
|
|
1267
|
+
let out = "";
|
|
1268
|
+
let index = 0;
|
|
1269
|
+
while (index < sql.length) {
|
|
1270
|
+
const quoted = quotedSpanEnd(sql, index);
|
|
1271
|
+
if (quoted !== void 0) {
|
|
1272
|
+
out += sql.slice(index, quoted);
|
|
1273
|
+
index = quoted;
|
|
1274
|
+
} else if (sql.startsWith("--", index)) {
|
|
1275
|
+
const end = sql.indexOf("\n", index);
|
|
1276
|
+
index = end === -1 ? sql.length : end;
|
|
1277
|
+
} else if (sql.startsWith("/*", index)) {
|
|
1278
|
+
const end = sql.indexOf("*/", index + 2);
|
|
1279
|
+
const stop = end === -1 ? sql.length : end + 2;
|
|
1280
|
+
out += sql.slice(index, stop).replaceAll(/[^\n]/g, "");
|
|
1281
|
+
index = stop;
|
|
1282
|
+
} else {
|
|
1283
|
+
out += sql.charAt(index);
|
|
1284
|
+
index += 1;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return out;
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// src/sql/splitSqlStatements.ts
|
|
1291
|
+
var splitSqlStatements = (sql) => {
|
|
1292
|
+
const text = stripSqlComments(sql);
|
|
1293
|
+
const statements = [];
|
|
1294
|
+
let start = 0;
|
|
1295
|
+
const flush = (end) => {
|
|
1296
|
+
const raw = text.slice(start, end);
|
|
1297
|
+
const leading = raw.length - raw.trimStart().length;
|
|
1298
|
+
const body = raw.trim();
|
|
1299
|
+
if (body) {
|
|
1300
|
+
const line = 1 + (text.slice(0, start + leading).match(/\n/g)?.length ?? 0);
|
|
1301
|
+
statements.push({ text: body, line });
|
|
1302
|
+
}
|
|
1303
|
+
start = end + 1;
|
|
1304
|
+
};
|
|
1305
|
+
let index = 0;
|
|
1306
|
+
while (index < text.length) {
|
|
1307
|
+
const quoted = quotedSpanEnd(text, index);
|
|
1308
|
+
if (quoted !== void 0) index = quoted;
|
|
1309
|
+
else {
|
|
1310
|
+
if (text[index] === ";") flush(index);
|
|
1311
|
+
index += 1;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
flush(text.length);
|
|
1315
|
+
return statements;
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
// src/adapters/sqlite/readQueryFiles.ts
|
|
1319
|
+
var readQueryFiles = (root, paths) => queryFilesUnder(root, paths).map((path) => ({
|
|
1320
|
+
path,
|
|
1321
|
+
statements: splitSqlStatements(
|
|
1322
|
+
readFileSync4(join8(root, path), "utf8")
|
|
1323
|
+
).filter(isQueryStatement)
|
|
1324
|
+
}));
|
|
1325
|
+
|
|
1326
|
+
// src/adapters/sqlite/statementAtMatch.ts
|
|
1327
|
+
var statementAtMatch = (statement, index) => ({
|
|
1328
|
+
text: statement.text,
|
|
1329
|
+
line: statement.line + (statement.text.slice(0, index).match(/\n/g)?.length ?? 0)
|
|
1330
|
+
});
|
|
1331
|
+
|
|
965
1332
|
// src/model/normalizeSqlText.ts
|
|
966
1333
|
var normalizeSqlText = (text) => text.toLowerCase().replaceAll(/\s+/g, " ").trim();
|
|
967
1334
|
|
|
1335
|
+
// src/rules/makeSqlFinding.ts
|
|
1336
|
+
var makeSqlFinding = (rule, { file, statement }, subject, message) => {
|
|
1337
|
+
const partial = {
|
|
1338
|
+
code: rule.code,
|
|
1339
|
+
severity: rule.severity,
|
|
1340
|
+
path: file.path,
|
|
1341
|
+
line: statement.line,
|
|
1342
|
+
message,
|
|
1343
|
+
subject
|
|
1344
|
+
};
|
|
1345
|
+
return {
|
|
1346
|
+
...partial,
|
|
1347
|
+
fingerprint: fingerprintFinding(partial, normalizeSqlText(statement.text))
|
|
1348
|
+
};
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
// src/adapters/sqlite/commaListMembership.ts
|
|
1352
|
+
var commaListMembership = {
|
|
1353
|
+
code: "BDB405",
|
|
1354
|
+
name: "comma-list-membership",
|
|
1355
|
+
severity: "warn",
|
|
1356
|
+
// instr(',' || ?1 || ',', ',' || col || ','): membership in a comma-joined
|
|
1357
|
+
// list, which no index can answer.
|
|
1358
|
+
run: (set) => set.flatMap(
|
|
1359
|
+
(file) => file.statements.flatMap((statement) => {
|
|
1360
|
+
const match = /\binstr\s*\(\s*','\s*\|\|\s*(?<param>\?\d*|[:@$][a-z_]\w*)\s*\|\|\s*','/i.exec(
|
|
1361
|
+
statement.text
|
|
1362
|
+
);
|
|
1363
|
+
if (!match) return [];
|
|
1364
|
+
const param = match.groups?.["param"] ?? "";
|
|
1365
|
+
return [
|
|
1366
|
+
makeSqlFinding(
|
|
1367
|
+
commaListMembership,
|
|
1368
|
+
{ file, statement: statementAtMatch(statement, match.index) },
|
|
1369
|
+
param,
|
|
1370
|
+
`instr() over a comma-joined ${param} cannot use an index; bind a JSON array and filter with col IN (SELECT value FROM json_each(${param}))`
|
|
1371
|
+
)
|
|
1372
|
+
];
|
|
1373
|
+
})
|
|
1374
|
+
)
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
// src/adapters/sqlite/optionalParameterGuard.ts
|
|
1378
|
+
var optionalParameterGuard = {
|
|
1379
|
+
code: "BDB404",
|
|
1380
|
+
name: "optional-parameter-guard",
|
|
1381
|
+
severity: "warn",
|
|
1382
|
+
// `?1 IS NULL OR col = ?1` and `col = ?1 OR ?1 IS NULL`, for every parameter
|
|
1383
|
+
// spelling SQLite binds: ?, ?NNN, :name, @name and $name.
|
|
1384
|
+
run: (set) => set.flatMap(
|
|
1385
|
+
(file) => file.statements.flatMap((statement) => {
|
|
1386
|
+
const match = /(?<param>\?\d*|[:@$][a-z_]\w*)\s+is\s+null\s+or\b|\bor\s+(?<trailing>\?\d*|[:@$][a-z_]\w*)\s+is\s+null\b/i.exec(
|
|
1387
|
+
statement.text
|
|
1388
|
+
);
|
|
1389
|
+
if (!match) return [];
|
|
1390
|
+
const param = match.groups?.["param"] ?? match.groups?.["trailing"] ?? "";
|
|
1391
|
+
return [
|
|
1392
|
+
makeSqlFinding(
|
|
1393
|
+
optionalParameterGuard,
|
|
1394
|
+
{ file, statement: statementAtMatch(statement, match.index) },
|
|
1395
|
+
param,
|
|
1396
|
+
`"${param} IS NULL OR" hides the index from the planner: SQLite picks the plan at prepare time, before the value is bound; split the query per case, or bind a non-null sentinel or a json_each list`
|
|
1397
|
+
)
|
|
1398
|
+
];
|
|
1399
|
+
})
|
|
1400
|
+
)
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
// src/adapters/sqlite/SQLITE_QUERY_RULES.ts
|
|
1404
|
+
var SQLITE_QUERY_RULES = [
|
|
1405
|
+
optionalParameterGuard,
|
|
1406
|
+
commaListMembership
|
|
1407
|
+
];
|
|
1408
|
+
|
|
1409
|
+
// src/adapters/sqlite/runSqliteQueryChecks.ts
|
|
1410
|
+
var runSqliteQueryChecks = (runner, root, queries, disabled) => {
|
|
1411
|
+
const files = readQueryFiles(root, queries.paths);
|
|
1412
|
+
const findings = SQLITE_QUERY_RULES.filter(
|
|
1413
|
+
(rule) => !isDisabled(rule.code, disabled)
|
|
1414
|
+
).flatMap((rule) => rule.run(files));
|
|
1415
|
+
if (queries.database === void 0) return findings;
|
|
1416
|
+
const probe = probeQueryDatabase(runner, root, queries.database);
|
|
1417
|
+
if ("unavailable" in probe) return findings;
|
|
1418
|
+
return [
|
|
1419
|
+
...findings,
|
|
1420
|
+
...queryPlanFindings(
|
|
1421
|
+
{
|
|
1422
|
+
runner,
|
|
1423
|
+
root,
|
|
1424
|
+
database: probe.path,
|
|
1425
|
+
tables: probe.tables,
|
|
1426
|
+
minRows: queries.minRows,
|
|
1427
|
+
disabled
|
|
1428
|
+
},
|
|
1429
|
+
files
|
|
1430
|
+
)
|
|
1431
|
+
];
|
|
1432
|
+
};
|
|
1433
|
+
|
|
968
1434
|
// src/sql/statementAtLine.ts
|
|
969
1435
|
var statementAtLine = (file, line) => [...file.statements].reverse().find((statement) => statement.line <= line);
|
|
970
1436
|
|
|
@@ -1027,8 +1493,8 @@ var runSquawk = (runner, root, set, disabled) => {
|
|
|
1027
1493
|
};
|
|
1028
1494
|
|
|
1029
1495
|
// src/postgrest/runPostgrestRules.ts
|
|
1030
|
-
import { readFileSync as
|
|
1031
|
-
import { join as
|
|
1496
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1497
|
+
import { join as join10 } from "path";
|
|
1032
1498
|
|
|
1033
1499
|
// src/postgrest/ITERATION_METHODS.ts
|
|
1034
1500
|
var ITERATION_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -1307,45 +1773,28 @@ var POSTGREST_RULES = [
|
|
|
1307
1773
|
];
|
|
1308
1774
|
|
|
1309
1775
|
// src/postgrest/sourceFilesUnder.ts
|
|
1310
|
-
import { readdirSync } from "fs";
|
|
1311
|
-
import { join as
|
|
1312
|
-
|
|
1313
|
-
// src/config/isDirectory.ts
|
|
1314
|
-
import { existsSync as existsSync5, statSync } from "fs";
|
|
1315
|
-
var isDirectory = (path) => existsSync5(path) && statSync(path).isDirectory();
|
|
1316
|
-
|
|
1317
|
-
// src/postgrest/SKIPPED_SOURCE_DIRS.ts
|
|
1318
|
-
var SKIPPED_SOURCE_DIRS = /* @__PURE__ */ new Set([
|
|
1319
|
-
"node_modules",
|
|
1320
|
-
"dist",
|
|
1321
|
-
"build",
|
|
1322
|
-
".next",
|
|
1323
|
-
"coverage",
|
|
1324
|
-
"__tests__",
|
|
1325
|
-
"__mocks__"
|
|
1326
|
-
]);
|
|
1327
|
-
|
|
1328
|
-
// src/postgrest/sourceFilesUnder.ts
|
|
1776
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
1777
|
+
import { join as join9, relative as relative3 } from "path";
|
|
1329
1778
|
var sourceFilesUnder = (root, roots) => {
|
|
1330
1779
|
for (const base of roots) {
|
|
1331
|
-
if (!isDirectory(
|
|
1780
|
+
if (!isDirectory(join9(root, base)))
|
|
1332
1781
|
throw new ConfigError(`postgrest.roots: "${base}" is not a directory`);
|
|
1333
1782
|
}
|
|
1334
1783
|
const files = [];
|
|
1335
1784
|
const walk = (dir) => {
|
|
1336
|
-
for (const entry of
|
|
1785
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1337
1786
|
if (entry.isDirectory()) {
|
|
1338
1787
|
if (!SKIPPED_SOURCE_DIRS.has(entry.name) && !entry.name.startsWith("."))
|
|
1339
|
-
walk(
|
|
1788
|
+
walk(join9(dir, entry.name));
|
|
1340
1789
|
continue;
|
|
1341
1790
|
}
|
|
1342
1791
|
const name = entry.name;
|
|
1343
1792
|
if (!/\.tsx?$/.test(name) || name.endsWith(".d.ts") || /\.(?:test|spec)\.tsx?$/.test(name))
|
|
1344
1793
|
continue;
|
|
1345
|
-
files.push(
|
|
1794
|
+
files.push(relative3(root, join9(dir, name)));
|
|
1346
1795
|
}
|
|
1347
1796
|
};
|
|
1348
|
-
for (const base of roots) walk(
|
|
1797
|
+
for (const base of roots) walk(join9(root, base));
|
|
1349
1798
|
return files.sort();
|
|
1350
1799
|
};
|
|
1351
1800
|
|
|
@@ -1355,12 +1804,12 @@ var runPostgrestRules = (root, roots, knowledge, disabled) => {
|
|
|
1355
1804
|
(rule) => !isDisabled(rule.code, disabled)
|
|
1356
1805
|
);
|
|
1357
1806
|
const context = { knowledge };
|
|
1358
|
-
const compiler = loadTypeScript([
|
|
1807
|
+
const compiler = loadTypeScript([join10(root, "package.json"), import.meta.url]);
|
|
1359
1808
|
return sourceFilesUnder(root, roots).flatMap(
|
|
1360
1809
|
(path) => collectPostgrestChains(
|
|
1361
1810
|
compiler,
|
|
1362
1811
|
path,
|
|
1363
|
-
|
|
1812
|
+
readFileSync5(join10(root, path), "utf8")
|
|
1364
1813
|
)
|
|
1365
1814
|
).flatMap((chain) => rules.map((rule) => rule.run(chain, context))).filter((finding) => finding !== void 0).sort(compareFindings);
|
|
1366
1815
|
};
|
|
@@ -1491,22 +1940,6 @@ var indexedColumns = (set) => {
|
|
|
1491
1940
|
return knowledge;
|
|
1492
1941
|
};
|
|
1493
1942
|
|
|
1494
|
-
// src/rules/makeSqlFinding.ts
|
|
1495
|
-
var makeSqlFinding = (rule, { file, statement }, subject, message) => {
|
|
1496
|
-
const partial = {
|
|
1497
|
-
code: rule.code,
|
|
1498
|
-
severity: rule.severity,
|
|
1499
|
-
path: file.path,
|
|
1500
|
-
line: statement.line,
|
|
1501
|
-
message,
|
|
1502
|
-
subject
|
|
1503
|
-
};
|
|
1504
|
-
return {
|
|
1505
|
-
...partial,
|
|
1506
|
-
fingerprint: fingerprintFinding(partial, normalizeSqlText(statement.text))
|
|
1507
|
-
};
|
|
1508
|
-
};
|
|
1509
|
-
|
|
1510
1943
|
// src/rules/policyTable.ts
|
|
1511
1944
|
var policyTable = (statement) => {
|
|
1512
1945
|
const match = /^create policy .+? on ((?:"[^"]+"|[\w$]+)(?:\.(?:"[^"]+"|[\w$]+))?)/.exec(
|
|
@@ -1647,80 +2080,12 @@ var SQL_RULES = [
|
|
|
1647
2080
|
var runSqlRules = (set, disabled) => SQL_RULES.filter((rule) => !isDisabled(rule.code, disabled)).flatMap((rule) => rule.run(set)).sort(compareFindings);
|
|
1648
2081
|
|
|
1649
2082
|
// src/sql/readMigrationSet.ts
|
|
1650
|
-
import { readdirSync as
|
|
1651
|
-
import { join as
|
|
1652
|
-
|
|
1653
|
-
// src/sql/quotedSpanEnd.ts
|
|
1654
|
-
var quotedSpanEnd = (sql, index) => {
|
|
1655
|
-
const dollar = /^\$[a-z_]*\$/i.exec(sql.slice(index, index + 64));
|
|
1656
|
-
if (dollar) {
|
|
1657
|
-
const end2 = sql.indexOf(dollar[0], index + dollar[0].length);
|
|
1658
|
-
return end2 === -1 ? sql.length : end2 + dollar[0].length;
|
|
1659
|
-
}
|
|
1660
|
-
const quote = sql[index];
|
|
1661
|
-
if (quote !== "'" && quote !== '"') return void 0;
|
|
1662
|
-
const end = sql.indexOf(quote, index + 1);
|
|
1663
|
-
return end === -1 ? sql.length : end + 1;
|
|
1664
|
-
};
|
|
1665
|
-
|
|
1666
|
-
// src/sql/stripSqlComments.ts
|
|
1667
|
-
var stripSqlComments = (sql) => {
|
|
1668
|
-
let out = "";
|
|
1669
|
-
let index = 0;
|
|
1670
|
-
while (index < sql.length) {
|
|
1671
|
-
const quoted = quotedSpanEnd(sql, index);
|
|
1672
|
-
if (quoted !== void 0) {
|
|
1673
|
-
out += sql.slice(index, quoted);
|
|
1674
|
-
index = quoted;
|
|
1675
|
-
} else if (sql.startsWith("--", index)) {
|
|
1676
|
-
const end = sql.indexOf("\n", index);
|
|
1677
|
-
index = end === -1 ? sql.length : end;
|
|
1678
|
-
} else if (sql.startsWith("/*", index)) {
|
|
1679
|
-
const end = sql.indexOf("*/", index + 2);
|
|
1680
|
-
const stop = end === -1 ? sql.length : end + 2;
|
|
1681
|
-
out += sql.slice(index, stop).replaceAll(/[^\n]/g, "");
|
|
1682
|
-
index = stop;
|
|
1683
|
-
} else {
|
|
1684
|
-
out += sql.charAt(index);
|
|
1685
|
-
index += 1;
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
return out;
|
|
1689
|
-
};
|
|
1690
|
-
|
|
1691
|
-
// src/sql/splitSqlStatements.ts
|
|
1692
|
-
var splitSqlStatements = (sql) => {
|
|
1693
|
-
const text = stripSqlComments(sql);
|
|
1694
|
-
const statements = [];
|
|
1695
|
-
let start = 0;
|
|
1696
|
-
const flush = (end) => {
|
|
1697
|
-
const raw = text.slice(start, end);
|
|
1698
|
-
const leading = raw.length - raw.trimStart().length;
|
|
1699
|
-
const body = raw.trim();
|
|
1700
|
-
if (body) {
|
|
1701
|
-
const line = 1 + (text.slice(0, start + leading).match(/\n/g)?.length ?? 0);
|
|
1702
|
-
statements.push({ text: body, line });
|
|
1703
|
-
}
|
|
1704
|
-
start = end + 1;
|
|
1705
|
-
};
|
|
1706
|
-
let index = 0;
|
|
1707
|
-
while (index < text.length) {
|
|
1708
|
-
const quoted = quotedSpanEnd(text, index);
|
|
1709
|
-
if (quoted !== void 0) index = quoted;
|
|
1710
|
-
else {
|
|
1711
|
-
if (text[index] === ";") flush(index);
|
|
1712
|
-
index += 1;
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
flush(text.length);
|
|
1716
|
-
return statements;
|
|
1717
|
-
};
|
|
1718
|
-
|
|
1719
|
-
// src/sql/readMigrationSet.ts
|
|
1720
|
-
var readMigrationSet = (root, migrationsDir) => readdirSync2(join8(root, migrationsDir)).filter((name) => name.endsWith(".sql")).sort().map((name) => ({
|
|
2083
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
2084
|
+
import { join as join11 } from "path";
|
|
2085
|
+
var readMigrationSet = (root, migrationsDir) => readdirSync3(join11(root, migrationsDir)).filter((name) => name.endsWith(".sql")).sort().map((name) => ({
|
|
1721
2086
|
path: `${migrationsDir.replaceAll("\\", "/")}/${name}`,
|
|
1722
2087
|
statements: splitSqlStatements(
|
|
1723
|
-
|
|
2088
|
+
readFileSync6(join11(root, migrationsDir, name), "utf8")
|
|
1724
2089
|
)
|
|
1725
2090
|
}));
|
|
1726
2091
|
|
|
@@ -1747,6 +2112,15 @@ var runCheck = ({ root, config, runner }) => {
|
|
|
1747
2112
|
findings.push(
|
|
1748
2113
|
...runSqliteChecks(runner, root, config.sqlite.files, config.disable)
|
|
1749
2114
|
);
|
|
2115
|
+
if (config.sqlite.queries)
|
|
2116
|
+
findings.push(
|
|
2117
|
+
...runSqliteQueryChecks(
|
|
2118
|
+
runner,
|
|
2119
|
+
root,
|
|
2120
|
+
config.sqlite.queries,
|
|
2121
|
+
config.disable
|
|
2122
|
+
)
|
|
2123
|
+
);
|
|
1750
2124
|
}
|
|
1751
2125
|
if (config.postgrest) {
|
|
1752
2126
|
findings.push(
|
|
@@ -1761,6 +2135,20 @@ var runCheck = ({ root, config, runner }) => {
|
|
|
1761
2135
|
return findings.sort(compareFindings);
|
|
1762
2136
|
};
|
|
1763
2137
|
|
|
2138
|
+
// src/check/sqliteQueriesNotice.ts
|
|
2139
|
+
var sqliteQueriesNotice = (runner, root, config) => {
|
|
2140
|
+
const database = config.sqlite?.queries?.database;
|
|
2141
|
+
if (database === void 0 || isDisabled("BDB406", config.disable))
|
|
2142
|
+
return void 0;
|
|
2143
|
+
try {
|
|
2144
|
+
const probe = probeQueryDatabase(runner, root, database);
|
|
2145
|
+
return "unavailable" in probe ? `sqlite.queries.database ${probe.path} ${probe.unavailable}; the query-plan check BDB406 was skipped` : void 0;
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
if (error instanceof ToolMissingError) return void 0;
|
|
2148
|
+
throw error;
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
|
|
1764
2152
|
// src/packageVersion.ts
|
|
1765
2153
|
import { createRequire as createRequire2 } from "module";
|
|
1766
2154
|
var PACKAGE_VERSION = createRequire2(import.meta.url)("../package.json").version;
|
|
@@ -1772,7 +2160,7 @@ var baselineCommand = (argv2, io) => {
|
|
|
1772
2160
|
"check-stale": { type: "boolean" }
|
|
1773
2161
|
});
|
|
1774
2162
|
const action = baselineActionFrom(positionals);
|
|
1775
|
-
if (action === "create" &&
|
|
2163
|
+
if (action === "create" && existsSync7(join12(io.root, BASELINE_FILENAME))) {
|
|
1776
2164
|
throw new ConfigError(
|
|
1777
2165
|
`${BASELINE_FILENAME} exists; use "baseline update" to rewrite it`
|
|
1778
2166
|
);
|
|
@@ -1780,6 +2168,9 @@ var baselineCommand = (argv2, io) => {
|
|
|
1780
2168
|
const config = readConfig(io.root);
|
|
1781
2169
|
const notice = legacyConfigNotice(config);
|
|
1782
2170
|
if (notice) io.stderr(`${notice}
|
|
2171
|
+
`);
|
|
2172
|
+
const skipped = sqliteQueriesNotice(io.runner, io.root, config);
|
|
2173
|
+
if (skipped) io.stderr(`${skipped}
|
|
1783
2174
|
`);
|
|
1784
2175
|
const findings = runCheck({
|
|
1785
2176
|
root: io.root,
|
|
@@ -1816,6 +2207,9 @@ var checkCommand = (argv2, io) => {
|
|
|
1816
2207
|
const config = readConfig(io.root);
|
|
1817
2208
|
const notice = legacyConfigNotice(config);
|
|
1818
2209
|
if (notice) io.stderr(`${notice}
|
|
2210
|
+
`);
|
|
2211
|
+
const skipped = sqliteQueriesNotice(io.runner, io.root, config);
|
|
2212
|
+
if (skipped) io.stderr(`${skipped}
|
|
1819
2213
|
`);
|
|
1820
2214
|
const findings = runCheck({
|
|
1821
2215
|
root: io.root,
|
|
@@ -1842,24 +2236,24 @@ var gateExitCode = (results) => {
|
|
|
1842
2236
|
};
|
|
1843
2237
|
|
|
1844
2238
|
// src/gate/gateStages.ts
|
|
1845
|
-
import { existsSync as
|
|
1846
|
-
import { join as
|
|
2239
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2240
|
+
import { join as join18 } from "path";
|
|
1847
2241
|
|
|
1848
2242
|
// src/gate/perfStage.ts
|
|
1849
|
-
import { existsSync as
|
|
1850
|
-
import { join as
|
|
2243
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2244
|
+
import { join as join17 } from "path";
|
|
1851
2245
|
|
|
1852
2246
|
// src/bench/BENCH_RECORD_FILENAME.ts
|
|
1853
2247
|
var BENCH_RECORD_FILENAME = ".codeality-db-bench.json";
|
|
1854
2248
|
|
|
1855
2249
|
// src/bench/readBenchRecord.ts
|
|
1856
|
-
import { existsSync as
|
|
1857
|
-
import { join as
|
|
2250
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
2251
|
+
import { join as join13 } from "path";
|
|
1858
2252
|
var readBenchRecord = (root) => {
|
|
1859
|
-
const path =
|
|
1860
|
-
if (!
|
|
2253
|
+
const path = join13(root, BENCH_RECORD_FILENAME);
|
|
2254
|
+
if (!existsSync8(path)) return void 0;
|
|
1861
2255
|
const document = JSON.parse(
|
|
1862
|
-
|
|
2256
|
+
readFileSync7(path, "utf8")
|
|
1863
2257
|
);
|
|
1864
2258
|
if (document.schemaVersion !== 1)
|
|
1865
2259
|
throw new ConfigError(
|
|
@@ -1926,18 +2320,18 @@ var benchFindings = (input, recorded, current, judgement) => {
|
|
|
1926
2320
|
};
|
|
1927
2321
|
|
|
1928
2322
|
// src/bench/readBenchQueries.ts
|
|
1929
|
-
import { readdirSync as
|
|
1930
|
-
import { join as
|
|
2323
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
|
|
2324
|
+
import { join as join14 } from "path";
|
|
1931
2325
|
|
|
1932
2326
|
// src/bench/RUNS_HEADER.ts
|
|
1933
2327
|
var RUNS_HEADER = /^--\s*runs:\s*(\S+)/;
|
|
1934
2328
|
|
|
1935
2329
|
// src/bench/readBenchQueries.ts
|
|
1936
2330
|
var readBenchQueries = (root, benchDir, defaultRuns) => {
|
|
1937
|
-
const dir =
|
|
2331
|
+
const dir = join14(root, benchDir);
|
|
1938
2332
|
if (!isDirectory(dir)) throw new ConfigError(`${benchDir} is not a directory`);
|
|
1939
|
-
return
|
|
1940
|
-
const raw =
|
|
2333
|
+
return readdirSync4(dir).filter((name) => name.endsWith(".sql")).sort().map((name) => {
|
|
2334
|
+
const raw = readFileSync8(join14(dir, name), "utf8");
|
|
1941
2335
|
const match = RUNS_HEADER.exec(raw);
|
|
1942
2336
|
const runs = match?.[1] ? Number(match[1]) : defaultRuns;
|
|
1943
2337
|
if (!isPositiveInteger(runs))
|
|
@@ -2336,22 +2730,22 @@ var diffSnapshots = (previous, current, perf, disabled) => {
|
|
|
2336
2730
|
var QUERY_TIMEOUT_MS = 3e4;
|
|
2337
2731
|
|
|
2338
2732
|
// src/perf/readPerfSnapshot.ts
|
|
2339
|
-
import { existsSync as
|
|
2340
|
-
import { join as
|
|
2733
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
2734
|
+
import { join as join15 } from "path";
|
|
2341
2735
|
|
|
2342
2736
|
// src/perf/PERF_SNAPSHOT_FILENAME.ts
|
|
2343
2737
|
var PERF_SNAPSHOT_FILENAME = ".codeality-db-perf.json";
|
|
2344
2738
|
|
|
2345
2739
|
// src/perf/readPerfSnapshot.ts
|
|
2346
2740
|
var readPerfSnapshot = (root) => {
|
|
2347
|
-
const path =
|
|
2348
|
-
if (!
|
|
2741
|
+
const path = join15(root, PERF_SNAPSHOT_FILENAME);
|
|
2742
|
+
if (!existsSync9(path)) {
|
|
2349
2743
|
throw new ConfigError(
|
|
2350
2744
|
`${PERF_SNAPSHOT_FILENAME} not found; run "codeality-db perf snapshot"`
|
|
2351
2745
|
);
|
|
2352
2746
|
}
|
|
2353
2747
|
const document = JSON.parse(
|
|
2354
|
-
|
|
2748
|
+
readFileSync9(path, "utf8")
|
|
2355
2749
|
);
|
|
2356
2750
|
if (document.schemaVersion !== 1) {
|
|
2357
2751
|
throw new ConfigError(
|
|
@@ -2472,8 +2866,8 @@ var perfDiffFindings = ({ root, config, runner }, target) => {
|
|
|
2472
2866
|
};
|
|
2473
2867
|
|
|
2474
2868
|
// src/postgres/resolvePostgresTarget.ts
|
|
2475
|
-
import { existsSync as
|
|
2476
|
-
import { join as
|
|
2869
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
2870
|
+
import { join as join16 } from "path";
|
|
2477
2871
|
import { env } from "process";
|
|
2478
2872
|
|
|
2479
2873
|
// src/postgres/DB_PASSWORD_ENV.ts
|
|
@@ -2530,12 +2924,12 @@ var targetFromUrl = (raw, source, fallbackPassword) => {
|
|
|
2530
2924
|
var resolvePostgresTarget = (root, flags) => {
|
|
2531
2925
|
const explicit = flags["db-url"];
|
|
2532
2926
|
if (explicit !== void 0) return targetFromUrl(explicit, "--db-url");
|
|
2533
|
-
const poolerPath =
|
|
2927
|
+
const poolerPath = join16(root, POOLER_URL_FILE);
|
|
2534
2928
|
const password = env[DB_PASSWORD_ENV];
|
|
2535
|
-
if (!
|
|
2929
|
+
if (!existsSync10(poolerPath) || password === void 0 || password === "")
|
|
2536
2930
|
return void 0;
|
|
2537
2931
|
return targetFromUrl(
|
|
2538
|
-
|
|
2932
|
+
readFileSync10(poolerPath, "utf8").trim(),
|
|
2539
2933
|
POOLER_URL_FILE,
|
|
2540
2934
|
password
|
|
2541
2935
|
);
|
|
@@ -2551,8 +2945,8 @@ var perfStage = (context) => ({
|
|
|
2551
2945
|
return {
|
|
2552
2946
|
skipped: "no linked project with SUPABASE_DB_PASSWORD"
|
|
2553
2947
|
};
|
|
2554
|
-
const hasSnapshot =
|
|
2555
|
-
const hasRecord =
|
|
2948
|
+
const hasSnapshot = existsSync11(join17(context.root, PERF_SNAPSHOT_FILENAME));
|
|
2949
|
+
const hasRecord = existsSync11(join17(context.root, BENCH_RECORD_FILENAME));
|
|
2556
2950
|
if (!hasSnapshot && !hasRecord)
|
|
2557
2951
|
return {
|
|
2558
2952
|
skipped: 'no perf snapshot and no bench record; run "perf snapshot" or "perf bench --record"'
|
|
@@ -2566,8 +2960,8 @@ var perfStage = (context) => ({
|
|
|
2566
2960
|
|
|
2567
2961
|
// src/gate/gateStages.ts
|
|
2568
2962
|
var gateStages = (context) => {
|
|
2569
|
-
const hasBaseline =
|
|
2570
|
-
const linked =
|
|
2963
|
+
const hasBaseline = existsSync12(join18(context.root, BASELINE_FILENAME));
|
|
2964
|
+
const linked = existsSync12(join18(context.root, "supabase/.temp/project-ref"));
|
|
2571
2965
|
return [
|
|
2572
2966
|
hasBaseline ? {
|
|
2573
2967
|
name: "baseline-check",
|
|
@@ -2621,6 +3015,9 @@ var gateCommand = (argv2, io) => {
|
|
|
2621
3015
|
const config = readConfig(io.root);
|
|
2622
3016
|
const notice = legacyConfigNotice(config);
|
|
2623
3017
|
if (notice) io.stderr(`${notice}
|
|
3018
|
+
`);
|
|
3019
|
+
const skipped = sqliteQueriesNotice(io.runner, io.root, config);
|
|
3020
|
+
if (skipped) io.stderr(`${skipped}
|
|
2624
3021
|
`);
|
|
2625
3022
|
const results = runGate(
|
|
2626
3023
|
gateStages({
|
|
@@ -2639,12 +3036,12 @@ var gateCommand = (argv2, io) => {
|
|
|
2639
3036
|
};
|
|
2640
3037
|
|
|
2641
3038
|
// src/init/adoptionPhase.ts
|
|
2642
|
-
import { existsSync as
|
|
2643
|
-
import { join as
|
|
3039
|
+
import { existsSync as existsSync13 } from "fs";
|
|
3040
|
+
import { join as join19 } from "path";
|
|
2644
3041
|
var adoptionPhase = (root, config) => {
|
|
2645
3042
|
if (config.schemaVersion !== 2) return 0;
|
|
2646
|
-
const hasSnapshot =
|
|
2647
|
-
const hasBench =
|
|
3043
|
+
const hasSnapshot = existsSync13(join19(root, PERF_SNAPSHOT_FILENAME));
|
|
3044
|
+
const hasBench = existsSync13(join19(root, BENCH_RECORD_FILENAME));
|
|
2648
3045
|
if (config.perf.inGate && (hasSnapshot || hasBench)) return 4;
|
|
2649
3046
|
if (hasBench) return 3;
|
|
2650
3047
|
if (hasSnapshot) return 2;
|
|
@@ -2652,12 +3049,12 @@ var adoptionPhase = (root, config) => {
|
|
|
2652
3049
|
};
|
|
2653
3050
|
|
|
2654
3051
|
// src/init/baselineStep.ts
|
|
2655
|
-
import { existsSync as
|
|
2656
|
-
import { join as
|
|
3052
|
+
import { existsSync as existsSync14 } from "fs";
|
|
3053
|
+
import { join as join20 } from "path";
|
|
2657
3054
|
var baselineStep = (context) => {
|
|
2658
3055
|
const findings = runCheck(context);
|
|
2659
3056
|
if (findings.length === 0) return "covered";
|
|
2660
|
-
if (!
|
|
3057
|
+
if (!existsSync14(join20(context.root, BASELINE_FILENAME))) return "create";
|
|
2661
3058
|
const classified = classifyFindings(findings, readBaseline(context.root));
|
|
2662
3059
|
return classified.new.length > 0 ? "update" : "covered";
|
|
2663
3060
|
};
|
|
@@ -2677,19 +3074,19 @@ var adoptionStanding = (context) => {
|
|
|
2677
3074
|
|
|
2678
3075
|
// src/init/applyInit.ts
|
|
2679
3076
|
import { mkdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
2680
|
-
import { dirname, join as
|
|
3077
|
+
import { dirname, join as join21 } from "path";
|
|
2681
3078
|
var applyInit = (root, plan) => {
|
|
2682
3079
|
for (const file of plan) {
|
|
2683
3080
|
const writes = file.disposition === "create" || file.disposition === "merge";
|
|
2684
3081
|
if (!writes || file.content === void 0) continue;
|
|
2685
|
-
mkdirSync(dirname(
|
|
2686
|
-
writeFileSync3(
|
|
3082
|
+
mkdirSync(dirname(join21(root, file.path)), { recursive: true });
|
|
3083
|
+
writeFileSync3(join21(root, file.path), file.content);
|
|
2687
3084
|
}
|
|
2688
3085
|
};
|
|
2689
3086
|
|
|
2690
3087
|
// src/init/planBenchReadme.ts
|
|
2691
|
-
import { existsSync as
|
|
2692
|
-
import { join as
|
|
3088
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3089
|
+
import { join as join22 } from "path";
|
|
2693
3090
|
|
|
2694
3091
|
// src/init/BENCH_README.ts
|
|
2695
3092
|
var BENCH_README = `# Bench queries
|
|
@@ -2721,12 +3118,12 @@ var planBenchReadme = (root) => {
|
|
|
2721
3118
|
return PERF_DEFAULTS.benchDir;
|
|
2722
3119
|
}
|
|
2723
3120
|
})();
|
|
2724
|
-
const
|
|
2725
|
-
if (
|
|
2726
|
-
return { path:
|
|
3121
|
+
const relative5 = `${benchDir}/README.md`;
|
|
3122
|
+
if (existsSync15(join22(root, relative5))) {
|
|
3123
|
+
return { path: relative5, disposition: "unchanged", detail: "present" };
|
|
2727
3124
|
}
|
|
2728
3125
|
return {
|
|
2729
|
-
path:
|
|
3126
|
+
path: relative5,
|
|
2730
3127
|
disposition: "create",
|
|
2731
3128
|
detail: "explains the bench convention",
|
|
2732
3129
|
content: BENCH_README
|
|
@@ -2734,26 +3131,26 @@ var planBenchReadme = (root) => {
|
|
|
2734
3131
|
};
|
|
2735
3132
|
|
|
2736
3133
|
// src/init/planConfigFile.ts
|
|
2737
|
-
import { existsSync as
|
|
2738
|
-
import { join as
|
|
3134
|
+
import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
|
|
3135
|
+
import { join as join26 } from "path";
|
|
2739
3136
|
|
|
2740
3137
|
// src/config/detectStacks.ts
|
|
2741
|
-
import { existsSync as
|
|
2742
|
-
import { join as
|
|
3138
|
+
import { existsSync as existsSync17 } from "fs";
|
|
3139
|
+
import { join as join25 } from "path";
|
|
2743
3140
|
|
|
2744
3141
|
// src/config/findSqliteFiles.ts
|
|
2745
|
-
import { readdirSync as
|
|
2746
|
-
import { join as
|
|
3142
|
+
import { readdirSync as readdirSync5 } from "fs";
|
|
3143
|
+
import { join as join23, relative as relative4 } from "path";
|
|
2747
3144
|
var findSqliteFiles = (root) => {
|
|
2748
3145
|
const skipped = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
|
|
2749
3146
|
const found = [];
|
|
2750
3147
|
const walk = (directory, depth) => {
|
|
2751
|
-
for (const entry of
|
|
3148
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
2752
3149
|
if (skipped.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
2753
|
-
const path =
|
|
3150
|
+
const path = join23(directory, entry.name);
|
|
2754
3151
|
if (entry.isDirectory() && depth < 2) walk(path, depth + 1);
|
|
2755
3152
|
else if (entry.isFile() && /\.(?:db|sqlite|sqlite3)$/.test(entry.name)) {
|
|
2756
|
-
found.push(
|
|
3153
|
+
found.push(relative4(root, path).replaceAll("\\", "/"));
|
|
2757
3154
|
}
|
|
2758
3155
|
}
|
|
2759
3156
|
};
|
|
@@ -2765,13 +3162,13 @@ var findSqliteFiles = (root) => {
|
|
|
2765
3162
|
var hasPostgrestSources = (root, name) => sourceFilesUnder(root, [name]).length > 0;
|
|
2766
3163
|
|
|
2767
3164
|
// src/config/hasSupabaseJsDependency.ts
|
|
2768
|
-
import { existsSync as
|
|
2769
|
-
import { join as
|
|
3165
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
|
|
3166
|
+
import { join as join24 } from "path";
|
|
2770
3167
|
var hasSupabaseJsDependency = (root) => {
|
|
2771
|
-
const manifestPath =
|
|
2772
|
-
if (!
|
|
3168
|
+
const manifestPath = join24(root, "package.json");
|
|
3169
|
+
if (!existsSync16(manifestPath)) return false;
|
|
2773
3170
|
const manifest = JSON.parse(
|
|
2774
|
-
|
|
3171
|
+
readFileSync11(manifestPath, "utf8")
|
|
2775
3172
|
);
|
|
2776
3173
|
const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
2777
3174
|
return "@supabase/supabase-js" in dependencies;
|
|
@@ -2781,17 +3178,17 @@ var hasSupabaseJsDependency = (root) => {
|
|
|
2781
3178
|
var detectStacks = (root) => {
|
|
2782
3179
|
const sqliteFiles = findSqliteFiles(root);
|
|
2783
3180
|
const hasDrizzle = ["ts", "js", "mjs"].some(
|
|
2784
|
-
(ext) =>
|
|
3181
|
+
(ext) => existsSync17(join25(root, `drizzle.config.${ext}`))
|
|
2785
3182
|
);
|
|
2786
3183
|
const drizzleRoots = ["src", "server", "app", "lib", "db"].filter(
|
|
2787
|
-
(name) => isDirectory(
|
|
3184
|
+
(name) => isDirectory(join25(root, name))
|
|
2788
3185
|
);
|
|
2789
3186
|
const postgrestRoots = ["src", "app", "supabase/functions"].filter(
|
|
2790
|
-
(name) => isDirectory(
|
|
3187
|
+
(name) => isDirectory(join25(root, name)) && hasPostgrestSources(root, name)
|
|
2791
3188
|
);
|
|
2792
3189
|
return {
|
|
2793
|
-
...isDirectory(
|
|
2794
|
-
...
|
|
3190
|
+
...isDirectory(join25(root, "supabase/migrations")) ? { supabase: { migrations: "supabase/migrations" } } : {},
|
|
3191
|
+
...existsSync17(join25(root, "prisma/schema.prisma")) ? { prisma: { schema: "prisma/schema.prisma" } } : {},
|
|
2795
3192
|
...hasDrizzle ? { drizzle: { roots: drizzleRoots, objectNames: ["db", "tx"] } } : {},
|
|
2796
3193
|
...sqliteFiles.length > 0 ? { sqlite: { files: sqliteFiles } } : {},
|
|
2797
3194
|
...hasSupabaseJsDependency(root) && postgrestRoots.length > 0 ? { postgrest: { roots: postgrestRoots } } : {}
|
|
@@ -2819,14 +3216,14 @@ var upgradedConfigDocument = (raw, detected) => {
|
|
|
2819
3216
|
|
|
2820
3217
|
// src/init/planConfigFile.ts
|
|
2821
3218
|
var planConfigFile = (root, force) => {
|
|
2822
|
-
const path =
|
|
3219
|
+
const path = join26(root, CONFIG_FILENAME);
|
|
2823
3220
|
const freshContent = `${JSON.stringify(
|
|
2824
3221
|
{ schemaVersion: 2, ...detectStacks(root), perf: PERF_INIT_SECTION },
|
|
2825
3222
|
null,
|
|
2826
3223
|
2
|
|
2827
3224
|
)}
|
|
2828
3225
|
`;
|
|
2829
|
-
if (!
|
|
3226
|
+
if (!existsSync18(path)) {
|
|
2830
3227
|
return {
|
|
2831
3228
|
path: CONFIG_FILENAME,
|
|
2832
3229
|
disposition: "create",
|
|
@@ -2835,7 +3232,7 @@ var planConfigFile = (root, force) => {
|
|
|
2835
3232
|
};
|
|
2836
3233
|
}
|
|
2837
3234
|
try {
|
|
2838
|
-
const raw = JSON.parse(
|
|
3235
|
+
const raw = JSON.parse(readFileSync12(path, "utf8"));
|
|
2839
3236
|
const config = configFromDocument(raw);
|
|
2840
3237
|
if (config.schemaVersion === 1) {
|
|
2841
3238
|
return {
|
|
@@ -2866,23 +3263,23 @@ var planConfigFile = (root, force) => {
|
|
|
2866
3263
|
};
|
|
2867
3264
|
|
|
2868
3265
|
// src/init/planPackageScript.ts
|
|
2869
|
-
import { existsSync as
|
|
2870
|
-
import { join as
|
|
3266
|
+
import { existsSync as existsSync19, readFileSync as readFileSync13 } from "fs";
|
|
3267
|
+
import { join as join27 } from "path";
|
|
2871
3268
|
|
|
2872
3269
|
// src/init/PACKAGE_MANIFEST.ts
|
|
2873
3270
|
var PACKAGE_MANIFEST = "package.json";
|
|
2874
3271
|
|
|
2875
3272
|
// src/init/planPackageScript.ts
|
|
2876
3273
|
var planPackageScript = (root, force) => {
|
|
2877
|
-
const path =
|
|
2878
|
-
if (!
|
|
3274
|
+
const path = join27(root, PACKAGE_MANIFEST);
|
|
3275
|
+
if (!existsSync19(path)) {
|
|
2879
3276
|
return {
|
|
2880
3277
|
path: PACKAGE_MANIFEST,
|
|
2881
3278
|
disposition: "unchanged",
|
|
2882
3279
|
detail: "no package.json"
|
|
2883
3280
|
};
|
|
2884
3281
|
}
|
|
2885
|
-
const manifest = JSON.parse(
|
|
3282
|
+
const manifest = JSON.parse(readFileSync13(path, "utf8"));
|
|
2886
3283
|
const current = manifest.scripts?.["db:gate"];
|
|
2887
3284
|
if (current === "codeality-db gate") {
|
|
2888
3285
|
return {
|
|
@@ -2910,8 +3307,8 @@ var planPackageScript = (root, force) => {
|
|
|
2910
3307
|
};
|
|
2911
3308
|
|
|
2912
3309
|
// src/init/planWorkflow.ts
|
|
2913
|
-
import { existsSync as
|
|
2914
|
-
import { join as
|
|
3310
|
+
import { existsSync as existsSync20, readFileSync as readFileSync14 } from "fs";
|
|
3311
|
+
import { join as join28 } from "path";
|
|
2915
3312
|
|
|
2916
3313
|
// src/init/sha256Of.ts
|
|
2917
3314
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -2924,33 +3321,33 @@ var SHIPPED_WORKFLOW_HASHES = {
|
|
|
2924
3321
|
|
|
2925
3322
|
// src/init/planWorkflow.ts
|
|
2926
3323
|
var planWorkflow = (root, force) => {
|
|
2927
|
-
const
|
|
2928
|
-
const content =
|
|
2929
|
-
const path =
|
|
2930
|
-
if (!
|
|
3324
|
+
const relative5 = ".github/workflows/db-quality.yml";
|
|
3325
|
+
const content = readFileSync14(assetPath("db-quality.yml"), "utf8");
|
|
3326
|
+
const path = join28(root, relative5);
|
|
3327
|
+
if (!existsSync20(path)) {
|
|
2931
3328
|
const detail = "runs codeality-db gate on push and pull request";
|
|
2932
|
-
return { path:
|
|
3329
|
+
return { path: relative5, disposition: "create", detail, content };
|
|
2933
3330
|
}
|
|
2934
|
-
const existing =
|
|
3331
|
+
const existing = readFileSync14(path, "utf8");
|
|
2935
3332
|
if (existing === content) {
|
|
2936
|
-
return { path:
|
|
3333
|
+
return { path: relative5, disposition: "unchanged", detail: "identical" };
|
|
2937
3334
|
}
|
|
2938
3335
|
const shippedVersion = SHIPPED_WORKFLOW_HASHES[sha256Of(existing)];
|
|
2939
3336
|
if (shippedVersion !== void 0) {
|
|
2940
3337
|
return {
|
|
2941
|
-
path:
|
|
3338
|
+
path: relative5,
|
|
2942
3339
|
disposition: "merge",
|
|
2943
3340
|
detail: `upgraded from the ${shippedVersion} workflow`,
|
|
2944
3341
|
content
|
|
2945
3342
|
};
|
|
2946
3343
|
}
|
|
2947
3344
|
return force ? {
|
|
2948
|
-
path:
|
|
3345
|
+
path: relative5,
|
|
2949
3346
|
disposition: "merge",
|
|
2950
3347
|
detail: "replaced (--force)",
|
|
2951
3348
|
content
|
|
2952
3349
|
} : {
|
|
2953
|
-
path:
|
|
3350
|
+
path: relative5,
|
|
2954
3351
|
disposition: "conflict",
|
|
2955
3352
|
detail: "differs from the shipped workflow"
|
|
2956
3353
|
};
|
|
@@ -3091,9 +3488,9 @@ var renderBenchJson = (result, recorded) => JSON.stringify(
|
|
|
3091
3488
|
|
|
3092
3489
|
// src/bench/writeBenchRecord.ts
|
|
3093
3490
|
import { renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
3094
|
-
import { join as
|
|
3491
|
+
import { join as join29 } from "path";
|
|
3095
3492
|
var writeBenchRecord = (root, record) => {
|
|
3096
|
-
const path =
|
|
3493
|
+
const path = join29(root, BENCH_RECORD_FILENAME);
|
|
3097
3494
|
writeFileSync4(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
|
|
3098
3495
|
`);
|
|
3099
3496
|
renameSync2(`${path}.tmp`, path);
|
|
@@ -3196,9 +3593,9 @@ var perfDiffAction = ({
|
|
|
3196
3593
|
|
|
3197
3594
|
// src/perf/writePerfSnapshot.ts
|
|
3198
3595
|
import { renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3199
|
-
import { join as
|
|
3596
|
+
import { join as join30 } from "path";
|
|
3200
3597
|
var writePerfSnapshot = (root, snapshot) => {
|
|
3201
|
-
const path =
|
|
3598
|
+
const path = join30(root, PERF_SNAPSHOT_FILENAME);
|
|
3202
3599
|
writeFileSync5(`${path}.tmp`, `${JSON.stringify(snapshot, null, 2)}
|
|
3203
3600
|
`);
|
|
3204
3601
|
renameSync3(`${path}.tmp`, path);
|
|
@@ -3330,7 +3727,7 @@ var runCli = (argv2, io) => {
|
|
|
3330
3727
|
|
|
3331
3728
|
// src/tools/spawnRunner.ts
|
|
3332
3729
|
import { spawnSync } from "child_process";
|
|
3333
|
-
import { delimiter, join as
|
|
3730
|
+
import { delimiter, join as join31 } from "path";
|
|
3334
3731
|
import { env as processEnv } from "process";
|
|
3335
3732
|
|
|
3336
3733
|
// src/tools/packageBinPath.ts
|
|
@@ -3340,7 +3737,7 @@ var packageBinPath = () => fileURLToPath2(new URL("../node_modules/.bin", import
|
|
|
3340
3737
|
// src/tools/spawnRunner.ts
|
|
3341
3738
|
var spawnRunner = (command, args, options) => {
|
|
3342
3739
|
const path = options.systemPathOnly ? processEnv["PATH"] ?? "" : [
|
|
3343
|
-
|
|
3740
|
+
join31(options.cwd, "node_modules/.bin"),
|
|
3344
3741
|
packageBinPath(),
|
|
3345
3742
|
processEnv["PATH"] ?? ""
|
|
3346
3743
|
].join(delimiter);
|