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/README.md +337 -233
- package/dist/cli/index.js +1013 -104
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +23 -4
- package/dist/index.d.ts +23 -4
- package/dist/index.js +385 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +375 -55
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -3
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/rules/
|
|
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/
|
|
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/
|
|
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/
|
|
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: "
|
|
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.
|
|
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
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
|
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(
|
|
2064
|
-
if (!
|
|
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 (
|
|
2277
|
+
if (path3[i] !== prefix[i]) {
|
|
2069
2278
|
return false;
|
|
2070
2279
|
}
|
|
2071
2280
|
}
|
|
2072
2281
|
return true;
|
|
2073
2282
|
}
|
|
2074
|
-
function isLikelyResourceIdPath(
|
|
2075
|
-
if (!
|
|
2283
|
+
function isLikelyResourceIdPath(path3) {
|
|
2284
|
+
if (!path3 || path3.length < 3) {
|
|
2076
2285
|
return false;
|
|
2077
2286
|
}
|
|
2078
|
-
if (!hasPathPrefix(
|
|
2287
|
+
if (!hasPathPrefix(path3, ["req", "params"]) && !hasPathPrefix(path3, ["req", "body"]) && !hasPathPrefix(path3, ["req", "query"])) {
|
|
2079
2288
|
return false;
|
|
2080
2289
|
}
|
|
2081
|
-
const last =
|
|
2290
|
+
const last = path3[path3.length - 1].toLowerCase();
|
|
2082
2291
|
return last === "id" || last.endsWith("id");
|
|
2083
2292
|
}
|
|
2084
|
-
function isReqUserPath(
|
|
2085
|
-
return hasPathPrefix(
|
|
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
|
|
2125
|
-
if (isLikelyResourceIdPath(
|
|
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/
|
|
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/
|
|
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
|
-
//
|
|
2708
|
+
// ── Reliability ────────────────────────────────────────────────────────────
|
|
2709
|
+
// Critical (error): low-noise, high-impact correctness failures.
|
|
2398
2710
|
"ai-guard/no-empty-catch": "error",
|
|
2399
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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/
|
|
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
|
-
//
|
|
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.
|
|
2794
|
+
version: "1.2.0"
|
|
2475
2795
|
},
|
|
2476
2796
|
rules: allRules,
|
|
2477
2797
|
configs: {
|