@syntopica/db-quality 0.2.0 → 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 +615 -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
|
};
|
|
@@ -2761,14 +3158,17 @@ var findSqliteFiles = (root) => {
|
|
|
2761
3158
|
return found.sort();
|
|
2762
3159
|
};
|
|
2763
3160
|
|
|
3161
|
+
// src/config/hasPostgrestSources.ts
|
|
3162
|
+
var hasPostgrestSources = (root, name) => sourceFilesUnder(root, [name]).length > 0;
|
|
3163
|
+
|
|
2764
3164
|
// src/config/hasSupabaseJsDependency.ts
|
|
2765
|
-
import { existsSync as
|
|
2766
|
-
import { join as
|
|
3165
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
|
|
3166
|
+
import { join as join24 } from "path";
|
|
2767
3167
|
var hasSupabaseJsDependency = (root) => {
|
|
2768
|
-
const manifestPath =
|
|
2769
|
-
if (!
|
|
3168
|
+
const manifestPath = join24(root, "package.json");
|
|
3169
|
+
if (!existsSync16(manifestPath)) return false;
|
|
2770
3170
|
const manifest = JSON.parse(
|
|
2771
|
-
|
|
3171
|
+
readFileSync11(manifestPath, "utf8")
|
|
2772
3172
|
);
|
|
2773
3173
|
const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
2774
3174
|
return "@supabase/supabase-js" in dependencies;
|
|
@@ -2778,17 +3178,17 @@ var hasSupabaseJsDependency = (root) => {
|
|
|
2778
3178
|
var detectStacks = (root) => {
|
|
2779
3179
|
const sqliteFiles = findSqliteFiles(root);
|
|
2780
3180
|
const hasDrizzle = ["ts", "js", "mjs"].some(
|
|
2781
|
-
(ext) =>
|
|
3181
|
+
(ext) => existsSync17(join25(root, `drizzle.config.${ext}`))
|
|
2782
3182
|
);
|
|
2783
3183
|
const drizzleRoots = ["src", "server", "app", "lib", "db"].filter(
|
|
2784
|
-
(name) => isDirectory(
|
|
3184
|
+
(name) => isDirectory(join25(root, name))
|
|
2785
3185
|
);
|
|
2786
3186
|
const postgrestRoots = ["src", "app", "supabase/functions"].filter(
|
|
2787
|
-
(name) => isDirectory(
|
|
3187
|
+
(name) => isDirectory(join25(root, name)) && hasPostgrestSources(root, name)
|
|
2788
3188
|
);
|
|
2789
3189
|
return {
|
|
2790
|
-
...isDirectory(
|
|
2791
|
-
...
|
|
3190
|
+
...isDirectory(join25(root, "supabase/migrations")) ? { supabase: { migrations: "supabase/migrations" } } : {},
|
|
3191
|
+
...existsSync17(join25(root, "prisma/schema.prisma")) ? { prisma: { schema: "prisma/schema.prisma" } } : {},
|
|
2792
3192
|
...hasDrizzle ? { drizzle: { roots: drizzleRoots, objectNames: ["db", "tx"] } } : {},
|
|
2793
3193
|
...sqliteFiles.length > 0 ? { sqlite: { files: sqliteFiles } } : {},
|
|
2794
3194
|
...hasSupabaseJsDependency(root) && postgrestRoots.length > 0 ? { postgrest: { roots: postgrestRoots } } : {}
|
|
@@ -2816,14 +3216,14 @@ var upgradedConfigDocument = (raw, detected) => {
|
|
|
2816
3216
|
|
|
2817
3217
|
// src/init/planConfigFile.ts
|
|
2818
3218
|
var planConfigFile = (root, force) => {
|
|
2819
|
-
const path =
|
|
3219
|
+
const path = join26(root, CONFIG_FILENAME);
|
|
2820
3220
|
const freshContent = `${JSON.stringify(
|
|
2821
3221
|
{ schemaVersion: 2, ...detectStacks(root), perf: PERF_INIT_SECTION },
|
|
2822
3222
|
null,
|
|
2823
3223
|
2
|
|
2824
3224
|
)}
|
|
2825
3225
|
`;
|
|
2826
|
-
if (!
|
|
3226
|
+
if (!existsSync18(path)) {
|
|
2827
3227
|
return {
|
|
2828
3228
|
path: CONFIG_FILENAME,
|
|
2829
3229
|
disposition: "create",
|
|
@@ -2832,7 +3232,7 @@ var planConfigFile = (root, force) => {
|
|
|
2832
3232
|
};
|
|
2833
3233
|
}
|
|
2834
3234
|
try {
|
|
2835
|
-
const raw = JSON.parse(
|
|
3235
|
+
const raw = JSON.parse(readFileSync12(path, "utf8"));
|
|
2836
3236
|
const config = configFromDocument(raw);
|
|
2837
3237
|
if (config.schemaVersion === 1) {
|
|
2838
3238
|
return {
|
|
@@ -2863,23 +3263,23 @@ var planConfigFile = (root, force) => {
|
|
|
2863
3263
|
};
|
|
2864
3264
|
|
|
2865
3265
|
// src/init/planPackageScript.ts
|
|
2866
|
-
import { existsSync as
|
|
2867
|
-
import { join as
|
|
3266
|
+
import { existsSync as existsSync19, readFileSync as readFileSync13 } from "fs";
|
|
3267
|
+
import { join as join27 } from "path";
|
|
2868
3268
|
|
|
2869
3269
|
// src/init/PACKAGE_MANIFEST.ts
|
|
2870
3270
|
var PACKAGE_MANIFEST = "package.json";
|
|
2871
3271
|
|
|
2872
3272
|
// src/init/planPackageScript.ts
|
|
2873
3273
|
var planPackageScript = (root, force) => {
|
|
2874
|
-
const path =
|
|
2875
|
-
if (!
|
|
3274
|
+
const path = join27(root, PACKAGE_MANIFEST);
|
|
3275
|
+
if (!existsSync19(path)) {
|
|
2876
3276
|
return {
|
|
2877
3277
|
path: PACKAGE_MANIFEST,
|
|
2878
3278
|
disposition: "unchanged",
|
|
2879
3279
|
detail: "no package.json"
|
|
2880
3280
|
};
|
|
2881
3281
|
}
|
|
2882
|
-
const manifest = JSON.parse(
|
|
3282
|
+
const manifest = JSON.parse(readFileSync13(path, "utf8"));
|
|
2883
3283
|
const current = manifest.scripts?.["db:gate"];
|
|
2884
3284
|
if (current === "codeality-db gate") {
|
|
2885
3285
|
return {
|
|
@@ -2907,8 +3307,8 @@ var planPackageScript = (root, force) => {
|
|
|
2907
3307
|
};
|
|
2908
3308
|
|
|
2909
3309
|
// src/init/planWorkflow.ts
|
|
2910
|
-
import { existsSync as
|
|
2911
|
-
import { join as
|
|
3310
|
+
import { existsSync as existsSync20, readFileSync as readFileSync14 } from "fs";
|
|
3311
|
+
import { join as join28 } from "path";
|
|
2912
3312
|
|
|
2913
3313
|
// src/init/sha256Of.ts
|
|
2914
3314
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -2921,33 +3321,33 @@ var SHIPPED_WORKFLOW_HASHES = {
|
|
|
2921
3321
|
|
|
2922
3322
|
// src/init/planWorkflow.ts
|
|
2923
3323
|
var planWorkflow = (root, force) => {
|
|
2924
|
-
const
|
|
2925
|
-
const content =
|
|
2926
|
-
const path =
|
|
2927
|
-
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)) {
|
|
2928
3328
|
const detail = "runs codeality-db gate on push and pull request";
|
|
2929
|
-
return { path:
|
|
3329
|
+
return { path: relative5, disposition: "create", detail, content };
|
|
2930
3330
|
}
|
|
2931
|
-
const existing =
|
|
3331
|
+
const existing = readFileSync14(path, "utf8");
|
|
2932
3332
|
if (existing === content) {
|
|
2933
|
-
return { path:
|
|
3333
|
+
return { path: relative5, disposition: "unchanged", detail: "identical" };
|
|
2934
3334
|
}
|
|
2935
3335
|
const shippedVersion = SHIPPED_WORKFLOW_HASHES[sha256Of(existing)];
|
|
2936
3336
|
if (shippedVersion !== void 0) {
|
|
2937
3337
|
return {
|
|
2938
|
-
path:
|
|
3338
|
+
path: relative5,
|
|
2939
3339
|
disposition: "merge",
|
|
2940
3340
|
detail: `upgraded from the ${shippedVersion} workflow`,
|
|
2941
3341
|
content
|
|
2942
3342
|
};
|
|
2943
3343
|
}
|
|
2944
3344
|
return force ? {
|
|
2945
|
-
path:
|
|
3345
|
+
path: relative5,
|
|
2946
3346
|
disposition: "merge",
|
|
2947
3347
|
detail: "replaced (--force)",
|
|
2948
3348
|
content
|
|
2949
3349
|
} : {
|
|
2950
|
-
path:
|
|
3350
|
+
path: relative5,
|
|
2951
3351
|
disposition: "conflict",
|
|
2952
3352
|
detail: "differs from the shipped workflow"
|
|
2953
3353
|
};
|
|
@@ -3088,9 +3488,9 @@ var renderBenchJson = (result, recorded) => JSON.stringify(
|
|
|
3088
3488
|
|
|
3089
3489
|
// src/bench/writeBenchRecord.ts
|
|
3090
3490
|
import { renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
3091
|
-
import { join as
|
|
3491
|
+
import { join as join29 } from "path";
|
|
3092
3492
|
var writeBenchRecord = (root, record) => {
|
|
3093
|
-
const path =
|
|
3493
|
+
const path = join29(root, BENCH_RECORD_FILENAME);
|
|
3094
3494
|
writeFileSync4(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
|
|
3095
3495
|
`);
|
|
3096
3496
|
renameSync2(`${path}.tmp`, path);
|
|
@@ -3193,9 +3593,9 @@ var perfDiffAction = ({
|
|
|
3193
3593
|
|
|
3194
3594
|
// src/perf/writePerfSnapshot.ts
|
|
3195
3595
|
import { renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3196
|
-
import { join as
|
|
3596
|
+
import { join as join30 } from "path";
|
|
3197
3597
|
var writePerfSnapshot = (root, snapshot) => {
|
|
3198
|
-
const path =
|
|
3598
|
+
const path = join30(root, PERF_SNAPSHOT_FILENAME);
|
|
3199
3599
|
writeFileSync5(`${path}.tmp`, `${JSON.stringify(snapshot, null, 2)}
|
|
3200
3600
|
`);
|
|
3201
3601
|
renameSync3(`${path}.tmp`, path);
|
|
@@ -3327,7 +3727,7 @@ var runCli = (argv2, io) => {
|
|
|
3327
3727
|
|
|
3328
3728
|
// src/tools/spawnRunner.ts
|
|
3329
3729
|
import { spawnSync } from "child_process";
|
|
3330
|
-
import { delimiter, join as
|
|
3730
|
+
import { delimiter, join as join31 } from "path";
|
|
3331
3731
|
import { env as processEnv } from "process";
|
|
3332
3732
|
|
|
3333
3733
|
// src/tools/packageBinPath.ts
|
|
@@ -3337,7 +3737,7 @@ var packageBinPath = () => fileURLToPath2(new URL("../node_modules/.bin", import
|
|
|
3337
3737
|
// src/tools/spawnRunner.ts
|
|
3338
3738
|
var spawnRunner = (command, args, options) => {
|
|
3339
3739
|
const path = options.systemPathOnly ? processEnv["PATH"] ?? "" : [
|
|
3340
|
-
|
|
3740
|
+
join31(options.cwd, "node_modules/.bin"),
|
|
3341
3741
|
packageBinPath(),
|
|
3342
3742
|
processEnv["PATH"] ?? ""
|
|
3343
3743
|
].join(delimiter);
|