eslint-plugin-ai-guard 1.1.7 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -265,13 +265,43 @@ var createRule5 = import_utils5.ESLintUtils.RuleCreator(
265
265
  );
266
266
  var ARRAY_CALLBACK_METHODS = ["map", "filter", "forEach", "reduce", "flatMap", "find", "findIndex", "some", "every"];
267
267
  var PROMISE_COMBINATORS = ["all", "allSettled", "race", "any"];
268
+ var METHODS_ALLOWING_PROMISE_COLLECTION = /* @__PURE__ */ new Set(["map", "flatMap"]);
269
+ function isNode(value) {
270
+ return typeof value === "object" && value !== null && "type" in value;
271
+ }
272
+ function getChildNodes(node) {
273
+ const children = [];
274
+ for (const [key, value] of Object.entries(node)) {
275
+ if (key === "parent") continue;
276
+ if (!value) continue;
277
+ if (Array.isArray(value)) {
278
+ for (const item of value) {
279
+ if (isNode(item)) {
280
+ children.push(item);
281
+ }
282
+ }
283
+ continue;
284
+ }
285
+ if (isNode(value)) {
286
+ children.push(value);
287
+ }
288
+ }
289
+ return children;
290
+ }
291
+ function isPromiseCombinatorCall(node) {
292
+ return node.callee.type === import_utils5.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils5.AST_NODE_TYPES.Identifier && node.callee.object.name === "Promise" && node.callee.property.type === import_utils5.AST_NODE_TYPES.Identifier && PROMISE_COMBINATORS.includes(
293
+ node.callee.property.name
294
+ );
295
+ }
268
296
  function isWrappedInPromiseCombinator(node, context) {
269
297
  const ancestors = context.sourceCode.getAncestors(node);
298
+ const directParent = node.parent;
299
+ if (directParent && directParent.type === import_utils5.AST_NODE_TYPES.CallExpression && isPromiseCombinatorCall(directParent)) {
300
+ return true;
301
+ }
270
302
  for (let i = ancestors.length - 1; i >= 0; i--) {
271
303
  const ancestor = ancestors[i];
272
- if (ancestor.type === import_utils5.AST_NODE_TYPES.CallExpression && ancestor.callee.type === import_utils5.AST_NODE_TYPES.MemberExpression && ancestor.callee.object.type === import_utils5.AST_NODE_TYPES.Identifier && ancestor.callee.object.name === "Promise" && ancestor.callee.property.type === import_utils5.AST_NODE_TYPES.Identifier && PROMISE_COMBINATORS.includes(
273
- ancestor.callee.property.name
274
- )) {
304
+ if (ancestor.type === import_utils5.AST_NODE_TYPES.CallExpression && isPromiseCombinatorCall(ancestor)) {
275
305
  return true;
276
306
  }
277
307
  if (ancestor.type === import_utils5.AST_NODE_TYPES.CallExpression) {
@@ -280,6 +310,52 @@ function isWrappedInPromiseCombinator(node, context) {
280
310
  }
281
311
  return false;
282
312
  }
313
+ function isIdentifierConsumedByPromiseCombinator(node, identifierName) {
314
+ let found = false;
315
+ const visit = (current) => {
316
+ if (found) return;
317
+ if (current.type === import_utils5.AST_NODE_TYPES.CallExpression && isPromiseCombinatorCall(current)) {
318
+ for (const arg of current.arguments) {
319
+ if (arg.type === import_utils5.AST_NODE_TYPES.Identifier && arg.name === identifierName) {
320
+ found = true;
321
+ return;
322
+ }
323
+ }
324
+ }
325
+ for (const child of getChildNodes(current)) {
326
+ visit(child);
327
+ if (found) return;
328
+ }
329
+ };
330
+ visit(node);
331
+ return found;
332
+ }
333
+ function isAssignedAndConsumedByPromiseCombinator(node) {
334
+ if (!node.parent || node.parent.type !== import_utils5.AST_NODE_TYPES.VariableDeclarator || node.parent.id.type !== import_utils5.AST_NODE_TYPES.Identifier) {
335
+ return false;
336
+ }
337
+ const variableName = node.parent.id.name;
338
+ const declaration = node.parent.parent;
339
+ if (!declaration || declaration.type !== import_utils5.AST_NODE_TYPES.VariableDeclaration) {
340
+ return false;
341
+ }
342
+ const container = declaration.parent;
343
+ if (!container || container.type !== import_utils5.AST_NODE_TYPES.Program && container.type !== import_utils5.AST_NODE_TYPES.BlockStatement) {
344
+ return false;
345
+ }
346
+ const body = container.body;
347
+ const declarationIndex = body.findIndex((statement) => statement === declaration);
348
+ if (declarationIndex === -1) {
349
+ return false;
350
+ }
351
+ for (let i = declarationIndex + 1; i < body.length; i++) {
352
+ const statement = body[i];
353
+ if (isIdentifierConsumedByPromiseCombinator(statement, variableName)) {
354
+ return true;
355
+ }
356
+ }
357
+ return false;
358
+ }
283
359
  function isAsyncCallback(node) {
284
360
  return (node.type === import_utils5.AST_NODE_TYPES.FunctionExpression || node.type === import_utils5.AST_NODE_TYPES.ArrowFunctionExpression) && node.async;
285
361
  }
@@ -317,6 +393,9 @@ var noAsyncArrayCallback = createRule5({
317
393
  if (isWrappedInPromiseCombinator(node, context)) {
318
394
  return;
319
395
  }
396
+ if (METHODS_ALLOWING_PROMISE_COLLECTION.has(methodName) && isAssignedAndConsumedByPromiseCombinator(node)) {
397
+ return;
398
+ }
320
399
  context.report({
321
400
  node: callback,
322
401
  messageId: "asyncArrayCallback",
@@ -335,26 +414,66 @@ var import_utils6 = require("@typescript-eslint/utils");
335
414
  var createRule6 = import_utils6.ESLintUtils.RuleCreator(
336
415
  (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
337
416
  );
338
- function looksLikeAsyncCall(node) {
417
+ var KNOWN_PROMISE_FACTORIES = /* @__PURE__ */ new Set([
418
+ "fetch"
419
+ ]);
420
+ var PROMISE_STATIC_METHODS = /* @__PURE__ */ new Set([
421
+ "resolve",
422
+ "reject",
423
+ "all",
424
+ "allSettled",
425
+ "race",
426
+ "any"
427
+ ]);
428
+ function isLocallyAsyncCallee(node, asyncFunctionNames) {
339
429
  if (node.callee.type === import_utils6.AST_NODE_TYPES.Identifier) {
340
- return hasAsyncishName(node.callee.name);
430
+ if (asyncFunctionNames.has(node.callee.name)) {
431
+ return true;
432
+ }
341
433
  }
342
- if (node.callee.type === import_utils6.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils6.AST_NODE_TYPES.Identifier) {
343
- return hasAsyncishName(node.callee.property.name);
434
+ if ((node.callee.type === import_utils6.AST_NODE_TYPES.FunctionExpression || node.callee.type === import_utils6.AST_NODE_TYPES.ArrowFunctionExpression) && node.callee.async) {
435
+ return true;
344
436
  }
345
437
  return false;
346
438
  }
347
- function hasAsyncishName(name) {
348
- const lower = name.toLowerCase();
349
- return lower.startsWith("fetch") || lower.startsWith("load") || lower.startsWith("save") || lower.startsWith("delete") || lower.startsWith("create") || lower.startsWith("update") || lower.startsWith("get") || lower.startsWith("post") || lower.startsWith("put") || lower.startsWith("patch") || lower.startsWith("send") || lower.startsWith("upload") || lower.startsWith("download") || lower.startsWith("connect") || lower.startsWith("disconnect") || lower.startsWith("subscribe") || lower.startsWith("publish") || lower.startsWith("request") || lower.startsWith("query") || lower === "axios" || lower === "fetch";
350
- }
351
- function isCalleeAsync(node, asyncFunctionNames) {
439
+ function isKnownPromiseFactoryCall(node) {
352
440
  if (node.callee.type === import_utils6.AST_NODE_TYPES.Identifier) {
353
- if (asyncFunctionNames.has(node.callee.name)) {
354
- return true;
441
+ return KNOWN_PROMISE_FACTORIES.has(node.callee.name);
442
+ }
443
+ if (node.callee.type === import_utils6.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils6.AST_NODE_TYPES.Identifier && node.callee.object.name === "Promise" && node.callee.property.type === import_utils6.AST_NODE_TYPES.Identifier) {
444
+ return PROMISE_STATIC_METHODS.has(node.callee.property.name);
445
+ }
446
+ return false;
447
+ }
448
+ function getParserServices(context) {
449
+ const services = context.sourceCode.parserServices;
450
+ if (!services || typeof services !== "object") {
451
+ return null;
452
+ }
453
+ return services;
454
+ }
455
+ function isPromiseLikeByTypeInfo(node, context) {
456
+ const services = getParserServices(context);
457
+ if (!services?.program || !services.esTreeNodeToTSNodeMap) {
458
+ return false;
459
+ }
460
+ try {
461
+ const tsNode = services.esTreeNodeToTSNodeMap.get(node);
462
+ if (!tsNode) {
463
+ return false;
355
464
  }
465
+ const checker = services.program.getTypeChecker();
466
+ const type = checker.getTypeAtLocation(tsNode);
467
+ if (typeof checker.getPromisedTypeOfPromise === "function") {
468
+ if (checker.getPromisedTypeOfPromise(type)) {
469
+ return true;
470
+ }
471
+ }
472
+ const text = checker.typeToString(type).toLowerCase();
473
+ return text === "promise" || text.includes("promise<") || text.includes("promiselike<") || text.includes("thenable");
474
+ } catch {
475
+ return false;
356
476
  }
357
- return looksLikeAsyncCall(node);
358
477
  }
359
478
  function isExpressionHandled(node) {
360
479
  const expr = node.expression;
@@ -404,6 +523,13 @@ var noFloatingPromise = createRule6({
404
523
  },
405
524
  // Check ExpressionStatements — standalone call expressions
406
525
  ExpressionStatement(node) {
526
+ if (node.expression.type === import_utils6.AST_NODE_TYPES.NewExpression && node.expression.callee.type === import_utils6.AST_NODE_TYPES.Identifier && node.expression.callee.name === "Promise") {
527
+ context.report({
528
+ node,
529
+ messageId: "floatingPromise"
530
+ });
531
+ return;
532
+ }
407
533
  if (node.expression.type !== import_utils6.AST_NODE_TYPES.CallExpression) {
408
534
  return;
409
535
  }
@@ -411,7 +537,7 @@ var noFloatingPromise = createRule6({
411
537
  return;
412
538
  }
413
539
  const callExpr = node.expression;
414
- if (isCalleeAsync(callExpr, asyncFunctionNames)) {
540
+ if (isLocallyAsyncCallee(callExpr, asyncFunctionNames) || isKnownPromiseFactoryCall(callExpr) || isPromiseLikeByTypeInfo(callExpr, context)) {
415
541
  context.report({
416
542
  node,
417
543
  messageId: "floatingPromise"
@@ -434,6 +560,80 @@ var LOOP_TYPES = /* @__PURE__ */ new Set([
434
560
  import_utils7.AST_NODE_TYPES.WhileStatement,
435
561
  import_utils7.AST_NODE_TYPES.DoWhileStatement
436
562
  ]);
563
+ var SEQUENTIAL_CALL_HINTS = [
564
+ "read",
565
+ "next",
566
+ "retry",
567
+ "sleep",
568
+ "delay",
569
+ "wait",
570
+ "throttle",
571
+ "ratelimit",
572
+ "acquire",
573
+ "release",
574
+ "drain",
575
+ "flush",
576
+ "consume"
577
+ ];
578
+ var INTENT_COMMENT_HINTS = [
579
+ "sequential",
580
+ "ordered",
581
+ "order matters",
582
+ "intentional",
583
+ "rate limit",
584
+ "rate-limit",
585
+ "retry",
586
+ "stream",
587
+ "backpressure"
588
+ ];
589
+ function getCalleeName(callee) {
590
+ if (callee.type === import_utils7.AST_NODE_TYPES.Identifier) {
591
+ return callee.name;
592
+ }
593
+ if (callee.type === import_utils7.AST_NODE_TYPES.MemberExpression && callee.property.type === import_utils7.AST_NODE_TYPES.Identifier) {
594
+ return callee.property.name;
595
+ }
596
+ return null;
597
+ }
598
+ function isLikelySequentialAwaitCall(node) {
599
+ if (node.argument.type !== import_utils7.AST_NODE_TYPES.CallExpression) {
600
+ return false;
601
+ }
602
+ const calleeName = getCalleeName(node.argument.callee);
603
+ if (!calleeName) {
604
+ return false;
605
+ }
606
+ const lower = calleeName.toLowerCase();
607
+ return SEQUENTIAL_CALL_HINTS.some((hint) => lower.includes(hint));
608
+ }
609
+ function hasIntentionalSequentialComment(loopNode, sourceCode) {
610
+ const directComments = sourceCode.getCommentsBefore(loopNode);
611
+ const allComments = sourceCode.getAllComments();
612
+ const nearComments = allComments.filter((comment) => {
613
+ if (!comment.range || !loopNode.range) {
614
+ return false;
615
+ }
616
+ const [commentStart, commentEnd] = comment.range;
617
+ const [loopStart, loopEnd] = loopNode.range;
618
+ const insideLoop = commentStart >= loopStart && commentEnd <= loopEnd;
619
+ const immediatelyBeforeLoop = commentEnd <= loopStart && loopStart - commentEnd < 120;
620
+ return insideLoop || immediatelyBeforeLoop;
621
+ });
622
+ const combined = [...directComments, ...nearComments];
623
+ return combined.some((comment) => {
624
+ const lower = comment.value.toLowerCase();
625
+ return INTENT_COMMENT_HINTS.some((hint) => lower.includes(hint));
626
+ });
627
+ }
628
+ function shouldAllowIntentionalSequentialAwait(loopNode, awaitNode, context) {
629
+ if (loopNode.type === import_utils7.AST_NODE_TYPES.ForOfStatement && loopNode.await) {
630
+ return true;
631
+ }
632
+ if (hasIntentionalSequentialComment(loopNode, context.sourceCode)) {
633
+ return true;
634
+ }
635
+ return isLikelySequentialAwaitCall(awaitNode);
636
+ }
437
637
  var noAwaitInLoop = createRule7({
438
638
  name: "no-await-in-loop",
439
639
  meta: {
@@ -472,6 +672,10 @@ var noAwaitInLoop = createRule7({
472
672
  break;
473
673
  }
474
674
  if (LOOP_TYPES.has(ancestor.type)) {
675
+ const loopNode = ancestor;
676
+ if (shouldAllowIntentionalSequentialAwait(loopNode, node, context)) {
677
+ return;
678
+ }
475
679
  const loopName = getLoopName(ancestor.type);
476
680
  context.report({
477
681
  node,
@@ -859,6 +1063,60 @@ var createRule12 = import_utils12.ESLintUtils.RuleCreator(
859
1063
  (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
860
1064
  );
861
1065
  var SQL_STATEMENT_PATTERN = /\b(select\s+[\s\S]*\s+from|insert\s+into|update\s+\S+\s+set|delete\s+from|drop\s+table|create\s+(table|database)|alter\s+table|truncate\s+table|exec(?:ute)?\s+\S+|union\s+select)\b/i;
1066
+ var SQL_SINK_METHODS = /* @__PURE__ */ new Set([
1067
+ "query",
1068
+ "execute",
1069
+ "queryraw",
1070
+ "queryrawunsafe",
1071
+ "executeraw",
1072
+ "executerawunsafe",
1073
+ "raw",
1074
+ "run",
1075
+ "all",
1076
+ "get",
1077
+ "prepare"
1078
+ ]);
1079
+ var SQL_SINK_FUNCTIONS = /* @__PURE__ */ new Set(["query", "execute"]);
1080
+ function getIdentifierName(node) {
1081
+ if (node.type === import_utils12.AST_NODE_TYPES.Identifier) {
1082
+ return node.name;
1083
+ }
1084
+ return null;
1085
+ }
1086
+ function isSqlSinkCall(node) {
1087
+ if (node.callee.type === import_utils12.AST_NODE_TYPES.Identifier) {
1088
+ return SQL_SINK_FUNCTIONS.has(node.callee.name.toLowerCase());
1089
+ }
1090
+ if (node.callee.type === import_utils12.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils12.AST_NODE_TYPES.Identifier) {
1091
+ const methodName = node.callee.property.name.toLowerCase().replace(/^\$/, "");
1092
+ return SQL_SINK_METHODS.has(methodName);
1093
+ }
1094
+ return false;
1095
+ }
1096
+ function resolveExpression(node, variableMap) {
1097
+ const identifier = getIdentifierName(node);
1098
+ if (identifier && variableMap.has(identifier)) {
1099
+ return variableMap.get(identifier);
1100
+ }
1101
+ return node;
1102
+ }
1103
+ function isDynamicSqlExpression(node) {
1104
+ if (node.type === import_utils12.AST_NODE_TYPES.TemplateLiteral) {
1105
+ if (node.expressions.length === 0) {
1106
+ return false;
1107
+ }
1108
+ const staticText = node.quasis.map((q) => q.value.raw).join(" ");
1109
+ return SQL_STATEMENT_PATTERN.test(staticText);
1110
+ }
1111
+ if (node.type === import_utils12.AST_NODE_TYPES.BinaryExpression && node.operator === "+") {
1112
+ const staticText = collectStaticText(node);
1113
+ if (!SQL_STATEMENT_PATTERN.test(staticText)) {
1114
+ return false;
1115
+ }
1116
+ return hasDynamicParts(node);
1117
+ }
1118
+ return false;
1119
+ }
862
1120
  var noSqlStringConcat = createRule12({
863
1121
  name: "no-sql-string-concat",
864
1122
  meta: {
@@ -874,32 +1132,34 @@ var noSqlStringConcat = createRule12({
874
1132
  },
875
1133
  defaultOptions: [],
876
1134
  create(context) {
1135
+ const variableMap = /* @__PURE__ */ new Map();
877
1136
  return {
878
- // Template literals: `SELECT * FROM users WHERE id = ${userId}`
879
- TemplateLiteral(node) {
880
- if (node.expressions.length === 0) return;
881
- const staticText = node.quasis.map((q) => q.value.raw).join("");
882
- if (SQL_STATEMENT_PATTERN.test(staticText)) {
883
- context.report({
884
- node,
885
- messageId: "sqlStringConcat"
886
- });
1137
+ VariableDeclarator(node) {
1138
+ if (node.id.type === import_utils12.AST_NODE_TYPES.Identifier && node.init && node.init.type !== import_utils12.AST_NODE_TYPES.AwaitExpression) {
1139
+ variableMap.set(node.id.name, node.init);
1140
+ }
1141
+ },
1142
+ AssignmentExpression(node) {
1143
+ if (node.left.type === import_utils12.AST_NODE_TYPES.Identifier && node.right.type !== import_utils12.AST_NODE_TYPES.AwaitExpression) {
1144
+ variableMap.set(node.left.name, node.right);
887
1145
  }
888
1146
  },
889
- // Binary expressions: "SELECT * FROM users WHERE id = " + userId
890
- BinaryExpression(node) {
891
- if (node.operator !== "+") return;
892
- if (node.parent?.type === import_utils12.AST_NODE_TYPES.BinaryExpression && node.parent.operator === "+") {
1147
+ CallExpression(node) {
1148
+ if (!isSqlSinkCall(node)) {
893
1149
  return;
894
1150
  }
895
- const staticText = collectStaticText(node);
896
- if (!SQL_STATEMENT_PATTERN.test(staticText)) return;
897
- if (hasDynamicParts(node)) {
898
- context.report({
899
- node,
900
- messageId: "sqlStringConcat"
901
- });
1151
+ const firstArg = node.arguments[0];
1152
+ if (!firstArg || firstArg.type === import_utils12.AST_NODE_TYPES.SpreadElement) {
1153
+ return;
902
1154
  }
1155
+ const resolved = resolveExpression(firstArg, variableMap);
1156
+ if (!isDynamicSqlExpression(resolved)) {
1157
+ return;
1158
+ }
1159
+ context.report({
1160
+ node: firstArg,
1161
+ messageId: "sqlStringConcat"
1162
+ });
903
1163
  }
904
1164
  };
905
1165
  }