@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.js
CHANGED
|
@@ -316,12 +316,10 @@ var no_comment_cruft_default = ESLintUtils3.RuleCreator(
|
|
|
316
316
|
}
|
|
317
317
|
const first = leading[0];
|
|
318
318
|
if (first === void 0 || leading.length < LEADING_PREAMBLE_MIN) return;
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
context.report({ node: first, messageId: "fileHeaderPreamble" });
|
|
324
|
-
}
|
|
319
|
+
const bodies = leading.map((c) => stripCommentMarker(c.value));
|
|
320
|
+
if (bodies.some((body) => LICENSE_RE.test(body))) return;
|
|
321
|
+
if (bodies.some((body) => isProse(body))) return;
|
|
322
|
+
context.report({ node: first, messageId: "fileHeaderPreamble" });
|
|
325
323
|
}
|
|
326
324
|
return {
|
|
327
325
|
Program() {
|
|
@@ -799,35 +797,15 @@ var LOGGER_NAMES = /* @__PURE__ */ new Set([
|
|
|
799
797
|
"_logger",
|
|
800
798
|
"_log"
|
|
801
799
|
]);
|
|
800
|
+
var LOGGER_FACTORIES = /* @__PURE__ */ new Set([
|
|
801
|
+
"getlogger",
|
|
802
|
+
"get_logger"
|
|
803
|
+
]);
|
|
802
804
|
var REPORT_NAME_RE = /error|report|capture|log|trace|warn/i;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
case "MemberExpression": {
|
|
808
|
-
const { property, object } = expr;
|
|
809
|
-
if (!expr.computed && property.type === "Identifier" && LOGGER_NAMES.has(property.name.toLowerCase())) {
|
|
810
|
-
return true;
|
|
811
|
-
}
|
|
812
|
-
return isLoggerReceiver(object);
|
|
813
|
-
}
|
|
814
|
-
default:
|
|
815
|
-
return false;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
function isLoggingCall(expr) {
|
|
819
|
-
if (expr.type !== "CallExpression") {
|
|
820
|
-
return false;
|
|
821
|
-
}
|
|
822
|
-
const callee = expr.callee;
|
|
823
|
-
if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") {
|
|
824
|
-
return false;
|
|
825
|
-
}
|
|
826
|
-
if (!LOG_METHODS.has(callee.property.name.toLowerCase())) {
|
|
827
|
-
return false;
|
|
828
|
-
}
|
|
829
|
-
return isLoggerReceiver(callee.object);
|
|
830
|
-
}
|
|
805
|
+
var LOGGING_OPTION_PROPERTIES = {
|
|
806
|
+
loggerNames: { type: "array", items: { type: "string" } },
|
|
807
|
+
logFunctions: { type: "array", items: { type: "string" } }
|
|
808
|
+
};
|
|
831
809
|
function calleeName(callee) {
|
|
832
810
|
if (callee.type === "Identifier") {
|
|
833
811
|
return callee.name;
|
|
@@ -837,6 +815,65 @@ function calleeName(callee) {
|
|
|
837
815
|
}
|
|
838
816
|
return null;
|
|
839
817
|
}
|
|
818
|
+
function createLogMatcher(options = {}) {
|
|
819
|
+
const loggerNames = /* @__PURE__ */ new Set([
|
|
820
|
+
...LOGGER_NAMES,
|
|
821
|
+
...(options.loggerNames ?? []).map((name) => name.toLowerCase())
|
|
822
|
+
]);
|
|
823
|
+
const logFunctions = new Set(options.logFunctions ?? []);
|
|
824
|
+
function isLoggerReceiver(expr) {
|
|
825
|
+
switch (expr.type) {
|
|
826
|
+
case "Identifier":
|
|
827
|
+
return loggerNames.has(expr.name.toLowerCase());
|
|
828
|
+
case "MemberExpression": {
|
|
829
|
+
const { property, object } = expr;
|
|
830
|
+
if (!expr.computed && property.type === "Identifier") {
|
|
831
|
+
const lowered = property.name.toLowerCase();
|
|
832
|
+
if (loggerNames.has(lowered) || LOGGER_FACTORIES.has(lowered)) {
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return isLoggerReceiver(object);
|
|
837
|
+
}
|
|
838
|
+
case "CallExpression": {
|
|
839
|
+
const callee = expr.callee;
|
|
840
|
+
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && LOGGER_FACTORIES.has(callee.property.name.toLowerCase())) {
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
if (callee.type !== "Super") {
|
|
844
|
+
return isLoggerReceiver(callee);
|
|
845
|
+
}
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
default:
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function isLogFunctionCall(expr) {
|
|
853
|
+
if (expr.type !== "CallExpression" || logFunctions.size === 0) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
const name = calleeName(expr.callee);
|
|
857
|
+
return name !== null && logFunctions.has(name);
|
|
858
|
+
}
|
|
859
|
+
function isLoggingCall(expr) {
|
|
860
|
+
if (expr.type !== "CallExpression") {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
if (isLogFunctionCall(expr)) {
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
const callee = expr.callee;
|
|
867
|
+
if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") {
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
if (!LOG_METHODS.has(callee.property.name.toLowerCase())) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
return isLoggerReceiver(callee.object);
|
|
874
|
+
}
|
|
875
|
+
return { isLoggerReceiver, isLogFunctionCall, isLoggingCall };
|
|
876
|
+
}
|
|
840
877
|
|
|
841
878
|
// src/rules/no-log-only-catch.ts
|
|
842
879
|
var DEFAULT_IGNORE_PATTERNS2 = [
|
|
@@ -844,12 +881,6 @@ var DEFAULT_IGNORE_PATTERNS2 = [
|
|
|
844
881
|
/\.spec\./,
|
|
845
882
|
/[\\/]__tests__[\\/]/
|
|
846
883
|
];
|
|
847
|
-
function isLoggingCallStatement(statement) {
|
|
848
|
-
if (statement.type !== "ExpressionStatement") {
|
|
849
|
-
return false;
|
|
850
|
-
}
|
|
851
|
-
return isLoggingCall(statement.expression);
|
|
852
|
-
}
|
|
853
884
|
var no_log_only_catch_default = ESLintUtils7.RuleCreator(
|
|
854
885
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
855
886
|
)({
|
|
@@ -859,15 +890,28 @@ var no_log_only_catch_default = ESLintUtils7.RuleCreator(
|
|
|
859
890
|
docs: {
|
|
860
891
|
description: "Disallow `catch` clauses that only log (or silently do nothing) and then swallow the error; rethrow or handle it instead."
|
|
861
892
|
},
|
|
862
|
-
schema: [
|
|
893
|
+
schema: [
|
|
894
|
+
{
|
|
895
|
+
type: "object",
|
|
896
|
+
additionalProperties: false,
|
|
897
|
+
properties: { ...LOGGING_OPTION_PROPERTIES }
|
|
898
|
+
}
|
|
899
|
+
],
|
|
863
900
|
messages: {
|
|
864
901
|
noLogOnlyCatch: "Logging then swallowing the error hides failures. Rethrow the error or handle it for real.",
|
|
865
902
|
emptyCatch: "Empty catch silently swallows the error. Rethrow it, handle it, or add a comment explaining why it is safe to ignore."
|
|
866
903
|
}
|
|
867
904
|
},
|
|
868
|
-
defaultOptions: [],
|
|
869
|
-
create(context) {
|
|
905
|
+
defaultOptions: [{}],
|
|
906
|
+
create(context, [loggingOptions]) {
|
|
907
|
+
const matcher = createLogMatcher(loggingOptions);
|
|
870
908
|
const filename = context.filename;
|
|
909
|
+
function isLoggingCallStatement(statement) {
|
|
910
|
+
if (statement.type !== "ExpressionStatement") {
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
return matcher.isLoggingCall(statement.expression);
|
|
914
|
+
}
|
|
871
915
|
const isIgnoredByDefault = DEFAULT_IGNORE_PATTERNS2.some(
|
|
872
916
|
(re) => re.test(filename)
|
|
873
917
|
);
|
|
@@ -1040,46 +1084,131 @@ function containsThrow(node) {
|
|
|
1040
1084
|
(current) => current.type === AST_NODE_TYPES3.ThrowStatement
|
|
1041
1085
|
);
|
|
1042
1086
|
}
|
|
1043
|
-
function
|
|
1044
|
-
|
|
1045
|
-
|
|
1087
|
+
function bindsName(param, name) {
|
|
1088
|
+
switch (param.type) {
|
|
1089
|
+
case AST_NODE_TYPES3.Identifier:
|
|
1090
|
+
return param.name === name;
|
|
1091
|
+
case AST_NODE_TYPES3.AssignmentPattern:
|
|
1092
|
+
return bindsName(param.left, name);
|
|
1093
|
+
case AST_NODE_TYPES3.RestElement:
|
|
1094
|
+
return bindsName(param.argument, name);
|
|
1095
|
+
case AST_NODE_TYPES3.ArrayPattern:
|
|
1096
|
+
return param.elements.some(
|
|
1097
|
+
(element) => element !== null && bindsName(element, name)
|
|
1098
|
+
);
|
|
1099
|
+
case AST_NODE_TYPES3.ObjectPattern:
|
|
1100
|
+
return param.properties.some(
|
|
1101
|
+
(property) => property.type === AST_NODE_TYPES3.RestElement ? bindsName(property.argument, name) : bindsName(property.value, name)
|
|
1102
|
+
);
|
|
1103
|
+
default:
|
|
1104
|
+
return false;
|
|
1046
1105
|
}
|
|
1047
|
-
return args.some(
|
|
1048
|
-
(arg) => arg.type === AST_NODE_TYPES3.Identifier && arg.name === caughtName
|
|
1049
|
-
);
|
|
1050
1106
|
}
|
|
1051
|
-
function
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1107
|
+
function subtreeReadsName(node, name) {
|
|
1108
|
+
let found = false;
|
|
1109
|
+
const shadowsName = (fn) => isFunctionNode(fn) && fn.params.some(
|
|
1110
|
+
(param) => bindsName(param, name)
|
|
1111
|
+
);
|
|
1112
|
+
const recurse = (current) => {
|
|
1113
|
+
if (found) {
|
|
1114
|
+
return;
|
|
1055
1115
|
}
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1116
|
+
if (current.type === AST_NODE_TYPES3.Identifier && current.name === name) {
|
|
1117
|
+
found = true;
|
|
1118
|
+
return;
|
|
1058
1119
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1120
|
+
if (shadowsName(current)) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
for (const key of Object.keys(current)) {
|
|
1124
|
+
if (key === "parent") {
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (key === "key" && current.type === AST_NODE_TYPES3.Property && !current.computed) {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
if (key === "property" && current.type === AST_NODE_TYPES3.MemberExpression && !current.computed) {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
const value = current[key];
|
|
1134
|
+
if (Array.isArray(value)) {
|
|
1135
|
+
for (const child of value) {
|
|
1136
|
+
if (isNode(child)) {
|
|
1137
|
+
recurse(child);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
} else if (isNode(value)) {
|
|
1141
|
+
recurse(value);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
recurse(node);
|
|
1146
|
+
return found;
|
|
1147
|
+
}
|
|
1148
|
+
function argsIncludeBinding(args, caughtName) {
|
|
1149
|
+
if (caughtName === null) {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
return args.some((arg) => subtreeReadsName(arg, caughtName));
|
|
1062
1153
|
}
|
|
1063
1154
|
function tryBlockOf(catchNode) {
|
|
1064
1155
|
return catchNode.parent.block;
|
|
1065
1156
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1157
|
+
var SAFE_PARSE_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
1158
|
+
"RegExp",
|
|
1159
|
+
"URL",
|
|
1160
|
+
"URLPattern"
|
|
1161
|
+
]);
|
|
1162
|
+
function isParseShapedNode(node) {
|
|
1163
|
+
if (node.type === AST_NODE_TYPES3.CallExpression && node.callee.type === AST_NODE_TYPES3.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES3.Identifier) {
|
|
1164
|
+
return node.callee.property.name === "parse";
|
|
1072
1165
|
}
|
|
1073
|
-
if (
|
|
1074
|
-
return
|
|
1166
|
+
if (node.type === AST_NODE_TYPES3.NewExpression && node.callee.type === AST_NODE_TYPES3.Identifier) {
|
|
1167
|
+
return SAFE_PARSE_CONSTRUCTORS.has(node.callee.name);
|
|
1075
1168
|
}
|
|
1076
1169
|
return false;
|
|
1077
1170
|
}
|
|
1171
|
+
var BODY_DECODE_METHODS = /* @__PURE__ */ new Set([
|
|
1172
|
+
"json",
|
|
1173
|
+
"text",
|
|
1174
|
+
"arrayBuffer"
|
|
1175
|
+
]);
|
|
1176
|
+
function isBodyDecodeNode(node) {
|
|
1177
|
+
return node.type === AST_NODE_TYPES3.CallExpression && node.callee.type === AST_NODE_TYPES3.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES3.Identifier && BODY_DECODE_METHODS.has(node.callee.property.name);
|
|
1178
|
+
}
|
|
1179
|
+
function returnsMatching(stmt, predicate) {
|
|
1180
|
+
return stmt.type === AST_NODE_TYPES3.ReturnStatement && stmt.argument !== null && walkWithinScope(stmt.argument, predicate);
|
|
1181
|
+
}
|
|
1182
|
+
function enclosingReturnTypeNode(node) {
|
|
1183
|
+
let current = node.parent;
|
|
1184
|
+
while (current !== void 0 && current !== null) {
|
|
1185
|
+
if (isFunctionNode(current) && "returnType" in current) {
|
|
1186
|
+
return current.returnType?.typeAnnotation ?? null;
|
|
1187
|
+
}
|
|
1188
|
+
current = current.parent;
|
|
1189
|
+
}
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
function isDeclaredBooleanPredicate(catchNode, kind) {
|
|
1193
|
+
if (kind !== "boolean") {
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
let declared = enclosingReturnTypeNode(catchNode);
|
|
1197
|
+
if (declared?.type === AST_NODE_TYPES3.TSTypeReference && declared.typeName.type === AST_NODE_TYPES3.Identifier && declared.typeName.name === "Promise") {
|
|
1198
|
+
declared = declared.typeArguments?.params[0] ?? null;
|
|
1199
|
+
}
|
|
1200
|
+
return declared?.type === AST_NODE_TYPES3.TSBooleanKeyword;
|
|
1201
|
+
}
|
|
1078
1202
|
function tryReturnsSafeParse(catchNode) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1203
|
+
const tryBlock = tryBlockOf(catchNode);
|
|
1204
|
+
if (walkWithinScope(
|
|
1205
|
+
tryBlock,
|
|
1206
|
+
(current) => returnsMatching(current, isParseShapedNode)
|
|
1207
|
+
)) {
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
const only = tryBlock.body.length === 1 ? tryBlock.body[0] : void 0;
|
|
1211
|
+
return only !== void 0 && returnsMatching(only, isBodyDecodeNode);
|
|
1083
1212
|
}
|
|
1084
1213
|
function enclosingFunctionBody(node) {
|
|
1085
1214
|
let current = node.parent;
|
|
@@ -1125,13 +1254,32 @@ var no_sentinel_return_on_catch_default = ESLintUtils9.RuleCreator(
|
|
|
1125
1254
|
docs: {
|
|
1126
1255
|
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."
|
|
1127
1256
|
},
|
|
1128
|
-
schema: [
|
|
1257
|
+
schema: [
|
|
1258
|
+
{
|
|
1259
|
+
type: "object",
|
|
1260
|
+
additionalProperties: false,
|
|
1261
|
+
properties: { ...LOGGING_OPTION_PROPERTIES }
|
|
1262
|
+
}
|
|
1263
|
+
],
|
|
1129
1264
|
messages: {
|
|
1130
1265
|
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."
|
|
1131
1266
|
}
|
|
1132
1267
|
},
|
|
1133
|
-
defaultOptions: [],
|
|
1134
|
-
create(context) {
|
|
1268
|
+
defaultOptions: [{}],
|
|
1269
|
+
create(context, [loggingOptions]) {
|
|
1270
|
+
const matcher = createLogMatcher(loggingOptions);
|
|
1271
|
+
function logsOrReportsError(catchBody, caughtName) {
|
|
1272
|
+
return walkWithinScope(catchBody, (current) => {
|
|
1273
|
+
if (current.type !== AST_NODE_TYPES3.CallExpression) {
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
if (matcher.isLoggingCall(current)) {
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
const name = calleeName(current.callee);
|
|
1280
|
+
return name !== null && REPORT_NAME_RE.test(name) && argsIncludeBinding(current.arguments, caughtName);
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1135
1283
|
return {
|
|
1136
1284
|
CatchClause(node) {
|
|
1137
1285
|
const body = node.body.body;
|
|
@@ -1156,6 +1304,9 @@ var no_sentinel_return_on_catch_default = ESLintUtils9.RuleCreator(
|
|
|
1156
1304
|
return;
|
|
1157
1305
|
}
|
|
1158
1306
|
const kind = sentinelKind(last.argument);
|
|
1307
|
+
if (kind !== null && isDeclaredBooleanPredicate(node, kind)) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1159
1310
|
if (kind !== null && functionReturnsSameSentinelKindElsewhere(node, kind)) {
|
|
1160
1311
|
return;
|
|
1161
1312
|
}
|
|
@@ -1260,6 +1411,28 @@ function isThreadedAccumulator(node) {
|
|
|
1260
1411
|
}
|
|
1261
1412
|
return referencesName(node.argument, target);
|
|
1262
1413
|
}
|
|
1414
|
+
function namesReadBy(test) {
|
|
1415
|
+
const names = /* @__PURE__ */ new Set();
|
|
1416
|
+
visitScope(test, (node) => {
|
|
1417
|
+
if (node.type === "Identifier") {
|
|
1418
|
+
names.add(node.name);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
return names;
|
|
1422
|
+
}
|
|
1423
|
+
function testStateIsAssignedInBody(test, body) {
|
|
1424
|
+
const testNames = namesReadBy(test);
|
|
1425
|
+
if (testNames.size === 0) {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
let found = false;
|
|
1429
|
+
visitScope(body, (node) => {
|
|
1430
|
+
if (node.type === "AssignmentExpression" && node.operator === "=" && node.left.type === "Identifier" && testNames.has(node.left.name)) {
|
|
1431
|
+
found = true;
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
return found;
|
|
1435
|
+
}
|
|
1263
1436
|
function shouldReport(awaits, earlyExit, iterableText) {
|
|
1264
1437
|
if (awaits.length === 0) {
|
|
1265
1438
|
return false;
|
|
@@ -1306,6 +1479,9 @@ var no_sequential_await_default = ESLintUtils10.RuleCreator(
|
|
|
1306
1479
|
return null;
|
|
1307
1480
|
}
|
|
1308
1481
|
function checkLoop(node) {
|
|
1482
|
+
if ((node.type === "WhileStatement" || node.type === "DoWhileStatement") && testStateIsAssignedInBody(node.test, node.body)) {
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1309
1485
|
const awaits = [];
|
|
1310
1486
|
let earlyExit = false;
|
|
1311
1487
|
for (const part of loopParts(node)) {
|
|
@@ -1734,7 +1910,7 @@ var unwrap = (node) => {
|
|
|
1734
1910
|
}
|
|
1735
1911
|
return current ?? null;
|
|
1736
1912
|
};
|
|
1737
|
-
var
|
|
1913
|
+
var isRawPayloadSource = (node) => {
|
|
1738
1914
|
let current = unwrap(node);
|
|
1739
1915
|
if (current === null) return false;
|
|
1740
1916
|
if (current.type === AST_NODE_TYPES6.AwaitExpression) {
|
|
@@ -1748,7 +1924,14 @@ var isJsonCall = (node) => {
|
|
|
1748
1924
|
return false;
|
|
1749
1925
|
}
|
|
1750
1926
|
const property = unwrap(callee.property);
|
|
1751
|
-
|
|
1927
|
+
if (property === null || property.type !== AST_NODE_TYPES6.Identifier) {
|
|
1928
|
+
return false;
|
|
1929
|
+
}
|
|
1930
|
+
if (property.name === "json") {
|
|
1931
|
+
return true;
|
|
1932
|
+
}
|
|
1933
|
+
const object = unwrap(callee.object);
|
|
1934
|
+
return property.name === "parse" && object !== null && object.type === AST_NODE_TYPES6.Identifier && object.name === "JSON";
|
|
1752
1935
|
};
|
|
1753
1936
|
var findVariable2 = (scope, name) => {
|
|
1754
1937
|
let current = scope;
|
|
@@ -1798,18 +1981,18 @@ var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
|
|
|
1798
1981
|
meta: {
|
|
1799
1982
|
type: "problem",
|
|
1800
1983
|
docs: {
|
|
1801
|
-
description: "Require Zod (or similar) schema validation on `response.json()` before property access."
|
|
1984
|
+
description: "Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access."
|
|
1802
1985
|
},
|
|
1803
1986
|
schema: [],
|
|
1804
1987
|
messages: {
|
|
1805
|
-
unparsedJsonAccess: "Property access on
|
|
1988
|
+
unparsedJsonAccess: "Property access on an unvalidated payload (`response.json()` / `JSON.parse()`) without a schema parse. Pipe through `XSchema.parse(...)` (Zod) before reading fields."
|
|
1806
1989
|
}
|
|
1807
1990
|
},
|
|
1808
1991
|
defaultOptions: [],
|
|
1809
1992
|
create(context) {
|
|
1810
1993
|
const unvalidatedVariables = /* @__PURE__ */ new Set();
|
|
1811
1994
|
const trackInitializer = (declarator) => {
|
|
1812
|
-
if (!
|
|
1995
|
+
if (!isRawPayloadSource(declarator.init)) return;
|
|
1813
1996
|
const declaredVars = context.sourceCode.getDeclaredVariables(declarator);
|
|
1814
1997
|
const variable = declaredVars[0];
|
|
1815
1998
|
if (variable !== void 0) {
|
|
@@ -1824,7 +2007,7 @@ var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
|
|
|
1824
2007
|
return;
|
|
1825
2008
|
}
|
|
1826
2009
|
if (node.id.type === AST_NODE_TYPES6.ObjectPattern || node.id.type === AST_NODE_TYPES6.ArrayPattern) {
|
|
1827
|
-
if (
|
|
2010
|
+
if (isRawPayloadSource(node.init)) {
|
|
1828
2011
|
context.report({ node: node.id, messageId: "unparsedJsonAccess" });
|
|
1829
2012
|
return;
|
|
1830
2013
|
}
|
|
@@ -1838,7 +2021,7 @@ var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
|
|
|
1838
2021
|
if (node.left.type === AST_NODE_TYPES6.Identifier) {
|
|
1839
2022
|
const variable = findVariable2(scope, node.left.name);
|
|
1840
2023
|
if (variable === null) return;
|
|
1841
|
-
if (
|
|
2024
|
+
if (isRawPayloadSource(node.right)) {
|
|
1842
2025
|
unvalidatedVariables.add(variable);
|
|
1843
2026
|
} else {
|
|
1844
2027
|
unvalidatedVariables.delete(variable);
|
|
@@ -1846,7 +2029,7 @@ var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
|
|
|
1846
2029
|
return;
|
|
1847
2030
|
}
|
|
1848
2031
|
if (node.left.type === AST_NODE_TYPES6.ObjectPattern || node.left.type === AST_NODE_TYPES6.ArrayPattern) {
|
|
1849
|
-
if (
|
|
2032
|
+
if (isRawPayloadSource(node.right)) {
|
|
1850
2033
|
context.report({
|
|
1851
2034
|
node: node.left,
|
|
1852
2035
|
messageId: "unparsedJsonAccess"
|
|
@@ -1880,7 +2063,7 @@ var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
|
|
|
1880
2063
|
MemberExpression(node) {
|
|
1881
2064
|
const scope = context.sourceCode.getScope(node);
|
|
1882
2065
|
const obj = unwrap(node.object);
|
|
1883
|
-
if (
|
|
2066
|
+
if (isRawPayloadSource(obj)) {
|
|
1884
2067
|
const parent = node.parent;
|
|
1885
2068
|
if (parent.type === AST_NODE_TYPES6.CallExpression && parent.callee === node && node.property.type === AST_NODE_TYPES6.Identifier && (node.property.name === "parse" || node.property.name === "safeParse")) {
|
|
1886
2069
|
return;
|
|
@@ -2397,7 +2580,13 @@ var require_assert_never_default = ESLintUtils18.RuleCreator(
|
|
|
2397
2580
|
|
|
2398
2581
|
// src/rules/require-zod-form-validation.ts
|
|
2399
2582
|
import { ESLintUtils as ESLintUtils19, AST_NODE_TYPES as AST_NODE_TYPES10 } from "@typescript-eslint/utils";
|
|
2583
|
+
|
|
2584
|
+
// src/rules/_zod.ts
|
|
2585
|
+
var ZOD_PREFIX_RE = /^Z[A-Z]/;
|
|
2586
|
+
var ZOD_SUFFIX_RE = /Schema$/;
|
|
2400
2587
|
var ZOD_SCHEMA_NAME_RE = /Schema$|^Z[A-Z]/;
|
|
2588
|
+
|
|
2589
|
+
// src/rules/require-zod-form-validation.ts
|
|
2401
2590
|
var looksLikeZodSchema = (node) => {
|
|
2402
2591
|
let current = node;
|
|
2403
2592
|
while (true) {
|
|
@@ -2475,14 +2664,38 @@ var require_zod_form_validation_default = ESLintUtils19.RuleCreator(
|
|
|
2475
2664
|
}
|
|
2476
2665
|
return isFormSourceIdentifier(callee.object);
|
|
2477
2666
|
};
|
|
2667
|
+
const hasZodParseAncestor = (node) => {
|
|
2668
|
+
let parent = node.parent;
|
|
2669
|
+
while (parent !== null && parent !== void 0) {
|
|
2670
|
+
if (isZodParseCall(parent)) return true;
|
|
2671
|
+
parent = parent.parent;
|
|
2672
|
+
}
|
|
2673
|
+
return false;
|
|
2674
|
+
};
|
|
2675
|
+
const isInstanceofNarrowing = (node) => {
|
|
2676
|
+
const parent = node.parent;
|
|
2677
|
+
return parent !== null && parent !== void 0 && parent.type === AST_NODE_TYPES10.BinaryExpression && parent.operator === "instanceof" && parent.left === node;
|
|
2678
|
+
};
|
|
2679
|
+
const boundDeclarator = (node) => {
|
|
2680
|
+
const parent = node.parent;
|
|
2681
|
+
if (parent.type === AST_NODE_TYPES10.VariableDeclarator && parent.init === node && parent.id.type === AST_NODE_TYPES10.Identifier) {
|
|
2682
|
+
return parent;
|
|
2683
|
+
}
|
|
2684
|
+
return null;
|
|
2685
|
+
};
|
|
2686
|
+
const bindingIsValidated = (declarator) => {
|
|
2687
|
+
const variable = context.sourceCode.getDeclaredVariables(declarator)[0];
|
|
2688
|
+
if (variable === void 0) return false;
|
|
2689
|
+
return variable.references.some(
|
|
2690
|
+
(ref) => hasZodParseAncestor(ref.identifier) || isInstanceofNarrowing(ref.identifier)
|
|
2691
|
+
);
|
|
2692
|
+
};
|
|
2478
2693
|
return {
|
|
2479
2694
|
CallExpression(node) {
|
|
2480
2695
|
if (!isFormDataGetCall(node)) return;
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
parent = parent.parent;
|
|
2485
|
-
}
|
|
2696
|
+
if (hasZodParseAncestor(node) || isInstanceofNarrowing(node)) return;
|
|
2697
|
+
const declarator = boundDeclarator(node);
|
|
2698
|
+
if (declarator !== null && bindingIsValidated(declarator)) return;
|
|
2486
2699
|
context.report({
|
|
2487
2700
|
node,
|
|
2488
2701
|
messageId: "missingZodValidation"
|
|
@@ -2494,6 +2707,11 @@ var require_zod_form_validation_default = ESLintUtils19.RuleCreator(
|
|
|
2494
2707
|
|
|
2495
2708
|
// src/rules/zod-naming-convention.ts
|
|
2496
2709
|
import { ESLintUtils as ESLintUtils20, AST_NODE_TYPES as AST_NODE_TYPES11 } from "@typescript-eslint/utils";
|
|
2710
|
+
var CONVENTIONS = {
|
|
2711
|
+
prefix: { test: ZOD_PREFIX_RE, messageId: "zPrefix" },
|
|
2712
|
+
suffix: { test: ZOD_SUFFIX_RE, messageId: "schemaSuffix" },
|
|
2713
|
+
either: { test: ZOD_SCHEMA_NAME_RE, messageId: "zodSchemaName" }
|
|
2714
|
+
};
|
|
2497
2715
|
var calleeChainStartsWithZ = (node) => {
|
|
2498
2716
|
let current = node;
|
|
2499
2717
|
while (current.type === AST_NODE_TYPES11.MemberExpression) {
|
|
@@ -2516,15 +2734,29 @@ var zod_naming_convention_default = ESLintUtils20.RuleCreator(
|
|
|
2516
2734
|
meta: {
|
|
2517
2735
|
type: "suggestion",
|
|
2518
2736
|
docs: {
|
|
2519
|
-
description: "Enforce Zod
|
|
2737
|
+
description: "Enforce a consistent Zod schema naming convention \u2014 a `Z` prefix (`ZUser`) or a `Schema` suffix (`userSchema`); both are accepted by default."
|
|
2520
2738
|
},
|
|
2521
|
-
schema: [
|
|
2739
|
+
schema: [
|
|
2740
|
+
{
|
|
2741
|
+
type: "object",
|
|
2742
|
+
additionalProperties: false,
|
|
2743
|
+
properties: {
|
|
2744
|
+
convention: {
|
|
2745
|
+
type: "string",
|
|
2746
|
+
enum: ["prefix", "suffix", "either"]
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
],
|
|
2522
2751
|
messages: {
|
|
2523
|
-
zPrefix: "Zod schema names should start with Z"
|
|
2752
|
+
zPrefix: "Zod schema names should start with Z (e.g. `ZUser`)",
|
|
2753
|
+
schemaSuffix: "Zod schema names should end with Schema (e.g. `userSchema`)",
|
|
2754
|
+
zodSchemaName: "Zod schema names should start with Z (`ZUser`) or end with Schema (`userSchema`)"
|
|
2524
2755
|
}
|
|
2525
2756
|
},
|
|
2526
|
-
defaultOptions: [],
|
|
2527
|
-
create(context) {
|
|
2757
|
+
defaultOptions: [{}],
|
|
2758
|
+
create(context, [optionsArg]) {
|
|
2759
|
+
const { test, messageId } = CONVENTIONS[optionsArg?.convention ?? "either"];
|
|
2528
2760
|
return {
|
|
2529
2761
|
VariableDeclarator(node) {
|
|
2530
2762
|
const init = node.init;
|
|
@@ -2534,11 +2766,10 @@ var zod_naming_convention_default = ESLintUtils20.RuleCreator(
|
|
|
2534
2766
|
if (callee.type !== AST_NODE_TYPES11.MemberExpression) return;
|
|
2535
2767
|
if (!calleeChainStartsWithZ(callee)) return;
|
|
2536
2768
|
if (node.id.type !== AST_NODE_TYPES11.Identifier) return;
|
|
2537
|
-
|
|
2538
|
-
if (variableName.startsWith("Z")) return;
|
|
2769
|
+
if (test.test(node.id.name)) return;
|
|
2539
2770
|
context.report({
|
|
2540
2771
|
node: node.id,
|
|
2541
|
-
messageId
|
|
2772
|
+
messageId
|
|
2542
2773
|
});
|
|
2543
2774
|
}
|
|
2544
2775
|
};
|
|
@@ -2755,27 +2986,314 @@ var no_cors_wildcard_with_credentials_default = ESLintUtils21.RuleCreator(
|
|
|
2755
2986
|
}
|
|
2756
2987
|
});
|
|
2757
2988
|
|
|
2758
|
-
// src/rules/no-
|
|
2989
|
+
// src/rules/no-silent-promise-catch.ts
|
|
2990
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES12, ESLintUtils as ESLintUtils22 } from "@typescript-eslint/utils";
|
|
2991
|
+
|
|
2992
|
+
// src/rules/_paths.ts
|
|
2993
|
+
var SCRIPT_FILE_RE = /([\\/]scripts[\\/])|(\.mjs$)/;
|
|
2994
|
+
function isTestFile(filename) {
|
|
2995
|
+
const normalized = filename.replaceAll("\\", "/");
|
|
2996
|
+
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
2997
|
+
if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(base)) {
|
|
2998
|
+
return true;
|
|
2999
|
+
}
|
|
3000
|
+
return /(^|\/)(tests?|__tests__|__mocks__|fixtures)\//.test(normalized);
|
|
3001
|
+
}
|
|
3002
|
+
function isScriptFile(filename) {
|
|
3003
|
+
return SCRIPT_FILE_RE.test(filename);
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
// src/rules/no-silent-promise-catch.ts
|
|
3007
|
+
function isBodyParseCall(node) {
|
|
3008
|
+
return node.type === AST_NODE_TYPES12.CallExpression && node.arguments.length === 0 && node.callee.type === AST_NODE_TYPES12.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES12.Identifier && (node.callee.property.name === "json" || node.callee.property.name === "text");
|
|
3009
|
+
}
|
|
3010
|
+
function isSilentExpression(node) {
|
|
3011
|
+
switch (node.type) {
|
|
3012
|
+
case AST_NODE_TYPES12.Literal:
|
|
3013
|
+
return !("regex" in node);
|
|
3014
|
+
case AST_NODE_TYPES12.Identifier:
|
|
3015
|
+
return node.name === "undefined";
|
|
3016
|
+
case AST_NODE_TYPES12.UnaryExpression:
|
|
3017
|
+
return node.operator === "void" && node.argument.type === AST_NODE_TYPES12.Literal;
|
|
3018
|
+
case AST_NODE_TYPES12.ObjectExpression:
|
|
3019
|
+
return node.properties.length === 0;
|
|
3020
|
+
case AST_NODE_TYPES12.ArrayExpression:
|
|
3021
|
+
return node.elements.length === 0;
|
|
3022
|
+
case AST_NODE_TYPES12.TSAsExpression:
|
|
3023
|
+
return isSilentExpression(node.expression);
|
|
3024
|
+
default:
|
|
3025
|
+
return false;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
function isSilentHandler(handler) {
|
|
3029
|
+
const body = handler.body;
|
|
3030
|
+
if (body.type !== AST_NODE_TYPES12.BlockStatement) {
|
|
3031
|
+
return isSilentExpression(body);
|
|
3032
|
+
}
|
|
3033
|
+
if (body.body.length === 0) {
|
|
3034
|
+
return true;
|
|
3035
|
+
}
|
|
3036
|
+
if (body.body.length === 1) {
|
|
3037
|
+
const only = body.body[0];
|
|
3038
|
+
if (only !== void 0 && only.type === AST_NODE_TYPES12.ReturnStatement) {
|
|
3039
|
+
return only.argument === null || isSilentExpression(only.argument);
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
return false;
|
|
3043
|
+
}
|
|
3044
|
+
var no_silent_promise_catch_default = ESLintUtils22.RuleCreator(
|
|
3045
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3046
|
+
)({
|
|
3047
|
+
name: "no-silent-promise-catch",
|
|
3048
|
+
meta: {
|
|
3049
|
+
type: "problem",
|
|
3050
|
+
docs: {
|
|
3051
|
+
description: "Disallow `.catch()` handlers that silently swallow the rejection (e.g. `.catch(() => null)`); log, rethrow, or handle the error."
|
|
3052
|
+
},
|
|
3053
|
+
schema: [],
|
|
3054
|
+
messages: {
|
|
3055
|
+
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."
|
|
3056
|
+
}
|
|
3057
|
+
},
|
|
3058
|
+
defaultOptions: [],
|
|
3059
|
+
create(context) {
|
|
3060
|
+
if (isTestFile(context.filename)) {
|
|
3061
|
+
return {};
|
|
3062
|
+
}
|
|
3063
|
+
return {
|
|
3064
|
+
CallExpression(node) {
|
|
3065
|
+
if (node.callee.type !== AST_NODE_TYPES12.MemberExpression || node.callee.computed || node.callee.property.type !== AST_NODE_TYPES12.Identifier || node.callee.property.name !== "catch") {
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
if (isBodyParseCall(node.callee.object)) {
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
if (node.arguments.length !== 1) {
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
const handler = node.arguments[0];
|
|
3075
|
+
if (handler === void 0 || handler.type !== AST_NODE_TYPES12.ArrowFunctionExpression && handler.type !== AST_NODE_TYPES12.FunctionExpression) {
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
if (isSilentHandler(handler)) {
|
|
3079
|
+
context.report({ node, messageId: "silentCatch" });
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
});
|
|
3085
|
+
|
|
3086
|
+
// src/rules/require-fetch-timeout.ts
|
|
2759
3087
|
import {
|
|
2760
|
-
|
|
2761
|
-
|
|
3088
|
+
AST_NODE_TYPES as AST_NODE_TYPES13,
|
|
3089
|
+
ASTUtils,
|
|
3090
|
+
ESLintUtils as ESLintUtils23
|
|
2762
3091
|
} from "@typescript-eslint/utils";
|
|
2763
|
-
var
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
AST_NODE_TYPES12.ArrowFunctionExpression
|
|
3092
|
+
var GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
|
|
3093
|
+
"globalThis",
|
|
3094
|
+
"window",
|
|
3095
|
+
"self"
|
|
2768
3096
|
]);
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
3097
|
+
function matchesAnyPattern2(filename, patterns) {
|
|
3098
|
+
for (const pattern of patterns) {
|
|
3099
|
+
const regexSource = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::DOUBLESTAR::").replace(/\*/g, "[^/\\\\]*").replace(/::DOUBLESTAR::/g, ".*");
|
|
3100
|
+
if (new RegExp(`^${regexSource}$`).test(filename)) {
|
|
3101
|
+
return true;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
return false;
|
|
3105
|
+
}
|
|
3106
|
+
function initProvablyLacksSignal(init) {
|
|
3107
|
+
if (init.type !== AST_NODE_TYPES13.ObjectExpression) {
|
|
3108
|
+
return false;
|
|
3109
|
+
}
|
|
3110
|
+
for (const prop of init.properties) {
|
|
3111
|
+
if (prop.type === AST_NODE_TYPES13.SpreadElement) {
|
|
3112
|
+
return false;
|
|
3113
|
+
}
|
|
3114
|
+
if (prop.key.type === AST_NODE_TYPES13.Identifier && prop.key.name === "signal" || prop.key.type === AST_NODE_TYPES13.Literal && prop.key.value === "signal") {
|
|
3115
|
+
return false;
|
|
3116
|
+
}
|
|
3117
|
+
if (prop.computed) {
|
|
3118
|
+
return false;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
return true;
|
|
3122
|
+
}
|
|
3123
|
+
function isStringish(node) {
|
|
3124
|
+
return node.type === AST_NODE_TYPES13.Literal && typeof node.value === "string" || node.type === AST_NODE_TYPES13.TemplateLiteral;
|
|
3125
|
+
}
|
|
3126
|
+
var require_fetch_timeout_default = ESLintUtils23.RuleCreator(
|
|
3127
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3128
|
+
)({
|
|
3129
|
+
name: "require-fetch-timeout",
|
|
3130
|
+
meta: {
|
|
3131
|
+
type: "problem",
|
|
3132
|
+
docs: {
|
|
3133
|
+
description: "Require an abort `signal` (e.g. `AbortSignal.timeout(ms)`) on global `fetch()` calls so stalled upstreams cannot hang the caller forever."
|
|
3134
|
+
},
|
|
3135
|
+
schema: [
|
|
3136
|
+
{
|
|
3137
|
+
type: "object",
|
|
3138
|
+
additionalProperties: false,
|
|
3139
|
+
properties: {
|
|
3140
|
+
allowIn: {
|
|
3141
|
+
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`).",
|
|
3142
|
+
type: "array",
|
|
3143
|
+
items: { type: "string" }
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
],
|
|
3148
|
+
messages: {
|
|
3149
|
+
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."
|
|
3150
|
+
}
|
|
3151
|
+
},
|
|
3152
|
+
defaultOptions: [{}],
|
|
3153
|
+
create(context, [optionsArg]) {
|
|
3154
|
+
if (isTestFile(context.filename) || isScriptFile(context.filename)) {
|
|
3155
|
+
return {};
|
|
3156
|
+
}
|
|
3157
|
+
const allowIn = optionsArg?.allowIn ?? [];
|
|
3158
|
+
if (allowIn.length > 0 && matchesAnyPattern2(context.filename, allowIn)) {
|
|
3159
|
+
return {};
|
|
3160
|
+
}
|
|
3161
|
+
function resolvesToGlobal(identifier) {
|
|
3162
|
+
const scope = context.sourceCode.getScope(identifier);
|
|
3163
|
+
const variable = ASTUtils.findVariable(scope, identifier.name);
|
|
3164
|
+
return variable === null || variable.defs.length === 0;
|
|
3165
|
+
}
|
|
3166
|
+
function isGlobalFetchCall2(callee) {
|
|
3167
|
+
if (callee.type === AST_NODE_TYPES13.Identifier) {
|
|
3168
|
+
return callee.name === "fetch" && resolvesToGlobal(callee);
|
|
3169
|
+
}
|
|
3170
|
+
return callee.type === AST_NODE_TYPES13.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES13.Identifier && callee.property.name === "fetch" && callee.object.type === AST_NODE_TYPES13.Identifier && GLOBAL_OBJECTS.has(callee.object.name) && resolvesToGlobal(callee.object);
|
|
3171
|
+
}
|
|
3172
|
+
return {
|
|
3173
|
+
CallExpression(node) {
|
|
3174
|
+
if (!isGlobalFetchCall2(node.callee)) {
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
const [first, init] = node.arguments;
|
|
3178
|
+
if (node.arguments.length === 1 && first !== void 0 && !isStringish(first)) {
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
if (init === void 0 || initProvablyLacksSignal(init)) {
|
|
3182
|
+
context.report({ node, messageId: "missingSignal" });
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
});
|
|
3188
|
+
|
|
3189
|
+
// src/rules/require-schema-validate-search.ts
|
|
3190
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES14, ESLintUtils as ESLintUtils24 } from "@typescript-eslint/utils";
|
|
3191
|
+
var VALIDATOR_METHODS = /* @__PURE__ */ new Set([
|
|
3192
|
+
"parse",
|
|
3193
|
+
"safeParse",
|
|
3194
|
+
"decode"
|
|
3195
|
+
]);
|
|
3196
|
+
function isConstTypeAnnotation(typeAnnotation) {
|
|
3197
|
+
return typeAnnotation.type === AST_NODE_TYPES14.TSTypeReference && typeAnnotation.typeName.type === AST_NODE_TYPES14.Identifier && typeAnnotation.typeName.name === "const";
|
|
3198
|
+
}
|
|
3199
|
+
function isValidatorCall(node) {
|
|
3200
|
+
return node.callee.type === AST_NODE_TYPES14.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES14.Identifier && VALIDATOR_METHODS.has(node.callee.property.name);
|
|
3201
|
+
}
|
|
3202
|
+
function findCastExpression(node, insideValidatorArg) {
|
|
3203
|
+
if ((node.type === AST_NODE_TYPES14.TSAsExpression || node.type === AST_NODE_TYPES14.TSTypeAssertion) && !isConstTypeAnnotation(node.typeAnnotation) && !insideValidatorArg) {
|
|
3204
|
+
return node;
|
|
3205
|
+
}
|
|
3206
|
+
if (node.type === AST_NODE_TYPES14.CallExpression && isValidatorCall(node)) {
|
|
3207
|
+
const inCallee = findCastExpression(node.callee, insideValidatorArg);
|
|
3208
|
+
if (inCallee !== null) {
|
|
3209
|
+
return inCallee;
|
|
3210
|
+
}
|
|
3211
|
+
for (const arg of node.arguments) {
|
|
3212
|
+
const found = findCastExpression(arg, true);
|
|
3213
|
+
if (found !== null) {
|
|
3214
|
+
return found;
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
return null;
|
|
3218
|
+
}
|
|
3219
|
+
for (const key of Object.keys(node)) {
|
|
3220
|
+
if (key === "parent") {
|
|
3221
|
+
continue;
|
|
3222
|
+
}
|
|
3223
|
+
const value = node[key];
|
|
3224
|
+
const children = Array.isArray(value) ? value : [value];
|
|
3225
|
+
for (const child of children) {
|
|
3226
|
+
if (child !== null && typeof child === "object" && "type" in child && typeof child.type === "string") {
|
|
3227
|
+
const found = findCastExpression(
|
|
3228
|
+
child,
|
|
3229
|
+
insideValidatorArg
|
|
3230
|
+
);
|
|
3231
|
+
if (found !== null) {
|
|
3232
|
+
return found;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
return null;
|
|
3238
|
+
}
|
|
3239
|
+
var require_schema_validate_search_default = ESLintUtils24.RuleCreator(
|
|
3240
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3241
|
+
)({
|
|
3242
|
+
name: "require-schema-validate-search",
|
|
3243
|
+
meta: {
|
|
3244
|
+
type: "problem",
|
|
3245
|
+
docs: {
|
|
3246
|
+
description: "Disallow `as` casts inside hand-rolled `validateSearch` functions; use a schema validator (e.g. zodValidator) so search params are validated at runtime."
|
|
3247
|
+
},
|
|
3248
|
+
schema: [],
|
|
3249
|
+
messages: {
|
|
3250
|
+
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."
|
|
3251
|
+
}
|
|
3252
|
+
},
|
|
3253
|
+
defaultOptions: [],
|
|
3254
|
+
create(context) {
|
|
3255
|
+
if (isTestFile(context.filename)) {
|
|
3256
|
+
return {};
|
|
3257
|
+
}
|
|
3258
|
+
return {
|
|
3259
|
+
Property(node) {
|
|
3260
|
+
const isValidateSearchKey = !node.computed && node.key.type === AST_NODE_TYPES14.Identifier && node.key.name === "validateSearch" || node.key.type === AST_NODE_TYPES14.Literal && node.key.value === "validateSearch";
|
|
3261
|
+
if (!isValidateSearchKey) {
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
if (node.value.type !== AST_NODE_TYPES14.ArrowFunctionExpression && node.value.type !== AST_NODE_TYPES14.FunctionExpression) {
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
const cast = findCastExpression(node.value.body, false);
|
|
3268
|
+
if (cast !== null) {
|
|
3269
|
+
context.report({ node: cast, messageId: "castInValidateSearch" });
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
});
|
|
3275
|
+
|
|
3276
|
+
// src/rules/no-fat-try-blocks.ts
|
|
3277
|
+
import {
|
|
3278
|
+
ESLintUtils as ESLintUtils25,
|
|
3279
|
+
AST_NODE_TYPES as AST_NODE_TYPES15
|
|
3280
|
+
} from "@typescript-eslint/utils";
|
|
3281
|
+
var MAX_TRY_BODY_STATEMENTS = 3;
|
|
3282
|
+
var NESTED_FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
3283
|
+
AST_NODE_TYPES15.FunctionDeclaration,
|
|
3284
|
+
AST_NODE_TYPES15.FunctionExpression,
|
|
3285
|
+
AST_NODE_TYPES15.ArrowFunctionExpression
|
|
3286
|
+
]);
|
|
3287
|
+
var PURE_METHODS = /* @__PURE__ */ new Set([
|
|
3288
|
+
"map",
|
|
3289
|
+
"filter",
|
|
3290
|
+
"forEach",
|
|
3291
|
+
"reduce",
|
|
3292
|
+
"reduceRight",
|
|
3293
|
+
"find",
|
|
3294
|
+
"findIndex",
|
|
3295
|
+
"findLast",
|
|
3296
|
+
"findLastIndex",
|
|
2779
3297
|
"some",
|
|
2780
3298
|
"every",
|
|
2781
3299
|
"push",
|
|
@@ -2862,20 +3380,20 @@ function isNode4(value) {
|
|
|
2862
3380
|
}
|
|
2863
3381
|
function isPureCall(node) {
|
|
2864
3382
|
const callee = node.callee;
|
|
2865
|
-
if (callee.type !==
|
|
3383
|
+
if (callee.type !== AST_NODE_TYPES15.MemberExpression) {
|
|
2866
3384
|
return false;
|
|
2867
3385
|
}
|
|
2868
3386
|
const property = callee.property;
|
|
2869
|
-
if (property.type !==
|
|
3387
|
+
if (property.type !== AST_NODE_TYPES15.Identifier) {
|
|
2870
3388
|
return false;
|
|
2871
3389
|
}
|
|
2872
|
-
if (callee.object.type ===
|
|
3390
|
+
if (callee.object.type === AST_NODE_TYPES15.Identifier && PURE_NAMESPACES.has(callee.object.name)) {
|
|
2873
3391
|
return true;
|
|
2874
3392
|
}
|
|
2875
3393
|
return PURE_METHODS.has(property.name);
|
|
2876
3394
|
}
|
|
2877
3395
|
function isPureNew(node) {
|
|
2878
|
-
return node.callee.type ===
|
|
3396
|
+
return node.callee.type === AST_NODE_TYPES15.Identifier && PURE_CONSTRUCTORS.has(node.callee.name);
|
|
2879
3397
|
}
|
|
2880
3398
|
function subtreeMatches(stmt, predicate) {
|
|
2881
3399
|
let found = false;
|
|
@@ -2912,25 +3430,34 @@ function subtreeMatches(stmt, predicate) {
|
|
|
2912
3430
|
visit(stmt);
|
|
2913
3431
|
return found;
|
|
2914
3432
|
}
|
|
2915
|
-
var hasAwait = (
|
|
2916
|
-
var hasThrowingCallOrNew = (
|
|
2917
|
-
|
|
2918
|
-
(n) => n.type ===
|
|
3433
|
+
var hasAwait = (node) => subtreeMatches(node, (n) => n.type === AST_NODE_TYPES15.AwaitExpression);
|
|
3434
|
+
var hasThrowingCallOrNew = (node) => subtreeMatches(
|
|
3435
|
+
node,
|
|
3436
|
+
(n) => n.type === AST_NODE_TYPES15.CallExpression && !isPureCall(n) || n.type === AST_NODE_TYPES15.NewExpression && !isPureNew(n)
|
|
2919
3437
|
);
|
|
2920
3438
|
function unwrap2(expr) {
|
|
2921
3439
|
let current = expr;
|
|
2922
|
-
while (current.type ===
|
|
3440
|
+
while (current.type === AST_NODE_TYPES15.ChainExpression || current.type === AST_NODE_TYPES15.TSNonNullExpression) {
|
|
2923
3441
|
current = current.expression;
|
|
2924
3442
|
}
|
|
2925
3443
|
return current;
|
|
2926
3444
|
}
|
|
3445
|
+
function isBareCallStatement(stmt) {
|
|
3446
|
+
return stmt.type === AST_NODE_TYPES15.ExpressionStatement && unwrap2(stmt.expression).type === AST_NODE_TYPES15.CallExpression;
|
|
3447
|
+
}
|
|
2927
3448
|
function canThrow(stmt) {
|
|
2928
3449
|
if (hasAwait(stmt)) {
|
|
2929
3450
|
return true;
|
|
2930
3451
|
}
|
|
2931
|
-
if (
|
|
3452
|
+
if (isBareCallStatement(stmt)) {
|
|
2932
3453
|
return false;
|
|
2933
3454
|
}
|
|
3455
|
+
if (stmt.type === AST_NODE_TYPES15.BlockStatement) {
|
|
3456
|
+
return stmt.body.some(canThrow);
|
|
3457
|
+
}
|
|
3458
|
+
if (stmt.type === AST_NODE_TYPES15.IfStatement) {
|
|
3459
|
+
return hasThrowingCallOrNew(stmt.test) || canThrow(stmt.consequent) || stmt.alternate !== null && canThrow(stmt.alternate);
|
|
3460
|
+
}
|
|
2934
3461
|
return hasThrowingCallOrNew(stmt);
|
|
2935
3462
|
}
|
|
2936
3463
|
function handlerRethrows(handler) {
|
|
@@ -2939,9 +3466,9 @@ function handlerRethrows(handler) {
|
|
|
2939
3466
|
}
|
|
2940
3467
|
const body = handler.body.body;
|
|
2941
3468
|
const last = body[body.length - 1];
|
|
2942
|
-
return last !== void 0 && last.type ===
|
|
3469
|
+
return last !== void 0 && last.type === AST_NODE_TYPES15.ThrowStatement;
|
|
2943
3470
|
}
|
|
2944
|
-
var no_fat_try_blocks_default =
|
|
3471
|
+
var no_fat_try_blocks_default = ESLintUtils25.RuleCreator(
|
|
2945
3472
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
2946
3473
|
)({
|
|
2947
3474
|
name: "no-fat-try-blocks",
|
|
@@ -2982,30 +3509,9 @@ var no_fat_try_blocks_default = ESLintUtils22.RuleCreator(
|
|
|
2982
3509
|
});
|
|
2983
3510
|
|
|
2984
3511
|
// src/rules/no-secret-in-log.ts
|
|
2985
|
-
import { ESLintUtils as
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
"info",
|
|
2989
|
-
"warn",
|
|
2990
|
-
"warning",
|
|
2991
|
-
"error",
|
|
2992
|
-
"exception",
|
|
2993
|
-
"critical",
|
|
2994
|
-
"trace",
|
|
2995
|
-
"log",
|
|
2996
|
-
"fatal",
|
|
2997
|
-
"success"
|
|
2998
|
-
]);
|
|
2999
|
-
var LOGGER_NAMES2 = /* @__PURE__ */ new Set([
|
|
3000
|
-
"logger",
|
|
3001
|
-
"log",
|
|
3002
|
-
"logging",
|
|
3003
|
-
"loguru",
|
|
3004
|
-
"console",
|
|
3005
|
-
"_logger",
|
|
3006
|
-
"_log"
|
|
3007
|
-
]);
|
|
3008
|
-
var LOGGER_FACTORIES = /* @__PURE__ */ new Set(["getlogger", "get_logger"]);
|
|
3512
|
+
import { ESLintUtils as ESLintUtils26 } from "@typescript-eslint/utils";
|
|
3513
|
+
|
|
3514
|
+
// src/rules/_secret_names.ts
|
|
3009
3515
|
var SECRET_WORDS = /* @__PURE__ */ new Set([
|
|
3010
3516
|
"token",
|
|
3011
3517
|
"secret",
|
|
@@ -3021,7 +3527,8 @@ var SECRET_WORDS = /* @__PURE__ */ new Set([
|
|
|
3021
3527
|
"hmac",
|
|
3022
3528
|
"digest",
|
|
3023
3529
|
"hash",
|
|
3024
|
-
"apikey"
|
|
3530
|
+
"apikey",
|
|
3531
|
+
"bearer"
|
|
3025
3532
|
]);
|
|
3026
3533
|
var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
|
|
3027
3534
|
"count",
|
|
@@ -3044,10 +3551,113 @@ var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
|
|
|
3044
3551
|
"valid",
|
|
3045
3552
|
"invalid",
|
|
3046
3553
|
"exists",
|
|
3554
|
+
"type",
|
|
3555
|
+
"types"
|
|
3556
|
+
]);
|
|
3557
|
+
var DESCRIPTOR_WORDS = /* @__PURE__ */ new Set([
|
|
3047
3558
|
"type",
|
|
3048
3559
|
"types",
|
|
3049
3560
|
"name",
|
|
3050
3561
|
"names",
|
|
3562
|
+
"id",
|
|
3563
|
+
"ids",
|
|
3564
|
+
"kind",
|
|
3565
|
+
"kinds"
|
|
3566
|
+
]);
|
|
3567
|
+
var CATEGORY_WORDS = /* @__PURE__ */ new Set(["type", "types", "kind", "kinds"]);
|
|
3568
|
+
var FLAG_PREFIXES = /* @__PURE__ */ new Set([
|
|
3569
|
+
"is",
|
|
3570
|
+
"has",
|
|
3571
|
+
"was",
|
|
3572
|
+
"are",
|
|
3573
|
+
"can",
|
|
3574
|
+
"should"
|
|
3575
|
+
]);
|
|
3576
|
+
var AUTH_WORDS = /* @__PURE__ */ new Set([
|
|
3577
|
+
"token",
|
|
3578
|
+
"secret",
|
|
3579
|
+
"secrets",
|
|
3580
|
+
"password",
|
|
3581
|
+
"passwd",
|
|
3582
|
+
"passwords",
|
|
3583
|
+
"jwt",
|
|
3584
|
+
"credential",
|
|
3585
|
+
"credentials",
|
|
3586
|
+
"authorization",
|
|
3587
|
+
"signature",
|
|
3588
|
+
"hmac",
|
|
3589
|
+
"apikey",
|
|
3590
|
+
"bearer"
|
|
3591
|
+
]);
|
|
3592
|
+
var CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+/g;
|
|
3593
|
+
var SEGMENT_RE = /[^A-Za-z0-9]+/;
|
|
3594
|
+
function tokenize(identifier) {
|
|
3595
|
+
const tokens = [];
|
|
3596
|
+
for (const segment of identifier.split(SEGMENT_RE)) {
|
|
3597
|
+
if (!segment) {
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
tokens.push(segment.toLowerCase());
|
|
3601
|
+
for (const part of segment.match(CAMEL_RE) ?? []) {
|
|
3602
|
+
tokens.push(part.toLowerCase());
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
return tokens;
|
|
3606
|
+
}
|
|
3607
|
+
function leadingWord(identifier) {
|
|
3608
|
+
for (const segment of identifier.split(SEGMENT_RE)) {
|
|
3609
|
+
if (segment) {
|
|
3610
|
+
return (segment.match(CAMEL_RE) ?? [segment])[0]?.toLowerCase();
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
return void 0;
|
|
3614
|
+
}
|
|
3615
|
+
function hasApiKey(tokens) {
|
|
3616
|
+
for (let i = 0; i + 1 < tokens.length; i++) {
|
|
3617
|
+
if (tokens[i] === "api" && tokens[i + 1] === "key") {
|
|
3618
|
+
return true;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
return false;
|
|
3622
|
+
}
|
|
3623
|
+
function isSecretName(identifier, innocuous = INNOCUOUS_WORDS) {
|
|
3624
|
+
const tokens = tokenize(identifier);
|
|
3625
|
+
const last = tokens.at(-1);
|
|
3626
|
+
if (last !== void 0 && innocuous.has(last)) {
|
|
3627
|
+
return false;
|
|
3628
|
+
}
|
|
3629
|
+
if (tokens.some((tok) => SECRET_WORDS.has(tok))) {
|
|
3630
|
+
return true;
|
|
3631
|
+
}
|
|
3632
|
+
return hasApiKey(tokens);
|
|
3633
|
+
}
|
|
3634
|
+
function isAuthSecretName(identifier) {
|
|
3635
|
+
if (!isSecretName(identifier)) {
|
|
3636
|
+
return false;
|
|
3637
|
+
}
|
|
3638
|
+
const tokens = tokenize(identifier);
|
|
3639
|
+
const first = leadingWord(identifier);
|
|
3640
|
+
if (first !== void 0 && FLAG_PREFIXES.has(first)) {
|
|
3641
|
+
return false;
|
|
3642
|
+
}
|
|
3643
|
+
const last = tokens.at(-1);
|
|
3644
|
+
if (last !== void 0 && DESCRIPTOR_WORDS.has(last)) {
|
|
3645
|
+
return false;
|
|
3646
|
+
}
|
|
3647
|
+
if (tokens.some((tok) => CATEGORY_WORDS.has(tok))) {
|
|
3648
|
+
return false;
|
|
3649
|
+
}
|
|
3650
|
+
if (tokens.some((tok) => AUTH_WORDS.has(tok))) {
|
|
3651
|
+
return true;
|
|
3652
|
+
}
|
|
3653
|
+
return hasApiKey(tokens);
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
// src/rules/no-secret-in-log.ts
|
|
3657
|
+
var LOG_INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
|
|
3658
|
+
...INNOCUOUS_WORDS,
|
|
3659
|
+
"name",
|
|
3660
|
+
"names",
|
|
3051
3661
|
"label",
|
|
3052
3662
|
"labels",
|
|
3053
3663
|
"title",
|
|
@@ -3091,80 +3701,18 @@ var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
|
|
|
3091
3701
|
]);
|
|
3092
3702
|
var REDACTION_RE = /prefix|suffix|redact|mask|hash|hint|_len|length/i;
|
|
3093
3703
|
var WHOLE_TOKEN_REDACTION_MARKERS = /* @__PURE__ */ new Set(["tag"]);
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
const tokens = [];
|
|
3098
|
-
for (const segment of identifier.split(SEGMENT_RE)) {
|
|
3099
|
-
if (!segment) {
|
|
3100
|
-
continue;
|
|
3101
|
-
}
|
|
3102
|
-
tokens.push(segment.toLowerCase());
|
|
3103
|
-
for (const part of segment.match(CAMEL_RE) ?? []) {
|
|
3104
|
-
tokens.push(part.toLowerCase());
|
|
3105
|
-
}
|
|
3704
|
+
function isSecretKeyword(name) {
|
|
3705
|
+
if (REDACTION_RE.test(name)) {
|
|
3706
|
+
return false;
|
|
3106
3707
|
}
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
function hasApiKey(tokens) {
|
|
3110
|
-
for (let i = 0; i + 1 < tokens.length; i++) {
|
|
3111
|
-
if (tokens[i] === "api" && tokens[i + 1] === "key") {
|
|
3112
|
-
return true;
|
|
3113
|
-
}
|
|
3708
|
+
if (tokenize(name).some((tok) => WHOLE_TOKEN_REDACTION_MARKERS.has(tok))) {
|
|
3709
|
+
return false;
|
|
3114
3710
|
}
|
|
3115
|
-
return
|
|
3711
|
+
return isSecretName(name, LOG_INNOCUOUS_WORDS);
|
|
3116
3712
|
}
|
|
3117
|
-
function
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
if (last !== void 0 && INNOCUOUS_WORDS.has(last)) {
|
|
3121
|
-
return false;
|
|
3122
|
-
}
|
|
3123
|
-
if (tokens.some((tok) => SECRET_WORDS.has(tok))) {
|
|
3124
|
-
return true;
|
|
3125
|
-
}
|
|
3126
|
-
return hasApiKey(tokens);
|
|
3127
|
-
}
|
|
3128
|
-
function isSecretKeyword(name) {
|
|
3129
|
-
if (REDACTION_RE.test(name)) {
|
|
3130
|
-
return false;
|
|
3131
|
-
}
|
|
3132
|
-
if (tokenize(name).some((tok) => WHOLE_TOKEN_REDACTION_MARKERS.has(tok))) {
|
|
3133
|
-
return false;
|
|
3134
|
-
}
|
|
3135
|
-
return isSecretName(name);
|
|
3136
|
-
}
|
|
3137
|
-
function isLoggerExpr(expr) {
|
|
3138
|
-
switch (expr.type) {
|
|
3139
|
-
case "Identifier":
|
|
3140
|
-
return LOGGER_NAMES2.has(expr.name.toLowerCase());
|
|
3141
|
-
case "MemberExpression": {
|
|
3142
|
-
const { property, object } = expr;
|
|
3143
|
-
if (!expr.computed && property.type === "Identifier") {
|
|
3144
|
-
const lowered = property.name.toLowerCase();
|
|
3145
|
-
if (LOGGER_NAMES2.has(lowered) || LOGGER_FACTORIES.has(lowered)) {
|
|
3146
|
-
return true;
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
return isLoggerExpr(object);
|
|
3150
|
-
}
|
|
3151
|
-
case "CallExpression": {
|
|
3152
|
-
const callee = expr.callee;
|
|
3153
|
-
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && LOGGER_FACTORIES.has(callee.property.name.toLowerCase())) {
|
|
3154
|
-
return true;
|
|
3155
|
-
}
|
|
3156
|
-
if (callee.type !== "Super") {
|
|
3157
|
-
return isLoggerExpr(callee);
|
|
3158
|
-
}
|
|
3159
|
-
return false;
|
|
3160
|
-
}
|
|
3161
|
-
default:
|
|
3162
|
-
return false;
|
|
3163
|
-
}
|
|
3164
|
-
}
|
|
3165
|
-
function isRawSecretValue(prop) {
|
|
3166
|
-
if (prop.shorthand) {
|
|
3167
|
-
return true;
|
|
3713
|
+
function isRawSecretValue(prop) {
|
|
3714
|
+
if (prop.shorthand) {
|
|
3715
|
+
return true;
|
|
3168
3716
|
}
|
|
3169
3717
|
return prop.value.type === "Identifier" || prop.value.type === "MemberExpression";
|
|
3170
3718
|
}
|
|
@@ -3180,7 +3728,7 @@ function propertyKeyName2(prop) {
|
|
|
3180
3728
|
}
|
|
3181
3729
|
return null;
|
|
3182
3730
|
}
|
|
3183
|
-
var no_secret_in_log_default =
|
|
3731
|
+
var no_secret_in_log_default = ESLintUtils26.RuleCreator(
|
|
3184
3732
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3185
3733
|
)({
|
|
3186
3734
|
name: "no-secret-in-log",
|
|
@@ -3189,20 +3737,23 @@ var no_secret_in_log_default = ESLintUtils23.RuleCreator(
|
|
|
3189
3737
|
docs: {
|
|
3190
3738
|
description: "Disallow passing a secret-named value to a logging call; it leaks to log sinks. Redact or omit it."
|
|
3191
3739
|
},
|
|
3192
|
-
schema: [
|
|
3740
|
+
schema: [
|
|
3741
|
+
{
|
|
3742
|
+
type: "object",
|
|
3743
|
+
additionalProperties: false,
|
|
3744
|
+
properties: { ...LOGGING_OPTION_PROPERTIES }
|
|
3745
|
+
}
|
|
3746
|
+
],
|
|
3193
3747
|
messages: {
|
|
3194
3748
|
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."
|
|
3195
3749
|
}
|
|
3196
3750
|
},
|
|
3197
|
-
defaultOptions: [],
|
|
3198
|
-
create(context) {
|
|
3751
|
+
defaultOptions: [{}],
|
|
3752
|
+
create(context, [loggingOptions]) {
|
|
3753
|
+
const matcher = createLogMatcher(loggingOptions);
|
|
3199
3754
|
return {
|
|
3200
3755
|
CallExpression(node) {
|
|
3201
|
-
|
|
3202
|
-
if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || !LOG_METHODS2.has(callee.property.name)) {
|
|
3203
|
-
return;
|
|
3204
|
-
}
|
|
3205
|
-
if (!isLoggerExpr(callee.object)) {
|
|
3756
|
+
if (!matcher.isLoggingCall(node)) {
|
|
3206
3757
|
return;
|
|
3207
3758
|
}
|
|
3208
3759
|
for (const arg of node.arguments) {
|
|
@@ -3248,15 +3799,15 @@ var no_secret_in_log_default = ESLintUtils23.RuleCreator(
|
|
|
3248
3799
|
});
|
|
3249
3800
|
|
|
3250
3801
|
// src/rules/no-unsafe-cast.ts
|
|
3251
|
-
import { ESLintUtils as
|
|
3252
|
-
import { AST_NODE_TYPES as
|
|
3802
|
+
import { ESLintUtils as ESLintUtils27 } from "@typescript-eslint/utils";
|
|
3803
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES16 } from "@typescript-eslint/utils";
|
|
3253
3804
|
function isAnyAnnotation(node) {
|
|
3254
|
-
return node.type ===
|
|
3805
|
+
return node.type === AST_NODE_TYPES16.TSAnyKeyword;
|
|
3255
3806
|
}
|
|
3256
3807
|
function isConstAssertion(typeAnnotation) {
|
|
3257
|
-
return typeAnnotation.type ===
|
|
3808
|
+
return typeAnnotation.type === AST_NODE_TYPES16.TSTypeReference && typeAnnotation.typeName.type === AST_NODE_TYPES16.Identifier && typeAnnotation.typeName.name === "const";
|
|
3258
3809
|
}
|
|
3259
|
-
var no_unsafe_cast_default =
|
|
3810
|
+
var no_unsafe_cast_default = ESLintUtils27.RuleCreator(
|
|
3260
3811
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3261
3812
|
)({
|
|
3262
3813
|
name: "no-unsafe-cast",
|
|
@@ -3282,7 +3833,7 @@ var no_unsafe_cast_default = ESLintUtils24.RuleCreator(
|
|
|
3282
3833
|
return;
|
|
3283
3834
|
}
|
|
3284
3835
|
const inner = node.expression;
|
|
3285
|
-
if (inner.type ===
|
|
3836
|
+
if (inner.type === AST_NODE_TYPES16.TSAsExpression || inner.type === AST_NODE_TYPES16.TSTypeAssertion) {
|
|
3286
3837
|
context.report({ node, messageId: "doubleCast" });
|
|
3287
3838
|
}
|
|
3288
3839
|
}
|
|
@@ -3295,8 +3846,8 @@ var no_unsafe_cast_default = ESLintUtils24.RuleCreator(
|
|
|
3295
3846
|
|
|
3296
3847
|
// src/rules/prefer-string-literal-union.ts
|
|
3297
3848
|
import {
|
|
3298
|
-
ESLintUtils as
|
|
3299
|
-
AST_NODE_TYPES as
|
|
3849
|
+
ESLintUtils as ESLintUtils28,
|
|
3850
|
+
AST_NODE_TYPES as AST_NODE_TYPES17
|
|
3300
3851
|
} from "@typescript-eslint/utils";
|
|
3301
3852
|
import * as ts from "typescript";
|
|
3302
3853
|
var CHOICE_TOKENS = /* @__PURE__ */ new Set([
|
|
@@ -3340,19 +3891,19 @@ function isChoiceLikeName(name) {
|
|
|
3340
3891
|
return CHOICE_TOKENS.has(lastWord(name));
|
|
3341
3892
|
}
|
|
3342
3893
|
function keyName(key) {
|
|
3343
|
-
if (key.type ===
|
|
3894
|
+
if (key.type === AST_NODE_TYPES17.Identifier) {
|
|
3344
3895
|
return key.name;
|
|
3345
3896
|
}
|
|
3346
|
-
if (key.type ===
|
|
3897
|
+
if (key.type === AST_NODE_TYPES17.Literal && typeof key.value === "string") {
|
|
3347
3898
|
return key.value;
|
|
3348
3899
|
}
|
|
3349
3900
|
return null;
|
|
3350
3901
|
}
|
|
3351
3902
|
function isStringLiteralMember(t) {
|
|
3352
|
-
return t.type ===
|
|
3903
|
+
return t.type === AST_NODE_TYPES17.TSLiteralType && t.literal.type === AST_NODE_TYPES17.Literal && typeof t.literal.value === "string";
|
|
3353
3904
|
}
|
|
3354
3905
|
function isStringLiteralUnion(node) {
|
|
3355
|
-
if (node?.type !==
|
|
3906
|
+
if (node?.type !== AST_NODE_TYPES17.TSUnionType) {
|
|
3356
3907
|
return false;
|
|
3357
3908
|
}
|
|
3358
3909
|
return node.types.filter(isStringLiteralMember).length >= MIN_CLUSTER_SIZE;
|
|
@@ -3381,12 +3932,12 @@ function bindingSourceExpression(decl) {
|
|
|
3381
3932
|
return ts.isForOfStatement(node) ? node.expression : node.initializer;
|
|
3382
3933
|
}
|
|
3383
3934
|
function refKey(node) {
|
|
3384
|
-
if (node.type ===
|
|
3935
|
+
if (node.type === AST_NODE_TYPES17.Identifier) {
|
|
3385
3936
|
return node.name;
|
|
3386
3937
|
}
|
|
3387
|
-
if (node.type ===
|
|
3938
|
+
if (node.type === AST_NODE_TYPES17.MemberExpression && !node.computed) {
|
|
3388
3939
|
const inner = refKey(node.object);
|
|
3389
|
-
if (inner === null || node.property.type !==
|
|
3940
|
+
if (inner === null || node.property.type !== AST_NODE_TYPES17.Identifier) {
|
|
3390
3941
|
return null;
|
|
3391
3942
|
}
|
|
3392
3943
|
return `${inner}.${node.property.name}`;
|
|
@@ -3394,12 +3945,12 @@ function refKey(node) {
|
|
|
3394
3945
|
return null;
|
|
3395
3946
|
}
|
|
3396
3947
|
function strLiteral(node) {
|
|
3397
|
-
if (node.type ===
|
|
3948
|
+
if (node.type === AST_NODE_TYPES17.Literal && typeof node.value === "string") {
|
|
3398
3949
|
return node.value;
|
|
3399
3950
|
}
|
|
3400
3951
|
return null;
|
|
3401
3952
|
}
|
|
3402
|
-
var prefer_string_literal_union_default =
|
|
3953
|
+
var prefer_string_literal_union_default = ESLintUtils28.RuleCreator(
|
|
3403
3954
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3404
3955
|
)({
|
|
3405
3956
|
name: "prefer-string-literal-union",
|
|
@@ -3408,22 +3959,36 @@ var prefer_string_literal_union_default = ESLintUtils25.RuleCreator(
|
|
|
3408
3959
|
docs: {
|
|
3409
3960
|
description: "Flag raw `string` choice fields and string-literal comparison clusters; prefer a string-literal union type."
|
|
3410
3961
|
},
|
|
3411
|
-
schema: [
|
|
3962
|
+
schema: [
|
|
3963
|
+
{
|
|
3964
|
+
type: "object",
|
|
3965
|
+
additionalProperties: false,
|
|
3966
|
+
properties: {
|
|
3967
|
+
ignoreFields: {
|
|
3968
|
+
type: "array",
|
|
3969
|
+
items: { type: "string" }
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
],
|
|
3412
3974
|
messages: {
|
|
3413
3975
|
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.',
|
|
3414
3976
|
comparisonCluster: '`{{key}}` is compared against a closed set of string literals \u2014 define a string-literal union type (e.g. `type X = "a" | "b"`).'
|
|
3415
3977
|
}
|
|
3416
3978
|
},
|
|
3417
|
-
defaultOptions: [],
|
|
3418
|
-
create(context) {
|
|
3979
|
+
defaultOptions: [{}],
|
|
3980
|
+
create(context, [optionsArg]) {
|
|
3419
3981
|
const filename = context.filename;
|
|
3420
3982
|
const sourceText = context.sourceCode.getText();
|
|
3421
3983
|
if (isIgnoredFile(filename, sourceText)) {
|
|
3422
3984
|
return {};
|
|
3423
3985
|
}
|
|
3986
|
+
const ignoredFields = new Set(
|
|
3987
|
+
(optionsArg?.ignoreFields ?? []).map((name) => name.toLowerCase())
|
|
3988
|
+
);
|
|
3424
3989
|
let services;
|
|
3425
3990
|
try {
|
|
3426
|
-
services =
|
|
3991
|
+
services = ESLintUtils28.getParserServices(context);
|
|
3427
3992
|
} catch {
|
|
3428
3993
|
services = null;
|
|
3429
3994
|
}
|
|
@@ -3468,18 +4033,46 @@ var prefer_string_literal_union_default = ESLintUtils25.RuleCreator(
|
|
|
3468
4033
|
function operandIsFlaggable(node) {
|
|
3469
4034
|
return operandIsRawString(node) && !originIsExternal(services?.esTreeNodeToTSNodeMap.get(node), 0);
|
|
3470
4035
|
}
|
|
3471
|
-
function
|
|
3472
|
-
|
|
4036
|
+
function declaredReturnLiterals(fn) {
|
|
4037
|
+
const annotation = fn.returnType?.typeAnnotation;
|
|
4038
|
+
if (annotation === void 0 || services === null) {
|
|
4039
|
+
return null;
|
|
4040
|
+
}
|
|
4041
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(annotation);
|
|
4042
|
+
if (!ts.isTypeNode(tsNode)) {
|
|
4043
|
+
return null;
|
|
4044
|
+
}
|
|
4045
|
+
const checker = services.program.getTypeChecker();
|
|
4046
|
+
const declared = checker.getTypeFromTypeNode(tsNode);
|
|
4047
|
+
const type = checker.getAwaitedType(declared) ?? declared;
|
|
4048
|
+
const literals = /* @__PURE__ */ new Set();
|
|
4049
|
+
for (const part of type.isUnion() ? type.types : [type]) {
|
|
4050
|
+
if (part.isStringLiteral()) {
|
|
4051
|
+
literals.add(part.value);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
return literals.size >= MIN_CLUSTER_SIZE ? literals : null;
|
|
4055
|
+
}
|
|
4056
|
+
function pushScope(node) {
|
|
4057
|
+
scopeStack.push({ clusters: /* @__PURE__ */ new Map(), fn: node });
|
|
3473
4058
|
}
|
|
3474
4059
|
function popScope() {
|
|
3475
4060
|
const scope = scopeStack.pop();
|
|
3476
4061
|
if (scope === void 0) {
|
|
3477
4062
|
return;
|
|
3478
4063
|
}
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
4064
|
+
const candidates = [...scope.clusters.values()].filter(
|
|
4065
|
+
(entry) => entry.allTokens && entry.literals.size >= MIN_CLUSTER_SIZE
|
|
4066
|
+
);
|
|
4067
|
+
if (candidates.length === 0) {
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
const returnLiterals = scope.fn === null ? null : declaredReturnLiterals(scope.fn);
|
|
4071
|
+
for (const entry of candidates) {
|
|
4072
|
+
if (returnLiterals !== null && [...entry.literals].every((lit) => returnLiterals.has(lit))) {
|
|
4073
|
+
continue;
|
|
3482
4074
|
}
|
|
4075
|
+
validClusters.push(entry.node);
|
|
3483
4076
|
}
|
|
3484
4077
|
}
|
|
3485
4078
|
function accumulate(key, literals, node) {
|
|
@@ -3507,11 +4100,11 @@ var prefer_string_literal_union_default = ESLintUtils25.RuleCreator(
|
|
|
3507
4100
|
containersWithUnion.add(container);
|
|
3508
4101
|
return;
|
|
3509
4102
|
}
|
|
3510
|
-
if (typeNode?.type !==
|
|
4103
|
+
if (typeNode?.type !== AST_NODE_TYPES17.TSStringKeyword) {
|
|
3511
4104
|
return;
|
|
3512
4105
|
}
|
|
3513
4106
|
const name = keyName(key);
|
|
3514
|
-
if (name === null || !isChoiceLikeName(name)) {
|
|
4107
|
+
if (name === null || !isChoiceLikeName(name) || ignoredFields.has(name.toLowerCase())) {
|
|
3515
4108
|
return;
|
|
3516
4109
|
}
|
|
3517
4110
|
bareChoiceProps.push({ name, container, node });
|
|
@@ -3595,10 +4188,10 @@ var prefer_string_literal_union_default = ESLintUtils25.RuleCreator(
|
|
|
3595
4188
|
}
|
|
3596
4189
|
};
|
|
3597
4190
|
function refKeyText(node) {
|
|
3598
|
-
if (node.type ===
|
|
4191
|
+
if (node.type === AST_NODE_TYPES17.BinaryExpression) {
|
|
3599
4192
|
return refKey(node.left) ?? refKey(node.right) ?? "value";
|
|
3600
4193
|
}
|
|
3601
|
-
if (node.type ===
|
|
4194
|
+
if (node.type === AST_NODE_TYPES17.SwitchStatement) {
|
|
3602
4195
|
return refKey(node.discriminant) ?? "value";
|
|
3603
4196
|
}
|
|
3604
4197
|
return "value";
|
|
@@ -3607,7 +4200,7 @@ var prefer_string_literal_union_default = ESLintUtils25.RuleCreator(
|
|
|
3607
4200
|
});
|
|
3608
4201
|
|
|
3609
4202
|
// src/rules/single-public-export.ts
|
|
3610
|
-
import { ESLintUtils as
|
|
4203
|
+
import { ESLintUtils as ESLintUtils29, AST_NODE_TYPES as AST_NODE_TYPES18 } from "@typescript-eslint/utils";
|
|
3611
4204
|
var JUNK_DRAWER_STEMS = /* @__PURE__ */ new Set([
|
|
3612
4205
|
"util",
|
|
3613
4206
|
"utils",
|
|
@@ -3641,12 +4234,12 @@ var kebabCase2 = (name) => {
|
|
|
3641
4234
|
}
|
|
3642
4235
|
return normalized.replace(CAMEL_BOUNDARY_RE, "-").toLowerCase();
|
|
3643
4236
|
};
|
|
3644
|
-
var isFunctionExpression = (node) => node !== null && (node.type ===
|
|
4237
|
+
var isFunctionExpression = (node) => node !== null && (node.type === AST_NODE_TYPES18.ArrowFunctionExpression || node.type === AST_NODE_TYPES18.FunctionExpression);
|
|
3645
4238
|
var functionConstName = (decl) => {
|
|
3646
4239
|
if (decl.declarations.length !== 1) return null;
|
|
3647
4240
|
const [declarator] = decl.declarations;
|
|
3648
4241
|
if (declarator === void 0) return null;
|
|
3649
|
-
if (declarator.id.type !==
|
|
4242
|
+
if (declarator.id.type !== AST_NODE_TYPES18.Identifier) return null;
|
|
3650
4243
|
if (!isFunctionExpression(declarator.init)) return null;
|
|
3651
4244
|
return declarator.id.name;
|
|
3652
4245
|
};
|
|
@@ -3660,20 +4253,20 @@ var summarizeExports = (body) => {
|
|
|
3660
4253
|
};
|
|
3661
4254
|
for (const statement of body) {
|
|
3662
4255
|
switch (statement.type) {
|
|
3663
|
-
case
|
|
4256
|
+
case AST_NODE_TYPES18.ExportAllDeclaration:
|
|
3664
4257
|
hasReExport = true;
|
|
3665
4258
|
break;
|
|
3666
|
-
case
|
|
4259
|
+
case AST_NODE_TYPES18.ExportDefaultDeclaration: {
|
|
3667
4260
|
names += 1;
|
|
3668
4261
|
const decl = statement.declaration;
|
|
3669
|
-
if (decl.type ===
|
|
4262
|
+
if (decl.type === AST_NODE_TYPES18.FunctionDeclaration && decl.id !== null) {
|
|
3670
4263
|
candidate = { name: decl.id.name, node: statement };
|
|
3671
|
-
} else if (decl.type ===
|
|
4264
|
+
} else if (decl.type === AST_NODE_TYPES18.ClassDeclaration && decl.id !== null) {
|
|
3672
4265
|
candidate = { name: decl.id.name, node: statement };
|
|
3673
4266
|
}
|
|
3674
4267
|
break;
|
|
3675
4268
|
}
|
|
3676
|
-
case
|
|
4269
|
+
case AST_NODE_TYPES18.ExportNamedDeclaration: {
|
|
3677
4270
|
if (statement.source !== null) {
|
|
3678
4271
|
hasReExport = true;
|
|
3679
4272
|
break;
|
|
@@ -3684,15 +4277,15 @@ var summarizeExports = (body) => {
|
|
|
3684
4277
|
break;
|
|
3685
4278
|
}
|
|
3686
4279
|
switch (decl.type) {
|
|
3687
|
-
case
|
|
4280
|
+
case AST_NODE_TYPES18.FunctionDeclaration:
|
|
3688
4281
|
if (decl.id !== null) addCandidate(decl.id.name, statement);
|
|
3689
4282
|
else names += 1;
|
|
3690
4283
|
break;
|
|
3691
|
-
case
|
|
4284
|
+
case AST_NODE_TYPES18.ClassDeclaration:
|
|
3692
4285
|
if (decl.id !== null) addCandidate(decl.id.name, statement);
|
|
3693
4286
|
else names += 1;
|
|
3694
4287
|
break;
|
|
3695
|
-
case
|
|
4288
|
+
case AST_NODE_TYPES18.VariableDeclaration: {
|
|
3696
4289
|
const fnName = functionConstName(decl);
|
|
3697
4290
|
if (fnName !== null && decl.declarations.length === 1) {
|
|
3698
4291
|
addCandidate(fnName, statement);
|
|
@@ -3712,7 +4305,7 @@ var summarizeExports = (body) => {
|
|
|
3712
4305
|
}
|
|
3713
4306
|
return { names, hasReExport, candidate };
|
|
3714
4307
|
};
|
|
3715
|
-
var single_public_export_default =
|
|
4308
|
+
var single_public_export_default = ESLintUtils29.RuleCreator(
|
|
3716
4309
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3717
4310
|
)({
|
|
3718
4311
|
name: "single-public-export",
|
|
@@ -3751,6 +4344,975 @@ var single_public_export_default = ESLintUtils26.RuleCreator(
|
|
|
3751
4344
|
}
|
|
3752
4345
|
});
|
|
3753
4346
|
|
|
4347
|
+
// src/rules/no-offset-pagination.ts
|
|
4348
|
+
import { ESLintUtils as ESLintUtils30 } from "@typescript-eslint/utils";
|
|
4349
|
+
|
|
4350
|
+
// src/rules/_sql.ts
|
|
4351
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES19 } from "@typescript-eslint/utils";
|
|
4352
|
+
function stripSqlNoise(text) {
|
|
4353
|
+
const out = [...text];
|
|
4354
|
+
const n = text.length;
|
|
4355
|
+
let i = 0;
|
|
4356
|
+
while (i < n) {
|
|
4357
|
+
const ch = text[i];
|
|
4358
|
+
if (ch === "'" || ch === '"') {
|
|
4359
|
+
out[i] = " ";
|
|
4360
|
+
i += 1;
|
|
4361
|
+
while (i < n) {
|
|
4362
|
+
const c = text[i];
|
|
4363
|
+
if (c === ch) {
|
|
4364
|
+
if (i + 1 < n && text[i + 1] === ch) {
|
|
4365
|
+
out[i] = " ";
|
|
4366
|
+
out[i + 1] = " ";
|
|
4367
|
+
i += 2;
|
|
4368
|
+
continue;
|
|
4369
|
+
}
|
|
4370
|
+
out[i] = " ";
|
|
4371
|
+
i += 1;
|
|
4372
|
+
break;
|
|
4373
|
+
}
|
|
4374
|
+
if (c !== "\n") {
|
|
4375
|
+
out[i] = " ";
|
|
4376
|
+
}
|
|
4377
|
+
i += 1;
|
|
4378
|
+
}
|
|
4379
|
+
continue;
|
|
4380
|
+
}
|
|
4381
|
+
if (ch === "-" && text[i + 1] === "-") {
|
|
4382
|
+
while (i < n && text[i] !== "\n") {
|
|
4383
|
+
out[i] = " ";
|
|
4384
|
+
i += 1;
|
|
4385
|
+
}
|
|
4386
|
+
continue;
|
|
4387
|
+
}
|
|
4388
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
4389
|
+
out[i] = " ";
|
|
4390
|
+
out[i + 1] = " ";
|
|
4391
|
+
i += 2;
|
|
4392
|
+
while (i < n && !(text[i] === "*" && text[i + 1] === "/")) {
|
|
4393
|
+
if (text[i] !== "\n") {
|
|
4394
|
+
out[i] = " ";
|
|
4395
|
+
}
|
|
4396
|
+
i += 1;
|
|
4397
|
+
}
|
|
4398
|
+
if (i < n) {
|
|
4399
|
+
out[i] = " ";
|
|
4400
|
+
out[i + 1] = " ";
|
|
4401
|
+
i += 2;
|
|
4402
|
+
}
|
|
4403
|
+
continue;
|
|
4404
|
+
}
|
|
4405
|
+
i += 1;
|
|
4406
|
+
}
|
|
4407
|
+
return out.join("");
|
|
4408
|
+
}
|
|
4409
|
+
var SUBSTITUTION_MARKER = "?";
|
|
4410
|
+
function sqlTextOf(node) {
|
|
4411
|
+
switch (node.type) {
|
|
4412
|
+
case AST_NODE_TYPES19.Literal:
|
|
4413
|
+
return typeof node.value === "string" ? node.value : null;
|
|
4414
|
+
case AST_NODE_TYPES19.TemplateLiteral:
|
|
4415
|
+
return node.quasis.map((q) => q.value.cooked ?? q.value.raw).join(SUBSTITUTION_MARKER);
|
|
4416
|
+
case AST_NODE_TYPES19.TaggedTemplateExpression:
|
|
4417
|
+
return sqlTextOf(node.quasi);
|
|
4418
|
+
case AST_NODE_TYPES19.BinaryExpression: {
|
|
4419
|
+
if (node.operator !== "+") {
|
|
4420
|
+
return null;
|
|
4421
|
+
}
|
|
4422
|
+
const left = sqlTextOf(node.left);
|
|
4423
|
+
const right = sqlTextOf(node.right);
|
|
4424
|
+
return left !== null && right !== null ? left + right : null;
|
|
4425
|
+
}
|
|
4426
|
+
case AST_NODE_TYPES19.ArrayExpression: {
|
|
4427
|
+
const parts = [];
|
|
4428
|
+
for (const element of node.elements) {
|
|
4429
|
+
if (element === null) {
|
|
4430
|
+
return null;
|
|
4431
|
+
}
|
|
4432
|
+
const part = sqlTextOf(element);
|
|
4433
|
+
if (part === null) {
|
|
4434
|
+
return null;
|
|
4435
|
+
}
|
|
4436
|
+
parts.push(part);
|
|
4437
|
+
}
|
|
4438
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
4439
|
+
}
|
|
4440
|
+
default:
|
|
4441
|
+
return null;
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
function isJoinedFragmentArray(node) {
|
|
4445
|
+
const parent = node.parent;
|
|
4446
|
+
return parent?.type === AST_NODE_TYPES19.MemberExpression && parent.object === node && !parent.computed && parent.property.type === AST_NODE_TYPES19.Identifier && parent.property.name === "join" && parent.parent?.type === AST_NODE_TYPES19.CallExpression;
|
|
4447
|
+
}
|
|
4448
|
+
function markConsumed(node, consumed) {
|
|
4449
|
+
consumed.add(node);
|
|
4450
|
+
for (const key of Object.keys(node)) {
|
|
4451
|
+
if (key === "parent") {
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
const value = node[key];
|
|
4455
|
+
for (const child of Array.isArray(value) ? value : [value]) {
|
|
4456
|
+
if (child !== null && typeof child === "object" && "type" in child) {
|
|
4457
|
+
markConsumed(child, consumed);
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
function createSqlListener(handler) {
|
|
4463
|
+
const consumed = /* @__PURE__ */ new WeakSet();
|
|
4464
|
+
const visit = (node) => {
|
|
4465
|
+
if (consumed.has(node)) {
|
|
4466
|
+
return;
|
|
4467
|
+
}
|
|
4468
|
+
const text = sqlTextOf(node);
|
|
4469
|
+
if (text === null) {
|
|
4470
|
+
return;
|
|
4471
|
+
}
|
|
4472
|
+
markConsumed(node, consumed);
|
|
4473
|
+
handler(stripSqlNoise(text), node);
|
|
4474
|
+
};
|
|
4475
|
+
return {
|
|
4476
|
+
BinaryExpression: (node) => {
|
|
4477
|
+
visit(node);
|
|
4478
|
+
},
|
|
4479
|
+
ArrayExpression: (node) => {
|
|
4480
|
+
if (isJoinedFragmentArray(node)) {
|
|
4481
|
+
visit(node);
|
|
4482
|
+
}
|
|
4483
|
+
},
|
|
4484
|
+
TemplateLiteral: (node) => {
|
|
4485
|
+
visit(node);
|
|
4486
|
+
},
|
|
4487
|
+
Literal: (node) => {
|
|
4488
|
+
visit(node);
|
|
4489
|
+
}
|
|
4490
|
+
};
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
// src/rules/no-offset-pagination.ts
|
|
4494
|
+
var OFFSET_PAGINATION = /\bOFFSET\s+(?:\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
|
|
4495
|
+
var OFFSET_GATE = /offset/i;
|
|
4496
|
+
var no_offset_pagination_default = ESLintUtils30.RuleCreator(
|
|
4497
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4498
|
+
)({
|
|
4499
|
+
name: "no-offset-pagination",
|
|
4500
|
+
meta: {
|
|
4501
|
+
type: "problem",
|
|
4502
|
+
docs: {
|
|
4503
|
+
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."
|
|
4504
|
+
},
|
|
4505
|
+
schema: [],
|
|
4506
|
+
messages: {
|
|
4507
|
+
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 ?`."
|
|
4508
|
+
}
|
|
4509
|
+
},
|
|
4510
|
+
defaultOptions: [],
|
|
4511
|
+
create(context) {
|
|
4512
|
+
if (isTestFile(context.filename) || !OFFSET_GATE.test(context.sourceCode.text)) {
|
|
4513
|
+
return {};
|
|
4514
|
+
}
|
|
4515
|
+
return createSqlListener((sql, node) => {
|
|
4516
|
+
if (!OFFSET_PAGINATION.test(sql)) {
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
context.report({ node, messageId: "noOffsetPagination" });
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4522
|
+
});
|
|
4523
|
+
|
|
4524
|
+
// src/rules/no-positional-tuple-return.ts
|
|
4525
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES20, ESLintUtils as ESLintUtils31 } from "@typescript-eslint/utils";
|
|
4526
|
+
var MIN_ELEMENTS = 2;
|
|
4527
|
+
var AWAITABLE_TYPES = /* @__PURE__ */ new Set(["Promise", "PromiseLike", "Awaited"]);
|
|
4528
|
+
function tupleReturnType(node) {
|
|
4529
|
+
if (node.type === AST_NODE_TYPES20.TSTupleType) {
|
|
4530
|
+
return node;
|
|
4531
|
+
}
|
|
4532
|
+
if (node.type === AST_NODE_TYPES20.TSTypeReference && node.typeName.type === AST_NODE_TYPES20.Identifier && AWAITABLE_TYPES.has(node.typeName.name)) {
|
|
4533
|
+
const argument = node.typeArguments?.params[0];
|
|
4534
|
+
return argument === void 0 ? null : tupleReturnType(argument);
|
|
4535
|
+
}
|
|
4536
|
+
return null;
|
|
4537
|
+
}
|
|
4538
|
+
function normalizedText(sourceCode, node) {
|
|
4539
|
+
return sourceCode.getText(node).replaceAll(/\s+/g, " ").trim();
|
|
4540
|
+
}
|
|
4541
|
+
function isPermittedTuple(tuple, sourceCode) {
|
|
4542
|
+
const elements = tuple.elementTypes;
|
|
4543
|
+
if (elements.length < MIN_ELEMENTS) {
|
|
4544
|
+
return true;
|
|
4545
|
+
}
|
|
4546
|
+
if (elements.some((element) => element.type === AST_NODE_TYPES20.TSRestType)) {
|
|
4547
|
+
return true;
|
|
4548
|
+
}
|
|
4549
|
+
if (elements.some((element) => element.type === AST_NODE_TYPES20.TSNamedTupleMember)) {
|
|
4550
|
+
return true;
|
|
4551
|
+
}
|
|
4552
|
+
if (elements[0]?.type === AST_NODE_TYPES20.TSLiteralType) {
|
|
4553
|
+
return true;
|
|
4554
|
+
}
|
|
4555
|
+
const texts = new Set(elements.map((element) => normalizedText(sourceCode, element)));
|
|
4556
|
+
return texts.size === 1;
|
|
4557
|
+
}
|
|
4558
|
+
function functionName(node) {
|
|
4559
|
+
if (node.type === AST_NODE_TYPES20.FunctionDeclaration) {
|
|
4560
|
+
return node.id?.name ?? null;
|
|
4561
|
+
}
|
|
4562
|
+
const parent = node.parent;
|
|
4563
|
+
if (parent?.type === AST_NODE_TYPES20.VariableDeclarator && parent.id.type === AST_NODE_TYPES20.Identifier) {
|
|
4564
|
+
return parent.id.name;
|
|
4565
|
+
}
|
|
4566
|
+
if ((parent?.type === AST_NODE_TYPES20.MethodDefinition || parent?.type === AST_NODE_TYPES20.PropertyDefinition || parent?.type === AST_NODE_TYPES20.Property) && parent.key.type === AST_NODE_TYPES20.Identifier) {
|
|
4567
|
+
return parent.key.name;
|
|
4568
|
+
}
|
|
4569
|
+
return null;
|
|
4570
|
+
}
|
|
4571
|
+
function isExported(node) {
|
|
4572
|
+
for (let current = node; current != null; current = current.parent) {
|
|
4573
|
+
const parent = current.parent;
|
|
4574
|
+
if (parent?.type === AST_NODE_TYPES20.ExportNamedDeclaration || parent?.type === AST_NODE_TYPES20.ExportDefaultDeclaration) {
|
|
4575
|
+
return true;
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
return false;
|
|
4579
|
+
}
|
|
4580
|
+
var no_positional_tuple_return_default = ESLintUtils31.RuleCreator(
|
|
4581
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4582
|
+
)({
|
|
4583
|
+
name: "no-positional-tuple-return",
|
|
4584
|
+
meta: {
|
|
4585
|
+
type: "suggestion",
|
|
4586
|
+
docs: {
|
|
4587
|
+
description: "Disallow returning a positional tuple of distinct fields from an exported function; return a named object so call sites cannot mismatch slots."
|
|
4588
|
+
},
|
|
4589
|
+
schema: [],
|
|
4590
|
+
messages: {
|
|
4591
|
+
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."
|
|
4592
|
+
}
|
|
4593
|
+
},
|
|
4594
|
+
defaultOptions: [],
|
|
4595
|
+
create(context) {
|
|
4596
|
+
const check = (node) => {
|
|
4597
|
+
const annotation = node.returnType?.typeAnnotation;
|
|
4598
|
+
if (annotation === void 0) {
|
|
4599
|
+
return;
|
|
4600
|
+
}
|
|
4601
|
+
const tuple = tupleReturnType(annotation);
|
|
4602
|
+
if (tuple === null || isPermittedTuple(tuple, context.sourceCode)) {
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4605
|
+
const name = functionName(node);
|
|
4606
|
+
if (name === null || name.startsWith("_") || /^use[A-Z]/.test(name)) {
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
if (!isExported(node)) {
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
context.report({
|
|
4613
|
+
node: tuple,
|
|
4614
|
+
messageId: "noPositionalTupleReturn",
|
|
4615
|
+
data: { name, count: String(tuple.elementTypes.length) }
|
|
4616
|
+
});
|
|
4617
|
+
};
|
|
4618
|
+
return {
|
|
4619
|
+
FunctionDeclaration: check,
|
|
4620
|
+
FunctionExpression: check,
|
|
4621
|
+
ArrowFunctionExpression: check
|
|
4622
|
+
};
|
|
4623
|
+
}
|
|
4624
|
+
});
|
|
4625
|
+
|
|
4626
|
+
// src/rules/no-repeated-string-literal.ts
|
|
4627
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES21, ESLintUtils as ESLintUtils32 } from "@typescript-eslint/utils";
|
|
4628
|
+
var MIN_LENGTH = 40;
|
|
4629
|
+
var MIN_OCCURRENCES = 3;
|
|
4630
|
+
var MIN_DISTINCT_SCOPES = 2;
|
|
4631
|
+
var PREVIEW_LENGTH = 40;
|
|
4632
|
+
var SQL_KEYWORD_RE = /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|VALUES|ON CONFLICT|RETURNING|GROUP BY|ORDER BY)\b/;
|
|
4633
|
+
var IDENTIFIER_RE = /^[a-z_][a-z0-9_.]*$/;
|
|
4634
|
+
var FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
4635
|
+
AST_NODE_TYPES21.FunctionDeclaration,
|
|
4636
|
+
AST_NODE_TYPES21.FunctionExpression,
|
|
4637
|
+
AST_NODE_TYPES21.ArrowFunctionExpression
|
|
4638
|
+
]);
|
|
4639
|
+
function isStructured(value) {
|
|
4640
|
+
return value.includes("\n") || SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value);
|
|
4641
|
+
}
|
|
4642
|
+
function preview(value) {
|
|
4643
|
+
const oneLine = value.replaceAll("\n", " ").trim();
|
|
4644
|
+
return oneLine.length <= PREVIEW_LENGTH ? oneLine : `${oneLine.slice(0, PREVIEW_LENGTH)}...`;
|
|
4645
|
+
}
|
|
4646
|
+
function enclosingFunction(node) {
|
|
4647
|
+
for (let current = node.parent; current != null; current = current.parent) {
|
|
4648
|
+
if (FUNCTION_TYPES.has(current.type)) {
|
|
4649
|
+
return current;
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
return null;
|
|
4653
|
+
}
|
|
4654
|
+
function isScaffolding(node) {
|
|
4655
|
+
const parent = node.parent;
|
|
4656
|
+
if (parent === void 0) {
|
|
4657
|
+
return true;
|
|
4658
|
+
}
|
|
4659
|
+
return parent.type === AST_NODE_TYPES21.ImportDeclaration || parent.type === AST_NODE_TYPES21.ExportNamedDeclaration || parent.type === AST_NODE_TYPES21.ExportAllDeclaration || parent.type === AST_NODE_TYPES21.TSImportType || parent.type === AST_NODE_TYPES21.JSXAttribute || parent.type === AST_NODE_TYPES21.TSLiteralType;
|
|
4660
|
+
}
|
|
4661
|
+
var no_repeated_string_literal_default = ESLintUtils32.RuleCreator(
|
|
4662
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4663
|
+
)({
|
|
4664
|
+
name: "no-repeated-string-literal",
|
|
4665
|
+
meta: {
|
|
4666
|
+
type: "suggestion",
|
|
4667
|
+
docs: {
|
|
4668
|
+
description: "Disallow a long structured string literal repeated across functions; the copies drift when one is edited. Extract a module-level constant."
|
|
4669
|
+
},
|
|
4670
|
+
schema: [],
|
|
4671
|
+
messages: {
|
|
4672
|
+
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.'
|
|
4673
|
+
}
|
|
4674
|
+
},
|
|
4675
|
+
defaultOptions: [],
|
|
4676
|
+
create(context) {
|
|
4677
|
+
if (isTestFile(context.filename)) {
|
|
4678
|
+
return {};
|
|
4679
|
+
}
|
|
4680
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
4681
|
+
const scopes = /* @__PURE__ */ new WeakMap();
|
|
4682
|
+
const record = (value, node) => {
|
|
4683
|
+
if (value.length < MIN_LENGTH || !isStructured(value) || isScaffolding(node)) {
|
|
4684
|
+
return;
|
|
4685
|
+
}
|
|
4686
|
+
const existing = occurrences.get(value);
|
|
4687
|
+
if (existing === void 0) {
|
|
4688
|
+
occurrences.set(value, [node]);
|
|
4689
|
+
} else {
|
|
4690
|
+
existing.push(node);
|
|
4691
|
+
}
|
|
4692
|
+
scopes.set(node, enclosingFunction(node));
|
|
4693
|
+
};
|
|
4694
|
+
return {
|
|
4695
|
+
Literal(node) {
|
|
4696
|
+
if (typeof node.value === "string") {
|
|
4697
|
+
record(node.value, node);
|
|
4698
|
+
}
|
|
4699
|
+
},
|
|
4700
|
+
TemplateLiteral(node) {
|
|
4701
|
+
const [only] = node.quasis;
|
|
4702
|
+
if (node.expressions.length === 0 && only !== void 0) {
|
|
4703
|
+
record(only.value.cooked ?? only.value.raw, node);
|
|
4704
|
+
}
|
|
4705
|
+
},
|
|
4706
|
+
"Program:exit": () => {
|
|
4707
|
+
for (const [value, nodes] of occurrences) {
|
|
4708
|
+
if (nodes.length < MIN_OCCURRENCES) {
|
|
4709
|
+
continue;
|
|
4710
|
+
}
|
|
4711
|
+
const distinctScopes = new Set(
|
|
4712
|
+
nodes.map((node) => scopes.get(node)).filter((scope) => scope != null)
|
|
4713
|
+
);
|
|
4714
|
+
if (distinctScopes.size < MIN_DISTINCT_SCOPES) {
|
|
4715
|
+
continue;
|
|
4716
|
+
}
|
|
4717
|
+
const [first, ...repeats] = nodes;
|
|
4718
|
+
if (first === void 0) {
|
|
4719
|
+
continue;
|
|
4720
|
+
}
|
|
4721
|
+
for (const node of repeats) {
|
|
4722
|
+
context.report({
|
|
4723
|
+
node,
|
|
4724
|
+
messageId: "noRepeatedStringLiteral",
|
|
4725
|
+
data: { preview: preview(value), line: String(first.loc.start.line) }
|
|
4726
|
+
});
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
};
|
|
4731
|
+
}
|
|
4732
|
+
});
|
|
4733
|
+
|
|
4734
|
+
// src/rules/no-select-star.ts
|
|
4735
|
+
import { ESLintUtils as ESLintUtils33 } from "@typescript-eslint/utils";
|
|
4736
|
+
var QUERY_SHAPE = /\bSELECT\b[\s\S]*?\bFROM\b/i;
|
|
4737
|
+
var SELECT_KEYWORD = /\bSELECT\b/gi;
|
|
4738
|
+
var FROM_KEYWORD = /^FROM\b/i;
|
|
4739
|
+
var EXISTS_BEFORE = /\bEXISTS\s*\(\s*$/i;
|
|
4740
|
+
var QUALIFIED_PREFIX = /\w\.$/;
|
|
4741
|
+
var SELECT_GATE = /select/i;
|
|
4742
|
+
function isProjectionStar(sql, pos) {
|
|
4743
|
+
if (QUALIFIED_PREFIX.test(sql.slice(0, pos))) {
|
|
4744
|
+
return true;
|
|
4745
|
+
}
|
|
4746
|
+
let before = pos - 1;
|
|
4747
|
+
while (before >= 0 && /\s/.test(sql[before] ?? "")) {
|
|
4748
|
+
before -= 1;
|
|
4749
|
+
}
|
|
4750
|
+
let after = pos + 1;
|
|
4751
|
+
while (after < sql.length && /\s/.test(sql[after] ?? "")) {
|
|
4752
|
+
after += 1;
|
|
4753
|
+
}
|
|
4754
|
+
const beforeChar = before >= 0 ? sql[before] ?? "" : "";
|
|
4755
|
+
const afterChar = after < sql.length ? sql[after] ?? "" : "";
|
|
4756
|
+
const terminates = afterChar === "" || afterChar === "," || afterChar === ")" || FROM_KEYWORD.test(sql.slice(after));
|
|
4757
|
+
if (!terminates) {
|
|
4758
|
+
return false;
|
|
4759
|
+
}
|
|
4760
|
+
return !(beforeChar === "(" && afterChar === ")");
|
|
4761
|
+
}
|
|
4762
|
+
function hasRealSelectStar(sql) {
|
|
4763
|
+
const selects = [...sql.matchAll(SELECT_KEYWORD)].map((m) => m.index);
|
|
4764
|
+
for (let pos = 0; pos < sql.length; pos++) {
|
|
4765
|
+
if (sql[pos] !== "*" || !isProjectionStar(sql, pos)) {
|
|
4766
|
+
continue;
|
|
4767
|
+
}
|
|
4768
|
+
const owning = selects.filter((start) => start < pos).at(-1);
|
|
4769
|
+
if (owning !== void 0 && !EXISTS_BEFORE.test(sql.slice(0, owning))) {
|
|
4770
|
+
return true;
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
return false;
|
|
4774
|
+
}
|
|
4775
|
+
var no_select_star_default = ESLintUtils33.RuleCreator(
|
|
4776
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4777
|
+
)({
|
|
4778
|
+
name: "no-select-star",
|
|
4779
|
+
meta: {
|
|
4780
|
+
type: "problem",
|
|
4781
|
+
docs: {
|
|
4782
|
+
description: "Disallow SELECT * in embedded SQL; it over-fetches and leaves the row contract implicit, so a schema change breaks row parsing silently."
|
|
4783
|
+
},
|
|
4784
|
+
schema: [],
|
|
4785
|
+
messages: {
|
|
4786
|
+
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."
|
|
4787
|
+
}
|
|
4788
|
+
},
|
|
4789
|
+
defaultOptions: [],
|
|
4790
|
+
create(context) {
|
|
4791
|
+
if (isTestFile(context.filename) || !SELECT_GATE.test(context.sourceCode.text)) {
|
|
4792
|
+
return {};
|
|
4793
|
+
}
|
|
4794
|
+
return createSqlListener((sql, node) => {
|
|
4795
|
+
if (!QUERY_SHAPE.test(sql) || !hasRealSelectStar(sql)) {
|
|
4796
|
+
return;
|
|
4797
|
+
}
|
|
4798
|
+
context.report({ node, messageId: "noSelectStar" });
|
|
4799
|
+
});
|
|
4800
|
+
}
|
|
4801
|
+
});
|
|
4802
|
+
|
|
4803
|
+
// src/rules/no-sleep-in-test-body.ts
|
|
4804
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES22, ESLintUtils as ESLintUtils34 } from "@typescript-eslint/utils";
|
|
4805
|
+
var SLEEP_HELPERS = /* @__PURE__ */ new Set(["sleep", "delay", "wait", "pause"]);
|
|
4806
|
+
var TEST_CALLERS = /* @__PURE__ */ new Set([
|
|
4807
|
+
"it",
|
|
4808
|
+
"test",
|
|
4809
|
+
"beforeEach",
|
|
4810
|
+
"afterEach"
|
|
4811
|
+
]);
|
|
4812
|
+
var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
|
|
4813
|
+
AST_NODE_TYPES22.FunctionDeclaration,
|
|
4814
|
+
AST_NODE_TYPES22.FunctionExpression,
|
|
4815
|
+
AST_NODE_TYPES22.ArrowFunctionExpression
|
|
4816
|
+
]);
|
|
4817
|
+
function isNonzeroNumericLiteral(node) {
|
|
4818
|
+
return node?.type === AST_NODE_TYPES22.Literal && typeof node.value === "number" && node.value !== 0;
|
|
4819
|
+
}
|
|
4820
|
+
function isTimedSetTimeout(node) {
|
|
4821
|
+
return node.type === AST_NODE_TYPES22.CallExpression && node.callee.type === AST_NODE_TYPES22.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
|
|
4822
|
+
}
|
|
4823
|
+
function isPromiseSleep(node) {
|
|
4824
|
+
if (node.callee.type !== AST_NODE_TYPES22.Identifier || node.callee.name !== "Promise") {
|
|
4825
|
+
return false;
|
|
4826
|
+
}
|
|
4827
|
+
const executor = node.arguments[0];
|
|
4828
|
+
if (executor?.type !== AST_NODE_TYPES22.ArrowFunctionExpression && executor?.type !== AST_NODE_TYPES22.FunctionExpression) {
|
|
4829
|
+
return false;
|
|
4830
|
+
}
|
|
4831
|
+
const body = executor.body;
|
|
4832
|
+
if (body.type !== AST_NODE_TYPES22.BlockStatement) {
|
|
4833
|
+
return isTimedSetTimeout(body);
|
|
4834
|
+
}
|
|
4835
|
+
return body.body.some(
|
|
4836
|
+
(stmt) => stmt.type === AST_NODE_TYPES22.ExpressionStatement && isTimedSetTimeout(stmt.expression)
|
|
4837
|
+
);
|
|
4838
|
+
}
|
|
4839
|
+
function isHelperSleep(node) {
|
|
4840
|
+
return node.callee.type === AST_NODE_TYPES22.Identifier && SLEEP_HELPERS.has(node.callee.name) && node.arguments.length >= 1 && isNonzeroNumericLiteral(node.arguments[0]);
|
|
4841
|
+
}
|
|
4842
|
+
function nearestEnclosingFunction(node) {
|
|
4843
|
+
for (let current = node.parent; current != null; current = current.parent) {
|
|
4844
|
+
if (!FUNCTION_TYPES2.has(current.type)) {
|
|
4845
|
+
continue;
|
|
4846
|
+
}
|
|
4847
|
+
const grandparent = current.parent;
|
|
4848
|
+
const isPromiseExecutor = grandparent?.type === AST_NODE_TYPES22.NewExpression && isPromiseSleep(grandparent);
|
|
4849
|
+
if (!isPromiseExecutor) {
|
|
4850
|
+
return current;
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
return null;
|
|
4854
|
+
}
|
|
4855
|
+
function testCallerName(callee) {
|
|
4856
|
+
if (callee.type === AST_NODE_TYPES22.Identifier) {
|
|
4857
|
+
return callee.name;
|
|
4858
|
+
}
|
|
4859
|
+
if (callee.type === AST_NODE_TYPES22.MemberExpression) {
|
|
4860
|
+
return testCallerName(callee.object);
|
|
4861
|
+
}
|
|
4862
|
+
if (callee.type === AST_NODE_TYPES22.CallExpression) {
|
|
4863
|
+
return testCallerName(callee.callee);
|
|
4864
|
+
}
|
|
4865
|
+
if (callee.type === AST_NODE_TYPES22.TaggedTemplateExpression) {
|
|
4866
|
+
return testCallerName(callee.tag);
|
|
4867
|
+
}
|
|
4868
|
+
return null;
|
|
4869
|
+
}
|
|
4870
|
+
function isTestBody(fn) {
|
|
4871
|
+
const call = fn.parent;
|
|
4872
|
+
if (call?.type !== AST_NODE_TYPES22.CallExpression || !call.arguments.some((argument) => argument === fn)) {
|
|
4873
|
+
return false;
|
|
4874
|
+
}
|
|
4875
|
+
const name = testCallerName(call.callee);
|
|
4876
|
+
return name !== null && TEST_CALLERS.has(name);
|
|
4877
|
+
}
|
|
4878
|
+
var no_sleep_in_test_body_default = ESLintUtils34.RuleCreator(
|
|
4879
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4880
|
+
)({
|
|
4881
|
+
name: "no-sleep-in-test-body",
|
|
4882
|
+
meta: {
|
|
4883
|
+
type: "problem",
|
|
4884
|
+
docs: {
|
|
4885
|
+
description: "Disallow a fixed timed sleep directly in a test body; it flakes under CI load. Synchronize on the signal or use fake timers."
|
|
4886
|
+
},
|
|
4887
|
+
schema: [],
|
|
4888
|
+
messages: {
|
|
4889
|
+
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)`."
|
|
4890
|
+
}
|
|
4891
|
+
},
|
|
4892
|
+
defaultOptions: [],
|
|
4893
|
+
create(context) {
|
|
4894
|
+
if (!isTestFile(context.filename)) {
|
|
4895
|
+
return {};
|
|
4896
|
+
}
|
|
4897
|
+
const report = (node) => {
|
|
4898
|
+
const enclosing = nearestEnclosingFunction(node);
|
|
4899
|
+
if (enclosing === null || !isTestBody(enclosing)) {
|
|
4900
|
+
return;
|
|
4901
|
+
}
|
|
4902
|
+
context.report({ node, messageId: "noSleepInTestBody" });
|
|
4903
|
+
};
|
|
4904
|
+
return {
|
|
4905
|
+
NewExpression(node) {
|
|
4906
|
+
if (isPromiseSleep(node)) {
|
|
4907
|
+
report(node);
|
|
4908
|
+
}
|
|
4909
|
+
},
|
|
4910
|
+
CallExpression(node) {
|
|
4911
|
+
if (isHelperSleep(node)) {
|
|
4912
|
+
report(node);
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
};
|
|
4916
|
+
}
|
|
4917
|
+
});
|
|
4918
|
+
|
|
4919
|
+
// src/rules/prefer-constant-time-secret-compare.ts
|
|
4920
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES23, ESLintUtils as ESLintUtils35 } from "@typescript-eslint/utils";
|
|
4921
|
+
var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["===", "!==", "==", "!="]);
|
|
4922
|
+
var SENTINEL_IDENTIFIERS = /* @__PURE__ */ new Set(["undefined", "NaN"]);
|
|
4923
|
+
var SENTINEL_WORDS = /(^|_)(SENTINEL|EMPTY|NONE|NULL|UNSET|MISSING|PLACEHOLDER|DUMMY|FAKE|EXAMPLE)(_|$)/;
|
|
4924
|
+
function isConstantReference(identifier) {
|
|
4925
|
+
if (isAuthSecretName(identifier) && !SENTINEL_WORDS.test(identifier)) return false;
|
|
4926
|
+
return identifier === identifier.toUpperCase() && /[A-Za-z]/.test(identifier);
|
|
4927
|
+
}
|
|
4928
|
+
function isExcludedOperand(node) {
|
|
4929
|
+
switch (node.type) {
|
|
4930
|
+
case AST_NODE_TYPES23.Literal:
|
|
4931
|
+
return true;
|
|
4932
|
+
case AST_NODE_TYPES23.TemplateLiteral:
|
|
4933
|
+
return node.expressions.length === 0;
|
|
4934
|
+
case AST_NODE_TYPES23.Identifier:
|
|
4935
|
+
return SENTINEL_IDENTIFIERS.has(node.name) || isConstantReference(node.name);
|
|
4936
|
+
case AST_NODE_TYPES23.MemberExpression:
|
|
4937
|
+
return !node.computed && node.property.type === AST_NODE_TYPES23.Identifier && isConstantReference(node.property.name);
|
|
4938
|
+
default:
|
|
4939
|
+
return false;
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
function operandName(node) {
|
|
4943
|
+
if (node.type === AST_NODE_TYPES23.Identifier) {
|
|
4944
|
+
return node.name;
|
|
4945
|
+
}
|
|
4946
|
+
if (node.type === AST_NODE_TYPES23.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES23.Identifier) {
|
|
4947
|
+
return node.property.name;
|
|
4948
|
+
}
|
|
4949
|
+
return null;
|
|
4950
|
+
}
|
|
4951
|
+
function isSecretOperand(node) {
|
|
4952
|
+
if (node.type === AST_NODE_TYPES23.TemplateLiteral) {
|
|
4953
|
+
return node.expressions.some((expression) => isSecretOperand(expression));
|
|
4954
|
+
}
|
|
4955
|
+
const name = operandName(node);
|
|
4956
|
+
return name !== null && isAuthSecretName(name);
|
|
4957
|
+
}
|
|
4958
|
+
function secretNameOf(node) {
|
|
4959
|
+
if (node.type === AST_NODE_TYPES23.TemplateLiteral) {
|
|
4960
|
+
for (const expression of node.expressions) {
|
|
4961
|
+
const nested = secretNameOf(expression);
|
|
4962
|
+
if (nested !== null) {
|
|
4963
|
+
return nested;
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
return null;
|
|
4967
|
+
}
|
|
4968
|
+
return operandName(node);
|
|
4969
|
+
}
|
|
4970
|
+
var prefer_constant_time_secret_compare_default = ESLintUtils35.RuleCreator(
|
|
4971
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4972
|
+
)({
|
|
4973
|
+
name: "prefer-constant-time-secret-compare",
|
|
4974
|
+
meta: {
|
|
4975
|
+
type: "problem",
|
|
4976
|
+
docs: {
|
|
4977
|
+
description: "Disallow `===`/`!==` on a secret-like value; short-circuiting comparison leaks the secret through timing. Use a constant-time compare."
|
|
4978
|
+
},
|
|
4979
|
+
schema: [],
|
|
4980
|
+
messages: {
|
|
4981
|
+
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)."
|
|
4982
|
+
}
|
|
4983
|
+
},
|
|
4984
|
+
defaultOptions: [],
|
|
4985
|
+
create(context) {
|
|
4986
|
+
if (isTestFile(context.filename)) {
|
|
4987
|
+
return {};
|
|
4988
|
+
}
|
|
4989
|
+
return {
|
|
4990
|
+
BinaryExpression(node) {
|
|
4991
|
+
if (!EQUALITY_OPERATORS.has(node.operator)) {
|
|
4992
|
+
return;
|
|
4993
|
+
}
|
|
4994
|
+
const { left, right } = node;
|
|
4995
|
+
if (isExcludedOperand(left) || isExcludedOperand(right)) {
|
|
4996
|
+
return;
|
|
4997
|
+
}
|
|
4998
|
+
const secret = [left, right].find((operand) => isSecretOperand(operand));
|
|
4999
|
+
if (secret === void 0) {
|
|
5000
|
+
return;
|
|
5001
|
+
}
|
|
5002
|
+
context.report({
|
|
5003
|
+
node,
|
|
5004
|
+
messageId: "preferConstantTimeSecretCompare",
|
|
5005
|
+
data: { operator: node.operator, name: secretNameOf(secret) ?? "" }
|
|
5006
|
+
});
|
|
5007
|
+
}
|
|
5008
|
+
};
|
|
5009
|
+
}
|
|
5010
|
+
});
|
|
5011
|
+
|
|
5012
|
+
// src/rules/store-insert-requires-on-conflict.ts
|
|
5013
|
+
import { ESLintUtils as ESLintUtils36 } from "@typescript-eslint/utils";
|
|
5014
|
+
var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
|
|
5015
|
+
var CONFLICT_HANDLED = /\bON\s+CONFLICT\b|\bON\s+DUPLICATE\s+KEY\b|\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b/i;
|
|
5016
|
+
var INSERT_GATE = /insert/i;
|
|
5017
|
+
var store_insert_requires_on_conflict_default = ESLintUtils36.RuleCreator(
|
|
5018
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
5019
|
+
)({
|
|
5020
|
+
name: "store-insert-requires-on-conflict",
|
|
5021
|
+
meta: {
|
|
5022
|
+
type: "problem",
|
|
5023
|
+
docs: {
|
|
5024
|
+
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."
|
|
5025
|
+
},
|
|
5026
|
+
schema: [],
|
|
5027
|
+
messages: {
|
|
5028
|
+
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`)."
|
|
5029
|
+
}
|
|
5030
|
+
},
|
|
5031
|
+
defaultOptions: [],
|
|
5032
|
+
create(context) {
|
|
5033
|
+
if (isTestFile(context.filename) || !INSERT_GATE.test(context.sourceCode.text)) {
|
|
5034
|
+
return {};
|
|
5035
|
+
}
|
|
5036
|
+
return createSqlListener((sql, node) => {
|
|
5037
|
+
if (!INSERT_WRITE.test(sql) || CONFLICT_HANDLED.test(sql)) {
|
|
5038
|
+
return;
|
|
5039
|
+
}
|
|
5040
|
+
context.report({ node, messageId: "storeInsertRequiresOnConflict" });
|
|
5041
|
+
});
|
|
5042
|
+
}
|
|
5043
|
+
});
|
|
5044
|
+
|
|
5045
|
+
// src/rules/no-dynamic-sql.ts
|
|
5046
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES24, ESLintUtils as ESLintUtils37 } from "@typescript-eslint/utils";
|
|
5047
|
+
var DEFAULT_METHODS = ["prepare", "exec", "query"];
|
|
5048
|
+
var CONSTANT_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
5049
|
+
function isStaticFragment(expression) {
|
|
5050
|
+
if (expression.type === AST_NODE_TYPES24.Identifier) {
|
|
5051
|
+
return CONSTANT_CASE_RE.test(expression.name);
|
|
5052
|
+
}
|
|
5053
|
+
if (expression.type === AST_NODE_TYPES24.MemberExpression && !expression.computed && expression.property.type === AST_NODE_TYPES24.Identifier) {
|
|
5054
|
+
return CONSTANT_CASE_RE.test(expression.property.name);
|
|
5055
|
+
}
|
|
5056
|
+
if (expression.type === AST_NODE_TYPES24.Literal) {
|
|
5057
|
+
return typeof expression.value === "string";
|
|
5058
|
+
}
|
|
5059
|
+
return false;
|
|
5060
|
+
}
|
|
5061
|
+
function runtimeInterpolations(template) {
|
|
5062
|
+
return template.expressions.filter(
|
|
5063
|
+
(expression) => !isStaticFragment(expression)
|
|
5064
|
+
);
|
|
5065
|
+
}
|
|
5066
|
+
function concatOperands(node) {
|
|
5067
|
+
if (node.type === AST_NODE_TYPES24.BinaryExpression && node.operator === "+") {
|
|
5068
|
+
return [...concatOperands(node.left), ...concatOperands(node.right)];
|
|
5069
|
+
}
|
|
5070
|
+
return [node];
|
|
5071
|
+
}
|
|
5072
|
+
function runtimeConcatOperands(node) {
|
|
5073
|
+
if (node.type !== AST_NODE_TYPES24.BinaryExpression || node.operator !== "+") {
|
|
5074
|
+
return [];
|
|
5075
|
+
}
|
|
5076
|
+
const operands = concatOperands(node);
|
|
5077
|
+
const hasStringLiteral = operands.some(
|
|
5078
|
+
(operand) => operand.type === AST_NODE_TYPES24.Literal && typeof operand.value === "string"
|
|
5079
|
+
);
|
|
5080
|
+
if (!hasStringLiteral) {
|
|
5081
|
+
return [];
|
|
5082
|
+
}
|
|
5083
|
+
return operands.filter(
|
|
5084
|
+
(operand) => operand.type !== AST_NODE_TYPES24.Literal && !isStaticFragment(operand)
|
|
5085
|
+
);
|
|
5086
|
+
}
|
|
5087
|
+
function statementMethodName(node, methods) {
|
|
5088
|
+
const callee = node.callee;
|
|
5089
|
+
if (callee.type !== AST_NODE_TYPES24.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES24.Identifier) {
|
|
5090
|
+
return null;
|
|
5091
|
+
}
|
|
5092
|
+
const name = callee.property.name;
|
|
5093
|
+
return methods.has(name) ? name : null;
|
|
5094
|
+
}
|
|
5095
|
+
var no_dynamic_sql_default = ESLintUtils37.RuleCreator(
|
|
5096
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
5097
|
+
)({
|
|
5098
|
+
name: "no-dynamic-sql",
|
|
5099
|
+
meta: {
|
|
5100
|
+
type: "problem",
|
|
5101
|
+
docs: {
|
|
5102
|
+
description: "Disallow interpolating or concatenating a runtime value into a SQL statement passed to `prepare`/`exec`/`query`; use a placeholder and bind the value."
|
|
5103
|
+
},
|
|
5104
|
+
schema: [
|
|
5105
|
+
{
|
|
5106
|
+
type: "object",
|
|
5107
|
+
properties: {
|
|
5108
|
+
methods: {
|
|
5109
|
+
type: "array",
|
|
5110
|
+
items: { type: "string" },
|
|
5111
|
+
description: "Statement-taking method names to inspect. Replaces the defaults."
|
|
5112
|
+
}
|
|
5113
|
+
},
|
|
5114
|
+
additionalProperties: false
|
|
5115
|
+
}
|
|
5116
|
+
],
|
|
5117
|
+
messages: {
|
|
5118
|
+
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."
|
|
5119
|
+
}
|
|
5120
|
+
},
|
|
5121
|
+
defaultOptions: [{}],
|
|
5122
|
+
create(context, [options]) {
|
|
5123
|
+
const methods = new Set(options?.methods ?? DEFAULT_METHODS);
|
|
5124
|
+
return {
|
|
5125
|
+
CallExpression(node) {
|
|
5126
|
+
const method = statementMethodName(node, methods);
|
|
5127
|
+
if (method === null) {
|
|
5128
|
+
return;
|
|
5129
|
+
}
|
|
5130
|
+
const statement = node.arguments[0];
|
|
5131
|
+
if (statement === void 0) {
|
|
5132
|
+
return;
|
|
5133
|
+
}
|
|
5134
|
+
const offenders = statement.type === AST_NODE_TYPES24.TemplateLiteral ? runtimeInterpolations(statement) : runtimeConcatOperands(statement);
|
|
5135
|
+
for (const offender of offenders) {
|
|
5136
|
+
context.report({
|
|
5137
|
+
node: offender,
|
|
5138
|
+
messageId: "dynamicSql",
|
|
5139
|
+
data: { method }
|
|
5140
|
+
});
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
};
|
|
5144
|
+
}
|
|
5145
|
+
});
|
|
5146
|
+
|
|
5147
|
+
// src/rules/no-raw-fetch-outside-clients.ts
|
|
5148
|
+
import { ESLintUtils as ESLintUtils38 } from "@typescript-eslint/utils";
|
|
5149
|
+
var DEFAULT_ALLOW = [
|
|
5150
|
+
"[\\\\/]clients?[\\\\/]",
|
|
5151
|
+
"-client\\.[cm]?[jt]sx?$",
|
|
5152
|
+
"[\\\\/]http-client\\.[cm]?[jt]sx?$",
|
|
5153
|
+
"\\.test\\.",
|
|
5154
|
+
"\\.spec\\.",
|
|
5155
|
+
"[\\\\/]__tests__[\\\\/]",
|
|
5156
|
+
"[\\\\/]__mocks__[\\\\/]"
|
|
5157
|
+
];
|
|
5158
|
+
var GLOBAL_RECEIVERS = /* @__PURE__ */ new Set([
|
|
5159
|
+
"globalThis",
|
|
5160
|
+
"window",
|
|
5161
|
+
"self"
|
|
5162
|
+
]);
|
|
5163
|
+
function isGlobalFetchCall(node) {
|
|
5164
|
+
const callee = node.callee;
|
|
5165
|
+
if (callee.type === "Identifier") {
|
|
5166
|
+
return callee.name === "fetch";
|
|
5167
|
+
}
|
|
5168
|
+
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "fetch" && callee.object.type === "Identifier") {
|
|
5169
|
+
return GLOBAL_RECEIVERS.has(callee.object.name);
|
|
5170
|
+
}
|
|
5171
|
+
return false;
|
|
5172
|
+
}
|
|
5173
|
+
function compile(patterns) {
|
|
5174
|
+
const compiled = [];
|
|
5175
|
+
for (const pattern of patterns) {
|
|
5176
|
+
try {
|
|
5177
|
+
compiled.push(new RegExp(pattern));
|
|
5178
|
+
} catch {
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
return compiled;
|
|
5182
|
+
}
|
|
5183
|
+
var no_raw_fetch_outside_clients_default = ESLintUtils38.RuleCreator(
|
|
5184
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
5185
|
+
)({
|
|
5186
|
+
name: "no-raw-fetch-outside-clients",
|
|
5187
|
+
meta: {
|
|
5188
|
+
type: "problem",
|
|
5189
|
+
docs: {
|
|
5190
|
+
description: "Disallow calling the global `fetch` outside the client layer; route outbound HTTP through a client module that owns retry, timeout and status handling."
|
|
5191
|
+
},
|
|
5192
|
+
schema: [
|
|
5193
|
+
{
|
|
5194
|
+
type: "object",
|
|
5195
|
+
properties: {
|
|
5196
|
+
allow: {
|
|
5197
|
+
type: "array",
|
|
5198
|
+
items: { type: "string" },
|
|
5199
|
+
description: "Regular-expression sources matched against the filename. Replaces the defaults."
|
|
5200
|
+
}
|
|
5201
|
+
},
|
|
5202
|
+
additionalProperties: false
|
|
5203
|
+
}
|
|
5204
|
+
],
|
|
5205
|
+
messages: {
|
|
5206
|
+
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."
|
|
5207
|
+
}
|
|
5208
|
+
},
|
|
5209
|
+
defaultOptions: [{}],
|
|
5210
|
+
create(context, [options]) {
|
|
5211
|
+
const patterns = options?.allow ?? DEFAULT_ALLOW;
|
|
5212
|
+
const allowed = compile(patterns);
|
|
5213
|
+
const filename = context.filename;
|
|
5214
|
+
if (allowed.some((re) => re.test(filename))) {
|
|
5215
|
+
return {};
|
|
5216
|
+
}
|
|
5217
|
+
return {
|
|
5218
|
+
CallExpression(node) {
|
|
5219
|
+
if (isGlobalFetchCall(node)) {
|
|
5220
|
+
context.report({ node, messageId: "rawFetch" });
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
};
|
|
5224
|
+
}
|
|
5225
|
+
});
|
|
5226
|
+
|
|
5227
|
+
// src/rules/no-storage-in-stateless-modules.ts
|
|
5228
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES25, ESLintUtils as ESLintUtils39 } from "@typescript-eslint/utils";
|
|
5229
|
+
var DEFAULT_METHODS2 = [
|
|
5230
|
+
"prepare",
|
|
5231
|
+
"put",
|
|
5232
|
+
"getWithMetadata"
|
|
5233
|
+
];
|
|
5234
|
+
var MIN_ARGUMENTS = /* @__PURE__ */ new Map([["put", 2]]);
|
|
5235
|
+
function compile2(patterns) {
|
|
5236
|
+
const compiled = [];
|
|
5237
|
+
for (const pattern of patterns) {
|
|
5238
|
+
try {
|
|
5239
|
+
compiled.push(new RegExp(pattern));
|
|
5240
|
+
} catch {
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
return compiled;
|
|
5244
|
+
}
|
|
5245
|
+
function storageMethodName(node, methods) {
|
|
5246
|
+
const callee = node.callee;
|
|
5247
|
+
if (callee.type !== AST_NODE_TYPES25.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES25.Identifier) {
|
|
5248
|
+
return null;
|
|
5249
|
+
}
|
|
5250
|
+
const name = callee.property.name;
|
|
5251
|
+
if (!methods.has(name)) {
|
|
5252
|
+
return null;
|
|
5253
|
+
}
|
|
5254
|
+
if (node.arguments.length < (MIN_ARGUMENTS.get(name) ?? 1)) {
|
|
5255
|
+
return null;
|
|
5256
|
+
}
|
|
5257
|
+
return name;
|
|
5258
|
+
}
|
|
5259
|
+
var no_storage_in_stateless_modules_default = ESLintUtils39.RuleCreator(
|
|
5260
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
5261
|
+
)({
|
|
5262
|
+
name: "no-storage-in-stateless-modules",
|
|
5263
|
+
meta: {
|
|
5264
|
+
type: "problem",
|
|
5265
|
+
docs: {
|
|
5266
|
+
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."
|
|
5267
|
+
},
|
|
5268
|
+
schema: [
|
|
5269
|
+
{
|
|
5270
|
+
type: "object",
|
|
5271
|
+
properties: {
|
|
5272
|
+
modules: {
|
|
5273
|
+
type: "array",
|
|
5274
|
+
items: { type: "string" },
|
|
5275
|
+
description: "Regex sources matched against the filename. Empty (the default) disables the rule."
|
|
5276
|
+
},
|
|
5277
|
+
methods: {
|
|
5278
|
+
type: "array",
|
|
5279
|
+
items: { type: "string" },
|
|
5280
|
+
description: "Storage method names to flag. Replaces the defaults."
|
|
5281
|
+
}
|
|
5282
|
+
},
|
|
5283
|
+
additionalProperties: false
|
|
5284
|
+
}
|
|
5285
|
+
],
|
|
5286
|
+
messages: {
|
|
5287
|
+
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."
|
|
5288
|
+
}
|
|
5289
|
+
},
|
|
5290
|
+
defaultOptions: [{}],
|
|
5291
|
+
create(context, [options]) {
|
|
5292
|
+
const modules = options?.modules ?? [];
|
|
5293
|
+
if (modules.length === 0) {
|
|
5294
|
+
return {};
|
|
5295
|
+
}
|
|
5296
|
+
const scoped = compile2(modules);
|
|
5297
|
+
if (!scoped.some((re) => re.test(context.filename))) {
|
|
5298
|
+
return {};
|
|
5299
|
+
}
|
|
5300
|
+
const methods = new Set(options?.methods ?? DEFAULT_METHODS2);
|
|
5301
|
+
return {
|
|
5302
|
+
CallExpression(node) {
|
|
5303
|
+
const method = storageMethodName(node, methods);
|
|
5304
|
+
if (method !== null) {
|
|
5305
|
+
context.report({
|
|
5306
|
+
node,
|
|
5307
|
+
messageId: "storageInStatelessModule",
|
|
5308
|
+
data: { method }
|
|
5309
|
+
});
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
};
|
|
5313
|
+
}
|
|
5314
|
+
});
|
|
5315
|
+
|
|
3754
5316
|
// src/index.ts
|
|
3755
5317
|
var rules = {
|
|
3756
5318
|
"enforce-file-structure": enforce_file_structure_default,
|
|
@@ -3778,12 +5340,25 @@ var rules = {
|
|
|
3778
5340
|
"no-secret-in-log": no_secret_in_log_default,
|
|
3779
5341
|
"no-unsafe-cast": no_unsafe_cast_default,
|
|
3780
5342
|
"prefer-string-literal-union": prefer_string_literal_union_default,
|
|
3781
|
-
"single-public-export": single_public_export_default
|
|
5343
|
+
"single-public-export": single_public_export_default,
|
|
5344
|
+
"no-silent-promise-catch": no_silent_promise_catch_default,
|
|
5345
|
+
"require-fetch-timeout": require_fetch_timeout_default,
|
|
5346
|
+
"require-schema-validate-search": require_schema_validate_search_default,
|
|
5347
|
+
"no-offset-pagination": no_offset_pagination_default,
|
|
5348
|
+
"no-positional-tuple-return": no_positional_tuple_return_default,
|
|
5349
|
+
"no-repeated-string-literal": no_repeated_string_literal_default,
|
|
5350
|
+
"no-select-star": no_select_star_default,
|
|
5351
|
+
"no-sleep-in-test-body": no_sleep_in_test_body_default,
|
|
5352
|
+
"prefer-constant-time-secret-compare": prefer_constant_time_secret_compare_default,
|
|
5353
|
+
"store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
|
|
5354
|
+
"no-dynamic-sql": no_dynamic_sql_default,
|
|
5355
|
+
"no-raw-fetch-outside-clients": no_raw_fetch_outside_clients_default,
|
|
5356
|
+
"no-storage-in-stateless-modules": no_storage_in_stateless_modules_default
|
|
3782
5357
|
};
|
|
3783
5358
|
var plugin = {
|
|
3784
5359
|
meta: {
|
|
3785
5360
|
name: "@sarj/eslint-plugin",
|
|
3786
|
-
version: "2.
|
|
5361
|
+
version: "2.8.0"
|
|
3787
5362
|
},
|
|
3788
5363
|
rules,
|
|
3789
5364
|
configs: {
|
|
@@ -3815,7 +5390,24 @@ var plugin = {
|
|
|
3815
5390
|
"@sarj/no-secret-in-log": "warn",
|
|
3816
5391
|
"@sarj/no-unsafe-cast": "warn",
|
|
3817
5392
|
"@sarj/single-public-export": "warn",
|
|
3818
|
-
"@sarj/prefer-string-literal-union": "warn"
|
|
5393
|
+
"@sarj/prefer-string-literal-union": "warn",
|
|
5394
|
+
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
5395
|
+
"@sarj/require-fetch-timeout": "warn",
|
|
5396
|
+
"@sarj/no-silent-promise-catch": "warn",
|
|
5397
|
+
"@sarj/require-schema-validate-search": "warn",
|
|
5398
|
+
// Second SARJ port wave — the TS/Python parity gap. Each targets a
|
|
5399
|
+
// defect class seen in production Workers code: timing-leaky secret
|
|
5400
|
+
// compares, non-idempotent store writes under queue redelivery,
|
|
5401
|
+
// O(N) pagination, implicit row contracts, flaky timed tests.
|
|
5402
|
+
"@sarj/prefer-constant-time-secret-compare": "error",
|
|
5403
|
+
"@sarj/store-insert-requires-on-conflict": "warn",
|
|
5404
|
+
"@sarj/no-offset-pagination": "warn",
|
|
5405
|
+
"@sarj/no-select-star": "warn",
|
|
5406
|
+
"@sarj/no-sleep-in-test-body": "warn",
|
|
5407
|
+
"@sarj/no-repeated-string-literal": "warn",
|
|
5408
|
+
"@sarj/no-positional-tuple-return": "warn",
|
|
5409
|
+
// Injection guard — low FP, applies to any repo touching SQL.
|
|
5410
|
+
"@sarj/no-dynamic-sql": "warn"
|
|
3819
5411
|
}
|
|
3820
5412
|
},
|
|
3821
5413
|
strict: {
|
|
@@ -3851,7 +5443,31 @@ var plugin = {
|
|
|
3851
5443
|
"@sarj/no-unsafe-cast": "warn",
|
|
3852
5444
|
"@sarj/single-public-export": "error",
|
|
3853
5445
|
// High-volume/stylistic — warn until rollout proves FP rate.
|
|
3854
|
-
"@sarj/prefer-string-literal-union": "warn"
|
|
5446
|
+
"@sarj/prefer-string-literal-union": "warn",
|
|
5447
|
+
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
5448
|
+
"@sarj/require-fetch-timeout": "error",
|
|
5449
|
+
"@sarj/no-silent-promise-catch": "error",
|
|
5450
|
+
"@sarj/require-schema-validate-search": "error",
|
|
5451
|
+
// Second SARJ port wave — the TS/Python parity gap.
|
|
5452
|
+
"@sarj/prefer-constant-time-secret-compare": "error",
|
|
5453
|
+
"@sarj/store-insert-requires-on-conflict": "error",
|
|
5454
|
+
"@sarj/no-offset-pagination": "error",
|
|
5455
|
+
"@sarj/no-select-star": "error",
|
|
5456
|
+
"@sarj/no-sleep-in-test-body": "error",
|
|
5457
|
+
"@sarj/no-repeated-string-literal": "error",
|
|
5458
|
+
// API-shape advice rather than a runtime defect — a corpus sweep found its
|
|
5459
|
+
// only hits are parser `[value, cursor]` returns, which are conventional.
|
|
5460
|
+
// Warn even in strict until a rollout justifies more.
|
|
5461
|
+
"@sarj/no-positional-tuple-return": "warn",
|
|
5462
|
+
"@sarj/no-dynamic-sql": "error",
|
|
5463
|
+
// Architectural: both need per-repo config to be meaningful, so they
|
|
5464
|
+
// are strict-only. `no-storage-in-stateless-modules` is a no-op until
|
|
5465
|
+
// its `modules` option names the directories a team declared stateless;
|
|
5466
|
+
// `no-raw-fetch-outside-clients` defaults to the `clients/` convention
|
|
5467
|
+
// and takes an `allow` list for repos that lay their client layer out
|
|
5468
|
+
// differently.
|
|
5469
|
+
"@sarj/no-raw-fetch-outside-clients": "error",
|
|
5470
|
+
"@sarj/no-storage-in-stateless-modules": "error"
|
|
3855
5471
|
}
|
|
3856
5472
|
}
|
|
3857
5473
|
}
|