@sarj/eslint-plugin 2.6.0 → 2.8.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 +126 -1
- package/dist/index.cjs +1884 -272
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +288 -10
- package/dist/index.d.ts +288 -10
- package/dist/index.js +1892 -276
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -350,12 +350,10 @@ var no_comment_cruft_default = import_utils3.ESLintUtils.RuleCreator(
|
|
|
350
350
|
}
|
|
351
351
|
const first = leading[0];
|
|
352
352
|
if (first === void 0 || leading.length < LEADING_PREAMBLE_MIN) return;
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
context.report({ node: first, messageId: "fileHeaderPreamble" });
|
|
358
|
-
}
|
|
353
|
+
const bodies = leading.map((c) => stripCommentMarker(c.value));
|
|
354
|
+
if (bodies.some((body) => LICENSE_RE.test(body))) return;
|
|
355
|
+
if (bodies.some((body) => isProse(body))) return;
|
|
356
|
+
context.report({ node: first, messageId: "fileHeaderPreamble" });
|
|
359
357
|
}
|
|
360
358
|
return {
|
|
361
359
|
Program() {
|
|
@@ -833,35 +831,15 @@ var LOGGER_NAMES = /* @__PURE__ */ new Set([
|
|
|
833
831
|
"_logger",
|
|
834
832
|
"_log"
|
|
835
833
|
]);
|
|
834
|
+
var LOGGER_FACTORIES = /* @__PURE__ */ new Set([
|
|
835
|
+
"getlogger",
|
|
836
|
+
"get_logger"
|
|
837
|
+
]);
|
|
836
838
|
var REPORT_NAME_RE = /error|report|capture|log|trace|warn/i;
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
case "MemberExpression": {
|
|
842
|
-
const { property, object } = expr;
|
|
843
|
-
if (!expr.computed && property.type === "Identifier" && LOGGER_NAMES.has(property.name.toLowerCase())) {
|
|
844
|
-
return true;
|
|
845
|
-
}
|
|
846
|
-
return isLoggerReceiver(object);
|
|
847
|
-
}
|
|
848
|
-
default:
|
|
849
|
-
return false;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
function isLoggingCall(expr) {
|
|
853
|
-
if (expr.type !== "CallExpression") {
|
|
854
|
-
return false;
|
|
855
|
-
}
|
|
856
|
-
const callee = expr.callee;
|
|
857
|
-
if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") {
|
|
858
|
-
return false;
|
|
859
|
-
}
|
|
860
|
-
if (!LOG_METHODS.has(callee.property.name.toLowerCase())) {
|
|
861
|
-
return false;
|
|
862
|
-
}
|
|
863
|
-
return isLoggerReceiver(callee.object);
|
|
864
|
-
}
|
|
839
|
+
var LOGGING_OPTION_PROPERTIES = {
|
|
840
|
+
loggerNames: { type: "array", items: { type: "string" } },
|
|
841
|
+
logFunctions: { type: "array", items: { type: "string" } }
|
|
842
|
+
};
|
|
865
843
|
function calleeName(callee) {
|
|
866
844
|
if (callee.type === "Identifier") {
|
|
867
845
|
return callee.name;
|
|
@@ -871,6 +849,65 @@ function calleeName(callee) {
|
|
|
871
849
|
}
|
|
872
850
|
return null;
|
|
873
851
|
}
|
|
852
|
+
function createLogMatcher(options = {}) {
|
|
853
|
+
const loggerNames = /* @__PURE__ */ new Set([
|
|
854
|
+
...LOGGER_NAMES,
|
|
855
|
+
...(options.loggerNames ?? []).map((name) => name.toLowerCase())
|
|
856
|
+
]);
|
|
857
|
+
const logFunctions = new Set(options.logFunctions ?? []);
|
|
858
|
+
function isLoggerReceiver(expr) {
|
|
859
|
+
switch (expr.type) {
|
|
860
|
+
case "Identifier":
|
|
861
|
+
return loggerNames.has(expr.name.toLowerCase());
|
|
862
|
+
case "MemberExpression": {
|
|
863
|
+
const { property, object } = expr;
|
|
864
|
+
if (!expr.computed && property.type === "Identifier") {
|
|
865
|
+
const lowered = property.name.toLowerCase();
|
|
866
|
+
if (loggerNames.has(lowered) || LOGGER_FACTORIES.has(lowered)) {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return isLoggerReceiver(object);
|
|
871
|
+
}
|
|
872
|
+
case "CallExpression": {
|
|
873
|
+
const callee = expr.callee;
|
|
874
|
+
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && LOGGER_FACTORIES.has(callee.property.name.toLowerCase())) {
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
if (callee.type !== "Super") {
|
|
878
|
+
return isLoggerReceiver(callee);
|
|
879
|
+
}
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
default:
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function isLogFunctionCall(expr) {
|
|
887
|
+
if (expr.type !== "CallExpression" || logFunctions.size === 0) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
const name = calleeName(expr.callee);
|
|
891
|
+
return name !== null && logFunctions.has(name);
|
|
892
|
+
}
|
|
893
|
+
function isLoggingCall(expr) {
|
|
894
|
+
if (expr.type !== "CallExpression") {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
if (isLogFunctionCall(expr)) {
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
const callee = expr.callee;
|
|
901
|
+
if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
if (!LOG_METHODS.has(callee.property.name.toLowerCase())) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
return isLoggerReceiver(callee.object);
|
|
908
|
+
}
|
|
909
|
+
return { isLoggerReceiver, isLogFunctionCall, isLoggingCall };
|
|
910
|
+
}
|
|
874
911
|
|
|
875
912
|
// src/rules/no-log-only-catch.ts
|
|
876
913
|
var DEFAULT_IGNORE_PATTERNS2 = [
|
|
@@ -878,12 +915,6 @@ var DEFAULT_IGNORE_PATTERNS2 = [
|
|
|
878
915
|
/\.spec\./,
|
|
879
916
|
/[\\/]__tests__[\\/]/
|
|
880
917
|
];
|
|
881
|
-
function isLoggingCallStatement(statement) {
|
|
882
|
-
if (statement.type !== "ExpressionStatement") {
|
|
883
|
-
return false;
|
|
884
|
-
}
|
|
885
|
-
return isLoggingCall(statement.expression);
|
|
886
|
-
}
|
|
887
918
|
var no_log_only_catch_default = import_utils8.ESLintUtils.RuleCreator(
|
|
888
919
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
889
920
|
)({
|
|
@@ -893,15 +924,28 @@ var no_log_only_catch_default = import_utils8.ESLintUtils.RuleCreator(
|
|
|
893
924
|
docs: {
|
|
894
925
|
description: "Disallow `catch` clauses that only log (or silently do nothing) and then swallow the error; rethrow or handle it instead."
|
|
895
926
|
},
|
|
896
|
-
schema: [
|
|
927
|
+
schema: [
|
|
928
|
+
{
|
|
929
|
+
type: "object",
|
|
930
|
+
additionalProperties: false,
|
|
931
|
+
properties: { ...LOGGING_OPTION_PROPERTIES }
|
|
932
|
+
}
|
|
933
|
+
],
|
|
897
934
|
messages: {
|
|
898
935
|
noLogOnlyCatch: "Logging then swallowing the error hides failures. Rethrow the error or handle it for real.",
|
|
899
936
|
emptyCatch: "Empty catch silently swallows the error. Rethrow it, handle it, or add a comment explaining why it is safe to ignore."
|
|
900
937
|
}
|
|
901
938
|
},
|
|
902
|
-
defaultOptions: [],
|
|
903
|
-
create(context) {
|
|
939
|
+
defaultOptions: [{}],
|
|
940
|
+
create(context, [loggingOptions]) {
|
|
941
|
+
const matcher = createLogMatcher(loggingOptions);
|
|
904
942
|
const filename = context.filename;
|
|
943
|
+
function isLoggingCallStatement(statement) {
|
|
944
|
+
if (statement.type !== "ExpressionStatement") {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
return matcher.isLoggingCall(statement.expression);
|
|
948
|
+
}
|
|
905
949
|
const isIgnoredByDefault = DEFAULT_IGNORE_PATTERNS2.some(
|
|
906
950
|
(re) => re.test(filename)
|
|
907
951
|
);
|
|
@@ -1071,46 +1115,131 @@ function containsThrow(node) {
|
|
|
1071
1115
|
(current) => current.type === import_utils10.AST_NODE_TYPES.ThrowStatement
|
|
1072
1116
|
);
|
|
1073
1117
|
}
|
|
1074
|
-
function
|
|
1075
|
-
|
|
1076
|
-
|
|
1118
|
+
function bindsName(param, name) {
|
|
1119
|
+
switch (param.type) {
|
|
1120
|
+
case import_utils10.AST_NODE_TYPES.Identifier:
|
|
1121
|
+
return param.name === name;
|
|
1122
|
+
case import_utils10.AST_NODE_TYPES.AssignmentPattern:
|
|
1123
|
+
return bindsName(param.left, name);
|
|
1124
|
+
case import_utils10.AST_NODE_TYPES.RestElement:
|
|
1125
|
+
return bindsName(param.argument, name);
|
|
1126
|
+
case import_utils10.AST_NODE_TYPES.ArrayPattern:
|
|
1127
|
+
return param.elements.some(
|
|
1128
|
+
(element) => element !== null && bindsName(element, name)
|
|
1129
|
+
);
|
|
1130
|
+
case import_utils10.AST_NODE_TYPES.ObjectPattern:
|
|
1131
|
+
return param.properties.some(
|
|
1132
|
+
(property) => property.type === import_utils10.AST_NODE_TYPES.RestElement ? bindsName(property.argument, name) : bindsName(property.value, name)
|
|
1133
|
+
);
|
|
1134
|
+
default:
|
|
1135
|
+
return false;
|
|
1077
1136
|
}
|
|
1078
|
-
return args.some(
|
|
1079
|
-
(arg) => arg.type === import_utils10.AST_NODE_TYPES.Identifier && arg.name === caughtName
|
|
1080
|
-
);
|
|
1081
1137
|
}
|
|
1082
|
-
function
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1138
|
+
function subtreeReadsName(node, name) {
|
|
1139
|
+
let found = false;
|
|
1140
|
+
const shadowsName = (fn) => isFunctionNode(fn) && fn.params.some(
|
|
1141
|
+
(param) => bindsName(param, name)
|
|
1142
|
+
);
|
|
1143
|
+
const recurse = (current) => {
|
|
1144
|
+
if (found) {
|
|
1145
|
+
return;
|
|
1086
1146
|
}
|
|
1087
|
-
if (
|
|
1088
|
-
|
|
1147
|
+
if (current.type === import_utils10.AST_NODE_TYPES.Identifier && current.name === name) {
|
|
1148
|
+
found = true;
|
|
1149
|
+
return;
|
|
1089
1150
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1151
|
+
if (shadowsName(current)) {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
for (const key of Object.keys(current)) {
|
|
1155
|
+
if (key === "parent") {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (key === "key" && current.type === import_utils10.AST_NODE_TYPES.Property && !current.computed) {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
if (key === "property" && current.type === import_utils10.AST_NODE_TYPES.MemberExpression && !current.computed) {
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
const value = current[key];
|
|
1165
|
+
if (Array.isArray(value)) {
|
|
1166
|
+
for (const child of value) {
|
|
1167
|
+
if (isNode(child)) {
|
|
1168
|
+
recurse(child);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
} else if (isNode(value)) {
|
|
1172
|
+
recurse(value);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
recurse(node);
|
|
1177
|
+
return found;
|
|
1178
|
+
}
|
|
1179
|
+
function argsIncludeBinding(args, caughtName) {
|
|
1180
|
+
if (caughtName === null) {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
return args.some((arg) => subtreeReadsName(arg, caughtName));
|
|
1093
1184
|
}
|
|
1094
1185
|
function tryBlockOf(catchNode) {
|
|
1095
1186
|
return catchNode.parent.block;
|
|
1096
1187
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1188
|
+
var SAFE_PARSE_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
1189
|
+
"RegExp",
|
|
1190
|
+
"URL",
|
|
1191
|
+
"URLPattern"
|
|
1192
|
+
]);
|
|
1193
|
+
function isParseShapedNode(node) {
|
|
1194
|
+
if (node.type === import_utils10.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils10.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils10.AST_NODE_TYPES.Identifier) {
|
|
1195
|
+
return node.callee.property.name === "parse";
|
|
1103
1196
|
}
|
|
1104
|
-
if (
|
|
1105
|
-
return
|
|
1197
|
+
if (node.type === import_utils10.AST_NODE_TYPES.NewExpression && node.callee.type === import_utils10.AST_NODE_TYPES.Identifier) {
|
|
1198
|
+
return SAFE_PARSE_CONSTRUCTORS.has(node.callee.name);
|
|
1106
1199
|
}
|
|
1107
1200
|
return false;
|
|
1108
1201
|
}
|
|
1202
|
+
var BODY_DECODE_METHODS = /* @__PURE__ */ new Set([
|
|
1203
|
+
"json",
|
|
1204
|
+
"text",
|
|
1205
|
+
"arrayBuffer"
|
|
1206
|
+
]);
|
|
1207
|
+
function isBodyDecodeNode(node) {
|
|
1208
|
+
return node.type === import_utils10.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils10.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils10.AST_NODE_TYPES.Identifier && BODY_DECODE_METHODS.has(node.callee.property.name);
|
|
1209
|
+
}
|
|
1210
|
+
function returnsMatching(stmt, predicate) {
|
|
1211
|
+
return stmt.type === import_utils10.AST_NODE_TYPES.ReturnStatement && stmt.argument !== null && walkWithinScope(stmt.argument, predicate);
|
|
1212
|
+
}
|
|
1213
|
+
function enclosingReturnTypeNode(node) {
|
|
1214
|
+
let current = node.parent;
|
|
1215
|
+
while (current !== void 0 && current !== null) {
|
|
1216
|
+
if (isFunctionNode(current) && "returnType" in current) {
|
|
1217
|
+
return current.returnType?.typeAnnotation ?? null;
|
|
1218
|
+
}
|
|
1219
|
+
current = current.parent;
|
|
1220
|
+
}
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
function isDeclaredBooleanPredicate(catchNode, kind) {
|
|
1224
|
+
if (kind !== "boolean") {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
let declared = enclosingReturnTypeNode(catchNode);
|
|
1228
|
+
if (declared?.type === import_utils10.AST_NODE_TYPES.TSTypeReference && declared.typeName.type === import_utils10.AST_NODE_TYPES.Identifier && declared.typeName.name === "Promise") {
|
|
1229
|
+
declared = declared.typeArguments?.params[0] ?? null;
|
|
1230
|
+
}
|
|
1231
|
+
return declared?.type === import_utils10.AST_NODE_TYPES.TSBooleanKeyword;
|
|
1232
|
+
}
|
|
1109
1233
|
function tryReturnsSafeParse(catchNode) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1234
|
+
const tryBlock = tryBlockOf(catchNode);
|
|
1235
|
+
if (walkWithinScope(
|
|
1236
|
+
tryBlock,
|
|
1237
|
+
(current) => returnsMatching(current, isParseShapedNode)
|
|
1238
|
+
)) {
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
const only = tryBlock.body.length === 1 ? tryBlock.body[0] : void 0;
|
|
1242
|
+
return only !== void 0 && returnsMatching(only, isBodyDecodeNode);
|
|
1114
1243
|
}
|
|
1115
1244
|
function enclosingFunctionBody(node) {
|
|
1116
1245
|
let current = node.parent;
|
|
@@ -1156,13 +1285,32 @@ var no_sentinel_return_on_catch_default = import_utils10.ESLintUtils.RuleCreator
|
|
|
1156
1285
|
docs: {
|
|
1157
1286
|
description: "Disallow swallowing a caught error by returning an empty sentinel (`null`, `undefined`, `false`, `[]`, `{}`) as the final statement of a `catch` block, unless the error is logged/reported or the sentinel is the declared safe-parse/predicate contract."
|
|
1158
1287
|
},
|
|
1159
|
-
schema: [
|
|
1288
|
+
schema: [
|
|
1289
|
+
{
|
|
1290
|
+
type: "object",
|
|
1291
|
+
additionalProperties: false,
|
|
1292
|
+
properties: { ...LOGGING_OPTION_PROPERTIES }
|
|
1293
|
+
}
|
|
1294
|
+
],
|
|
1160
1295
|
messages: {
|
|
1161
1296
|
noSentinelReturn: "This `catch` block swallows the error by returning an empty sentinel without logging it. Rethrow it, log/report it, or return a typed Result."
|
|
1162
1297
|
}
|
|
1163
1298
|
},
|
|
1164
|
-
defaultOptions: [],
|
|
1165
|
-
create(context) {
|
|
1299
|
+
defaultOptions: [{}],
|
|
1300
|
+
create(context, [loggingOptions]) {
|
|
1301
|
+
const matcher = createLogMatcher(loggingOptions);
|
|
1302
|
+
function logsOrReportsError(catchBody, caughtName) {
|
|
1303
|
+
return walkWithinScope(catchBody, (current) => {
|
|
1304
|
+
if (current.type !== import_utils10.AST_NODE_TYPES.CallExpression) {
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
if (matcher.isLoggingCall(current)) {
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
const name = calleeName(current.callee);
|
|
1311
|
+
return name !== null && REPORT_NAME_RE.test(name) && argsIncludeBinding(current.arguments, caughtName);
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1166
1314
|
return {
|
|
1167
1315
|
CatchClause(node) {
|
|
1168
1316
|
const body = node.body.body;
|
|
@@ -1187,6 +1335,9 @@ var no_sentinel_return_on_catch_default = import_utils10.ESLintUtils.RuleCreator
|
|
|
1187
1335
|
return;
|
|
1188
1336
|
}
|
|
1189
1337
|
const kind = sentinelKind(last.argument);
|
|
1338
|
+
if (kind !== null && isDeclaredBooleanPredicate(node, kind)) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1190
1341
|
if (kind !== null && functionReturnsSameSentinelKindElsewhere(node, kind)) {
|
|
1191
1342
|
return;
|
|
1192
1343
|
}
|
|
@@ -1291,6 +1442,28 @@ function isThreadedAccumulator(node) {
|
|
|
1291
1442
|
}
|
|
1292
1443
|
return referencesName(node.argument, target);
|
|
1293
1444
|
}
|
|
1445
|
+
function namesReadBy(test) {
|
|
1446
|
+
const names = /* @__PURE__ */ new Set();
|
|
1447
|
+
visitScope(test, (node) => {
|
|
1448
|
+
if (node.type === "Identifier") {
|
|
1449
|
+
names.add(node.name);
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
return names;
|
|
1453
|
+
}
|
|
1454
|
+
function testStateIsAssignedInBody(test, body) {
|
|
1455
|
+
const testNames = namesReadBy(test);
|
|
1456
|
+
if (testNames.size === 0) {
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
let found = false;
|
|
1460
|
+
visitScope(body, (node) => {
|
|
1461
|
+
if (node.type === "AssignmentExpression" && node.operator === "=" && node.left.type === "Identifier" && testNames.has(node.left.name)) {
|
|
1462
|
+
found = true;
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
return found;
|
|
1466
|
+
}
|
|
1294
1467
|
function shouldReport(awaits, earlyExit, iterableText) {
|
|
1295
1468
|
if (awaits.length === 0) {
|
|
1296
1469
|
return false;
|
|
@@ -1337,6 +1510,9 @@ var no_sequential_await_default = import_utils11.ESLintUtils.RuleCreator(
|
|
|
1337
1510
|
return null;
|
|
1338
1511
|
}
|
|
1339
1512
|
function checkLoop(node) {
|
|
1513
|
+
if ((node.type === "WhileStatement" || node.type === "DoWhileStatement") && testStateIsAssignedInBody(node.test, node.body)) {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1340
1516
|
const awaits = [];
|
|
1341
1517
|
let earlyExit = false;
|
|
1342
1518
|
for (const part of loopParts(node)) {
|
|
@@ -1759,7 +1935,7 @@ var unwrap = (node) => {
|
|
|
1759
1935
|
}
|
|
1760
1936
|
return current ?? null;
|
|
1761
1937
|
};
|
|
1762
|
-
var
|
|
1938
|
+
var isRawPayloadSource = (node) => {
|
|
1763
1939
|
let current = unwrap(node);
|
|
1764
1940
|
if (current === null) return false;
|
|
1765
1941
|
if (current.type === import_utils16.AST_NODE_TYPES.AwaitExpression) {
|
|
@@ -1773,7 +1949,14 @@ var isJsonCall = (node) => {
|
|
|
1773
1949
|
return false;
|
|
1774
1950
|
}
|
|
1775
1951
|
const property = unwrap(callee.property);
|
|
1776
|
-
|
|
1952
|
+
if (property === null || property.type !== import_utils16.AST_NODE_TYPES.Identifier) {
|
|
1953
|
+
return false;
|
|
1954
|
+
}
|
|
1955
|
+
if (property.name === "json") {
|
|
1956
|
+
return true;
|
|
1957
|
+
}
|
|
1958
|
+
const object = unwrap(callee.object);
|
|
1959
|
+
return property.name === "parse" && object !== null && object.type === import_utils16.AST_NODE_TYPES.Identifier && object.name === "JSON";
|
|
1777
1960
|
};
|
|
1778
1961
|
var findVariable2 = (scope, name) => {
|
|
1779
1962
|
let current = scope;
|
|
@@ -1823,18 +2006,18 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
|
|
|
1823
2006
|
meta: {
|
|
1824
2007
|
type: "problem",
|
|
1825
2008
|
docs: {
|
|
1826
|
-
description: "Require Zod (or similar) schema validation on `response.json()` before property access."
|
|
2009
|
+
description: "Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access."
|
|
1827
2010
|
},
|
|
1828
2011
|
schema: [],
|
|
1829
2012
|
messages: {
|
|
1830
|
-
unparsedJsonAccess: "Property access on
|
|
2013
|
+
unparsedJsonAccess: "Property access on an unvalidated payload (`response.json()` / `JSON.parse()`) without a schema parse. Pipe through `XSchema.parse(...)` (Zod) before reading fields."
|
|
1831
2014
|
}
|
|
1832
2015
|
},
|
|
1833
2016
|
defaultOptions: [],
|
|
1834
2017
|
create(context) {
|
|
1835
2018
|
const unvalidatedVariables = /* @__PURE__ */ new Set();
|
|
1836
2019
|
const trackInitializer = (declarator) => {
|
|
1837
|
-
if (!
|
|
2020
|
+
if (!isRawPayloadSource(declarator.init)) return;
|
|
1838
2021
|
const declaredVars = context.sourceCode.getDeclaredVariables(declarator);
|
|
1839
2022
|
const variable = declaredVars[0];
|
|
1840
2023
|
if (variable !== void 0) {
|
|
@@ -1849,7 +2032,7 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
|
|
|
1849
2032
|
return;
|
|
1850
2033
|
}
|
|
1851
2034
|
if (node.id.type === import_utils16.AST_NODE_TYPES.ObjectPattern || node.id.type === import_utils16.AST_NODE_TYPES.ArrayPattern) {
|
|
1852
|
-
if (
|
|
2035
|
+
if (isRawPayloadSource(node.init)) {
|
|
1853
2036
|
context.report({ node: node.id, messageId: "unparsedJsonAccess" });
|
|
1854
2037
|
return;
|
|
1855
2038
|
}
|
|
@@ -1863,7 +2046,7 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
|
|
|
1863
2046
|
if (node.left.type === import_utils16.AST_NODE_TYPES.Identifier) {
|
|
1864
2047
|
const variable = findVariable2(scope, node.left.name);
|
|
1865
2048
|
if (variable === null) return;
|
|
1866
|
-
if (
|
|
2049
|
+
if (isRawPayloadSource(node.right)) {
|
|
1867
2050
|
unvalidatedVariables.add(variable);
|
|
1868
2051
|
} else {
|
|
1869
2052
|
unvalidatedVariables.delete(variable);
|
|
@@ -1871,7 +2054,7 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
|
|
|
1871
2054
|
return;
|
|
1872
2055
|
}
|
|
1873
2056
|
if (node.left.type === import_utils16.AST_NODE_TYPES.ObjectPattern || node.left.type === import_utils16.AST_NODE_TYPES.ArrayPattern) {
|
|
1874
|
-
if (
|
|
2057
|
+
if (isRawPayloadSource(node.right)) {
|
|
1875
2058
|
context.report({
|
|
1876
2059
|
node: node.left,
|
|
1877
2060
|
messageId: "unparsedJsonAccess"
|
|
@@ -1905,7 +2088,7 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
|
|
|
1905
2088
|
MemberExpression(node) {
|
|
1906
2089
|
const scope = context.sourceCode.getScope(node);
|
|
1907
2090
|
const obj = unwrap(node.object);
|
|
1908
|
-
if (
|
|
2091
|
+
if (isRawPayloadSource(obj)) {
|
|
1909
2092
|
const parent = node.parent;
|
|
1910
2093
|
if (parent.type === import_utils16.AST_NODE_TYPES.CallExpression && parent.callee === node && node.property.type === import_utils16.AST_NODE_TYPES.Identifier && (node.property.name === "parse" || node.property.name === "safeParse")) {
|
|
1911
2094
|
return;
|
|
@@ -2416,7 +2599,13 @@ var require_assert_never_default = import_utils20.ESLintUtils.RuleCreator(
|
|
|
2416
2599
|
|
|
2417
2600
|
// src/rules/require-zod-form-validation.ts
|
|
2418
2601
|
var import_utils21 = require("@typescript-eslint/utils");
|
|
2602
|
+
|
|
2603
|
+
// src/rules/_zod.ts
|
|
2604
|
+
var ZOD_PREFIX_RE = /^Z[A-Z]/;
|
|
2605
|
+
var ZOD_SUFFIX_RE = /Schema$/;
|
|
2419
2606
|
var ZOD_SCHEMA_NAME_RE = /Schema$|^Z[A-Z]/;
|
|
2607
|
+
|
|
2608
|
+
// src/rules/require-zod-form-validation.ts
|
|
2420
2609
|
var looksLikeZodSchema = (node) => {
|
|
2421
2610
|
let current = node;
|
|
2422
2611
|
while (true) {
|
|
@@ -2494,14 +2683,38 @@ var require_zod_form_validation_default = import_utils21.ESLintUtils.RuleCreator
|
|
|
2494
2683
|
}
|
|
2495
2684
|
return isFormSourceIdentifier(callee.object);
|
|
2496
2685
|
};
|
|
2686
|
+
const hasZodParseAncestor = (node) => {
|
|
2687
|
+
let parent = node.parent;
|
|
2688
|
+
while (parent !== null && parent !== void 0) {
|
|
2689
|
+
if (isZodParseCall(parent)) return true;
|
|
2690
|
+
parent = parent.parent;
|
|
2691
|
+
}
|
|
2692
|
+
return false;
|
|
2693
|
+
};
|
|
2694
|
+
const isInstanceofNarrowing = (node) => {
|
|
2695
|
+
const parent = node.parent;
|
|
2696
|
+
return parent !== null && parent !== void 0 && parent.type === import_utils21.AST_NODE_TYPES.BinaryExpression && parent.operator === "instanceof" && parent.left === node;
|
|
2697
|
+
};
|
|
2698
|
+
const boundDeclarator = (node) => {
|
|
2699
|
+
const parent = node.parent;
|
|
2700
|
+
if (parent.type === import_utils21.AST_NODE_TYPES.VariableDeclarator && parent.init === node && parent.id.type === import_utils21.AST_NODE_TYPES.Identifier) {
|
|
2701
|
+
return parent;
|
|
2702
|
+
}
|
|
2703
|
+
return null;
|
|
2704
|
+
};
|
|
2705
|
+
const bindingIsValidated = (declarator) => {
|
|
2706
|
+
const variable = context.sourceCode.getDeclaredVariables(declarator)[0];
|
|
2707
|
+
if (variable === void 0) return false;
|
|
2708
|
+
return variable.references.some(
|
|
2709
|
+
(ref) => hasZodParseAncestor(ref.identifier) || isInstanceofNarrowing(ref.identifier)
|
|
2710
|
+
);
|
|
2711
|
+
};
|
|
2497
2712
|
return {
|
|
2498
2713
|
CallExpression(node) {
|
|
2499
2714
|
if (!isFormDataGetCall(node)) return;
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
parent = parent.parent;
|
|
2504
|
-
}
|
|
2715
|
+
if (hasZodParseAncestor(node) || isInstanceofNarrowing(node)) return;
|
|
2716
|
+
const declarator = boundDeclarator(node);
|
|
2717
|
+
if (declarator !== null && bindingIsValidated(declarator)) return;
|
|
2505
2718
|
context.report({
|
|
2506
2719
|
node,
|
|
2507
2720
|
messageId: "missingZodValidation"
|
|
@@ -2513,6 +2726,11 @@ var require_zod_form_validation_default = import_utils21.ESLintUtils.RuleCreator
|
|
|
2513
2726
|
|
|
2514
2727
|
// src/rules/zod-naming-convention.ts
|
|
2515
2728
|
var import_utils22 = require("@typescript-eslint/utils");
|
|
2729
|
+
var CONVENTIONS = {
|
|
2730
|
+
prefix: { test: ZOD_PREFIX_RE, messageId: "zPrefix" },
|
|
2731
|
+
suffix: { test: ZOD_SUFFIX_RE, messageId: "schemaSuffix" },
|
|
2732
|
+
either: { test: ZOD_SCHEMA_NAME_RE, messageId: "zodSchemaName" }
|
|
2733
|
+
};
|
|
2516
2734
|
var calleeChainStartsWithZ = (node) => {
|
|
2517
2735
|
let current = node;
|
|
2518
2736
|
while (current.type === import_utils22.AST_NODE_TYPES.MemberExpression) {
|
|
@@ -2535,15 +2753,29 @@ var zod_naming_convention_default = import_utils22.ESLintUtils.RuleCreator(
|
|
|
2535
2753
|
meta: {
|
|
2536
2754
|
type: "suggestion",
|
|
2537
2755
|
docs: {
|
|
2538
|
-
description: "Enforce Zod
|
|
2756
|
+
description: "Enforce a consistent Zod schema naming convention \u2014 a `Z` prefix (`ZUser`) or a `Schema` suffix (`userSchema`); both are accepted by default."
|
|
2539
2757
|
},
|
|
2540
|
-
schema: [
|
|
2758
|
+
schema: [
|
|
2759
|
+
{
|
|
2760
|
+
type: "object",
|
|
2761
|
+
additionalProperties: false,
|
|
2762
|
+
properties: {
|
|
2763
|
+
convention: {
|
|
2764
|
+
type: "string",
|
|
2765
|
+
enum: ["prefix", "suffix", "either"]
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
],
|
|
2541
2770
|
messages: {
|
|
2542
|
-
zPrefix: "Zod schema names should start with Z"
|
|
2771
|
+
zPrefix: "Zod schema names should start with Z (e.g. `ZUser`)",
|
|
2772
|
+
schemaSuffix: "Zod schema names should end with Schema (e.g. `userSchema`)",
|
|
2773
|
+
zodSchemaName: "Zod schema names should start with Z (`ZUser`) or end with Schema (`userSchema`)"
|
|
2543
2774
|
}
|
|
2544
2775
|
},
|
|
2545
|
-
defaultOptions: [],
|
|
2546
|
-
create(context) {
|
|
2776
|
+
defaultOptions: [{}],
|
|
2777
|
+
create(context, [optionsArg]) {
|
|
2778
|
+
const { test, messageId } = CONVENTIONS[optionsArg?.convention ?? "either"];
|
|
2547
2779
|
return {
|
|
2548
2780
|
VariableDeclarator(node) {
|
|
2549
2781
|
const init = node.init;
|
|
@@ -2553,11 +2785,10 @@ var zod_naming_convention_default = import_utils22.ESLintUtils.RuleCreator(
|
|
|
2553
2785
|
if (callee.type !== import_utils22.AST_NODE_TYPES.MemberExpression) return;
|
|
2554
2786
|
if (!calleeChainStartsWithZ(callee)) return;
|
|
2555
2787
|
if (node.id.type !== import_utils22.AST_NODE_TYPES.Identifier) return;
|
|
2556
|
-
|
|
2557
|
-
if (variableName.startsWith("Z")) return;
|
|
2788
|
+
if (test.test(node.id.name)) return;
|
|
2558
2789
|
context.report({
|
|
2559
2790
|
node: node.id,
|
|
2560
|
-
messageId
|
|
2791
|
+
messageId
|
|
2561
2792
|
});
|
|
2562
2793
|
}
|
|
2563
2794
|
};
|
|
@@ -2774,23 +3005,306 @@ var no_cors_wildcard_with_credentials_default = import_utils23.ESLintUtils.RuleC
|
|
|
2774
3005
|
}
|
|
2775
3006
|
});
|
|
2776
3007
|
|
|
2777
|
-
// src/rules/no-
|
|
3008
|
+
// src/rules/no-silent-promise-catch.ts
|
|
2778
3009
|
var import_utils24 = require("@typescript-eslint/utils");
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
3010
|
+
|
|
3011
|
+
// src/rules/_paths.ts
|
|
3012
|
+
var SCRIPT_FILE_RE = /([\\/]scripts[\\/])|(\.mjs$)/;
|
|
3013
|
+
function isTestFile(filename) {
|
|
3014
|
+
const normalized = filename.replaceAll("\\", "/");
|
|
3015
|
+
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
3016
|
+
if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(base)) {
|
|
3017
|
+
return true;
|
|
3018
|
+
}
|
|
3019
|
+
return /(^|\/)(tests?|__tests__|__mocks__|fixtures)\//.test(normalized);
|
|
3020
|
+
}
|
|
3021
|
+
function isScriptFile(filename) {
|
|
3022
|
+
return SCRIPT_FILE_RE.test(filename);
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// src/rules/no-silent-promise-catch.ts
|
|
3026
|
+
function isBodyParseCall(node) {
|
|
3027
|
+
return node.type === import_utils24.AST_NODE_TYPES.CallExpression && node.arguments.length === 0 && node.callee.type === import_utils24.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils24.AST_NODE_TYPES.Identifier && (node.callee.property.name === "json" || node.callee.property.name === "text");
|
|
3028
|
+
}
|
|
3029
|
+
function isSilentExpression(node) {
|
|
3030
|
+
switch (node.type) {
|
|
3031
|
+
case import_utils24.AST_NODE_TYPES.Literal:
|
|
3032
|
+
return !("regex" in node);
|
|
3033
|
+
case import_utils24.AST_NODE_TYPES.Identifier:
|
|
3034
|
+
return node.name === "undefined";
|
|
3035
|
+
case import_utils24.AST_NODE_TYPES.UnaryExpression:
|
|
3036
|
+
return node.operator === "void" && node.argument.type === import_utils24.AST_NODE_TYPES.Literal;
|
|
3037
|
+
case import_utils24.AST_NODE_TYPES.ObjectExpression:
|
|
3038
|
+
return node.properties.length === 0;
|
|
3039
|
+
case import_utils24.AST_NODE_TYPES.ArrayExpression:
|
|
3040
|
+
return node.elements.length === 0;
|
|
3041
|
+
case import_utils24.AST_NODE_TYPES.TSAsExpression:
|
|
3042
|
+
return isSilentExpression(node.expression);
|
|
3043
|
+
default:
|
|
3044
|
+
return false;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
function isSilentHandler(handler) {
|
|
3048
|
+
const body = handler.body;
|
|
3049
|
+
if (body.type !== import_utils24.AST_NODE_TYPES.BlockStatement) {
|
|
3050
|
+
return isSilentExpression(body);
|
|
3051
|
+
}
|
|
3052
|
+
if (body.body.length === 0) {
|
|
3053
|
+
return true;
|
|
3054
|
+
}
|
|
3055
|
+
if (body.body.length === 1) {
|
|
3056
|
+
const only = body.body[0];
|
|
3057
|
+
if (only !== void 0 && only.type === import_utils24.AST_NODE_TYPES.ReturnStatement) {
|
|
3058
|
+
return only.argument === null || isSilentExpression(only.argument);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
return false;
|
|
3062
|
+
}
|
|
3063
|
+
var no_silent_promise_catch_default = import_utils24.ESLintUtils.RuleCreator(
|
|
3064
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3065
|
+
)({
|
|
3066
|
+
name: "no-silent-promise-catch",
|
|
3067
|
+
meta: {
|
|
3068
|
+
type: "problem",
|
|
3069
|
+
docs: {
|
|
3070
|
+
description: "Disallow `.catch()` handlers that silently swallow the rejection (e.g. `.catch(() => null)`); log, rethrow, or handle the error."
|
|
3071
|
+
},
|
|
3072
|
+
schema: [],
|
|
3073
|
+
messages: {
|
|
3074
|
+
silentCatch: "This `.catch()` swallows the rejection without logging, rethrowing, or handling it \u2014 failures become invisible and callers get an indistinguishable sentinel. Log the error (and only then map to a fallback), or let it propagate."
|
|
3075
|
+
}
|
|
3076
|
+
},
|
|
3077
|
+
defaultOptions: [],
|
|
3078
|
+
create(context) {
|
|
3079
|
+
if (isTestFile(context.filename)) {
|
|
3080
|
+
return {};
|
|
3081
|
+
}
|
|
3082
|
+
return {
|
|
3083
|
+
CallExpression(node) {
|
|
3084
|
+
if (node.callee.type !== import_utils24.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.property.type !== import_utils24.AST_NODE_TYPES.Identifier || node.callee.property.name !== "catch") {
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
if (isBodyParseCall(node.callee.object)) {
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
if (node.arguments.length !== 1) {
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
const handler = node.arguments[0];
|
|
3094
|
+
if (handler === void 0 || handler.type !== import_utils24.AST_NODE_TYPES.ArrowFunctionExpression && handler.type !== import_utils24.AST_NODE_TYPES.FunctionExpression) {
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
if (isSilentHandler(handler)) {
|
|
3098
|
+
context.report({ node, messageId: "silentCatch" });
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
});
|
|
3104
|
+
|
|
3105
|
+
// src/rules/require-fetch-timeout.ts
|
|
3106
|
+
var import_utils25 = require("@typescript-eslint/utils");
|
|
3107
|
+
var GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
|
|
3108
|
+
"globalThis",
|
|
3109
|
+
"window",
|
|
3110
|
+
"self"
|
|
2784
3111
|
]);
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
3112
|
+
function matchesAnyPattern2(filename, patterns) {
|
|
3113
|
+
for (const pattern of patterns) {
|
|
3114
|
+
const regexSource = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::DOUBLESTAR::").replace(/\*/g, "[^/\\\\]*").replace(/::DOUBLESTAR::/g, ".*");
|
|
3115
|
+
if (new RegExp(`^${regexSource}$`).test(filename)) {
|
|
3116
|
+
return true;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
return false;
|
|
3120
|
+
}
|
|
3121
|
+
function initProvablyLacksSignal(init) {
|
|
3122
|
+
if (init.type !== import_utils25.AST_NODE_TYPES.ObjectExpression) {
|
|
3123
|
+
return false;
|
|
3124
|
+
}
|
|
3125
|
+
for (const prop of init.properties) {
|
|
3126
|
+
if (prop.type === import_utils25.AST_NODE_TYPES.SpreadElement) {
|
|
3127
|
+
return false;
|
|
3128
|
+
}
|
|
3129
|
+
if (prop.key.type === import_utils25.AST_NODE_TYPES.Identifier && prop.key.name === "signal" || prop.key.type === import_utils25.AST_NODE_TYPES.Literal && prop.key.value === "signal") {
|
|
3130
|
+
return false;
|
|
3131
|
+
}
|
|
3132
|
+
if (prop.computed) {
|
|
3133
|
+
return false;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
return true;
|
|
3137
|
+
}
|
|
3138
|
+
function isStringish(node) {
|
|
3139
|
+
return node.type === import_utils25.AST_NODE_TYPES.Literal && typeof node.value === "string" || node.type === import_utils25.AST_NODE_TYPES.TemplateLiteral;
|
|
3140
|
+
}
|
|
3141
|
+
var require_fetch_timeout_default = import_utils25.ESLintUtils.RuleCreator(
|
|
3142
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3143
|
+
)({
|
|
3144
|
+
name: "require-fetch-timeout",
|
|
3145
|
+
meta: {
|
|
3146
|
+
type: "problem",
|
|
3147
|
+
docs: {
|
|
3148
|
+
description: "Require an abort `signal` (e.g. `AbortSignal.timeout(ms)`) on global `fetch()` calls so stalled upstreams cannot hang the caller forever."
|
|
3149
|
+
},
|
|
3150
|
+
schema: [
|
|
3151
|
+
{
|
|
3152
|
+
type: "object",
|
|
3153
|
+
additionalProperties: false,
|
|
3154
|
+
properties: {
|
|
3155
|
+
allowIn: {
|
|
3156
|
+
description: "Glob patterns for wrapper modules exempt from the rule. Matched against the ABSOLUTE file path, so anchor with a `**/` prefix (e.g. `**/http-client.ts`).",
|
|
3157
|
+
type: "array",
|
|
3158
|
+
items: { type: "string" }
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
],
|
|
3163
|
+
messages: {
|
|
3164
|
+
missingSignal: "This `fetch()` has no abort `signal` \u2014 a stalled upstream will hang it forever. Pass `{ signal: AbortSignal.timeout(ms) }` or a signal from an AbortController."
|
|
3165
|
+
}
|
|
3166
|
+
},
|
|
3167
|
+
defaultOptions: [{}],
|
|
3168
|
+
create(context, [optionsArg]) {
|
|
3169
|
+
if (isTestFile(context.filename) || isScriptFile(context.filename)) {
|
|
3170
|
+
return {};
|
|
3171
|
+
}
|
|
3172
|
+
const allowIn = optionsArg?.allowIn ?? [];
|
|
3173
|
+
if (allowIn.length > 0 && matchesAnyPattern2(context.filename, allowIn)) {
|
|
3174
|
+
return {};
|
|
3175
|
+
}
|
|
3176
|
+
function resolvesToGlobal(identifier) {
|
|
3177
|
+
const scope = context.sourceCode.getScope(identifier);
|
|
3178
|
+
const variable = import_utils25.ASTUtils.findVariable(scope, identifier.name);
|
|
3179
|
+
return variable === null || variable.defs.length === 0;
|
|
3180
|
+
}
|
|
3181
|
+
function isGlobalFetchCall2(callee) {
|
|
3182
|
+
if (callee.type === import_utils25.AST_NODE_TYPES.Identifier) {
|
|
3183
|
+
return callee.name === "fetch" && resolvesToGlobal(callee);
|
|
3184
|
+
}
|
|
3185
|
+
return callee.type === import_utils25.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils25.AST_NODE_TYPES.Identifier && callee.property.name === "fetch" && callee.object.type === import_utils25.AST_NODE_TYPES.Identifier && GLOBAL_OBJECTS.has(callee.object.name) && resolvesToGlobal(callee.object);
|
|
3186
|
+
}
|
|
3187
|
+
return {
|
|
3188
|
+
CallExpression(node) {
|
|
3189
|
+
if (!isGlobalFetchCall2(node.callee)) {
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
const [first, init] = node.arguments;
|
|
3193
|
+
if (node.arguments.length === 1 && first !== void 0 && !isStringish(first)) {
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
if (init === void 0 || initProvablyLacksSignal(init)) {
|
|
3197
|
+
context.report({ node, messageId: "missingSignal" });
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
});
|
|
3203
|
+
|
|
3204
|
+
// src/rules/require-schema-validate-search.ts
|
|
3205
|
+
var import_utils26 = require("@typescript-eslint/utils");
|
|
3206
|
+
var VALIDATOR_METHODS = /* @__PURE__ */ new Set([
|
|
3207
|
+
"parse",
|
|
3208
|
+
"safeParse",
|
|
3209
|
+
"decode"
|
|
3210
|
+
]);
|
|
3211
|
+
function isConstTypeAnnotation(typeAnnotation) {
|
|
3212
|
+
return typeAnnotation.type === import_utils26.AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === import_utils26.AST_NODE_TYPES.Identifier && typeAnnotation.typeName.name === "const";
|
|
3213
|
+
}
|
|
3214
|
+
function isValidatorCall(node) {
|
|
3215
|
+
return node.callee.type === import_utils26.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils26.AST_NODE_TYPES.Identifier && VALIDATOR_METHODS.has(node.callee.property.name);
|
|
3216
|
+
}
|
|
3217
|
+
function findCastExpression(node, insideValidatorArg) {
|
|
3218
|
+
if ((node.type === import_utils26.AST_NODE_TYPES.TSAsExpression || node.type === import_utils26.AST_NODE_TYPES.TSTypeAssertion) && !isConstTypeAnnotation(node.typeAnnotation) && !insideValidatorArg) {
|
|
3219
|
+
return node;
|
|
3220
|
+
}
|
|
3221
|
+
if (node.type === import_utils26.AST_NODE_TYPES.CallExpression && isValidatorCall(node)) {
|
|
3222
|
+
const inCallee = findCastExpression(node.callee, insideValidatorArg);
|
|
3223
|
+
if (inCallee !== null) {
|
|
3224
|
+
return inCallee;
|
|
3225
|
+
}
|
|
3226
|
+
for (const arg of node.arguments) {
|
|
3227
|
+
const found = findCastExpression(arg, true);
|
|
3228
|
+
if (found !== null) {
|
|
3229
|
+
return found;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
return null;
|
|
3233
|
+
}
|
|
3234
|
+
for (const key of Object.keys(node)) {
|
|
3235
|
+
if (key === "parent") {
|
|
3236
|
+
continue;
|
|
3237
|
+
}
|
|
3238
|
+
const value = node[key];
|
|
3239
|
+
const children = Array.isArray(value) ? value : [value];
|
|
3240
|
+
for (const child of children) {
|
|
3241
|
+
if (child !== null && typeof child === "object" && "type" in child && typeof child.type === "string") {
|
|
3242
|
+
const found = findCastExpression(
|
|
3243
|
+
child,
|
|
3244
|
+
insideValidatorArg
|
|
3245
|
+
);
|
|
3246
|
+
if (found !== null) {
|
|
3247
|
+
return found;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
return null;
|
|
3253
|
+
}
|
|
3254
|
+
var require_schema_validate_search_default = import_utils26.ESLintUtils.RuleCreator(
|
|
3255
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3256
|
+
)({
|
|
3257
|
+
name: "require-schema-validate-search",
|
|
3258
|
+
meta: {
|
|
3259
|
+
type: "problem",
|
|
3260
|
+
docs: {
|
|
3261
|
+
description: "Disallow `as` casts inside hand-rolled `validateSearch` functions; use a schema validator (e.g. zodValidator) so search params are validated at runtime."
|
|
3262
|
+
},
|
|
3263
|
+
schema: [],
|
|
3264
|
+
messages: {
|
|
3265
|
+
castInValidateSearch: "This `validateSearch` asserts the search-param shape with `as` instead of validating it \u2014 malformed query params flow through typed as clean data. Use a schema validator (e.g. `zodValidator(searchSchema)` or `searchSchema.parse`) instead of casting."
|
|
3266
|
+
}
|
|
3267
|
+
},
|
|
3268
|
+
defaultOptions: [],
|
|
3269
|
+
create(context) {
|
|
3270
|
+
if (isTestFile(context.filename)) {
|
|
3271
|
+
return {};
|
|
3272
|
+
}
|
|
3273
|
+
return {
|
|
3274
|
+
Property(node) {
|
|
3275
|
+
const isValidateSearchKey = !node.computed && node.key.type === import_utils26.AST_NODE_TYPES.Identifier && node.key.name === "validateSearch" || node.key.type === import_utils26.AST_NODE_TYPES.Literal && node.key.value === "validateSearch";
|
|
3276
|
+
if (!isValidateSearchKey) {
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
if (node.value.type !== import_utils26.AST_NODE_TYPES.ArrowFunctionExpression && node.value.type !== import_utils26.AST_NODE_TYPES.FunctionExpression) {
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
const cast = findCastExpression(node.value.body, false);
|
|
3283
|
+
if (cast !== null) {
|
|
3284
|
+
context.report({ node: cast, messageId: "castInValidateSearch" });
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
});
|
|
3290
|
+
|
|
3291
|
+
// src/rules/no-fat-try-blocks.ts
|
|
3292
|
+
var import_utils27 = require("@typescript-eslint/utils");
|
|
3293
|
+
var MAX_TRY_BODY_STATEMENTS = 3;
|
|
3294
|
+
var NESTED_FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
3295
|
+
import_utils27.AST_NODE_TYPES.FunctionDeclaration,
|
|
3296
|
+
import_utils27.AST_NODE_TYPES.FunctionExpression,
|
|
3297
|
+
import_utils27.AST_NODE_TYPES.ArrowFunctionExpression
|
|
3298
|
+
]);
|
|
3299
|
+
var PURE_METHODS = /* @__PURE__ */ new Set([
|
|
3300
|
+
"map",
|
|
3301
|
+
"filter",
|
|
3302
|
+
"forEach",
|
|
3303
|
+
"reduce",
|
|
3304
|
+
"reduceRight",
|
|
3305
|
+
"find",
|
|
3306
|
+
"findIndex",
|
|
3307
|
+
"findLast",
|
|
2794
3308
|
"findLastIndex",
|
|
2795
3309
|
"some",
|
|
2796
3310
|
"every",
|
|
@@ -2878,20 +3392,20 @@ function isNode4(value) {
|
|
|
2878
3392
|
}
|
|
2879
3393
|
function isPureCall(node) {
|
|
2880
3394
|
const callee = node.callee;
|
|
2881
|
-
if (callee.type !==
|
|
3395
|
+
if (callee.type !== import_utils27.AST_NODE_TYPES.MemberExpression) {
|
|
2882
3396
|
return false;
|
|
2883
3397
|
}
|
|
2884
3398
|
const property = callee.property;
|
|
2885
|
-
if (property.type !==
|
|
3399
|
+
if (property.type !== import_utils27.AST_NODE_TYPES.Identifier) {
|
|
2886
3400
|
return false;
|
|
2887
3401
|
}
|
|
2888
|
-
if (callee.object.type ===
|
|
3402
|
+
if (callee.object.type === import_utils27.AST_NODE_TYPES.Identifier && PURE_NAMESPACES.has(callee.object.name)) {
|
|
2889
3403
|
return true;
|
|
2890
3404
|
}
|
|
2891
3405
|
return PURE_METHODS.has(property.name);
|
|
2892
3406
|
}
|
|
2893
3407
|
function isPureNew(node) {
|
|
2894
|
-
return node.callee.type ===
|
|
3408
|
+
return node.callee.type === import_utils27.AST_NODE_TYPES.Identifier && PURE_CONSTRUCTORS.has(node.callee.name);
|
|
2895
3409
|
}
|
|
2896
3410
|
function subtreeMatches(stmt, predicate) {
|
|
2897
3411
|
let found = false;
|
|
@@ -2928,25 +3442,34 @@ function subtreeMatches(stmt, predicate) {
|
|
|
2928
3442
|
visit(stmt);
|
|
2929
3443
|
return found;
|
|
2930
3444
|
}
|
|
2931
|
-
var hasAwait = (
|
|
2932
|
-
var hasThrowingCallOrNew = (
|
|
2933
|
-
|
|
2934
|
-
(n) => n.type ===
|
|
3445
|
+
var hasAwait = (node) => subtreeMatches(node, (n) => n.type === import_utils27.AST_NODE_TYPES.AwaitExpression);
|
|
3446
|
+
var hasThrowingCallOrNew = (node) => subtreeMatches(
|
|
3447
|
+
node,
|
|
3448
|
+
(n) => n.type === import_utils27.AST_NODE_TYPES.CallExpression && !isPureCall(n) || n.type === import_utils27.AST_NODE_TYPES.NewExpression && !isPureNew(n)
|
|
2935
3449
|
);
|
|
2936
3450
|
function unwrap2(expr) {
|
|
2937
3451
|
let current = expr;
|
|
2938
|
-
while (current.type ===
|
|
3452
|
+
while (current.type === import_utils27.AST_NODE_TYPES.ChainExpression || current.type === import_utils27.AST_NODE_TYPES.TSNonNullExpression) {
|
|
2939
3453
|
current = current.expression;
|
|
2940
3454
|
}
|
|
2941
3455
|
return current;
|
|
2942
3456
|
}
|
|
3457
|
+
function isBareCallStatement(stmt) {
|
|
3458
|
+
return stmt.type === import_utils27.AST_NODE_TYPES.ExpressionStatement && unwrap2(stmt.expression).type === import_utils27.AST_NODE_TYPES.CallExpression;
|
|
3459
|
+
}
|
|
2943
3460
|
function canThrow(stmt) {
|
|
2944
3461
|
if (hasAwait(stmt)) {
|
|
2945
3462
|
return true;
|
|
2946
3463
|
}
|
|
2947
|
-
if (
|
|
3464
|
+
if (isBareCallStatement(stmt)) {
|
|
2948
3465
|
return false;
|
|
2949
3466
|
}
|
|
3467
|
+
if (stmt.type === import_utils27.AST_NODE_TYPES.BlockStatement) {
|
|
3468
|
+
return stmt.body.some(canThrow);
|
|
3469
|
+
}
|
|
3470
|
+
if (stmt.type === import_utils27.AST_NODE_TYPES.IfStatement) {
|
|
3471
|
+
return hasThrowingCallOrNew(stmt.test) || canThrow(stmt.consequent) || stmt.alternate !== null && canThrow(stmt.alternate);
|
|
3472
|
+
}
|
|
2950
3473
|
return hasThrowingCallOrNew(stmt);
|
|
2951
3474
|
}
|
|
2952
3475
|
function handlerRethrows(handler) {
|
|
@@ -2955,9 +3478,9 @@ function handlerRethrows(handler) {
|
|
|
2955
3478
|
}
|
|
2956
3479
|
const body = handler.body.body;
|
|
2957
3480
|
const last = body[body.length - 1];
|
|
2958
|
-
return last !== void 0 && last.type ===
|
|
3481
|
+
return last !== void 0 && last.type === import_utils27.AST_NODE_TYPES.ThrowStatement;
|
|
2959
3482
|
}
|
|
2960
|
-
var no_fat_try_blocks_default =
|
|
3483
|
+
var no_fat_try_blocks_default = import_utils27.ESLintUtils.RuleCreator(
|
|
2961
3484
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
2962
3485
|
)({
|
|
2963
3486
|
name: "no-fat-try-blocks",
|
|
@@ -2998,30 +3521,9 @@ var no_fat_try_blocks_default = import_utils24.ESLintUtils.RuleCreator(
|
|
|
2998
3521
|
});
|
|
2999
3522
|
|
|
3000
3523
|
// src/rules/no-secret-in-log.ts
|
|
3001
|
-
var
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
"info",
|
|
3005
|
-
"warn",
|
|
3006
|
-
"warning",
|
|
3007
|
-
"error",
|
|
3008
|
-
"exception",
|
|
3009
|
-
"critical",
|
|
3010
|
-
"trace",
|
|
3011
|
-
"log",
|
|
3012
|
-
"fatal",
|
|
3013
|
-
"success"
|
|
3014
|
-
]);
|
|
3015
|
-
var LOGGER_NAMES2 = /* @__PURE__ */ new Set([
|
|
3016
|
-
"logger",
|
|
3017
|
-
"log",
|
|
3018
|
-
"logging",
|
|
3019
|
-
"loguru",
|
|
3020
|
-
"console",
|
|
3021
|
-
"_logger",
|
|
3022
|
-
"_log"
|
|
3023
|
-
]);
|
|
3024
|
-
var LOGGER_FACTORIES = /* @__PURE__ */ new Set(["getlogger", "get_logger"]);
|
|
3524
|
+
var import_utils28 = require("@typescript-eslint/utils");
|
|
3525
|
+
|
|
3526
|
+
// src/rules/_secret_names.ts
|
|
3025
3527
|
var SECRET_WORDS = /* @__PURE__ */ new Set([
|
|
3026
3528
|
"token",
|
|
3027
3529
|
"secret",
|
|
@@ -3037,7 +3539,8 @@ var SECRET_WORDS = /* @__PURE__ */ new Set([
|
|
|
3037
3539
|
"hmac",
|
|
3038
3540
|
"digest",
|
|
3039
3541
|
"hash",
|
|
3040
|
-
"apikey"
|
|
3542
|
+
"apikey",
|
|
3543
|
+
"bearer"
|
|
3041
3544
|
]);
|
|
3042
3545
|
var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
|
|
3043
3546
|
"count",
|
|
@@ -3060,10 +3563,113 @@ var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
|
|
|
3060
3563
|
"valid",
|
|
3061
3564
|
"invalid",
|
|
3062
3565
|
"exists",
|
|
3566
|
+
"type",
|
|
3567
|
+
"types"
|
|
3568
|
+
]);
|
|
3569
|
+
var DESCRIPTOR_WORDS = /* @__PURE__ */ new Set([
|
|
3063
3570
|
"type",
|
|
3064
3571
|
"types",
|
|
3065
3572
|
"name",
|
|
3066
3573
|
"names",
|
|
3574
|
+
"id",
|
|
3575
|
+
"ids",
|
|
3576
|
+
"kind",
|
|
3577
|
+
"kinds"
|
|
3578
|
+
]);
|
|
3579
|
+
var CATEGORY_WORDS = /* @__PURE__ */ new Set(["type", "types", "kind", "kinds"]);
|
|
3580
|
+
var FLAG_PREFIXES = /* @__PURE__ */ new Set([
|
|
3581
|
+
"is",
|
|
3582
|
+
"has",
|
|
3583
|
+
"was",
|
|
3584
|
+
"are",
|
|
3585
|
+
"can",
|
|
3586
|
+
"should"
|
|
3587
|
+
]);
|
|
3588
|
+
var AUTH_WORDS = /* @__PURE__ */ new Set([
|
|
3589
|
+
"token",
|
|
3590
|
+
"secret",
|
|
3591
|
+
"secrets",
|
|
3592
|
+
"password",
|
|
3593
|
+
"passwd",
|
|
3594
|
+
"passwords",
|
|
3595
|
+
"jwt",
|
|
3596
|
+
"credential",
|
|
3597
|
+
"credentials",
|
|
3598
|
+
"authorization",
|
|
3599
|
+
"signature",
|
|
3600
|
+
"hmac",
|
|
3601
|
+
"apikey",
|
|
3602
|
+
"bearer"
|
|
3603
|
+
]);
|
|
3604
|
+
var CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+/g;
|
|
3605
|
+
var SEGMENT_RE = /[^A-Za-z0-9]+/;
|
|
3606
|
+
function tokenize(identifier) {
|
|
3607
|
+
const tokens = [];
|
|
3608
|
+
for (const segment of identifier.split(SEGMENT_RE)) {
|
|
3609
|
+
if (!segment) {
|
|
3610
|
+
continue;
|
|
3611
|
+
}
|
|
3612
|
+
tokens.push(segment.toLowerCase());
|
|
3613
|
+
for (const part of segment.match(CAMEL_RE) ?? []) {
|
|
3614
|
+
tokens.push(part.toLowerCase());
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
return tokens;
|
|
3618
|
+
}
|
|
3619
|
+
function leadingWord(identifier) {
|
|
3620
|
+
for (const segment of identifier.split(SEGMENT_RE)) {
|
|
3621
|
+
if (segment) {
|
|
3622
|
+
return (segment.match(CAMEL_RE) ?? [segment])[0]?.toLowerCase();
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
return void 0;
|
|
3626
|
+
}
|
|
3627
|
+
function hasApiKey(tokens) {
|
|
3628
|
+
for (let i = 0; i + 1 < tokens.length; i++) {
|
|
3629
|
+
if (tokens[i] === "api" && tokens[i + 1] === "key") {
|
|
3630
|
+
return true;
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
return false;
|
|
3634
|
+
}
|
|
3635
|
+
function isSecretName(identifier, innocuous = INNOCUOUS_WORDS) {
|
|
3636
|
+
const tokens = tokenize(identifier);
|
|
3637
|
+
const last = tokens.at(-1);
|
|
3638
|
+
if (last !== void 0 && innocuous.has(last)) {
|
|
3639
|
+
return false;
|
|
3640
|
+
}
|
|
3641
|
+
if (tokens.some((tok) => SECRET_WORDS.has(tok))) {
|
|
3642
|
+
return true;
|
|
3643
|
+
}
|
|
3644
|
+
return hasApiKey(tokens);
|
|
3645
|
+
}
|
|
3646
|
+
function isAuthSecretName(identifier) {
|
|
3647
|
+
if (!isSecretName(identifier)) {
|
|
3648
|
+
return false;
|
|
3649
|
+
}
|
|
3650
|
+
const tokens = tokenize(identifier);
|
|
3651
|
+
const first = leadingWord(identifier);
|
|
3652
|
+
if (first !== void 0 && FLAG_PREFIXES.has(first)) {
|
|
3653
|
+
return false;
|
|
3654
|
+
}
|
|
3655
|
+
const last = tokens.at(-1);
|
|
3656
|
+
if (last !== void 0 && DESCRIPTOR_WORDS.has(last)) {
|
|
3657
|
+
return false;
|
|
3658
|
+
}
|
|
3659
|
+
if (tokens.some((tok) => CATEGORY_WORDS.has(tok))) {
|
|
3660
|
+
return false;
|
|
3661
|
+
}
|
|
3662
|
+
if (tokens.some((tok) => AUTH_WORDS.has(tok))) {
|
|
3663
|
+
return true;
|
|
3664
|
+
}
|
|
3665
|
+
return hasApiKey(tokens);
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
// src/rules/no-secret-in-log.ts
|
|
3669
|
+
var LOG_INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
|
|
3670
|
+
...INNOCUOUS_WORDS,
|
|
3671
|
+
"name",
|
|
3672
|
+
"names",
|
|
3067
3673
|
"label",
|
|
3068
3674
|
"labels",
|
|
3069
3675
|
"title",
|
|
@@ -3107,80 +3713,18 @@ var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
|
|
|
3107
3713
|
]);
|
|
3108
3714
|
var REDACTION_RE = /prefix|suffix|redact|mask|hash|hint|_len|length/i;
|
|
3109
3715
|
var WHOLE_TOKEN_REDACTION_MARKERS = /* @__PURE__ */ new Set(["tag"]);
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
const tokens = [];
|
|
3114
|
-
for (const segment of identifier.split(SEGMENT_RE)) {
|
|
3115
|
-
if (!segment) {
|
|
3116
|
-
continue;
|
|
3117
|
-
}
|
|
3118
|
-
tokens.push(segment.toLowerCase());
|
|
3119
|
-
for (const part of segment.match(CAMEL_RE) ?? []) {
|
|
3120
|
-
tokens.push(part.toLowerCase());
|
|
3121
|
-
}
|
|
3716
|
+
function isSecretKeyword(name) {
|
|
3717
|
+
if (REDACTION_RE.test(name)) {
|
|
3718
|
+
return false;
|
|
3122
3719
|
}
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
function hasApiKey(tokens) {
|
|
3126
|
-
for (let i = 0; i + 1 < tokens.length; i++) {
|
|
3127
|
-
if (tokens[i] === "api" && tokens[i + 1] === "key") {
|
|
3128
|
-
return true;
|
|
3129
|
-
}
|
|
3720
|
+
if (tokenize(name).some((tok) => WHOLE_TOKEN_REDACTION_MARKERS.has(tok))) {
|
|
3721
|
+
return false;
|
|
3130
3722
|
}
|
|
3131
|
-
return
|
|
3723
|
+
return isSecretName(name, LOG_INNOCUOUS_WORDS);
|
|
3132
3724
|
}
|
|
3133
|
-
function
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
if (last !== void 0 && INNOCUOUS_WORDS.has(last)) {
|
|
3137
|
-
return false;
|
|
3138
|
-
}
|
|
3139
|
-
if (tokens.some((tok) => SECRET_WORDS.has(tok))) {
|
|
3140
|
-
return true;
|
|
3141
|
-
}
|
|
3142
|
-
return hasApiKey(tokens);
|
|
3143
|
-
}
|
|
3144
|
-
function isSecretKeyword(name) {
|
|
3145
|
-
if (REDACTION_RE.test(name)) {
|
|
3146
|
-
return false;
|
|
3147
|
-
}
|
|
3148
|
-
if (tokenize(name).some((tok) => WHOLE_TOKEN_REDACTION_MARKERS.has(tok))) {
|
|
3149
|
-
return false;
|
|
3150
|
-
}
|
|
3151
|
-
return isSecretName(name);
|
|
3152
|
-
}
|
|
3153
|
-
function isLoggerExpr(expr) {
|
|
3154
|
-
switch (expr.type) {
|
|
3155
|
-
case "Identifier":
|
|
3156
|
-
return LOGGER_NAMES2.has(expr.name.toLowerCase());
|
|
3157
|
-
case "MemberExpression": {
|
|
3158
|
-
const { property, object } = expr;
|
|
3159
|
-
if (!expr.computed && property.type === "Identifier") {
|
|
3160
|
-
const lowered = property.name.toLowerCase();
|
|
3161
|
-
if (LOGGER_NAMES2.has(lowered) || LOGGER_FACTORIES.has(lowered)) {
|
|
3162
|
-
return true;
|
|
3163
|
-
}
|
|
3164
|
-
}
|
|
3165
|
-
return isLoggerExpr(object);
|
|
3166
|
-
}
|
|
3167
|
-
case "CallExpression": {
|
|
3168
|
-
const callee = expr.callee;
|
|
3169
|
-
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && LOGGER_FACTORIES.has(callee.property.name.toLowerCase())) {
|
|
3170
|
-
return true;
|
|
3171
|
-
}
|
|
3172
|
-
if (callee.type !== "Super") {
|
|
3173
|
-
return isLoggerExpr(callee);
|
|
3174
|
-
}
|
|
3175
|
-
return false;
|
|
3176
|
-
}
|
|
3177
|
-
default:
|
|
3178
|
-
return false;
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
function isRawSecretValue(prop) {
|
|
3182
|
-
if (prop.shorthand) {
|
|
3183
|
-
return true;
|
|
3725
|
+
function isRawSecretValue(prop) {
|
|
3726
|
+
if (prop.shorthand) {
|
|
3727
|
+
return true;
|
|
3184
3728
|
}
|
|
3185
3729
|
return prop.value.type === "Identifier" || prop.value.type === "MemberExpression";
|
|
3186
3730
|
}
|
|
@@ -3196,7 +3740,7 @@ function propertyKeyName2(prop) {
|
|
|
3196
3740
|
}
|
|
3197
3741
|
return null;
|
|
3198
3742
|
}
|
|
3199
|
-
var no_secret_in_log_default =
|
|
3743
|
+
var no_secret_in_log_default = import_utils28.ESLintUtils.RuleCreator(
|
|
3200
3744
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3201
3745
|
)({
|
|
3202
3746
|
name: "no-secret-in-log",
|
|
@@ -3205,20 +3749,23 @@ var no_secret_in_log_default = import_utils25.ESLintUtils.RuleCreator(
|
|
|
3205
3749
|
docs: {
|
|
3206
3750
|
description: "Disallow passing a secret-named value to a logging call; it leaks to log sinks. Redact or omit it."
|
|
3207
3751
|
},
|
|
3208
|
-
schema: [
|
|
3752
|
+
schema: [
|
|
3753
|
+
{
|
|
3754
|
+
type: "object",
|
|
3755
|
+
additionalProperties: false,
|
|
3756
|
+
properties: { ...LOGGING_OPTION_PROPERTIES }
|
|
3757
|
+
}
|
|
3758
|
+
],
|
|
3209
3759
|
messages: {
|
|
3210
3760
|
noSecretInLog: "Secret `{{name}}` passed to a logging call leaks it to log sinks. Redact (e.g. `{{name}}Prefix: {{name}}.slice(0, 6)`) or omit it."
|
|
3211
3761
|
}
|
|
3212
3762
|
},
|
|
3213
|
-
defaultOptions: [],
|
|
3214
|
-
create(context) {
|
|
3763
|
+
defaultOptions: [{}],
|
|
3764
|
+
create(context, [loggingOptions]) {
|
|
3765
|
+
const matcher = createLogMatcher(loggingOptions);
|
|
3215
3766
|
return {
|
|
3216
3767
|
CallExpression(node) {
|
|
3217
|
-
|
|
3218
|
-
if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || !LOG_METHODS2.has(callee.property.name)) {
|
|
3219
|
-
return;
|
|
3220
|
-
}
|
|
3221
|
-
if (!isLoggerExpr(callee.object)) {
|
|
3768
|
+
if (!matcher.isLoggingCall(node)) {
|
|
3222
3769
|
return;
|
|
3223
3770
|
}
|
|
3224
3771
|
for (const arg of node.arguments) {
|
|
@@ -3264,15 +3811,15 @@ var no_secret_in_log_default = import_utils25.ESLintUtils.RuleCreator(
|
|
|
3264
3811
|
});
|
|
3265
3812
|
|
|
3266
3813
|
// src/rules/no-unsafe-cast.ts
|
|
3267
|
-
var
|
|
3268
|
-
var
|
|
3814
|
+
var import_utils29 = require("@typescript-eslint/utils");
|
|
3815
|
+
var import_utils30 = require("@typescript-eslint/utils");
|
|
3269
3816
|
function isAnyAnnotation(node) {
|
|
3270
|
-
return node.type ===
|
|
3817
|
+
return node.type === import_utils30.AST_NODE_TYPES.TSAnyKeyword;
|
|
3271
3818
|
}
|
|
3272
3819
|
function isConstAssertion(typeAnnotation) {
|
|
3273
|
-
return typeAnnotation.type ===
|
|
3820
|
+
return typeAnnotation.type === import_utils30.AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === import_utils30.AST_NODE_TYPES.Identifier && typeAnnotation.typeName.name === "const";
|
|
3274
3821
|
}
|
|
3275
|
-
var no_unsafe_cast_default =
|
|
3822
|
+
var no_unsafe_cast_default = import_utils29.ESLintUtils.RuleCreator(
|
|
3276
3823
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3277
3824
|
)({
|
|
3278
3825
|
name: "no-unsafe-cast",
|
|
@@ -3298,7 +3845,7 @@ var no_unsafe_cast_default = import_utils26.ESLintUtils.RuleCreator(
|
|
|
3298
3845
|
return;
|
|
3299
3846
|
}
|
|
3300
3847
|
const inner = node.expression;
|
|
3301
|
-
if (inner.type ===
|
|
3848
|
+
if (inner.type === import_utils30.AST_NODE_TYPES.TSAsExpression || inner.type === import_utils30.AST_NODE_TYPES.TSTypeAssertion) {
|
|
3302
3849
|
context.report({ node, messageId: "doubleCast" });
|
|
3303
3850
|
}
|
|
3304
3851
|
}
|
|
@@ -3310,7 +3857,7 @@ var no_unsafe_cast_default = import_utils26.ESLintUtils.RuleCreator(
|
|
|
3310
3857
|
});
|
|
3311
3858
|
|
|
3312
3859
|
// src/rules/prefer-string-literal-union.ts
|
|
3313
|
-
var
|
|
3860
|
+
var import_utils31 = require("@typescript-eslint/utils");
|
|
3314
3861
|
var ts = __toESM(require("typescript"), 1);
|
|
3315
3862
|
var CHOICE_TOKENS = /* @__PURE__ */ new Set([
|
|
3316
3863
|
"status",
|
|
@@ -3353,19 +3900,19 @@ function isChoiceLikeName(name) {
|
|
|
3353
3900
|
return CHOICE_TOKENS.has(lastWord(name));
|
|
3354
3901
|
}
|
|
3355
3902
|
function keyName(key) {
|
|
3356
|
-
if (key.type ===
|
|
3903
|
+
if (key.type === import_utils31.AST_NODE_TYPES.Identifier) {
|
|
3357
3904
|
return key.name;
|
|
3358
3905
|
}
|
|
3359
|
-
if (key.type ===
|
|
3906
|
+
if (key.type === import_utils31.AST_NODE_TYPES.Literal && typeof key.value === "string") {
|
|
3360
3907
|
return key.value;
|
|
3361
3908
|
}
|
|
3362
3909
|
return null;
|
|
3363
3910
|
}
|
|
3364
3911
|
function isStringLiteralMember(t) {
|
|
3365
|
-
return t.type ===
|
|
3912
|
+
return t.type === import_utils31.AST_NODE_TYPES.TSLiteralType && t.literal.type === import_utils31.AST_NODE_TYPES.Literal && typeof t.literal.value === "string";
|
|
3366
3913
|
}
|
|
3367
3914
|
function isStringLiteralUnion(node) {
|
|
3368
|
-
if (node?.type !==
|
|
3915
|
+
if (node?.type !== import_utils31.AST_NODE_TYPES.TSUnionType) {
|
|
3369
3916
|
return false;
|
|
3370
3917
|
}
|
|
3371
3918
|
return node.types.filter(isStringLiteralMember).length >= MIN_CLUSTER_SIZE;
|
|
@@ -3394,12 +3941,12 @@ function bindingSourceExpression(decl) {
|
|
|
3394
3941
|
return ts.isForOfStatement(node) ? node.expression : node.initializer;
|
|
3395
3942
|
}
|
|
3396
3943
|
function refKey(node) {
|
|
3397
|
-
if (node.type ===
|
|
3944
|
+
if (node.type === import_utils31.AST_NODE_TYPES.Identifier) {
|
|
3398
3945
|
return node.name;
|
|
3399
3946
|
}
|
|
3400
|
-
if (node.type ===
|
|
3947
|
+
if (node.type === import_utils31.AST_NODE_TYPES.MemberExpression && !node.computed) {
|
|
3401
3948
|
const inner = refKey(node.object);
|
|
3402
|
-
if (inner === null || node.property.type !==
|
|
3949
|
+
if (inner === null || node.property.type !== import_utils31.AST_NODE_TYPES.Identifier) {
|
|
3403
3950
|
return null;
|
|
3404
3951
|
}
|
|
3405
3952
|
return `${inner}.${node.property.name}`;
|
|
@@ -3407,12 +3954,12 @@ function refKey(node) {
|
|
|
3407
3954
|
return null;
|
|
3408
3955
|
}
|
|
3409
3956
|
function strLiteral(node) {
|
|
3410
|
-
if (node.type ===
|
|
3957
|
+
if (node.type === import_utils31.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
3411
3958
|
return node.value;
|
|
3412
3959
|
}
|
|
3413
3960
|
return null;
|
|
3414
3961
|
}
|
|
3415
|
-
var prefer_string_literal_union_default =
|
|
3962
|
+
var prefer_string_literal_union_default = import_utils31.ESLintUtils.RuleCreator(
|
|
3416
3963
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3417
3964
|
)({
|
|
3418
3965
|
name: "prefer-string-literal-union",
|
|
@@ -3421,22 +3968,36 @@ var prefer_string_literal_union_default = import_utils28.ESLintUtils.RuleCreator
|
|
|
3421
3968
|
docs: {
|
|
3422
3969
|
description: "Flag raw `string` choice fields and string-literal comparison clusters; prefer a string-literal union type."
|
|
3423
3970
|
},
|
|
3424
|
-
schema: [
|
|
3971
|
+
schema: [
|
|
3972
|
+
{
|
|
3973
|
+
type: "object",
|
|
3974
|
+
additionalProperties: false,
|
|
3975
|
+
properties: {
|
|
3976
|
+
ignoreFields: {
|
|
3977
|
+
type: "array",
|
|
3978
|
+
items: { type: "string" }
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
],
|
|
3425
3983
|
messages: {
|
|
3426
3984
|
bareChoiceField: '`{{name}}: string` looks like a choice field \u2014 prefer a string-literal union type (e.g. `type X = "a" | "b"`). Enums are banned by `no-enum`; use a union.',
|
|
3427
3985
|
comparisonCluster: '`{{key}}` is compared against a closed set of string literals \u2014 define a string-literal union type (e.g. `type X = "a" | "b"`).'
|
|
3428
3986
|
}
|
|
3429
3987
|
},
|
|
3430
|
-
defaultOptions: [],
|
|
3431
|
-
create(context) {
|
|
3988
|
+
defaultOptions: [{}],
|
|
3989
|
+
create(context, [optionsArg]) {
|
|
3432
3990
|
const filename = context.filename;
|
|
3433
3991
|
const sourceText = context.sourceCode.getText();
|
|
3434
3992
|
if (isIgnoredFile(filename, sourceText)) {
|
|
3435
3993
|
return {};
|
|
3436
3994
|
}
|
|
3995
|
+
const ignoredFields = new Set(
|
|
3996
|
+
(optionsArg?.ignoreFields ?? []).map((name) => name.toLowerCase())
|
|
3997
|
+
);
|
|
3437
3998
|
let services;
|
|
3438
3999
|
try {
|
|
3439
|
-
services =
|
|
4000
|
+
services = import_utils31.ESLintUtils.getParserServices(context);
|
|
3440
4001
|
} catch {
|
|
3441
4002
|
services = null;
|
|
3442
4003
|
}
|
|
@@ -3481,18 +4042,46 @@ var prefer_string_literal_union_default = import_utils28.ESLintUtils.RuleCreator
|
|
|
3481
4042
|
function operandIsFlaggable(node) {
|
|
3482
4043
|
return operandIsRawString(node) && !originIsExternal(services?.esTreeNodeToTSNodeMap.get(node), 0);
|
|
3483
4044
|
}
|
|
3484
|
-
function
|
|
3485
|
-
|
|
4045
|
+
function declaredReturnLiterals(fn) {
|
|
4046
|
+
const annotation = fn.returnType?.typeAnnotation;
|
|
4047
|
+
if (annotation === void 0 || services === null) {
|
|
4048
|
+
return null;
|
|
4049
|
+
}
|
|
4050
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(annotation);
|
|
4051
|
+
if (!ts.isTypeNode(tsNode)) {
|
|
4052
|
+
return null;
|
|
4053
|
+
}
|
|
4054
|
+
const checker = services.program.getTypeChecker();
|
|
4055
|
+
const declared = checker.getTypeFromTypeNode(tsNode);
|
|
4056
|
+
const type = checker.getAwaitedType(declared) ?? declared;
|
|
4057
|
+
const literals = /* @__PURE__ */ new Set();
|
|
4058
|
+
for (const part of type.isUnion() ? type.types : [type]) {
|
|
4059
|
+
if (part.isStringLiteral()) {
|
|
4060
|
+
literals.add(part.value);
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
return literals.size >= MIN_CLUSTER_SIZE ? literals : null;
|
|
4064
|
+
}
|
|
4065
|
+
function pushScope(node) {
|
|
4066
|
+
scopeStack.push({ clusters: /* @__PURE__ */ new Map(), fn: node });
|
|
3486
4067
|
}
|
|
3487
4068
|
function popScope() {
|
|
3488
4069
|
const scope = scopeStack.pop();
|
|
3489
4070
|
if (scope === void 0) {
|
|
3490
4071
|
return;
|
|
3491
4072
|
}
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
4073
|
+
const candidates = [...scope.clusters.values()].filter(
|
|
4074
|
+
(entry) => entry.allTokens && entry.literals.size >= MIN_CLUSTER_SIZE
|
|
4075
|
+
);
|
|
4076
|
+
if (candidates.length === 0) {
|
|
4077
|
+
return;
|
|
4078
|
+
}
|
|
4079
|
+
const returnLiterals = scope.fn === null ? null : declaredReturnLiterals(scope.fn);
|
|
4080
|
+
for (const entry of candidates) {
|
|
4081
|
+
if (returnLiterals !== null && [...entry.literals].every((lit) => returnLiterals.has(lit))) {
|
|
4082
|
+
continue;
|
|
3495
4083
|
}
|
|
4084
|
+
validClusters.push(entry.node);
|
|
3496
4085
|
}
|
|
3497
4086
|
}
|
|
3498
4087
|
function accumulate(key, literals, node) {
|
|
@@ -3520,11 +4109,11 @@ var prefer_string_literal_union_default = import_utils28.ESLintUtils.RuleCreator
|
|
|
3520
4109
|
containersWithUnion.add(container);
|
|
3521
4110
|
return;
|
|
3522
4111
|
}
|
|
3523
|
-
if (typeNode?.type !==
|
|
4112
|
+
if (typeNode?.type !== import_utils31.AST_NODE_TYPES.TSStringKeyword) {
|
|
3524
4113
|
return;
|
|
3525
4114
|
}
|
|
3526
4115
|
const name = keyName(key);
|
|
3527
|
-
if (name === null || !isChoiceLikeName(name)) {
|
|
4116
|
+
if (name === null || !isChoiceLikeName(name) || ignoredFields.has(name.toLowerCase())) {
|
|
3528
4117
|
return;
|
|
3529
4118
|
}
|
|
3530
4119
|
bareChoiceProps.push({ name, container, node });
|
|
@@ -3608,10 +4197,10 @@ var prefer_string_literal_union_default = import_utils28.ESLintUtils.RuleCreator
|
|
|
3608
4197
|
}
|
|
3609
4198
|
};
|
|
3610
4199
|
function refKeyText(node) {
|
|
3611
|
-
if (node.type ===
|
|
4200
|
+
if (node.type === import_utils31.AST_NODE_TYPES.BinaryExpression) {
|
|
3612
4201
|
return refKey(node.left) ?? refKey(node.right) ?? "value";
|
|
3613
4202
|
}
|
|
3614
|
-
if (node.type ===
|
|
4203
|
+
if (node.type === import_utils31.AST_NODE_TYPES.SwitchStatement) {
|
|
3615
4204
|
return refKey(node.discriminant) ?? "value";
|
|
3616
4205
|
}
|
|
3617
4206
|
return "value";
|
|
@@ -3620,7 +4209,7 @@ var prefer_string_literal_union_default = import_utils28.ESLintUtils.RuleCreator
|
|
|
3620
4209
|
});
|
|
3621
4210
|
|
|
3622
4211
|
// src/rules/single-public-export.ts
|
|
3623
|
-
var
|
|
4212
|
+
var import_utils32 = require("@typescript-eslint/utils");
|
|
3624
4213
|
var JUNK_DRAWER_STEMS = /* @__PURE__ */ new Set([
|
|
3625
4214
|
"util",
|
|
3626
4215
|
"utils",
|
|
@@ -3654,12 +4243,12 @@ var kebabCase2 = (name) => {
|
|
|
3654
4243
|
}
|
|
3655
4244
|
return normalized.replace(CAMEL_BOUNDARY_RE, "-").toLowerCase();
|
|
3656
4245
|
};
|
|
3657
|
-
var isFunctionExpression = (node) => node !== null && (node.type ===
|
|
4246
|
+
var isFunctionExpression = (node) => node !== null && (node.type === import_utils32.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils32.AST_NODE_TYPES.FunctionExpression);
|
|
3658
4247
|
var functionConstName = (decl) => {
|
|
3659
4248
|
if (decl.declarations.length !== 1) return null;
|
|
3660
4249
|
const [declarator] = decl.declarations;
|
|
3661
4250
|
if (declarator === void 0) return null;
|
|
3662
|
-
if (declarator.id.type !==
|
|
4251
|
+
if (declarator.id.type !== import_utils32.AST_NODE_TYPES.Identifier) return null;
|
|
3663
4252
|
if (!isFunctionExpression(declarator.init)) return null;
|
|
3664
4253
|
return declarator.id.name;
|
|
3665
4254
|
};
|
|
@@ -3673,20 +4262,20 @@ var summarizeExports = (body) => {
|
|
|
3673
4262
|
};
|
|
3674
4263
|
for (const statement of body) {
|
|
3675
4264
|
switch (statement.type) {
|
|
3676
|
-
case
|
|
4265
|
+
case import_utils32.AST_NODE_TYPES.ExportAllDeclaration:
|
|
3677
4266
|
hasReExport = true;
|
|
3678
4267
|
break;
|
|
3679
|
-
case
|
|
4268
|
+
case import_utils32.AST_NODE_TYPES.ExportDefaultDeclaration: {
|
|
3680
4269
|
names += 1;
|
|
3681
4270
|
const decl = statement.declaration;
|
|
3682
|
-
if (decl.type ===
|
|
4271
|
+
if (decl.type === import_utils32.AST_NODE_TYPES.FunctionDeclaration && decl.id !== null) {
|
|
3683
4272
|
candidate = { name: decl.id.name, node: statement };
|
|
3684
|
-
} else if (decl.type ===
|
|
4273
|
+
} else if (decl.type === import_utils32.AST_NODE_TYPES.ClassDeclaration && decl.id !== null) {
|
|
3685
4274
|
candidate = { name: decl.id.name, node: statement };
|
|
3686
4275
|
}
|
|
3687
4276
|
break;
|
|
3688
4277
|
}
|
|
3689
|
-
case
|
|
4278
|
+
case import_utils32.AST_NODE_TYPES.ExportNamedDeclaration: {
|
|
3690
4279
|
if (statement.source !== null) {
|
|
3691
4280
|
hasReExport = true;
|
|
3692
4281
|
break;
|
|
@@ -3697,15 +4286,15 @@ var summarizeExports = (body) => {
|
|
|
3697
4286
|
break;
|
|
3698
4287
|
}
|
|
3699
4288
|
switch (decl.type) {
|
|
3700
|
-
case
|
|
4289
|
+
case import_utils32.AST_NODE_TYPES.FunctionDeclaration:
|
|
3701
4290
|
if (decl.id !== null) addCandidate(decl.id.name, statement);
|
|
3702
4291
|
else names += 1;
|
|
3703
4292
|
break;
|
|
3704
|
-
case
|
|
4293
|
+
case import_utils32.AST_NODE_TYPES.ClassDeclaration:
|
|
3705
4294
|
if (decl.id !== null) addCandidate(decl.id.name, statement);
|
|
3706
4295
|
else names += 1;
|
|
3707
4296
|
break;
|
|
3708
|
-
case
|
|
4297
|
+
case import_utils32.AST_NODE_TYPES.VariableDeclaration: {
|
|
3709
4298
|
const fnName = functionConstName(decl);
|
|
3710
4299
|
if (fnName !== null && decl.declarations.length === 1) {
|
|
3711
4300
|
addCandidate(fnName, statement);
|
|
@@ -3725,7 +4314,7 @@ var summarizeExports = (body) => {
|
|
|
3725
4314
|
}
|
|
3726
4315
|
return { names, hasReExport, candidate };
|
|
3727
4316
|
};
|
|
3728
|
-
var single_public_export_default =
|
|
4317
|
+
var single_public_export_default = import_utils32.ESLintUtils.RuleCreator(
|
|
3729
4318
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3730
4319
|
)({
|
|
3731
4320
|
name: "single-public-export",
|
|
@@ -3764,6 +4353,975 @@ var single_public_export_default = import_utils29.ESLintUtils.RuleCreator(
|
|
|
3764
4353
|
}
|
|
3765
4354
|
});
|
|
3766
4355
|
|
|
4356
|
+
// src/rules/no-offset-pagination.ts
|
|
4357
|
+
var import_utils34 = require("@typescript-eslint/utils");
|
|
4358
|
+
|
|
4359
|
+
// src/rules/_sql.ts
|
|
4360
|
+
var import_utils33 = require("@typescript-eslint/utils");
|
|
4361
|
+
function stripSqlNoise(text) {
|
|
4362
|
+
const out = [...text];
|
|
4363
|
+
const n = text.length;
|
|
4364
|
+
let i = 0;
|
|
4365
|
+
while (i < n) {
|
|
4366
|
+
const ch = text[i];
|
|
4367
|
+
if (ch === "'" || ch === '"') {
|
|
4368
|
+
out[i] = " ";
|
|
4369
|
+
i += 1;
|
|
4370
|
+
while (i < n) {
|
|
4371
|
+
const c = text[i];
|
|
4372
|
+
if (c === ch) {
|
|
4373
|
+
if (i + 1 < n && text[i + 1] === ch) {
|
|
4374
|
+
out[i] = " ";
|
|
4375
|
+
out[i + 1] = " ";
|
|
4376
|
+
i += 2;
|
|
4377
|
+
continue;
|
|
4378
|
+
}
|
|
4379
|
+
out[i] = " ";
|
|
4380
|
+
i += 1;
|
|
4381
|
+
break;
|
|
4382
|
+
}
|
|
4383
|
+
if (c !== "\n") {
|
|
4384
|
+
out[i] = " ";
|
|
4385
|
+
}
|
|
4386
|
+
i += 1;
|
|
4387
|
+
}
|
|
4388
|
+
continue;
|
|
4389
|
+
}
|
|
4390
|
+
if (ch === "-" && text[i + 1] === "-") {
|
|
4391
|
+
while (i < n && text[i] !== "\n") {
|
|
4392
|
+
out[i] = " ";
|
|
4393
|
+
i += 1;
|
|
4394
|
+
}
|
|
4395
|
+
continue;
|
|
4396
|
+
}
|
|
4397
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
4398
|
+
out[i] = " ";
|
|
4399
|
+
out[i + 1] = " ";
|
|
4400
|
+
i += 2;
|
|
4401
|
+
while (i < n && !(text[i] === "*" && text[i + 1] === "/")) {
|
|
4402
|
+
if (text[i] !== "\n") {
|
|
4403
|
+
out[i] = " ";
|
|
4404
|
+
}
|
|
4405
|
+
i += 1;
|
|
4406
|
+
}
|
|
4407
|
+
if (i < n) {
|
|
4408
|
+
out[i] = " ";
|
|
4409
|
+
out[i + 1] = " ";
|
|
4410
|
+
i += 2;
|
|
4411
|
+
}
|
|
4412
|
+
continue;
|
|
4413
|
+
}
|
|
4414
|
+
i += 1;
|
|
4415
|
+
}
|
|
4416
|
+
return out.join("");
|
|
4417
|
+
}
|
|
4418
|
+
var SUBSTITUTION_MARKER = "?";
|
|
4419
|
+
function sqlTextOf(node) {
|
|
4420
|
+
switch (node.type) {
|
|
4421
|
+
case import_utils33.AST_NODE_TYPES.Literal:
|
|
4422
|
+
return typeof node.value === "string" ? node.value : null;
|
|
4423
|
+
case import_utils33.AST_NODE_TYPES.TemplateLiteral:
|
|
4424
|
+
return node.quasis.map((q) => q.value.cooked ?? q.value.raw).join(SUBSTITUTION_MARKER);
|
|
4425
|
+
case import_utils33.AST_NODE_TYPES.TaggedTemplateExpression:
|
|
4426
|
+
return sqlTextOf(node.quasi);
|
|
4427
|
+
case import_utils33.AST_NODE_TYPES.BinaryExpression: {
|
|
4428
|
+
if (node.operator !== "+") {
|
|
4429
|
+
return null;
|
|
4430
|
+
}
|
|
4431
|
+
const left = sqlTextOf(node.left);
|
|
4432
|
+
const right = sqlTextOf(node.right);
|
|
4433
|
+
return left !== null && right !== null ? left + right : null;
|
|
4434
|
+
}
|
|
4435
|
+
case import_utils33.AST_NODE_TYPES.ArrayExpression: {
|
|
4436
|
+
const parts = [];
|
|
4437
|
+
for (const element of node.elements) {
|
|
4438
|
+
if (element === null) {
|
|
4439
|
+
return null;
|
|
4440
|
+
}
|
|
4441
|
+
const part = sqlTextOf(element);
|
|
4442
|
+
if (part === null) {
|
|
4443
|
+
return null;
|
|
4444
|
+
}
|
|
4445
|
+
parts.push(part);
|
|
4446
|
+
}
|
|
4447
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
4448
|
+
}
|
|
4449
|
+
default:
|
|
4450
|
+
return null;
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
function isJoinedFragmentArray(node) {
|
|
4454
|
+
const parent = node.parent;
|
|
4455
|
+
return parent?.type === import_utils33.AST_NODE_TYPES.MemberExpression && parent.object === node && !parent.computed && parent.property.type === import_utils33.AST_NODE_TYPES.Identifier && parent.property.name === "join" && parent.parent?.type === import_utils33.AST_NODE_TYPES.CallExpression;
|
|
4456
|
+
}
|
|
4457
|
+
function markConsumed(node, consumed) {
|
|
4458
|
+
consumed.add(node);
|
|
4459
|
+
for (const key of Object.keys(node)) {
|
|
4460
|
+
if (key === "parent") {
|
|
4461
|
+
continue;
|
|
4462
|
+
}
|
|
4463
|
+
const value = node[key];
|
|
4464
|
+
for (const child of Array.isArray(value) ? value : [value]) {
|
|
4465
|
+
if (child !== null && typeof child === "object" && "type" in child) {
|
|
4466
|
+
markConsumed(child, consumed);
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
function createSqlListener(handler) {
|
|
4472
|
+
const consumed = /* @__PURE__ */ new WeakSet();
|
|
4473
|
+
const visit = (node) => {
|
|
4474
|
+
if (consumed.has(node)) {
|
|
4475
|
+
return;
|
|
4476
|
+
}
|
|
4477
|
+
const text = sqlTextOf(node);
|
|
4478
|
+
if (text === null) {
|
|
4479
|
+
return;
|
|
4480
|
+
}
|
|
4481
|
+
markConsumed(node, consumed);
|
|
4482
|
+
handler(stripSqlNoise(text), node);
|
|
4483
|
+
};
|
|
4484
|
+
return {
|
|
4485
|
+
BinaryExpression: (node) => {
|
|
4486
|
+
visit(node);
|
|
4487
|
+
},
|
|
4488
|
+
ArrayExpression: (node) => {
|
|
4489
|
+
if (isJoinedFragmentArray(node)) {
|
|
4490
|
+
visit(node);
|
|
4491
|
+
}
|
|
4492
|
+
},
|
|
4493
|
+
TemplateLiteral: (node) => {
|
|
4494
|
+
visit(node);
|
|
4495
|
+
},
|
|
4496
|
+
Literal: (node) => {
|
|
4497
|
+
visit(node);
|
|
4498
|
+
}
|
|
4499
|
+
};
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
// src/rules/no-offset-pagination.ts
|
|
4503
|
+
var OFFSET_PAGINATION = /\bOFFSET\s+(?:\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
|
|
4504
|
+
var OFFSET_GATE = /offset/i;
|
|
4505
|
+
var no_offset_pagination_default = import_utils34.ESLintUtils.RuleCreator(
|
|
4506
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4507
|
+
)({
|
|
4508
|
+
name: "no-offset-pagination",
|
|
4509
|
+
meta: {
|
|
4510
|
+
type: "problem",
|
|
4511
|
+
docs: {
|
|
4512
|
+
description: "Disallow OFFSET pagination in embedded SQL; it is O(N) per page and drops or repeats rows under concurrent writes. Use a keyset cursor."
|
|
4513
|
+
},
|
|
4514
|
+
schema: [],
|
|
4515
|
+
messages: {
|
|
4516
|
+
noOffsetPagination: "OFFSET pagination scans and discards every skipped row (O(N) per page) and shifts under concurrent inserts, so rows get repeated or missed. Use a keyset cursor: `WHERE id > ? ORDER BY id LIMIT ?`."
|
|
4517
|
+
}
|
|
4518
|
+
},
|
|
4519
|
+
defaultOptions: [],
|
|
4520
|
+
create(context) {
|
|
4521
|
+
if (isTestFile(context.filename) || !OFFSET_GATE.test(context.sourceCode.text)) {
|
|
4522
|
+
return {};
|
|
4523
|
+
}
|
|
4524
|
+
return createSqlListener((sql, node) => {
|
|
4525
|
+
if (!OFFSET_PAGINATION.test(sql)) {
|
|
4526
|
+
return;
|
|
4527
|
+
}
|
|
4528
|
+
context.report({ node, messageId: "noOffsetPagination" });
|
|
4529
|
+
});
|
|
4530
|
+
}
|
|
4531
|
+
});
|
|
4532
|
+
|
|
4533
|
+
// src/rules/no-positional-tuple-return.ts
|
|
4534
|
+
var import_utils35 = require("@typescript-eslint/utils");
|
|
4535
|
+
var MIN_ELEMENTS = 2;
|
|
4536
|
+
var AWAITABLE_TYPES = /* @__PURE__ */ new Set(["Promise", "PromiseLike", "Awaited"]);
|
|
4537
|
+
function tupleReturnType(node) {
|
|
4538
|
+
if (node.type === import_utils35.AST_NODE_TYPES.TSTupleType) {
|
|
4539
|
+
return node;
|
|
4540
|
+
}
|
|
4541
|
+
if (node.type === import_utils35.AST_NODE_TYPES.TSTypeReference && node.typeName.type === import_utils35.AST_NODE_TYPES.Identifier && AWAITABLE_TYPES.has(node.typeName.name)) {
|
|
4542
|
+
const argument = node.typeArguments?.params[0];
|
|
4543
|
+
return argument === void 0 ? null : tupleReturnType(argument);
|
|
4544
|
+
}
|
|
4545
|
+
return null;
|
|
4546
|
+
}
|
|
4547
|
+
function normalizedText(sourceCode, node) {
|
|
4548
|
+
return sourceCode.getText(node).replaceAll(/\s+/g, " ").trim();
|
|
4549
|
+
}
|
|
4550
|
+
function isPermittedTuple(tuple, sourceCode) {
|
|
4551
|
+
const elements = tuple.elementTypes;
|
|
4552
|
+
if (elements.length < MIN_ELEMENTS) {
|
|
4553
|
+
return true;
|
|
4554
|
+
}
|
|
4555
|
+
if (elements.some((element) => element.type === import_utils35.AST_NODE_TYPES.TSRestType)) {
|
|
4556
|
+
return true;
|
|
4557
|
+
}
|
|
4558
|
+
if (elements.some((element) => element.type === import_utils35.AST_NODE_TYPES.TSNamedTupleMember)) {
|
|
4559
|
+
return true;
|
|
4560
|
+
}
|
|
4561
|
+
if (elements[0]?.type === import_utils35.AST_NODE_TYPES.TSLiteralType) {
|
|
4562
|
+
return true;
|
|
4563
|
+
}
|
|
4564
|
+
const texts = new Set(elements.map((element) => normalizedText(sourceCode, element)));
|
|
4565
|
+
return texts.size === 1;
|
|
4566
|
+
}
|
|
4567
|
+
function functionName(node) {
|
|
4568
|
+
if (node.type === import_utils35.AST_NODE_TYPES.FunctionDeclaration) {
|
|
4569
|
+
return node.id?.name ?? null;
|
|
4570
|
+
}
|
|
4571
|
+
const parent = node.parent;
|
|
4572
|
+
if (parent?.type === import_utils35.AST_NODE_TYPES.VariableDeclarator && parent.id.type === import_utils35.AST_NODE_TYPES.Identifier) {
|
|
4573
|
+
return parent.id.name;
|
|
4574
|
+
}
|
|
4575
|
+
if ((parent?.type === import_utils35.AST_NODE_TYPES.MethodDefinition || parent?.type === import_utils35.AST_NODE_TYPES.PropertyDefinition || parent?.type === import_utils35.AST_NODE_TYPES.Property) && parent.key.type === import_utils35.AST_NODE_TYPES.Identifier) {
|
|
4576
|
+
return parent.key.name;
|
|
4577
|
+
}
|
|
4578
|
+
return null;
|
|
4579
|
+
}
|
|
4580
|
+
function isExported(node) {
|
|
4581
|
+
for (let current = node; current != null; current = current.parent) {
|
|
4582
|
+
const parent = current.parent;
|
|
4583
|
+
if (parent?.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration || parent?.type === import_utils35.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
4584
|
+
return true;
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
return false;
|
|
4588
|
+
}
|
|
4589
|
+
var no_positional_tuple_return_default = import_utils35.ESLintUtils.RuleCreator(
|
|
4590
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4591
|
+
)({
|
|
4592
|
+
name: "no-positional-tuple-return",
|
|
4593
|
+
meta: {
|
|
4594
|
+
type: "suggestion",
|
|
4595
|
+
docs: {
|
|
4596
|
+
description: "Disallow returning a positional tuple of distinct fields from an exported function; return a named object so call sites cannot mismatch slots."
|
|
4597
|
+
},
|
|
4598
|
+
schema: [],
|
|
4599
|
+
messages: {
|
|
4600
|
+
noPositionalTupleReturn: "Exported `{{name}}` returns a positional {{count}}-tuple, so every call site re-invents the field names and can disagree. Return a named object (or label the tuple members) instead."
|
|
4601
|
+
}
|
|
4602
|
+
},
|
|
4603
|
+
defaultOptions: [],
|
|
4604
|
+
create(context) {
|
|
4605
|
+
const check = (node) => {
|
|
4606
|
+
const annotation = node.returnType?.typeAnnotation;
|
|
4607
|
+
if (annotation === void 0) {
|
|
4608
|
+
return;
|
|
4609
|
+
}
|
|
4610
|
+
const tuple = tupleReturnType(annotation);
|
|
4611
|
+
if (tuple === null || isPermittedTuple(tuple, context.sourceCode)) {
|
|
4612
|
+
return;
|
|
4613
|
+
}
|
|
4614
|
+
const name = functionName(node);
|
|
4615
|
+
if (name === null || name.startsWith("_") || /^use[A-Z]/.test(name)) {
|
|
4616
|
+
return;
|
|
4617
|
+
}
|
|
4618
|
+
if (!isExported(node)) {
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
context.report({
|
|
4622
|
+
node: tuple,
|
|
4623
|
+
messageId: "noPositionalTupleReturn",
|
|
4624
|
+
data: { name, count: String(tuple.elementTypes.length) }
|
|
4625
|
+
});
|
|
4626
|
+
};
|
|
4627
|
+
return {
|
|
4628
|
+
FunctionDeclaration: check,
|
|
4629
|
+
FunctionExpression: check,
|
|
4630
|
+
ArrowFunctionExpression: check
|
|
4631
|
+
};
|
|
4632
|
+
}
|
|
4633
|
+
});
|
|
4634
|
+
|
|
4635
|
+
// src/rules/no-repeated-string-literal.ts
|
|
4636
|
+
var import_utils36 = require("@typescript-eslint/utils");
|
|
4637
|
+
var MIN_LENGTH = 40;
|
|
4638
|
+
var MIN_OCCURRENCES = 3;
|
|
4639
|
+
var MIN_DISTINCT_SCOPES = 2;
|
|
4640
|
+
var PREVIEW_LENGTH = 40;
|
|
4641
|
+
var SQL_KEYWORD_RE = /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|VALUES|ON CONFLICT|RETURNING|GROUP BY|ORDER BY)\b/;
|
|
4642
|
+
var IDENTIFIER_RE = /^[a-z_][a-z0-9_.]*$/;
|
|
4643
|
+
var FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
4644
|
+
import_utils36.AST_NODE_TYPES.FunctionDeclaration,
|
|
4645
|
+
import_utils36.AST_NODE_TYPES.FunctionExpression,
|
|
4646
|
+
import_utils36.AST_NODE_TYPES.ArrowFunctionExpression
|
|
4647
|
+
]);
|
|
4648
|
+
function isStructured(value) {
|
|
4649
|
+
return value.includes("\n") || SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value);
|
|
4650
|
+
}
|
|
4651
|
+
function preview(value) {
|
|
4652
|
+
const oneLine = value.replaceAll("\n", " ").trim();
|
|
4653
|
+
return oneLine.length <= PREVIEW_LENGTH ? oneLine : `${oneLine.slice(0, PREVIEW_LENGTH)}...`;
|
|
4654
|
+
}
|
|
4655
|
+
function enclosingFunction(node) {
|
|
4656
|
+
for (let current = node.parent; current != null; current = current.parent) {
|
|
4657
|
+
if (FUNCTION_TYPES.has(current.type)) {
|
|
4658
|
+
return current;
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
return null;
|
|
4662
|
+
}
|
|
4663
|
+
function isScaffolding(node) {
|
|
4664
|
+
const parent = node.parent;
|
|
4665
|
+
if (parent === void 0) {
|
|
4666
|
+
return true;
|
|
4667
|
+
}
|
|
4668
|
+
return parent.type === import_utils36.AST_NODE_TYPES.ImportDeclaration || parent.type === import_utils36.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils36.AST_NODE_TYPES.ExportAllDeclaration || parent.type === import_utils36.AST_NODE_TYPES.TSImportType || parent.type === import_utils36.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils36.AST_NODE_TYPES.TSLiteralType;
|
|
4669
|
+
}
|
|
4670
|
+
var no_repeated_string_literal_default = import_utils36.ESLintUtils.RuleCreator(
|
|
4671
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4672
|
+
)({
|
|
4673
|
+
name: "no-repeated-string-literal",
|
|
4674
|
+
meta: {
|
|
4675
|
+
type: "suggestion",
|
|
4676
|
+
docs: {
|
|
4677
|
+
description: "Disallow a long structured string literal repeated across functions; the copies drift when one is edited. Extract a module-level constant."
|
|
4678
|
+
},
|
|
4679
|
+
schema: [],
|
|
4680
|
+
messages: {
|
|
4681
|
+
noRepeatedStringLiteral: 'Structured string literal "{{preview}}" is repeated across functions (first use on line {{line}}) \u2014 extract a module-level constant so the copies cannot drift.'
|
|
4682
|
+
}
|
|
4683
|
+
},
|
|
4684
|
+
defaultOptions: [],
|
|
4685
|
+
create(context) {
|
|
4686
|
+
if (isTestFile(context.filename)) {
|
|
4687
|
+
return {};
|
|
4688
|
+
}
|
|
4689
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
4690
|
+
const scopes = /* @__PURE__ */ new WeakMap();
|
|
4691
|
+
const record = (value, node) => {
|
|
4692
|
+
if (value.length < MIN_LENGTH || !isStructured(value) || isScaffolding(node)) {
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
const existing = occurrences.get(value);
|
|
4696
|
+
if (existing === void 0) {
|
|
4697
|
+
occurrences.set(value, [node]);
|
|
4698
|
+
} else {
|
|
4699
|
+
existing.push(node);
|
|
4700
|
+
}
|
|
4701
|
+
scopes.set(node, enclosingFunction(node));
|
|
4702
|
+
};
|
|
4703
|
+
return {
|
|
4704
|
+
Literal(node) {
|
|
4705
|
+
if (typeof node.value === "string") {
|
|
4706
|
+
record(node.value, node);
|
|
4707
|
+
}
|
|
4708
|
+
},
|
|
4709
|
+
TemplateLiteral(node) {
|
|
4710
|
+
const [only] = node.quasis;
|
|
4711
|
+
if (node.expressions.length === 0 && only !== void 0) {
|
|
4712
|
+
record(only.value.cooked ?? only.value.raw, node);
|
|
4713
|
+
}
|
|
4714
|
+
},
|
|
4715
|
+
"Program:exit": () => {
|
|
4716
|
+
for (const [value, nodes] of occurrences) {
|
|
4717
|
+
if (nodes.length < MIN_OCCURRENCES) {
|
|
4718
|
+
continue;
|
|
4719
|
+
}
|
|
4720
|
+
const distinctScopes = new Set(
|
|
4721
|
+
nodes.map((node) => scopes.get(node)).filter((scope) => scope != null)
|
|
4722
|
+
);
|
|
4723
|
+
if (distinctScopes.size < MIN_DISTINCT_SCOPES) {
|
|
4724
|
+
continue;
|
|
4725
|
+
}
|
|
4726
|
+
const [first, ...repeats] = nodes;
|
|
4727
|
+
if (first === void 0) {
|
|
4728
|
+
continue;
|
|
4729
|
+
}
|
|
4730
|
+
for (const node of repeats) {
|
|
4731
|
+
context.report({
|
|
4732
|
+
node,
|
|
4733
|
+
messageId: "noRepeatedStringLiteral",
|
|
4734
|
+
data: { preview: preview(value), line: String(first.loc.start.line) }
|
|
4735
|
+
});
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4741
|
+
});
|
|
4742
|
+
|
|
4743
|
+
// src/rules/no-select-star.ts
|
|
4744
|
+
var import_utils37 = require("@typescript-eslint/utils");
|
|
4745
|
+
var QUERY_SHAPE = /\bSELECT\b[\s\S]*?\bFROM\b/i;
|
|
4746
|
+
var SELECT_KEYWORD = /\bSELECT\b/gi;
|
|
4747
|
+
var FROM_KEYWORD = /^FROM\b/i;
|
|
4748
|
+
var EXISTS_BEFORE = /\bEXISTS\s*\(\s*$/i;
|
|
4749
|
+
var QUALIFIED_PREFIX = /\w\.$/;
|
|
4750
|
+
var SELECT_GATE = /select/i;
|
|
4751
|
+
function isProjectionStar(sql, pos) {
|
|
4752
|
+
if (QUALIFIED_PREFIX.test(sql.slice(0, pos))) {
|
|
4753
|
+
return true;
|
|
4754
|
+
}
|
|
4755
|
+
let before = pos - 1;
|
|
4756
|
+
while (before >= 0 && /\s/.test(sql[before] ?? "")) {
|
|
4757
|
+
before -= 1;
|
|
4758
|
+
}
|
|
4759
|
+
let after = pos + 1;
|
|
4760
|
+
while (after < sql.length && /\s/.test(sql[after] ?? "")) {
|
|
4761
|
+
after += 1;
|
|
4762
|
+
}
|
|
4763
|
+
const beforeChar = before >= 0 ? sql[before] ?? "" : "";
|
|
4764
|
+
const afterChar = after < sql.length ? sql[after] ?? "" : "";
|
|
4765
|
+
const terminates = afterChar === "" || afterChar === "," || afterChar === ")" || FROM_KEYWORD.test(sql.slice(after));
|
|
4766
|
+
if (!terminates) {
|
|
4767
|
+
return false;
|
|
4768
|
+
}
|
|
4769
|
+
return !(beforeChar === "(" && afterChar === ")");
|
|
4770
|
+
}
|
|
4771
|
+
function hasRealSelectStar(sql) {
|
|
4772
|
+
const selects = [...sql.matchAll(SELECT_KEYWORD)].map((m) => m.index);
|
|
4773
|
+
for (let pos = 0; pos < sql.length; pos++) {
|
|
4774
|
+
if (sql[pos] !== "*" || !isProjectionStar(sql, pos)) {
|
|
4775
|
+
continue;
|
|
4776
|
+
}
|
|
4777
|
+
const owning = selects.filter((start) => start < pos).at(-1);
|
|
4778
|
+
if (owning !== void 0 && !EXISTS_BEFORE.test(sql.slice(0, owning))) {
|
|
4779
|
+
return true;
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
return false;
|
|
4783
|
+
}
|
|
4784
|
+
var no_select_star_default = import_utils37.ESLintUtils.RuleCreator(
|
|
4785
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4786
|
+
)({
|
|
4787
|
+
name: "no-select-star",
|
|
4788
|
+
meta: {
|
|
4789
|
+
type: "problem",
|
|
4790
|
+
docs: {
|
|
4791
|
+
description: "Disallow SELECT * in embedded SQL; it over-fetches and leaves the row contract implicit, so a schema change breaks row parsing silently."
|
|
4792
|
+
},
|
|
4793
|
+
schema: [],
|
|
4794
|
+
messages: {
|
|
4795
|
+
noSelectStar: "`SELECT *` over-fetches and leaves the row shape implicit \u2014 a new or reordered column silently changes what this query returns. List the columns explicitly."
|
|
4796
|
+
}
|
|
4797
|
+
},
|
|
4798
|
+
defaultOptions: [],
|
|
4799
|
+
create(context) {
|
|
4800
|
+
if (isTestFile(context.filename) || !SELECT_GATE.test(context.sourceCode.text)) {
|
|
4801
|
+
return {};
|
|
4802
|
+
}
|
|
4803
|
+
return createSqlListener((sql, node) => {
|
|
4804
|
+
if (!QUERY_SHAPE.test(sql) || !hasRealSelectStar(sql)) {
|
|
4805
|
+
return;
|
|
4806
|
+
}
|
|
4807
|
+
context.report({ node, messageId: "noSelectStar" });
|
|
4808
|
+
});
|
|
4809
|
+
}
|
|
4810
|
+
});
|
|
4811
|
+
|
|
4812
|
+
// src/rules/no-sleep-in-test-body.ts
|
|
4813
|
+
var import_utils38 = require("@typescript-eslint/utils");
|
|
4814
|
+
var SLEEP_HELPERS = /* @__PURE__ */ new Set(["sleep", "delay", "wait", "pause"]);
|
|
4815
|
+
var TEST_CALLERS = /* @__PURE__ */ new Set([
|
|
4816
|
+
"it",
|
|
4817
|
+
"test",
|
|
4818
|
+
"beforeEach",
|
|
4819
|
+
"afterEach"
|
|
4820
|
+
]);
|
|
4821
|
+
var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
|
|
4822
|
+
import_utils38.AST_NODE_TYPES.FunctionDeclaration,
|
|
4823
|
+
import_utils38.AST_NODE_TYPES.FunctionExpression,
|
|
4824
|
+
import_utils38.AST_NODE_TYPES.ArrowFunctionExpression
|
|
4825
|
+
]);
|
|
4826
|
+
function isNonzeroNumericLiteral(node) {
|
|
4827
|
+
return node?.type === import_utils38.AST_NODE_TYPES.Literal && typeof node.value === "number" && node.value !== 0;
|
|
4828
|
+
}
|
|
4829
|
+
function isTimedSetTimeout(node) {
|
|
4830
|
+
return node.type === import_utils38.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils38.AST_NODE_TYPES.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
|
|
4831
|
+
}
|
|
4832
|
+
function isPromiseSleep(node) {
|
|
4833
|
+
if (node.callee.type !== import_utils38.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise") {
|
|
4834
|
+
return false;
|
|
4835
|
+
}
|
|
4836
|
+
const executor = node.arguments[0];
|
|
4837
|
+
if (executor?.type !== import_utils38.AST_NODE_TYPES.ArrowFunctionExpression && executor?.type !== import_utils38.AST_NODE_TYPES.FunctionExpression) {
|
|
4838
|
+
return false;
|
|
4839
|
+
}
|
|
4840
|
+
const body = executor.body;
|
|
4841
|
+
if (body.type !== import_utils38.AST_NODE_TYPES.BlockStatement) {
|
|
4842
|
+
return isTimedSetTimeout(body);
|
|
4843
|
+
}
|
|
4844
|
+
return body.body.some(
|
|
4845
|
+
(stmt) => stmt.type === import_utils38.AST_NODE_TYPES.ExpressionStatement && isTimedSetTimeout(stmt.expression)
|
|
4846
|
+
);
|
|
4847
|
+
}
|
|
4848
|
+
function isHelperSleep(node) {
|
|
4849
|
+
return node.callee.type === import_utils38.AST_NODE_TYPES.Identifier && SLEEP_HELPERS.has(node.callee.name) && node.arguments.length >= 1 && isNonzeroNumericLiteral(node.arguments[0]);
|
|
4850
|
+
}
|
|
4851
|
+
function nearestEnclosingFunction(node) {
|
|
4852
|
+
for (let current = node.parent; current != null; current = current.parent) {
|
|
4853
|
+
if (!FUNCTION_TYPES2.has(current.type)) {
|
|
4854
|
+
continue;
|
|
4855
|
+
}
|
|
4856
|
+
const grandparent = current.parent;
|
|
4857
|
+
const isPromiseExecutor = grandparent?.type === import_utils38.AST_NODE_TYPES.NewExpression && isPromiseSleep(grandparent);
|
|
4858
|
+
if (!isPromiseExecutor) {
|
|
4859
|
+
return current;
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4862
|
+
return null;
|
|
4863
|
+
}
|
|
4864
|
+
function testCallerName(callee) {
|
|
4865
|
+
if (callee.type === import_utils38.AST_NODE_TYPES.Identifier) {
|
|
4866
|
+
return callee.name;
|
|
4867
|
+
}
|
|
4868
|
+
if (callee.type === import_utils38.AST_NODE_TYPES.MemberExpression) {
|
|
4869
|
+
return testCallerName(callee.object);
|
|
4870
|
+
}
|
|
4871
|
+
if (callee.type === import_utils38.AST_NODE_TYPES.CallExpression) {
|
|
4872
|
+
return testCallerName(callee.callee);
|
|
4873
|
+
}
|
|
4874
|
+
if (callee.type === import_utils38.AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
4875
|
+
return testCallerName(callee.tag);
|
|
4876
|
+
}
|
|
4877
|
+
return null;
|
|
4878
|
+
}
|
|
4879
|
+
function isTestBody(fn) {
|
|
4880
|
+
const call = fn.parent;
|
|
4881
|
+
if (call?.type !== import_utils38.AST_NODE_TYPES.CallExpression || !call.arguments.some((argument) => argument === fn)) {
|
|
4882
|
+
return false;
|
|
4883
|
+
}
|
|
4884
|
+
const name = testCallerName(call.callee);
|
|
4885
|
+
return name !== null && TEST_CALLERS.has(name);
|
|
4886
|
+
}
|
|
4887
|
+
var no_sleep_in_test_body_default = import_utils38.ESLintUtils.RuleCreator(
|
|
4888
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4889
|
+
)({
|
|
4890
|
+
name: "no-sleep-in-test-body",
|
|
4891
|
+
meta: {
|
|
4892
|
+
type: "problem",
|
|
4893
|
+
docs: {
|
|
4894
|
+
description: "Disallow a fixed timed sleep directly in a test body; it flakes under CI load. Synchronize on the signal or use fake timers."
|
|
4895
|
+
},
|
|
4896
|
+
schema: [],
|
|
4897
|
+
messages: {
|
|
4898
|
+
noSleepInTestBody: "A fixed sleep in a test body asserts on wall-clock time and flakes under CI load. Await the promise the code returns, or drive time with `vi.useFakeTimers()` + `await vi.advanceTimersByTimeAsync(ms)`."
|
|
4899
|
+
}
|
|
4900
|
+
},
|
|
4901
|
+
defaultOptions: [],
|
|
4902
|
+
create(context) {
|
|
4903
|
+
if (!isTestFile(context.filename)) {
|
|
4904
|
+
return {};
|
|
4905
|
+
}
|
|
4906
|
+
const report = (node) => {
|
|
4907
|
+
const enclosing = nearestEnclosingFunction(node);
|
|
4908
|
+
if (enclosing === null || !isTestBody(enclosing)) {
|
|
4909
|
+
return;
|
|
4910
|
+
}
|
|
4911
|
+
context.report({ node, messageId: "noSleepInTestBody" });
|
|
4912
|
+
};
|
|
4913
|
+
return {
|
|
4914
|
+
NewExpression(node) {
|
|
4915
|
+
if (isPromiseSleep(node)) {
|
|
4916
|
+
report(node);
|
|
4917
|
+
}
|
|
4918
|
+
},
|
|
4919
|
+
CallExpression(node) {
|
|
4920
|
+
if (isHelperSleep(node)) {
|
|
4921
|
+
report(node);
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
};
|
|
4925
|
+
}
|
|
4926
|
+
});
|
|
4927
|
+
|
|
4928
|
+
// src/rules/prefer-constant-time-secret-compare.ts
|
|
4929
|
+
var import_utils39 = require("@typescript-eslint/utils");
|
|
4930
|
+
var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["===", "!==", "==", "!="]);
|
|
4931
|
+
var SENTINEL_IDENTIFIERS = /* @__PURE__ */ new Set(["undefined", "NaN"]);
|
|
4932
|
+
var SENTINEL_WORDS = /(^|_)(SENTINEL|EMPTY|NONE|NULL|UNSET|MISSING|PLACEHOLDER|DUMMY|FAKE|EXAMPLE)(_|$)/;
|
|
4933
|
+
function isConstantReference(identifier) {
|
|
4934
|
+
if (isAuthSecretName(identifier) && !SENTINEL_WORDS.test(identifier)) return false;
|
|
4935
|
+
return identifier === identifier.toUpperCase() && /[A-Za-z]/.test(identifier);
|
|
4936
|
+
}
|
|
4937
|
+
function isExcludedOperand(node) {
|
|
4938
|
+
switch (node.type) {
|
|
4939
|
+
case import_utils39.AST_NODE_TYPES.Literal:
|
|
4940
|
+
return true;
|
|
4941
|
+
case import_utils39.AST_NODE_TYPES.TemplateLiteral:
|
|
4942
|
+
return node.expressions.length === 0;
|
|
4943
|
+
case import_utils39.AST_NODE_TYPES.Identifier:
|
|
4944
|
+
return SENTINEL_IDENTIFIERS.has(node.name) || isConstantReference(node.name);
|
|
4945
|
+
case import_utils39.AST_NODE_TYPES.MemberExpression:
|
|
4946
|
+
return !node.computed && node.property.type === import_utils39.AST_NODE_TYPES.Identifier && isConstantReference(node.property.name);
|
|
4947
|
+
default:
|
|
4948
|
+
return false;
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
function operandName(node) {
|
|
4952
|
+
if (node.type === import_utils39.AST_NODE_TYPES.Identifier) {
|
|
4953
|
+
return node.name;
|
|
4954
|
+
}
|
|
4955
|
+
if (node.type === import_utils39.AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === import_utils39.AST_NODE_TYPES.Identifier) {
|
|
4956
|
+
return node.property.name;
|
|
4957
|
+
}
|
|
4958
|
+
return null;
|
|
4959
|
+
}
|
|
4960
|
+
function isSecretOperand(node) {
|
|
4961
|
+
if (node.type === import_utils39.AST_NODE_TYPES.TemplateLiteral) {
|
|
4962
|
+
return node.expressions.some((expression) => isSecretOperand(expression));
|
|
4963
|
+
}
|
|
4964
|
+
const name = operandName(node);
|
|
4965
|
+
return name !== null && isAuthSecretName(name);
|
|
4966
|
+
}
|
|
4967
|
+
function secretNameOf(node) {
|
|
4968
|
+
if (node.type === import_utils39.AST_NODE_TYPES.TemplateLiteral) {
|
|
4969
|
+
for (const expression of node.expressions) {
|
|
4970
|
+
const nested = secretNameOf(expression);
|
|
4971
|
+
if (nested !== null) {
|
|
4972
|
+
return nested;
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
return null;
|
|
4976
|
+
}
|
|
4977
|
+
return operandName(node);
|
|
4978
|
+
}
|
|
4979
|
+
var prefer_constant_time_secret_compare_default = import_utils39.ESLintUtils.RuleCreator(
|
|
4980
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4981
|
+
)({
|
|
4982
|
+
name: "prefer-constant-time-secret-compare",
|
|
4983
|
+
meta: {
|
|
4984
|
+
type: "problem",
|
|
4985
|
+
docs: {
|
|
4986
|
+
description: "Disallow `===`/`!==` on a secret-like value; short-circuiting comparison leaks the secret through timing. Use a constant-time compare."
|
|
4987
|
+
},
|
|
4988
|
+
schema: [],
|
|
4989
|
+
messages: {
|
|
4990
|
+
preferConstantTimeSecretCompare: "`{{operator}}` on secret `{{name}}` short-circuits on the first differing byte and leaks it through timing. Compare constant-time instead (`crypto.subtle.timingSafeEqual` over equal-length SHA-256 digests)."
|
|
4991
|
+
}
|
|
4992
|
+
},
|
|
4993
|
+
defaultOptions: [],
|
|
4994
|
+
create(context) {
|
|
4995
|
+
if (isTestFile(context.filename)) {
|
|
4996
|
+
return {};
|
|
4997
|
+
}
|
|
4998
|
+
return {
|
|
4999
|
+
BinaryExpression(node) {
|
|
5000
|
+
if (!EQUALITY_OPERATORS.has(node.operator)) {
|
|
5001
|
+
return;
|
|
5002
|
+
}
|
|
5003
|
+
const { left, right } = node;
|
|
5004
|
+
if (isExcludedOperand(left) || isExcludedOperand(right)) {
|
|
5005
|
+
return;
|
|
5006
|
+
}
|
|
5007
|
+
const secret = [left, right].find((operand) => isSecretOperand(operand));
|
|
5008
|
+
if (secret === void 0) {
|
|
5009
|
+
return;
|
|
5010
|
+
}
|
|
5011
|
+
context.report({
|
|
5012
|
+
node,
|
|
5013
|
+
messageId: "preferConstantTimeSecretCompare",
|
|
5014
|
+
data: { operator: node.operator, name: secretNameOf(secret) ?? "" }
|
|
5015
|
+
});
|
|
5016
|
+
}
|
|
5017
|
+
};
|
|
5018
|
+
}
|
|
5019
|
+
});
|
|
5020
|
+
|
|
5021
|
+
// src/rules/store-insert-requires-on-conflict.ts
|
|
5022
|
+
var import_utils40 = require("@typescript-eslint/utils");
|
|
5023
|
+
var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
|
|
5024
|
+
var CONFLICT_HANDLED = /\bON\s+CONFLICT\b|\bON\s+DUPLICATE\s+KEY\b|\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b/i;
|
|
5025
|
+
var INSERT_GATE = /insert/i;
|
|
5026
|
+
var store_insert_requires_on_conflict_default = import_utils40.ESLintUtils.RuleCreator(
|
|
5027
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
5028
|
+
)({
|
|
5029
|
+
name: "store-insert-requires-on-conflict",
|
|
5030
|
+
meta: {
|
|
5031
|
+
type: "problem",
|
|
5032
|
+
docs: {
|
|
5033
|
+
description: "Require an embedded SQL INSERT to carry ON CONFLICT; store writes replay under cron re-runs and queue redelivery and must be idempotent upserts."
|
|
5034
|
+
},
|
|
5035
|
+
schema: [],
|
|
5036
|
+
messages: {
|
|
5037
|
+
storeInsertRequiresOnConflict: "This INSERT is not replay-safe: a cron re-run or queue redelivery duplicates the row (or fails the handler on a unique-constraint violation). Add `ON CONFLICT (...) DO UPDATE` / `DO NOTHING` (or `INSERT OR IGNORE`)."
|
|
5038
|
+
}
|
|
5039
|
+
},
|
|
5040
|
+
defaultOptions: [],
|
|
5041
|
+
create(context) {
|
|
5042
|
+
if (isTestFile(context.filename) || !INSERT_GATE.test(context.sourceCode.text)) {
|
|
5043
|
+
return {};
|
|
5044
|
+
}
|
|
5045
|
+
return createSqlListener((sql, node) => {
|
|
5046
|
+
if (!INSERT_WRITE.test(sql) || CONFLICT_HANDLED.test(sql)) {
|
|
5047
|
+
return;
|
|
5048
|
+
}
|
|
5049
|
+
context.report({ node, messageId: "storeInsertRequiresOnConflict" });
|
|
5050
|
+
});
|
|
5051
|
+
}
|
|
5052
|
+
});
|
|
5053
|
+
|
|
5054
|
+
// src/rules/no-dynamic-sql.ts
|
|
5055
|
+
var import_utils41 = require("@typescript-eslint/utils");
|
|
5056
|
+
var DEFAULT_METHODS = ["prepare", "exec", "query"];
|
|
5057
|
+
var CONSTANT_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
5058
|
+
function isStaticFragment(expression) {
|
|
5059
|
+
if (expression.type === import_utils41.AST_NODE_TYPES.Identifier) {
|
|
5060
|
+
return CONSTANT_CASE_RE.test(expression.name);
|
|
5061
|
+
}
|
|
5062
|
+
if (expression.type === import_utils41.AST_NODE_TYPES.MemberExpression && !expression.computed && expression.property.type === import_utils41.AST_NODE_TYPES.Identifier) {
|
|
5063
|
+
return CONSTANT_CASE_RE.test(expression.property.name);
|
|
5064
|
+
}
|
|
5065
|
+
if (expression.type === import_utils41.AST_NODE_TYPES.Literal) {
|
|
5066
|
+
return typeof expression.value === "string";
|
|
5067
|
+
}
|
|
5068
|
+
return false;
|
|
5069
|
+
}
|
|
5070
|
+
function runtimeInterpolations(template) {
|
|
5071
|
+
return template.expressions.filter(
|
|
5072
|
+
(expression) => !isStaticFragment(expression)
|
|
5073
|
+
);
|
|
5074
|
+
}
|
|
5075
|
+
function concatOperands(node) {
|
|
5076
|
+
if (node.type === import_utils41.AST_NODE_TYPES.BinaryExpression && node.operator === "+") {
|
|
5077
|
+
return [...concatOperands(node.left), ...concatOperands(node.right)];
|
|
5078
|
+
}
|
|
5079
|
+
return [node];
|
|
5080
|
+
}
|
|
5081
|
+
function runtimeConcatOperands(node) {
|
|
5082
|
+
if (node.type !== import_utils41.AST_NODE_TYPES.BinaryExpression || node.operator !== "+") {
|
|
5083
|
+
return [];
|
|
5084
|
+
}
|
|
5085
|
+
const operands = concatOperands(node);
|
|
5086
|
+
const hasStringLiteral = operands.some(
|
|
5087
|
+
(operand) => operand.type === import_utils41.AST_NODE_TYPES.Literal && typeof operand.value === "string"
|
|
5088
|
+
);
|
|
5089
|
+
if (!hasStringLiteral) {
|
|
5090
|
+
return [];
|
|
5091
|
+
}
|
|
5092
|
+
return operands.filter(
|
|
5093
|
+
(operand) => operand.type !== import_utils41.AST_NODE_TYPES.Literal && !isStaticFragment(operand)
|
|
5094
|
+
);
|
|
5095
|
+
}
|
|
5096
|
+
function statementMethodName(node, methods) {
|
|
5097
|
+
const callee = node.callee;
|
|
5098
|
+
if (callee.type !== import_utils41.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils41.AST_NODE_TYPES.Identifier) {
|
|
5099
|
+
return null;
|
|
5100
|
+
}
|
|
5101
|
+
const name = callee.property.name;
|
|
5102
|
+
return methods.has(name) ? name : null;
|
|
5103
|
+
}
|
|
5104
|
+
var no_dynamic_sql_default = import_utils41.ESLintUtils.RuleCreator(
|
|
5105
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
5106
|
+
)({
|
|
5107
|
+
name: "no-dynamic-sql",
|
|
5108
|
+
meta: {
|
|
5109
|
+
type: "problem",
|
|
5110
|
+
docs: {
|
|
5111
|
+
description: "Disallow interpolating or concatenating a runtime value into a SQL statement passed to `prepare`/`exec`/`query`; use a placeholder and bind the value."
|
|
5112
|
+
},
|
|
5113
|
+
schema: [
|
|
5114
|
+
{
|
|
5115
|
+
type: "object",
|
|
5116
|
+
properties: {
|
|
5117
|
+
methods: {
|
|
5118
|
+
type: "array",
|
|
5119
|
+
items: { type: "string" },
|
|
5120
|
+
description: "Statement-taking method names to inspect. Replaces the defaults."
|
|
5121
|
+
}
|
|
5122
|
+
},
|
|
5123
|
+
additionalProperties: false
|
|
5124
|
+
}
|
|
5125
|
+
],
|
|
5126
|
+
messages: {
|
|
5127
|
+
dynamicSql: "Runtime value built into a SQL statement passed to `{{method}}()`. Use a `?` placeholder and pass the value through `.bind(...)` so the driver parameterises it."
|
|
5128
|
+
}
|
|
5129
|
+
},
|
|
5130
|
+
defaultOptions: [{}],
|
|
5131
|
+
create(context, [options]) {
|
|
5132
|
+
const methods = new Set(options?.methods ?? DEFAULT_METHODS);
|
|
5133
|
+
return {
|
|
5134
|
+
CallExpression(node) {
|
|
5135
|
+
const method = statementMethodName(node, methods);
|
|
5136
|
+
if (method === null) {
|
|
5137
|
+
return;
|
|
5138
|
+
}
|
|
5139
|
+
const statement = node.arguments[0];
|
|
5140
|
+
if (statement === void 0) {
|
|
5141
|
+
return;
|
|
5142
|
+
}
|
|
5143
|
+
const offenders = statement.type === import_utils41.AST_NODE_TYPES.TemplateLiteral ? runtimeInterpolations(statement) : runtimeConcatOperands(statement);
|
|
5144
|
+
for (const offender of offenders) {
|
|
5145
|
+
context.report({
|
|
5146
|
+
node: offender,
|
|
5147
|
+
messageId: "dynamicSql",
|
|
5148
|
+
data: { method }
|
|
5149
|
+
});
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
};
|
|
5153
|
+
}
|
|
5154
|
+
});
|
|
5155
|
+
|
|
5156
|
+
// src/rules/no-raw-fetch-outside-clients.ts
|
|
5157
|
+
var import_utils42 = require("@typescript-eslint/utils");
|
|
5158
|
+
var DEFAULT_ALLOW = [
|
|
5159
|
+
"[\\\\/]clients?[\\\\/]",
|
|
5160
|
+
"-client\\.[cm]?[jt]sx?$",
|
|
5161
|
+
"[\\\\/]http-client\\.[cm]?[jt]sx?$",
|
|
5162
|
+
"\\.test\\.",
|
|
5163
|
+
"\\.spec\\.",
|
|
5164
|
+
"[\\\\/]__tests__[\\\\/]",
|
|
5165
|
+
"[\\\\/]__mocks__[\\\\/]"
|
|
5166
|
+
];
|
|
5167
|
+
var GLOBAL_RECEIVERS = /* @__PURE__ */ new Set([
|
|
5168
|
+
"globalThis",
|
|
5169
|
+
"window",
|
|
5170
|
+
"self"
|
|
5171
|
+
]);
|
|
5172
|
+
function isGlobalFetchCall(node) {
|
|
5173
|
+
const callee = node.callee;
|
|
5174
|
+
if (callee.type === "Identifier") {
|
|
5175
|
+
return callee.name === "fetch";
|
|
5176
|
+
}
|
|
5177
|
+
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "fetch" && callee.object.type === "Identifier") {
|
|
5178
|
+
return GLOBAL_RECEIVERS.has(callee.object.name);
|
|
5179
|
+
}
|
|
5180
|
+
return false;
|
|
5181
|
+
}
|
|
5182
|
+
function compile(patterns) {
|
|
5183
|
+
const compiled = [];
|
|
5184
|
+
for (const pattern of patterns) {
|
|
5185
|
+
try {
|
|
5186
|
+
compiled.push(new RegExp(pattern));
|
|
5187
|
+
} catch {
|
|
5188
|
+
}
|
|
5189
|
+
}
|
|
5190
|
+
return compiled;
|
|
5191
|
+
}
|
|
5192
|
+
var no_raw_fetch_outside_clients_default = import_utils42.ESLintUtils.RuleCreator(
|
|
5193
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
5194
|
+
)({
|
|
5195
|
+
name: "no-raw-fetch-outside-clients",
|
|
5196
|
+
meta: {
|
|
5197
|
+
type: "problem",
|
|
5198
|
+
docs: {
|
|
5199
|
+
description: "Disallow calling the global `fetch` outside the client layer; route outbound HTTP through a client module that owns retry, timeout and status handling."
|
|
5200
|
+
},
|
|
5201
|
+
schema: [
|
|
5202
|
+
{
|
|
5203
|
+
type: "object",
|
|
5204
|
+
properties: {
|
|
5205
|
+
allow: {
|
|
5206
|
+
type: "array",
|
|
5207
|
+
items: { type: "string" },
|
|
5208
|
+
description: "Regular-expression sources matched against the filename. Replaces the defaults."
|
|
5209
|
+
}
|
|
5210
|
+
},
|
|
5211
|
+
additionalProperties: false
|
|
5212
|
+
}
|
|
5213
|
+
],
|
|
5214
|
+
messages: {
|
|
5215
|
+
rawFetch: "Raw `fetch()` outside a client module. Route the call through a client (e.g. `clients/*-client.ts`) so it inherits retry, timeout and status handling and stays stubbable in tests."
|
|
5216
|
+
}
|
|
5217
|
+
},
|
|
5218
|
+
defaultOptions: [{}],
|
|
5219
|
+
create(context, [options]) {
|
|
5220
|
+
const patterns = options?.allow ?? DEFAULT_ALLOW;
|
|
5221
|
+
const allowed = compile(patterns);
|
|
5222
|
+
const filename = context.filename;
|
|
5223
|
+
if (allowed.some((re) => re.test(filename))) {
|
|
5224
|
+
return {};
|
|
5225
|
+
}
|
|
5226
|
+
return {
|
|
5227
|
+
CallExpression(node) {
|
|
5228
|
+
if (isGlobalFetchCall(node)) {
|
|
5229
|
+
context.report({ node, messageId: "rawFetch" });
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
};
|
|
5233
|
+
}
|
|
5234
|
+
});
|
|
5235
|
+
|
|
5236
|
+
// src/rules/no-storage-in-stateless-modules.ts
|
|
5237
|
+
var import_utils43 = require("@typescript-eslint/utils");
|
|
5238
|
+
var DEFAULT_METHODS2 = [
|
|
5239
|
+
"prepare",
|
|
5240
|
+
"put",
|
|
5241
|
+
"getWithMetadata"
|
|
5242
|
+
];
|
|
5243
|
+
var MIN_ARGUMENTS = /* @__PURE__ */ new Map([["put", 2]]);
|
|
5244
|
+
function compile2(patterns) {
|
|
5245
|
+
const compiled = [];
|
|
5246
|
+
for (const pattern of patterns) {
|
|
5247
|
+
try {
|
|
5248
|
+
compiled.push(new RegExp(pattern));
|
|
5249
|
+
} catch {
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
return compiled;
|
|
5253
|
+
}
|
|
5254
|
+
function storageMethodName(node, methods) {
|
|
5255
|
+
const callee = node.callee;
|
|
5256
|
+
if (callee.type !== import_utils43.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils43.AST_NODE_TYPES.Identifier) {
|
|
5257
|
+
return null;
|
|
5258
|
+
}
|
|
5259
|
+
const name = callee.property.name;
|
|
5260
|
+
if (!methods.has(name)) {
|
|
5261
|
+
return null;
|
|
5262
|
+
}
|
|
5263
|
+
if (node.arguments.length < (MIN_ARGUMENTS.get(name) ?? 1)) {
|
|
5264
|
+
return null;
|
|
5265
|
+
}
|
|
5266
|
+
return name;
|
|
5267
|
+
}
|
|
5268
|
+
var no_storage_in_stateless_modules_default = import_utils43.ESLintUtils.RuleCreator(
|
|
5269
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
5270
|
+
)({
|
|
5271
|
+
name: "no-storage-in-stateless-modules",
|
|
5272
|
+
meta: {
|
|
5273
|
+
type: "problem",
|
|
5274
|
+
docs: {
|
|
5275
|
+
description: "Disallow SQL or key/value access inside modules a team has declared stateless; derive state from the systems of record instead. No-op until `modules` is configured."
|
|
5276
|
+
},
|
|
5277
|
+
schema: [
|
|
5278
|
+
{
|
|
5279
|
+
type: "object",
|
|
5280
|
+
properties: {
|
|
5281
|
+
modules: {
|
|
5282
|
+
type: "array",
|
|
5283
|
+
items: { type: "string" },
|
|
5284
|
+
description: "Regex sources matched against the filename. Empty (the default) disables the rule."
|
|
5285
|
+
},
|
|
5286
|
+
methods: {
|
|
5287
|
+
type: "array",
|
|
5288
|
+
items: { type: "string" },
|
|
5289
|
+
description: "Storage method names to flag. Replaces the defaults."
|
|
5290
|
+
}
|
|
5291
|
+
},
|
|
5292
|
+
additionalProperties: false
|
|
5293
|
+
}
|
|
5294
|
+
],
|
|
5295
|
+
messages: {
|
|
5296
|
+
storageInStatelessModule: "`{{method}}()` reaches for private storage inside a module declared stateless. Derive the state from a read against the system of record, or from a marker in the artefact this feature already produces."
|
|
5297
|
+
}
|
|
5298
|
+
},
|
|
5299
|
+
defaultOptions: [{}],
|
|
5300
|
+
create(context, [options]) {
|
|
5301
|
+
const modules = options?.modules ?? [];
|
|
5302
|
+
if (modules.length === 0) {
|
|
5303
|
+
return {};
|
|
5304
|
+
}
|
|
5305
|
+
const scoped = compile2(modules);
|
|
5306
|
+
if (!scoped.some((re) => re.test(context.filename))) {
|
|
5307
|
+
return {};
|
|
5308
|
+
}
|
|
5309
|
+
const methods = new Set(options?.methods ?? DEFAULT_METHODS2);
|
|
5310
|
+
return {
|
|
5311
|
+
CallExpression(node) {
|
|
5312
|
+
const method = storageMethodName(node, methods);
|
|
5313
|
+
if (method !== null) {
|
|
5314
|
+
context.report({
|
|
5315
|
+
node,
|
|
5316
|
+
messageId: "storageInStatelessModule",
|
|
5317
|
+
data: { method }
|
|
5318
|
+
});
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5323
|
+
});
|
|
5324
|
+
|
|
3767
5325
|
// src/index.ts
|
|
3768
5326
|
var rules = {
|
|
3769
5327
|
"enforce-file-structure": enforce_file_structure_default,
|
|
@@ -3791,12 +5349,25 @@ var rules = {
|
|
|
3791
5349
|
"no-secret-in-log": no_secret_in_log_default,
|
|
3792
5350
|
"no-unsafe-cast": no_unsafe_cast_default,
|
|
3793
5351
|
"prefer-string-literal-union": prefer_string_literal_union_default,
|
|
3794
|
-
"single-public-export": single_public_export_default
|
|
5352
|
+
"single-public-export": single_public_export_default,
|
|
5353
|
+
"no-silent-promise-catch": no_silent_promise_catch_default,
|
|
5354
|
+
"require-fetch-timeout": require_fetch_timeout_default,
|
|
5355
|
+
"require-schema-validate-search": require_schema_validate_search_default,
|
|
5356
|
+
"no-offset-pagination": no_offset_pagination_default,
|
|
5357
|
+
"no-positional-tuple-return": no_positional_tuple_return_default,
|
|
5358
|
+
"no-repeated-string-literal": no_repeated_string_literal_default,
|
|
5359
|
+
"no-select-star": no_select_star_default,
|
|
5360
|
+
"no-sleep-in-test-body": no_sleep_in_test_body_default,
|
|
5361
|
+
"prefer-constant-time-secret-compare": prefer_constant_time_secret_compare_default,
|
|
5362
|
+
"store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
|
|
5363
|
+
"no-dynamic-sql": no_dynamic_sql_default,
|
|
5364
|
+
"no-raw-fetch-outside-clients": no_raw_fetch_outside_clients_default,
|
|
5365
|
+
"no-storage-in-stateless-modules": no_storage_in_stateless_modules_default
|
|
3795
5366
|
};
|
|
3796
5367
|
var plugin = {
|
|
3797
5368
|
meta: {
|
|
3798
5369
|
name: "@sarj/eslint-plugin",
|
|
3799
|
-
version: "2.
|
|
5370
|
+
version: "2.8.0"
|
|
3800
5371
|
},
|
|
3801
5372
|
rules,
|
|
3802
5373
|
configs: {
|
|
@@ -3828,7 +5399,24 @@ var plugin = {
|
|
|
3828
5399
|
"@sarj/no-secret-in-log": "warn",
|
|
3829
5400
|
"@sarj/no-unsafe-cast": "warn",
|
|
3830
5401
|
"@sarj/single-public-export": "warn",
|
|
3831
|
-
"@sarj/prefer-string-literal-union": "warn"
|
|
5402
|
+
"@sarj/prefer-string-literal-union": "warn",
|
|
5403
|
+
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
5404
|
+
"@sarj/require-fetch-timeout": "warn",
|
|
5405
|
+
"@sarj/no-silent-promise-catch": "warn",
|
|
5406
|
+
"@sarj/require-schema-validate-search": "warn",
|
|
5407
|
+
// Second SARJ port wave — the TS/Python parity gap. Each targets a
|
|
5408
|
+
// defect class seen in production Workers code: timing-leaky secret
|
|
5409
|
+
// compares, non-idempotent store writes under queue redelivery,
|
|
5410
|
+
// O(N) pagination, implicit row contracts, flaky timed tests.
|
|
5411
|
+
"@sarj/prefer-constant-time-secret-compare": "error",
|
|
5412
|
+
"@sarj/store-insert-requires-on-conflict": "warn",
|
|
5413
|
+
"@sarj/no-offset-pagination": "warn",
|
|
5414
|
+
"@sarj/no-select-star": "warn",
|
|
5415
|
+
"@sarj/no-sleep-in-test-body": "warn",
|
|
5416
|
+
"@sarj/no-repeated-string-literal": "warn",
|
|
5417
|
+
"@sarj/no-positional-tuple-return": "warn",
|
|
5418
|
+
// Injection guard — low FP, applies to any repo touching SQL.
|
|
5419
|
+
"@sarj/no-dynamic-sql": "warn"
|
|
3832
5420
|
}
|
|
3833
5421
|
},
|
|
3834
5422
|
strict: {
|
|
@@ -3864,7 +5452,31 @@ var plugin = {
|
|
|
3864
5452
|
"@sarj/no-unsafe-cast": "warn",
|
|
3865
5453
|
"@sarj/single-public-export": "error",
|
|
3866
5454
|
// High-volume/stylistic — warn until rollout proves FP rate.
|
|
3867
|
-
"@sarj/prefer-string-literal-union": "warn"
|
|
5455
|
+
"@sarj/prefer-string-literal-union": "warn",
|
|
5456
|
+
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
5457
|
+
"@sarj/require-fetch-timeout": "error",
|
|
5458
|
+
"@sarj/no-silent-promise-catch": "error",
|
|
5459
|
+
"@sarj/require-schema-validate-search": "error",
|
|
5460
|
+
// Second SARJ port wave — the TS/Python parity gap.
|
|
5461
|
+
"@sarj/prefer-constant-time-secret-compare": "error",
|
|
5462
|
+
"@sarj/store-insert-requires-on-conflict": "error",
|
|
5463
|
+
"@sarj/no-offset-pagination": "error",
|
|
5464
|
+
"@sarj/no-select-star": "error",
|
|
5465
|
+
"@sarj/no-sleep-in-test-body": "error",
|
|
5466
|
+
"@sarj/no-repeated-string-literal": "error",
|
|
5467
|
+
// API-shape advice rather than a runtime defect — a corpus sweep found its
|
|
5468
|
+
// only hits are parser `[value, cursor]` returns, which are conventional.
|
|
5469
|
+
// Warn even in strict until a rollout justifies more.
|
|
5470
|
+
"@sarj/no-positional-tuple-return": "warn",
|
|
5471
|
+
"@sarj/no-dynamic-sql": "error",
|
|
5472
|
+
// Architectural: both need per-repo config to be meaningful, so they
|
|
5473
|
+
// are strict-only. `no-storage-in-stateless-modules` is a no-op until
|
|
5474
|
+
// its `modules` option names the directories a team declared stateless;
|
|
5475
|
+
// `no-raw-fetch-outside-clients` defaults to the `clients/` convention
|
|
5476
|
+
// and takes an `allow` list for repos that lay their client layer out
|
|
5477
|
+
// differently.
|
|
5478
|
+
"@sarj/no-raw-fetch-outside-clients": "error",
|
|
5479
|
+
"@sarj/no-storage-in-stateless-modules": "error"
|
|
3868
5480
|
}
|
|
3869
5481
|
}
|
|
3870
5482
|
}
|