eslint-plugin-ai-guard 1.1.13 → 1.2.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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // src/rules/error-handling/no-empty-catch.ts
1
+ // src/rules/reliability/no-empty-catch.ts
2
2
  import { ESLintUtils } from "@typescript-eslint/utils";
3
3
  var createRule = ESLintUtils.RuleCreator(
4
4
  (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
@@ -45,7 +45,7 @@ var noEmptyCatch = createRule({
45
45
  }
46
46
  });
47
47
 
48
- // src/rules/error-handling/no-broad-exception.ts
48
+ // src/rules/reliability/no-broad-exception.ts
49
49
  import { ESLintUtils as ESLintUtils2, AST_NODE_TYPES } from "@typescript-eslint/utils";
50
50
  var createRule2 = ESLintUtils2.RuleCreator(
51
51
  (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
@@ -122,7 +122,7 @@ function containsInstanceofCheck(node, paramName) {
122
122
  return false;
123
123
  }
124
124
 
125
- // src/rules/error-handling/no-catch-log-rethrow.ts
125
+ // src/rules/reliability/no-catch-log-rethrow.ts
126
126
  import { ESLintUtils as ESLintUtils3, AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils";
127
127
  var createRule3 = ESLintUtils3.RuleCreator(
128
128
  (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
@@ -184,7 +184,7 @@ var noCatchLogRethrow = createRule3({
184
184
  }
185
185
  });
186
186
 
187
- // src/rules/error-handling/no-catch-without-use.ts
187
+ // src/rules/reliability/no-catch-without-use.ts
188
188
  import { ESLintUtils as ESLintUtils4, AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
189
189
  var createRule4 = ESLintUtils4.RuleCreator(
190
190
  (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
@@ -618,6 +618,7 @@ var noFloatingPromise = createRule6({
618
618
 
619
619
  // src/rules/async/no-await-in-loop.ts
620
620
  import { ESLintUtils as ESLintUtils7, AST_NODE_TYPES as AST_NODE_TYPES6 } from "@typescript-eslint/utils";
621
+ import path from "path";
621
622
  var createRule7 = ESLintUtils7.RuleCreator(
622
623
  (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
623
624
  );
@@ -631,6 +632,7 @@ var LOOP_TYPES = /* @__PURE__ */ new Set([
631
632
  var SUPPRESSION_REGEX = /ai-guard-disable\s+no-await-in-loop\b/i;
632
633
  var RETRY_NAME_REGEX = /(retry|retries|attempt|attempts|fallback|tryagain|recovery)/i;
633
634
  var SEQUENTIAL_DEPENDENCY_NAME_REGEX = /(previous|prev|last|carry|accumulator|stateful)/i;
635
+ var SIMULATION_NAME_REGEX = /(simulat|animat|demo|visuali|step|scene|frame|render|tick|sequence|tutorial|lesson|walk)/i;
634
636
  var ERROR_CODE_HINTS = [
635
637
  "access-denied",
636
638
  "timeout",
@@ -645,6 +647,15 @@ var CONTROL_AWAIT_HINTS = [
645
647
  "ratelimit",
646
648
  "backoff"
647
649
  ];
650
+ var TIMER_FUNCTION_NAMES = /* @__PURE__ */ new Set([
651
+ "setinterval",
652
+ "settimeout",
653
+ "requestanimationframe",
654
+ "queuemicrotask",
655
+ "sleep",
656
+ "delay",
657
+ "wait"
658
+ ]);
648
659
  var MUTATION_METHOD_NAMES = /* @__PURE__ */ new Set([
649
660
  "push",
650
661
  "pop",
@@ -845,6 +856,9 @@ function analyzeIntent(loopNode) {
845
856
  if (SEQUENTIAL_DEPENDENCY_NAME_REGEX.test(name)) {
846
857
  hasSequentialDependency = true;
847
858
  }
859
+ if (SIMULATION_NAME_REGEX.test(name)) {
860
+ hasSequentialDependency = true;
861
+ }
848
862
  };
849
863
  walkNode(
850
864
  loopNode,
@@ -859,6 +873,9 @@ function analyzeIntent(loopNode) {
859
873
  if (CONTROL_AWAIT_HINTS.some((hint) => lower.includes(hint))) {
860
874
  hasControlAwait = true;
861
875
  }
876
+ if (TIMER_FUNCTION_NAMES.has(lower)) {
877
+ hasControlAwait = true;
878
+ }
862
879
  }
863
880
  }
864
881
  if (node.type === AST_NODE_TYPES6.UpdateExpression) {
@@ -995,16 +1012,43 @@ var noAwaitInLoop = createRule7({
995
1012
  description: "Disallow independent `await` usage inside loops, while allowing intentional retry/fallback/sequential workflows. AI tools frequently generate accidental sequential awaits where Promise.all would be safer and faster. Includes a safe autofix for simple independent loops."
996
1013
  },
997
1014
  fixable: "code",
998
- schema: [],
1015
+ schema: [
1016
+ {
1017
+ type: "object",
1018
+ properties: {
1019
+ allowPatterns: {
1020
+ type: "array",
1021
+ items: { type: "string" },
1022
+ description: "File glob patterns to skip (e.g., **/*.test.ts, **/simulation/**)"
1023
+ }
1024
+ },
1025
+ additionalProperties: false
1026
+ }
1027
+ ],
999
1028
  messages: {
1000
- awaitInLoop: "Unexpected `await` inside a {{loopType}}. AI tools frequently generate sequential awaits in loops, causing O(n) latency. Consider collecting promises and using `Promise.all()` for parallel execution."
1029
+ awaitInLoop: "Sequential `await` inside a {{loopType}} \u2014 if these items are independent, consider `Promise.all()` for parallel execution. If sequential order matters, this may be intentional."
1001
1030
  }
1002
1031
  },
1003
- defaultOptions: [],
1004
- create(context) {
1032
+ defaultOptions: [{}],
1033
+ create(context, [options]) {
1005
1034
  if (hasFileSuppression(context.sourceCode)) {
1006
1035
  return {};
1007
1036
  }
1037
+ const filename = context.filename ?? context.getFilename?.() ?? "";
1038
+ const fileBasename = path.basename(filename).toLowerCase();
1039
+ const filePath = filename.toLowerCase();
1040
+ const isTestFile = fileBasename.includes(".test.") || fileBasename.includes(".spec.") || filePath.includes("/__tests__/") || filePath.includes("/test/") || filePath.includes("/tests/");
1041
+ const isSimulationFile = SIMULATION_NAME_REGEX.test(fileBasename) || filePath.includes("/simulation/") || filePath.includes("/demo/") || filePath.includes("/animation/") || filePath.includes("/fixtures/");
1042
+ if (isTestFile || isSimulationFile) {
1043
+ return {};
1044
+ }
1045
+ const { allowPatterns = [] } = options;
1046
+ for (const pattern of allowPatterns) {
1047
+ const cleaned = pattern.replace(/\*\*/g, "").replace(/\*/g, "").replace(/\//g, path.sep);
1048
+ if (cleaned && filePath.includes(cleaned.toLowerCase())) {
1049
+ return {};
1050
+ }
1051
+ }
1008
1052
  const loopIntentCache = /* @__PURE__ */ new WeakMap();
1009
1053
  const reportedLoops = /* @__PURE__ */ new WeakSet();
1010
1054
  const functionBoundary = [];
@@ -1065,9 +1109,36 @@ var noAwaitInLoop = createRule7({
1065
1109
 
1066
1110
  // src/rules/async/no-async-without-await.ts
1067
1111
  import { ESLintUtils as ESLintUtils8, AST_NODE_TYPES as AST_NODE_TYPES7 } from "@typescript-eslint/utils";
1112
+ import path2 from "path";
1068
1113
  var createRule8 = ESLintUtils8.RuleCreator(
1069
1114
  (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
1070
1115
  );
1116
+ var FRAMEWORK_FILE_PATTERNS = [
1117
+ // Next.js App Router
1118
+ /[/\\]route\.(ts|js|tsx|jsx)$/,
1119
+ /[/\\]middleware\.(ts|js)$/,
1120
+ /[/\\]layout\.(tsx|jsx|ts|js)$/,
1121
+ /[/\\]page\.(tsx|jsx|ts|js)$/,
1122
+ /[/\\]loading\.(tsx|jsx|ts|js)$/,
1123
+ /[/\\]error\.(tsx|jsx|ts|js)$/,
1124
+ /[/\\]not-found\.(tsx|jsx|ts|js)$/,
1125
+ /[/\\]template\.(tsx|jsx|ts|js)$/,
1126
+ // Next.js Pages Router conventions
1127
+ /[/\\]_app\.(tsx|jsx)$/,
1128
+ /[/\\]_document\.(tsx|jsx)$/
1129
+ ];
1130
+ var HTTP_METHOD_EXPORTS = /* @__PURE__ */ new Set([
1131
+ "GET",
1132
+ "POST",
1133
+ "PUT",
1134
+ "PATCH",
1135
+ "DELETE",
1136
+ "HEAD",
1137
+ "OPTIONS"
1138
+ ]);
1139
+ var REACT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
1140
+ var HANDLER_NAME_REGEX = /^(handle|on[A-Z])/;
1141
+ var MIDDLEWARE_NAME_REGEX = /^(use[A-Z]|middleware|interceptor|guard|beforeEach|afterEach)/;
1071
1142
  function containsAwaitExpression(node) {
1072
1143
  if (node.type === AST_NODE_TYPES7.AwaitExpression) {
1073
1144
  return true;
@@ -1101,21 +1172,101 @@ function containsAwaitExpression(node) {
1101
1172
  }
1102
1173
  return false;
1103
1174
  }
1175
+ function isPassThroughWrapper(node) {
1176
+ if (node.body.type !== AST_NODE_TYPES7.BlockStatement) {
1177
+ const body = node.body;
1178
+ if (body.type === AST_NODE_TYPES7.CallExpression || body.type === AST_NODE_TYPES7.MemberExpression) {
1179
+ return true;
1180
+ }
1181
+ return false;
1182
+ }
1183
+ if (node.body.body.length !== 1) return false;
1184
+ const only = node.body.body[0];
1185
+ if (only.type !== AST_NODE_TYPES7.ReturnStatement) return false;
1186
+ if (!only.argument) return false;
1187
+ return only.argument.type === AST_NODE_TYPES7.CallExpression;
1188
+ }
1189
+ function isFunctionExportedWithHttpMethodName(node) {
1190
+ if (node.type === AST_NODE_TYPES7.FunctionDeclaration && node.id) {
1191
+ return HTTP_METHOD_EXPORTS.has(node.id.name);
1192
+ }
1193
+ if (node.parent?.type === AST_NODE_TYPES7.VariableDeclarator) {
1194
+ const declarator = node.parent;
1195
+ if (declarator.id.type === AST_NODE_TYPES7.Identifier && HTTP_METHOD_EXPORTS.has(declarator.id.name)) {
1196
+ return true;
1197
+ }
1198
+ }
1199
+ return false;
1200
+ }
1201
+ function getFunctionName(node) {
1202
+ if (node.type === AST_NODE_TYPES7.FunctionDeclaration && node.id) {
1203
+ return node.id.name;
1204
+ }
1205
+ if (node.parent?.type === AST_NODE_TYPES7.VariableDeclarator) {
1206
+ const declarator = node.parent;
1207
+ if (declarator.id.type === AST_NODE_TYPES7.Identifier) {
1208
+ return declarator.id.name;
1209
+ }
1210
+ }
1211
+ if (node.parent?.type === AST_NODE_TYPES7.Property || node.parent?.type === AST_NODE_TYPES7.MethodDefinition) {
1212
+ const parent = node.parent;
1213
+ if ("key" in parent && parent.key.type === AST_NODE_TYPES7.Identifier) {
1214
+ return parent.key.name;
1215
+ }
1216
+ }
1217
+ return null;
1218
+ }
1104
1219
  var noAsyncWithoutAwait = createRule8({
1105
1220
  name: "no-async-without-await",
1106
1221
  meta: {
1107
1222
  type: "suggestion",
1108
1223
  docs: {
1109
- description: "Disallow async functions that never use await. AI tools frequently add async by default, creating misleading signatures and unnecessary Promise wrappers. Includes a safe autofix for simple function bodies."
1224
+ description: "Disallow async functions that never use await. AI tools frequently add async by default, creating misleading signatures and unnecessary Promise wrappers. Framework-specific async conventions (Next.js route handlers, React event handlers) are excluded."
1110
1225
  },
1111
1226
  fixable: "code",
1112
- schema: [],
1227
+ schema: [
1228
+ {
1229
+ type: "object",
1230
+ properties: {
1231
+ allowedFunctionNames: {
1232
+ type: "array",
1233
+ items: { type: "string" },
1234
+ description: "Additional function names to skip (on top of HTTP methods)"
1235
+ },
1236
+ frameworkFiles: {
1237
+ type: "array",
1238
+ items: { type: "string" },
1239
+ description: "Additional file glob patterns where async-without-await is allowed"
1240
+ },
1241
+ ignorePassThroughWrappers: {
1242
+ type: "boolean",
1243
+ description: "Downgrade (not suppress) pass-through wrappers. Default: true."
1244
+ },
1245
+ ignoreHandlerFunctions: {
1246
+ type: "boolean",
1247
+ description: "Skip functions named handle* or on* in .tsx/.jsx files. Default: true."
1248
+ }
1249
+ },
1250
+ additionalProperties: false
1251
+ }
1252
+ ],
1113
1253
  messages: {
1114
- asyncWithoutAwait: "Async function does not contain `await`. AI tools frequently add `async` unnecessarily, which can mislead callers and mask intent. Remove `async` or add proper await logic."
1254
+ asyncWithoutAwait: "Async function does not contain `await`. AI tools frequently add `async` unnecessarily, which can mislead callers and mask intent. Remove `async` or add proper await logic.",
1255
+ asyncPassThrough: "Async pass-through wrapper \u2014 this may be intentional for Promise propagation. Consider whether `async` is needed here."
1115
1256
  }
1116
1257
  },
1117
- defaultOptions: [],
1118
- create(context) {
1258
+ defaultOptions: [{}],
1259
+ create(context, [options]) {
1260
+ const {
1261
+ allowedFunctionNames = [],
1262
+ ignorePassThroughWrappers = true,
1263
+ ignoreHandlerFunctions = true
1264
+ } = options;
1265
+ const skipNames = /* @__PURE__ */ new Set([...HTTP_METHOD_EXPORTS, ...allowedFunctionNames]);
1266
+ const filename = context.filename ?? context.getFilename?.() ?? "";
1267
+ const fileExt = path2.extname(filename).toLowerCase();
1268
+ const isFrameworkFile = FRAMEWORK_FILE_PATTERNS.some((pattern) => pattern.test(filename));
1269
+ const isReactFile = REACT_FILE_EXTENSIONS.has(fileExt);
1119
1270
  function buildSafeAutofix2(node) {
1120
1271
  const sourceCode = context.sourceCode;
1121
1272
  if (node.body.type !== AST_NODE_TYPES7.BlockStatement) {
@@ -1140,25 +1291,51 @@ var noAsyncWithoutAwait = createRule8({
1140
1291
  if (!node.async) {
1141
1292
  return;
1142
1293
  }
1294
+ if (isFrameworkFile) {
1295
+ return;
1296
+ }
1297
+ if (isFunctionExportedWithHttpMethodName(node)) {
1298
+ return;
1299
+ }
1300
+ const funcName = getFunctionName(node);
1301
+ if (funcName && skipNames.has(funcName)) {
1302
+ return;
1303
+ }
1304
+ if (funcName && MIDDLEWARE_NAME_REGEX.test(funcName)) {
1305
+ return;
1306
+ }
1307
+ if (ignoreHandlerFunctions && isReactFile && funcName && HANDLER_NAME_REGEX.test(funcName)) {
1308
+ return;
1309
+ }
1310
+ const bodyHasAwait = node.body.type === AST_NODE_TYPES7.BlockStatement ? containsAwaitExpression(node.body) : node.body.type === AST_NODE_TYPES7.AwaitExpression;
1311
+ if (bodyHasAwait) {
1312
+ return;
1313
+ }
1314
+ if (ignorePassThroughWrappers && isPassThroughWrapper(node)) {
1315
+ context.report({
1316
+ node,
1317
+ messageId: "asyncPassThrough"
1318
+ // No autofix for pass-through — it's intentional
1319
+ });
1320
+ return;
1321
+ }
1143
1322
  if (node.body.type !== AST_NODE_TYPES7.BlockStatement) {
1144
1323
  if (node.body.type !== AST_NODE_TYPES7.AwaitExpression) {
1145
- const fix = buildSafeAutofix2(node);
1324
+ const fix2 = buildSafeAutofix2(node);
1146
1325
  context.report({
1147
1326
  node,
1148
1327
  messageId: "asyncWithoutAwait",
1149
- fix
1328
+ fix: fix2
1150
1329
  });
1151
1330
  }
1152
1331
  return;
1153
1332
  }
1154
- if (!containsAwaitExpression(node.body)) {
1155
- const fix = buildSafeAutofix2(node);
1156
- context.report({
1157
- node,
1158
- messageId: "asyncWithoutAwait",
1159
- fix
1160
- });
1161
- }
1333
+ const fix = buildSafeAutofix2(node);
1334
+ context.report({
1335
+ node,
1336
+ messageId: "asyncWithoutAwait",
1337
+ fix
1338
+ });
1162
1339
  }
1163
1340
  return {
1164
1341
  FunctionDeclaration: reportIfNeeded,
@@ -1434,10 +1611,20 @@ var noEvalDynamic = createRule11({
1434
1611
  messageId: "evalDynamic",
1435
1612
  data: { callee: `${node.callee.object.name}.eval()` }
1436
1613
  });
1614
+ return;
1615
+ }
1616
+ if (isFunctionConstructorCallee(node.callee)) {
1617
+ if (node.arguments.length === 0) return;
1618
+ const allLiteral = node.arguments.every(isLiteral);
1619
+ if (allLiteral) return;
1620
+ context.report({
1621
+ node,
1622
+ messageId: "newFunctionDynamic"
1623
+ });
1437
1624
  }
1438
1625
  },
1439
1626
  NewExpression(node) {
1440
- if (node.callee.type === AST_NODE_TYPES10.Identifier && node.callee.name === "Function") {
1627
+ if (isFunctionConstructorCallee(node.callee)) {
1441
1628
  if (node.arguments.length === 0) return;
1442
1629
  const allLiteral = node.arguments.every(isLiteral);
1443
1630
  if (allLiteral) return;
@@ -1450,6 +1637,15 @@ var noEvalDynamic = createRule11({
1450
1637
  };
1451
1638
  }
1452
1639
  });
1640
+ function isFunctionConstructorCallee(callee) {
1641
+ if (callee.type === AST_NODE_TYPES10.Identifier && callee.name === "Function") {
1642
+ return true;
1643
+ }
1644
+ if (callee.type === AST_NODE_TYPES10.MemberExpression && callee.property.type === AST_NODE_TYPES10.Identifier && callee.property.name === "Function" && callee.object.type === AST_NODE_TYPES10.Identifier && (callee.object.name === "window" || callee.object.name === "globalThis")) {
1645
+ return true;
1646
+ }
1647
+ return false;
1648
+ }
1453
1649
  function isLiteral(node) {
1454
1650
  if (node.type === AST_NODE_TYPES10.Literal) return true;
1455
1651
  if (node.type === AST_NODE_TYPES10.TemplateLiteral && node.expressions.length === 0) {
@@ -1867,6 +2063,8 @@ var PUBLIC_ROUTE_PATTERNS = [
1867
2063
  // reset password
1868
2064
  /^\/webhook/,
1869
2065
  // webhooks
2066
+ /^\/callback/,
2067
+ // OAuth callback
1870
2068
  /^\/public/,
1871
2069
  // explicitly public
1872
2070
  /^\/assets/,
@@ -1877,8 +2075,12 @@ var PUBLIC_ROUTE_PATTERNS = [
1877
2075
  // favicon
1878
2076
  /^\/robots/,
1879
2077
  // robots.txt
1880
- /^\/sitemap/
2078
+ /^\/sitemap/,
1881
2079
  // sitemap
2080
+ /^\/api\/docs/,
2081
+ // API documentation (Swagger, etc.)
2082
+ /^\/docs/
2083
+ // documentation
1882
2084
  ];
1883
2085
  var requireAuthMiddleware = createRule14({
1884
2086
  name: "require-auth-middleware",
@@ -1975,6 +2177,13 @@ function isAuthMiddleware(node, authNames) {
1975
2177
  if (node.type === AST_NODE_TYPES13.Identifier) {
1976
2178
  return authNames.has(node.name);
1977
2179
  }
2180
+ if (node.type === AST_NODE_TYPES13.ObjectExpression) {
2181
+ for (const prop of node.properties) {
2182
+ if (prop.type === AST_NODE_TYPES13.Property && prop.key.type === AST_NODE_TYPES13.Identifier && (prop.key.name === "preHandler" || prop.key.name === "onRequest")) {
2183
+ if (isAuthMiddleware(prop.value, authNames)) return true;
2184
+ }
2185
+ }
2186
+ }
1978
2187
  if (node.type === AST_NODE_TYPES13.CallExpression) {
1979
2188
  if (node.callee.type === AST_NODE_TYPES13.Identifier) {
1980
2189
  return authNames.has(node.callee.name);
@@ -2060,29 +2269,29 @@ function getMemberPath(node) {
2060
2269
  }
2061
2270
  return [...objectPath, node.property.name];
2062
2271
  }
2063
- function hasPathPrefix(path, prefix) {
2064
- if (!path || path.length < prefix.length) {
2272
+ function hasPathPrefix(path3, prefix) {
2273
+ if (!path3 || path3.length < prefix.length) {
2065
2274
  return false;
2066
2275
  }
2067
2276
  for (let i = 0; i < prefix.length; i += 1) {
2068
- if (path[i] !== prefix[i]) {
2277
+ if (path3[i] !== prefix[i]) {
2069
2278
  return false;
2070
2279
  }
2071
2280
  }
2072
2281
  return true;
2073
2282
  }
2074
- function isLikelyResourceIdPath(path) {
2075
- if (!path || path.length < 3) {
2283
+ function isLikelyResourceIdPath(path3) {
2284
+ if (!path3 || path3.length < 3) {
2076
2285
  return false;
2077
2286
  }
2078
- if (!hasPathPrefix(path, ["req", "params"]) && !hasPathPrefix(path, ["req", "body"]) && !hasPathPrefix(path, ["req", "query"])) {
2287
+ if (!hasPathPrefix(path3, ["req", "params"]) && !hasPathPrefix(path3, ["req", "body"]) && !hasPathPrefix(path3, ["req", "query"])) {
2079
2288
  return false;
2080
2289
  }
2081
- const last = path[path.length - 1].toLowerCase();
2290
+ const last = path3[path3.length - 1].toLowerCase();
2082
2291
  return last === "id" || last.endsWith("id");
2083
2292
  }
2084
- function isReqUserPath(path) {
2085
- return hasPathPrefix(path, ["req", "user"]);
2293
+ function isReqUserPath(path3) {
2294
+ return hasPathPrefix(path3, ["req", "user"]);
2086
2295
  }
2087
2296
  function containsAuthorizationHelper(node) {
2088
2297
  if (node.type === AST_NODE_TYPES14.CallExpression && (node.callee.type === AST_NODE_TYPES14.Identifier && AUTHZ_HELPER_NAMES.includes(node.callee.name) || node.callee.type === AST_NODE_TYPES14.MemberExpression && node.callee.property.type === AST_NODE_TYPES14.Identifier && AUTHZ_HELPER_NAMES.includes(node.callee.property.name))) {
@@ -2121,8 +2330,8 @@ function collectBodySignals(node) {
2121
2330
  }
2122
2331
  }
2123
2332
  if (current.type === AST_NODE_TYPES14.MemberExpression) {
2124
- const path = getMemberPath(current);
2125
- if (isLikelyResourceIdPath(path)) {
2333
+ const path3 = getMemberPath(current);
2334
+ if (isLikelyResourceIdPath(path3)) {
2126
2335
  hasResourceIdAccess = true;
2127
2336
  }
2128
2337
  }
@@ -2191,7 +2400,7 @@ var requireAuthzCheck = createRule15({
2191
2400
  }
2192
2401
  });
2193
2402
 
2194
- // src/rules/quality/no-console-in-handler.ts
2403
+ // src/rules/ai-patterns/no-console-in-handler.ts
2195
2404
  import { ESLintUtils as ESLintUtils16, AST_NODE_TYPES as AST_NODE_TYPES15 } from "@typescript-eslint/utils";
2196
2405
  var createRule16 = ESLintUtils16.RuleCreator(
2197
2406
  (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
@@ -2301,7 +2510,7 @@ var noConsoleInHandler = createRule16({
2301
2510
  }
2302
2511
  });
2303
2512
 
2304
- // src/rules/logic/no-duplicate-logic-block.ts
2513
+ // src/rules/ai-patterns/no-duplicate-logic-block.ts
2305
2514
  import { ESLintUtils as ESLintUtils17, AST_NODE_TYPES as AST_NODE_TYPES16 } from "@typescript-eslint/utils";
2306
2515
  var createRule17 = ESLintUtils17.RuleCreator(
2307
2516
  (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
@@ -2366,25 +2575,127 @@ var noDuplicateLogicBlock = createRule17({
2366
2575
  }
2367
2576
  });
2368
2577
 
2578
+ // src/rules/ai-patterns/no-dead-branch.ts
2579
+ import { ESLintUtils as ESLintUtils18, AST_NODE_TYPES as AST_NODE_TYPES17 } from "@typescript-eslint/utils";
2580
+ var createRule18 = ESLintUtils18.RuleCreator(
2581
+ (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
2582
+ );
2583
+ function isBooleanLiteral(node, value) {
2584
+ return node.type === AST_NODE_TYPES17.Literal && typeof node.value === "boolean" && node.value === value;
2585
+ }
2586
+ function isZeroLiteral(node) {
2587
+ return node.type === AST_NODE_TYPES17.Literal && typeof node.value === "number" && node.value === 0;
2588
+ }
2589
+ function isEmptyStringLiteral(node) {
2590
+ return node.type === AST_NODE_TYPES17.Literal && typeof node.value === "string" && node.value === "";
2591
+ }
2592
+ function isAlwaysTrue(node) {
2593
+ if (isBooleanLiteral(node, true)) return true;
2594
+ if (node.type === AST_NODE_TYPES17.LogicalExpression && node.operator === "||") {
2595
+ const { left, right } = node;
2596
+ if (left.type === AST_NODE_TYPES17.Identifier && right.type === AST_NODE_TYPES17.UnaryExpression && right.operator === "!" && right.argument.type === AST_NODE_TYPES17.Identifier && left.name === right.argument.name) {
2597
+ return true;
2598
+ }
2599
+ }
2600
+ if (node.type === AST_NODE_TYPES17.BinaryExpression && (node.operator === "===" || node.operator === "==") && node.left.type === AST_NODE_TYPES17.Identifier && node.right.type === AST_NODE_TYPES17.Identifier && node.left.name === node.right.name) {
2601
+ return true;
2602
+ }
2603
+ return false;
2604
+ }
2605
+ function isAlwaysFalse(node) {
2606
+ if (isBooleanLiteral(node, false)) return true;
2607
+ if (isZeroLiteral(node)) return true;
2608
+ if (isEmptyStringLiteral(node)) return true;
2609
+ if (node.type === AST_NODE_TYPES17.LogicalExpression && node.operator === "&&") {
2610
+ const { left, right } = node;
2611
+ if (left.type === AST_NODE_TYPES17.Identifier && right.type === AST_NODE_TYPES17.UnaryExpression && right.operator === "!" && right.argument.type === AST_NODE_TYPES17.Identifier && left.name === right.argument.name) {
2612
+ return true;
2613
+ }
2614
+ }
2615
+ if (node.type === AST_NODE_TYPES17.BinaryExpression && (node.operator === "!==" || node.operator === "!=") && node.left.type === AST_NODE_TYPES17.Identifier && node.right.type === AST_NODE_TYPES17.Identifier && node.left.name === node.right.name) {
2616
+ return true;
2617
+ }
2618
+ return false;
2619
+ }
2620
+ var noDeadBranch = createRule18({
2621
+ name: "no-dead-branch",
2622
+ meta: {
2623
+ type: "suggestion",
2624
+ docs: {
2625
+ description: "Disallow conditions that are statically always-true or always-false. AI tools frequently generate dead branches like `if (true)`, `if (false)`, `if (x && !x)`, or `x === x` \u2014 leftover scaffolding that clutters the code and hides logical errors."
2626
+ },
2627
+ fixable: void 0,
2628
+ schema: [],
2629
+ messages: {
2630
+ alwaysTrue: "This condition is always `true` \u2014 the branch is never skipped. AI tools often generate this as leftover scaffolding. Remove the condition or replace it with the intended logic.",
2631
+ alwaysFalse: "This condition is always `false` \u2014 this branch is dead and never executed. AI tools often generate this as leftover scaffolding. Remove the dead branch entirely.",
2632
+ whileFalse: "This `while (false)` loop never executes. AI tools sometimes generate this as a placeholder. Remove it or replace with the intended loop condition."
2633
+ }
2634
+ },
2635
+ defaultOptions: [],
2636
+ create(context) {
2637
+ function checkCondition(testNode, reportNode, isBranchStatement) {
2638
+ if (!testNode) return;
2639
+ if (isAlwaysFalse(testNode)) {
2640
+ context.report({
2641
+ node: reportNode,
2642
+ messageId: isBranchStatement ? "alwaysFalse" : "whileFalse"
2643
+ });
2644
+ return;
2645
+ }
2646
+ if (isAlwaysTrue(testNode)) {
2647
+ if (reportNode.type === AST_NODE_TYPES17.WhileStatement) return;
2648
+ context.report({
2649
+ node: reportNode,
2650
+ messageId: "alwaysTrue"
2651
+ });
2652
+ }
2653
+ }
2654
+ return {
2655
+ IfStatement(node) {
2656
+ checkCondition(node.test, node, true);
2657
+ },
2658
+ WhileStatement(node) {
2659
+ if (isAlwaysFalse(node.test)) {
2660
+ context.report({ node, messageId: "whileFalse" });
2661
+ }
2662
+ },
2663
+ DoWhileStatement(node) {
2664
+ if (isAlwaysFalse(node.test)) {
2665
+ context.report({ node, messageId: "whileFalse" });
2666
+ }
2667
+ },
2668
+ ConditionalExpression(node) {
2669
+ checkCondition(node.test, node, true);
2670
+ }
2671
+ };
2672
+ }
2673
+ });
2674
+
2369
2675
  // src/rules/index.ts
2370
2676
  var allRules = {
2677
+ // Reliability
2371
2678
  "no-empty-catch": noEmptyCatch,
2372
2679
  "no-broad-exception": noBroadException,
2373
2680
  "no-catch-log-rethrow": noCatchLogRethrow,
2374
2681
  "no-catch-without-use": noCatchWithoutUse,
2682
+ // Async
2375
2683
  "no-async-array-callback": noAsyncArrayCallback,
2376
2684
  "no-floating-promise": noFloatingPromise,
2377
2685
  "no-await-in-loop": noAwaitInLoop,
2378
2686
  "no-async-without-await": noAsyncWithoutAwait,
2379
2687
  "no-redundant-await": noRedundantAwait,
2688
+ // Security
2380
2689
  "no-hardcoded-secret": noHardcodedSecret,
2381
2690
  "no-eval-dynamic": noEvalDynamic,
2382
2691
  "no-sql-string-concat": noSqlStringConcat,
2383
2692
  "no-unsafe-deserialize": noUnsafeDeserialize,
2384
2693
  "require-auth-middleware": requireAuthMiddleware,
2385
2694
  "require-authz-check": requireAuthzCheck,
2695
+ // AI Patterns
2386
2696
  "no-console-in-handler": noConsoleInHandler,
2387
- "no-duplicate-logic-block": noDuplicateLogicBlock
2697
+ "no-duplicate-logic-block": noDuplicateLogicBlock,
2698
+ "no-dead-branch": noDeadBranch
2388
2699
  };
2389
2700
 
2390
2701
  // src/configs/recommended.ts
@@ -2394,27 +2705,35 @@ var recommended = {
2394
2705
  // Adoption-first default:
2395
2706
  // Keep only high-confidence, high-impact rules at error so first run is actionable,
2396
2707
  // not overwhelming. Context-sensitive rules are warn/off by design.
2397
- // Critical (error): low-noise, high-impact correctness/security failures.
2708
+ // ── Reliability ────────────────────────────────────────────────────────────
2709
+ // Critical (error): low-noise, high-impact correctness failures.
2398
2710
  "ai-guard/no-empty-catch": "error",
2399
- "ai-guard/no-floating-promise": "error",
2400
- "ai-guard/no-hardcoded-secret": "error",
2401
- "ai-guard/no-eval-dynamic": "error",
2402
- // Important but noisier/context-dependent (warn): useful guidance without blocking.
2711
+ // Important but context-dependent (warn).
2403
2712
  "ai-guard/no-broad-exception": "warn",
2404
- "ai-guard/require-auth-middleware": "warn",
2713
+ // Off in recommended — too noisy in mixed codebases.
2714
+ "ai-guard/no-catch-without-use": "off",
2715
+ "ai-guard/no-catch-log-rethrow": "off",
2716
+ // ── Async ──────────────────────────────────────────────────────────────────
2717
+ "ai-guard/no-floating-promise": "error",
2405
2718
  "ai-guard/no-await-in-loop": "warn",
2406
2719
  "ai-guard/no-async-without-await": "warn",
2407
- "ai-guard/no-sql-string-concat": "warn",
2408
- // Kept as warn in recommended to reduce false positives in mixed codebases.
2409
2720
  "ai-guard/no-async-array-callback": "warn",
2410
- // Optional/contextual (off): available for teams via strict/custom configs.
2411
- // Intentional for trust and gradual adoption.
2412
- "ai-guard/no-console-in-handler": "off",
2721
+ // Off in recommended — reduces noise in common patterns.
2413
2722
  "ai-guard/no-redundant-await": "off",
2414
- "ai-guard/no-catch-without-use": "off",
2723
+ // ── Security ───────────────────────────────────────────────────────────────
2724
+ // Critical (error): direct security vulnerabilities.
2725
+ "ai-guard/no-hardcoded-secret": "error",
2726
+ "ai-guard/no-eval-dynamic": "error",
2727
+ // Context-sensitive (warn): useful but require project context.
2728
+ "ai-guard/no-sql-string-concat": "warn",
2415
2729
  "ai-guard/no-unsafe-deserialize": "warn",
2416
- "ai-guard/no-catch-log-rethrow": "off",
2730
+ "ai-guard/require-auth-middleware": "warn",
2417
2731
  "ai-guard/require-authz-check": "warn",
2732
+ // ── AI Patterns ────────────────────────────────────────────────────────────
2733
+ // Warn: useful signal, but may fire in intentional dead-code removal flows.
2734
+ "ai-guard/no-dead-branch": "warn",
2735
+ // Off in recommended — contextual rules for teams opting into stricter checks.
2736
+ "ai-guard/no-console-in-handler": "off",
2418
2737
  "ai-guard/no-duplicate-logic-block": "off"
2419
2738
  }
2420
2739
  };
@@ -2444,9 +2763,10 @@ var strict = {
2444
2763
  "ai-guard/no-unsafe-deserialize": "error",
2445
2764
  "ai-guard/require-auth-middleware": "error",
2446
2765
  "ai-guard/require-authz-check": "error",
2447
- // Quality - all at error
2766
+ // AI Patterns - all at error
2448
2767
  "ai-guard/no-console-in-handler": "error",
2449
- "ai-guard/no-duplicate-logic-block": "error"
2768
+ "ai-guard/no-duplicate-logic-block": "error",
2769
+ "ai-guard/no-dead-branch": "error"
2450
2770
  }
2451
2771
  };
2452
2772
  var strict_default = strict;
@@ -2471,7 +2791,7 @@ var security_default = security;
2471
2791
  var plugin = {
2472
2792
  meta: {
2473
2793
  name: "eslint-plugin-ai-guard",
2474
- version: "1.1.13"
2794
+ version: "1.2.0"
2475
2795
  },
2476
2796
  rules: allRules,
2477
2797
  configs: {