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.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -28,7 +38,7 @@ __export(index_exports, {
|
|
|
28
38
|
});
|
|
29
39
|
module.exports = __toCommonJS(index_exports);
|
|
30
40
|
|
|
31
|
-
// src/rules/
|
|
41
|
+
// src/rules/reliability/no-empty-catch.ts
|
|
32
42
|
var import_utils = require("@typescript-eslint/utils");
|
|
33
43
|
var createRule = import_utils.ESLintUtils.RuleCreator(
|
|
34
44
|
(name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
@@ -75,7 +85,7 @@ var noEmptyCatch = createRule({
|
|
|
75
85
|
}
|
|
76
86
|
});
|
|
77
87
|
|
|
78
|
-
// src/rules/
|
|
88
|
+
// src/rules/reliability/no-broad-exception.ts
|
|
79
89
|
var import_utils2 = require("@typescript-eslint/utils");
|
|
80
90
|
var createRule2 = import_utils2.ESLintUtils.RuleCreator(
|
|
81
91
|
(name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
@@ -152,7 +162,7 @@ function containsInstanceofCheck(node, paramName) {
|
|
|
152
162
|
return false;
|
|
153
163
|
}
|
|
154
164
|
|
|
155
|
-
// src/rules/
|
|
165
|
+
// src/rules/reliability/no-catch-log-rethrow.ts
|
|
156
166
|
var import_utils3 = require("@typescript-eslint/utils");
|
|
157
167
|
var createRule3 = import_utils3.ESLintUtils.RuleCreator(
|
|
158
168
|
(name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
@@ -214,7 +224,7 @@ var noCatchLogRethrow = createRule3({
|
|
|
214
224
|
}
|
|
215
225
|
});
|
|
216
226
|
|
|
217
|
-
// src/rules/
|
|
227
|
+
// src/rules/reliability/no-catch-without-use.ts
|
|
218
228
|
var import_utils4 = require("@typescript-eslint/utils");
|
|
219
229
|
var createRule4 = import_utils4.ESLintUtils.RuleCreator(
|
|
220
230
|
(name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
@@ -648,6 +658,7 @@ var noFloatingPromise = createRule6({
|
|
|
648
658
|
|
|
649
659
|
// src/rules/async/no-await-in-loop.ts
|
|
650
660
|
var import_utils7 = require("@typescript-eslint/utils");
|
|
661
|
+
var import_path = __toESM(require("path"));
|
|
651
662
|
var createRule7 = import_utils7.ESLintUtils.RuleCreator(
|
|
652
663
|
(name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
653
664
|
);
|
|
@@ -661,6 +672,7 @@ var LOOP_TYPES = /* @__PURE__ */ new Set([
|
|
|
661
672
|
var SUPPRESSION_REGEX = /ai-guard-disable\s+no-await-in-loop\b/i;
|
|
662
673
|
var RETRY_NAME_REGEX = /(retry|retries|attempt|attempts|fallback|tryagain|recovery)/i;
|
|
663
674
|
var SEQUENTIAL_DEPENDENCY_NAME_REGEX = /(previous|prev|last|carry|accumulator|stateful)/i;
|
|
675
|
+
var SIMULATION_NAME_REGEX = /(simulat|animat|demo|visuali|step|scene|frame|render|tick|sequence|tutorial|lesson|walk)/i;
|
|
664
676
|
var ERROR_CODE_HINTS = [
|
|
665
677
|
"access-denied",
|
|
666
678
|
"timeout",
|
|
@@ -675,6 +687,15 @@ var CONTROL_AWAIT_HINTS = [
|
|
|
675
687
|
"ratelimit",
|
|
676
688
|
"backoff"
|
|
677
689
|
];
|
|
690
|
+
var TIMER_FUNCTION_NAMES = /* @__PURE__ */ new Set([
|
|
691
|
+
"setinterval",
|
|
692
|
+
"settimeout",
|
|
693
|
+
"requestanimationframe",
|
|
694
|
+
"queuemicrotask",
|
|
695
|
+
"sleep",
|
|
696
|
+
"delay",
|
|
697
|
+
"wait"
|
|
698
|
+
]);
|
|
678
699
|
var MUTATION_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
679
700
|
"push",
|
|
680
701
|
"pop",
|
|
@@ -875,6 +896,9 @@ function analyzeIntent(loopNode) {
|
|
|
875
896
|
if (SEQUENTIAL_DEPENDENCY_NAME_REGEX.test(name)) {
|
|
876
897
|
hasSequentialDependency = true;
|
|
877
898
|
}
|
|
899
|
+
if (SIMULATION_NAME_REGEX.test(name)) {
|
|
900
|
+
hasSequentialDependency = true;
|
|
901
|
+
}
|
|
878
902
|
};
|
|
879
903
|
walkNode(
|
|
880
904
|
loopNode,
|
|
@@ -889,6 +913,9 @@ function analyzeIntent(loopNode) {
|
|
|
889
913
|
if (CONTROL_AWAIT_HINTS.some((hint) => lower.includes(hint))) {
|
|
890
914
|
hasControlAwait = true;
|
|
891
915
|
}
|
|
916
|
+
if (TIMER_FUNCTION_NAMES.has(lower)) {
|
|
917
|
+
hasControlAwait = true;
|
|
918
|
+
}
|
|
892
919
|
}
|
|
893
920
|
}
|
|
894
921
|
if (node.type === import_utils7.AST_NODE_TYPES.UpdateExpression) {
|
|
@@ -1025,16 +1052,43 @@ var noAwaitInLoop = createRule7({
|
|
|
1025
1052
|
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."
|
|
1026
1053
|
},
|
|
1027
1054
|
fixable: "code",
|
|
1028
|
-
schema: [
|
|
1055
|
+
schema: [
|
|
1056
|
+
{
|
|
1057
|
+
type: "object",
|
|
1058
|
+
properties: {
|
|
1059
|
+
allowPatterns: {
|
|
1060
|
+
type: "array",
|
|
1061
|
+
items: { type: "string" },
|
|
1062
|
+
description: "File glob patterns to skip (e.g., **/*.test.ts, **/simulation/**)"
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
additionalProperties: false
|
|
1066
|
+
}
|
|
1067
|
+
],
|
|
1029
1068
|
messages: {
|
|
1030
|
-
awaitInLoop: "
|
|
1069
|
+
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."
|
|
1031
1070
|
}
|
|
1032
1071
|
},
|
|
1033
|
-
defaultOptions: [],
|
|
1034
|
-
create(context) {
|
|
1072
|
+
defaultOptions: [{}],
|
|
1073
|
+
create(context, [options]) {
|
|
1035
1074
|
if (hasFileSuppression(context.sourceCode)) {
|
|
1036
1075
|
return {};
|
|
1037
1076
|
}
|
|
1077
|
+
const filename = context.filename ?? context.getFilename?.() ?? "";
|
|
1078
|
+
const fileBasename = import_path.default.basename(filename).toLowerCase();
|
|
1079
|
+
const filePath = filename.toLowerCase();
|
|
1080
|
+
const isTestFile = fileBasename.includes(".test.") || fileBasename.includes(".spec.") || filePath.includes("/__tests__/") || filePath.includes("/test/") || filePath.includes("/tests/");
|
|
1081
|
+
const isSimulationFile = SIMULATION_NAME_REGEX.test(fileBasename) || filePath.includes("/simulation/") || filePath.includes("/demo/") || filePath.includes("/animation/") || filePath.includes("/fixtures/");
|
|
1082
|
+
if (isTestFile || isSimulationFile) {
|
|
1083
|
+
return {};
|
|
1084
|
+
}
|
|
1085
|
+
const { allowPatterns = [] } = options;
|
|
1086
|
+
for (const pattern of allowPatterns) {
|
|
1087
|
+
const cleaned = pattern.replace(/\*\*/g, "").replace(/\*/g, "").replace(/\//g, import_path.default.sep);
|
|
1088
|
+
if (cleaned && filePath.includes(cleaned.toLowerCase())) {
|
|
1089
|
+
return {};
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1038
1092
|
const loopIntentCache = /* @__PURE__ */ new WeakMap();
|
|
1039
1093
|
const reportedLoops = /* @__PURE__ */ new WeakSet();
|
|
1040
1094
|
const functionBoundary = [];
|
|
@@ -1095,9 +1149,36 @@ var noAwaitInLoop = createRule7({
|
|
|
1095
1149
|
|
|
1096
1150
|
// src/rules/async/no-async-without-await.ts
|
|
1097
1151
|
var import_utils8 = require("@typescript-eslint/utils");
|
|
1152
|
+
var import_path2 = __toESM(require("path"));
|
|
1098
1153
|
var createRule8 = import_utils8.ESLintUtils.RuleCreator(
|
|
1099
1154
|
(name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
1100
1155
|
);
|
|
1156
|
+
var FRAMEWORK_FILE_PATTERNS = [
|
|
1157
|
+
// Next.js App Router
|
|
1158
|
+
/[/\\]route\.(ts|js|tsx|jsx)$/,
|
|
1159
|
+
/[/\\]middleware\.(ts|js)$/,
|
|
1160
|
+
/[/\\]layout\.(tsx|jsx|ts|js)$/,
|
|
1161
|
+
/[/\\]page\.(tsx|jsx|ts|js)$/,
|
|
1162
|
+
/[/\\]loading\.(tsx|jsx|ts|js)$/,
|
|
1163
|
+
/[/\\]error\.(tsx|jsx|ts|js)$/,
|
|
1164
|
+
/[/\\]not-found\.(tsx|jsx|ts|js)$/,
|
|
1165
|
+
/[/\\]template\.(tsx|jsx|ts|js)$/,
|
|
1166
|
+
// Next.js Pages Router conventions
|
|
1167
|
+
/[/\\]_app\.(tsx|jsx)$/,
|
|
1168
|
+
/[/\\]_document\.(tsx|jsx)$/
|
|
1169
|
+
];
|
|
1170
|
+
var HTTP_METHOD_EXPORTS = /* @__PURE__ */ new Set([
|
|
1171
|
+
"GET",
|
|
1172
|
+
"POST",
|
|
1173
|
+
"PUT",
|
|
1174
|
+
"PATCH",
|
|
1175
|
+
"DELETE",
|
|
1176
|
+
"HEAD",
|
|
1177
|
+
"OPTIONS"
|
|
1178
|
+
]);
|
|
1179
|
+
var REACT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
|
|
1180
|
+
var HANDLER_NAME_REGEX = /^(handle|on[A-Z])/;
|
|
1181
|
+
var MIDDLEWARE_NAME_REGEX = /^(use[A-Z]|middleware|interceptor|guard|beforeEach|afterEach)/;
|
|
1101
1182
|
function containsAwaitExpression(node) {
|
|
1102
1183
|
if (node.type === import_utils8.AST_NODE_TYPES.AwaitExpression) {
|
|
1103
1184
|
return true;
|
|
@@ -1131,21 +1212,101 @@ function containsAwaitExpression(node) {
|
|
|
1131
1212
|
}
|
|
1132
1213
|
return false;
|
|
1133
1214
|
}
|
|
1215
|
+
function isPassThroughWrapper(node) {
|
|
1216
|
+
if (node.body.type !== import_utils8.AST_NODE_TYPES.BlockStatement) {
|
|
1217
|
+
const body = node.body;
|
|
1218
|
+
if (body.type === import_utils8.AST_NODE_TYPES.CallExpression || body.type === import_utils8.AST_NODE_TYPES.MemberExpression) {
|
|
1219
|
+
return true;
|
|
1220
|
+
}
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
if (node.body.body.length !== 1) return false;
|
|
1224
|
+
const only = node.body.body[0];
|
|
1225
|
+
if (only.type !== import_utils8.AST_NODE_TYPES.ReturnStatement) return false;
|
|
1226
|
+
if (!only.argument) return false;
|
|
1227
|
+
return only.argument.type === import_utils8.AST_NODE_TYPES.CallExpression;
|
|
1228
|
+
}
|
|
1229
|
+
function isFunctionExportedWithHttpMethodName(node) {
|
|
1230
|
+
if (node.type === import_utils8.AST_NODE_TYPES.FunctionDeclaration && node.id) {
|
|
1231
|
+
return HTTP_METHOD_EXPORTS.has(node.id.name);
|
|
1232
|
+
}
|
|
1233
|
+
if (node.parent?.type === import_utils8.AST_NODE_TYPES.VariableDeclarator) {
|
|
1234
|
+
const declarator = node.parent;
|
|
1235
|
+
if (declarator.id.type === import_utils8.AST_NODE_TYPES.Identifier && HTTP_METHOD_EXPORTS.has(declarator.id.name)) {
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
function getFunctionName(node) {
|
|
1242
|
+
if (node.type === import_utils8.AST_NODE_TYPES.FunctionDeclaration && node.id) {
|
|
1243
|
+
return node.id.name;
|
|
1244
|
+
}
|
|
1245
|
+
if (node.parent?.type === import_utils8.AST_NODE_TYPES.VariableDeclarator) {
|
|
1246
|
+
const declarator = node.parent;
|
|
1247
|
+
if (declarator.id.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
1248
|
+
return declarator.id.name;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (node.parent?.type === import_utils8.AST_NODE_TYPES.Property || node.parent?.type === import_utils8.AST_NODE_TYPES.MethodDefinition) {
|
|
1252
|
+
const parent = node.parent;
|
|
1253
|
+
if ("key" in parent && parent.key.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
1254
|
+
return parent.key.name;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1134
1259
|
var noAsyncWithoutAwait = createRule8({
|
|
1135
1260
|
name: "no-async-without-await",
|
|
1136
1261
|
meta: {
|
|
1137
1262
|
type: "suggestion",
|
|
1138
1263
|
docs: {
|
|
1139
|
-
description: "Disallow async functions that never use await. AI tools frequently add async by default, creating misleading signatures and unnecessary Promise wrappers.
|
|
1264
|
+
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."
|
|
1140
1265
|
},
|
|
1141
1266
|
fixable: "code",
|
|
1142
|
-
schema: [
|
|
1267
|
+
schema: [
|
|
1268
|
+
{
|
|
1269
|
+
type: "object",
|
|
1270
|
+
properties: {
|
|
1271
|
+
allowedFunctionNames: {
|
|
1272
|
+
type: "array",
|
|
1273
|
+
items: { type: "string" },
|
|
1274
|
+
description: "Additional function names to skip (on top of HTTP methods)"
|
|
1275
|
+
},
|
|
1276
|
+
frameworkFiles: {
|
|
1277
|
+
type: "array",
|
|
1278
|
+
items: { type: "string" },
|
|
1279
|
+
description: "Additional file glob patterns where async-without-await is allowed"
|
|
1280
|
+
},
|
|
1281
|
+
ignorePassThroughWrappers: {
|
|
1282
|
+
type: "boolean",
|
|
1283
|
+
description: "Downgrade (not suppress) pass-through wrappers. Default: true."
|
|
1284
|
+
},
|
|
1285
|
+
ignoreHandlerFunctions: {
|
|
1286
|
+
type: "boolean",
|
|
1287
|
+
description: "Skip functions named handle* or on* in .tsx/.jsx files. Default: true."
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
additionalProperties: false
|
|
1291
|
+
}
|
|
1292
|
+
],
|
|
1143
1293
|
messages: {
|
|
1144
|
-
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."
|
|
1294
|
+
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.",
|
|
1295
|
+
asyncPassThrough: "Async pass-through wrapper \u2014 this may be intentional for Promise propagation. Consider whether `async` is needed here."
|
|
1145
1296
|
}
|
|
1146
1297
|
},
|
|
1147
|
-
defaultOptions: [],
|
|
1148
|
-
create(context) {
|
|
1298
|
+
defaultOptions: [{}],
|
|
1299
|
+
create(context, [options]) {
|
|
1300
|
+
const {
|
|
1301
|
+
allowedFunctionNames = [],
|
|
1302
|
+
ignorePassThroughWrappers = true,
|
|
1303
|
+
ignoreHandlerFunctions = true
|
|
1304
|
+
} = options;
|
|
1305
|
+
const skipNames = /* @__PURE__ */ new Set([...HTTP_METHOD_EXPORTS, ...allowedFunctionNames]);
|
|
1306
|
+
const filename = context.filename ?? context.getFilename?.() ?? "";
|
|
1307
|
+
const fileExt = import_path2.default.extname(filename).toLowerCase();
|
|
1308
|
+
const isFrameworkFile = FRAMEWORK_FILE_PATTERNS.some((pattern) => pattern.test(filename));
|
|
1309
|
+
const isReactFile = REACT_FILE_EXTENSIONS.has(fileExt);
|
|
1149
1310
|
function buildSafeAutofix2(node) {
|
|
1150
1311
|
const sourceCode = context.sourceCode;
|
|
1151
1312
|
if (node.body.type !== import_utils8.AST_NODE_TYPES.BlockStatement) {
|
|
@@ -1170,25 +1331,51 @@ var noAsyncWithoutAwait = createRule8({
|
|
|
1170
1331
|
if (!node.async) {
|
|
1171
1332
|
return;
|
|
1172
1333
|
}
|
|
1334
|
+
if (isFrameworkFile) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (isFunctionExportedWithHttpMethodName(node)) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const funcName = getFunctionName(node);
|
|
1341
|
+
if (funcName && skipNames.has(funcName)) {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (funcName && MIDDLEWARE_NAME_REGEX.test(funcName)) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (ignoreHandlerFunctions && isReactFile && funcName && HANDLER_NAME_REGEX.test(funcName)) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const bodyHasAwait = node.body.type === import_utils8.AST_NODE_TYPES.BlockStatement ? containsAwaitExpression(node.body) : node.body.type === import_utils8.AST_NODE_TYPES.AwaitExpression;
|
|
1351
|
+
if (bodyHasAwait) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (ignorePassThroughWrappers && isPassThroughWrapper(node)) {
|
|
1355
|
+
context.report({
|
|
1356
|
+
node,
|
|
1357
|
+
messageId: "asyncPassThrough"
|
|
1358
|
+
// No autofix for pass-through — it's intentional
|
|
1359
|
+
});
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1173
1362
|
if (node.body.type !== import_utils8.AST_NODE_TYPES.BlockStatement) {
|
|
1174
1363
|
if (node.body.type !== import_utils8.AST_NODE_TYPES.AwaitExpression) {
|
|
1175
|
-
const
|
|
1364
|
+
const fix2 = buildSafeAutofix2(node);
|
|
1176
1365
|
context.report({
|
|
1177
1366
|
node,
|
|
1178
1367
|
messageId: "asyncWithoutAwait",
|
|
1179
|
-
fix
|
|
1368
|
+
fix: fix2
|
|
1180
1369
|
});
|
|
1181
1370
|
}
|
|
1182
1371
|
return;
|
|
1183
1372
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1373
|
+
const fix = buildSafeAutofix2(node);
|
|
1374
|
+
context.report({
|
|
1375
|
+
node,
|
|
1376
|
+
messageId: "asyncWithoutAwait",
|
|
1377
|
+
fix
|
|
1378
|
+
});
|
|
1192
1379
|
}
|
|
1193
1380
|
return {
|
|
1194
1381
|
FunctionDeclaration: reportIfNeeded,
|
|
@@ -1464,10 +1651,20 @@ var noEvalDynamic = createRule11({
|
|
|
1464
1651
|
messageId: "evalDynamic",
|
|
1465
1652
|
data: { callee: `${node.callee.object.name}.eval()` }
|
|
1466
1653
|
});
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
if (isFunctionConstructorCallee(node.callee)) {
|
|
1657
|
+
if (node.arguments.length === 0) return;
|
|
1658
|
+
const allLiteral = node.arguments.every(isLiteral);
|
|
1659
|
+
if (allLiteral) return;
|
|
1660
|
+
context.report({
|
|
1661
|
+
node,
|
|
1662
|
+
messageId: "newFunctionDynamic"
|
|
1663
|
+
});
|
|
1467
1664
|
}
|
|
1468
1665
|
},
|
|
1469
1666
|
NewExpression(node) {
|
|
1470
|
-
if (node.callee
|
|
1667
|
+
if (isFunctionConstructorCallee(node.callee)) {
|
|
1471
1668
|
if (node.arguments.length === 0) return;
|
|
1472
1669
|
const allLiteral = node.arguments.every(isLiteral);
|
|
1473
1670
|
if (allLiteral) return;
|
|
@@ -1480,6 +1677,15 @@ var noEvalDynamic = createRule11({
|
|
|
1480
1677
|
};
|
|
1481
1678
|
}
|
|
1482
1679
|
});
|
|
1680
|
+
function isFunctionConstructorCallee(callee) {
|
|
1681
|
+
if (callee.type === import_utils11.AST_NODE_TYPES.Identifier && callee.name === "Function") {
|
|
1682
|
+
return true;
|
|
1683
|
+
}
|
|
1684
|
+
if (callee.type === import_utils11.AST_NODE_TYPES.MemberExpression && callee.property.type === import_utils11.AST_NODE_TYPES.Identifier && callee.property.name === "Function" && callee.object.type === import_utils11.AST_NODE_TYPES.Identifier && (callee.object.name === "window" || callee.object.name === "globalThis")) {
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1483
1689
|
function isLiteral(node) {
|
|
1484
1690
|
if (node.type === import_utils11.AST_NODE_TYPES.Literal) return true;
|
|
1485
1691
|
if (node.type === import_utils11.AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0) {
|
|
@@ -1897,6 +2103,8 @@ var PUBLIC_ROUTE_PATTERNS = [
|
|
|
1897
2103
|
// reset password
|
|
1898
2104
|
/^\/webhook/,
|
|
1899
2105
|
// webhooks
|
|
2106
|
+
/^\/callback/,
|
|
2107
|
+
// OAuth callback
|
|
1900
2108
|
/^\/public/,
|
|
1901
2109
|
// explicitly public
|
|
1902
2110
|
/^\/assets/,
|
|
@@ -1907,8 +2115,12 @@ var PUBLIC_ROUTE_PATTERNS = [
|
|
|
1907
2115
|
// favicon
|
|
1908
2116
|
/^\/robots/,
|
|
1909
2117
|
// robots.txt
|
|
1910
|
-
/^\/sitemap
|
|
2118
|
+
/^\/sitemap/,
|
|
1911
2119
|
// sitemap
|
|
2120
|
+
/^\/api\/docs/,
|
|
2121
|
+
// API documentation (Swagger, etc.)
|
|
2122
|
+
/^\/docs/
|
|
2123
|
+
// documentation
|
|
1912
2124
|
];
|
|
1913
2125
|
var requireAuthMiddleware = createRule14({
|
|
1914
2126
|
name: "require-auth-middleware",
|
|
@@ -2005,6 +2217,13 @@ function isAuthMiddleware(node, authNames) {
|
|
|
2005
2217
|
if (node.type === import_utils14.AST_NODE_TYPES.Identifier) {
|
|
2006
2218
|
return authNames.has(node.name);
|
|
2007
2219
|
}
|
|
2220
|
+
if (node.type === import_utils14.AST_NODE_TYPES.ObjectExpression) {
|
|
2221
|
+
for (const prop of node.properties) {
|
|
2222
|
+
if (prop.type === import_utils14.AST_NODE_TYPES.Property && prop.key.type === import_utils14.AST_NODE_TYPES.Identifier && (prop.key.name === "preHandler" || prop.key.name === "onRequest")) {
|
|
2223
|
+
if (isAuthMiddleware(prop.value, authNames)) return true;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2008
2227
|
if (node.type === import_utils14.AST_NODE_TYPES.CallExpression) {
|
|
2009
2228
|
if (node.callee.type === import_utils14.AST_NODE_TYPES.Identifier) {
|
|
2010
2229
|
return authNames.has(node.callee.name);
|
|
@@ -2090,29 +2309,29 @@ function getMemberPath(node) {
|
|
|
2090
2309
|
}
|
|
2091
2310
|
return [...objectPath, node.property.name];
|
|
2092
2311
|
}
|
|
2093
|
-
function hasPathPrefix(
|
|
2094
|
-
if (!
|
|
2312
|
+
function hasPathPrefix(path3, prefix) {
|
|
2313
|
+
if (!path3 || path3.length < prefix.length) {
|
|
2095
2314
|
return false;
|
|
2096
2315
|
}
|
|
2097
2316
|
for (let i = 0; i < prefix.length; i += 1) {
|
|
2098
|
-
if (
|
|
2317
|
+
if (path3[i] !== prefix[i]) {
|
|
2099
2318
|
return false;
|
|
2100
2319
|
}
|
|
2101
2320
|
}
|
|
2102
2321
|
return true;
|
|
2103
2322
|
}
|
|
2104
|
-
function isLikelyResourceIdPath(
|
|
2105
|
-
if (!
|
|
2323
|
+
function isLikelyResourceIdPath(path3) {
|
|
2324
|
+
if (!path3 || path3.length < 3) {
|
|
2106
2325
|
return false;
|
|
2107
2326
|
}
|
|
2108
|
-
if (!hasPathPrefix(
|
|
2327
|
+
if (!hasPathPrefix(path3, ["req", "params"]) && !hasPathPrefix(path3, ["req", "body"]) && !hasPathPrefix(path3, ["req", "query"])) {
|
|
2109
2328
|
return false;
|
|
2110
2329
|
}
|
|
2111
|
-
const last =
|
|
2330
|
+
const last = path3[path3.length - 1].toLowerCase();
|
|
2112
2331
|
return last === "id" || last.endsWith("id");
|
|
2113
2332
|
}
|
|
2114
|
-
function isReqUserPath(
|
|
2115
|
-
return hasPathPrefix(
|
|
2333
|
+
function isReqUserPath(path3) {
|
|
2334
|
+
return hasPathPrefix(path3, ["req", "user"]);
|
|
2116
2335
|
}
|
|
2117
2336
|
function containsAuthorizationHelper(node) {
|
|
2118
2337
|
if (node.type === import_utils15.AST_NODE_TYPES.CallExpression && (node.callee.type === import_utils15.AST_NODE_TYPES.Identifier && AUTHZ_HELPER_NAMES.includes(node.callee.name) || node.callee.type === import_utils15.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils15.AST_NODE_TYPES.Identifier && AUTHZ_HELPER_NAMES.includes(node.callee.property.name))) {
|
|
@@ -2151,8 +2370,8 @@ function collectBodySignals(node) {
|
|
|
2151
2370
|
}
|
|
2152
2371
|
}
|
|
2153
2372
|
if (current.type === import_utils15.AST_NODE_TYPES.MemberExpression) {
|
|
2154
|
-
const
|
|
2155
|
-
if (isLikelyResourceIdPath(
|
|
2373
|
+
const path3 = getMemberPath(current);
|
|
2374
|
+
if (isLikelyResourceIdPath(path3)) {
|
|
2156
2375
|
hasResourceIdAccess = true;
|
|
2157
2376
|
}
|
|
2158
2377
|
}
|
|
@@ -2221,7 +2440,7 @@ var requireAuthzCheck = createRule15({
|
|
|
2221
2440
|
}
|
|
2222
2441
|
});
|
|
2223
2442
|
|
|
2224
|
-
// src/rules/
|
|
2443
|
+
// src/rules/ai-patterns/no-console-in-handler.ts
|
|
2225
2444
|
var import_utils16 = require("@typescript-eslint/utils");
|
|
2226
2445
|
var createRule16 = import_utils16.ESLintUtils.RuleCreator(
|
|
2227
2446
|
(name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
@@ -2331,7 +2550,7 @@ var noConsoleInHandler = createRule16({
|
|
|
2331
2550
|
}
|
|
2332
2551
|
});
|
|
2333
2552
|
|
|
2334
|
-
// src/rules/
|
|
2553
|
+
// src/rules/ai-patterns/no-duplicate-logic-block.ts
|
|
2335
2554
|
var import_utils17 = require("@typescript-eslint/utils");
|
|
2336
2555
|
var createRule17 = import_utils17.ESLintUtils.RuleCreator(
|
|
2337
2556
|
(name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
@@ -2396,25 +2615,127 @@ var noDuplicateLogicBlock = createRule17({
|
|
|
2396
2615
|
}
|
|
2397
2616
|
});
|
|
2398
2617
|
|
|
2618
|
+
// src/rules/ai-patterns/no-dead-branch.ts
|
|
2619
|
+
var import_utils18 = require("@typescript-eslint/utils");
|
|
2620
|
+
var createRule18 = import_utils18.ESLintUtils.RuleCreator(
|
|
2621
|
+
(name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
2622
|
+
);
|
|
2623
|
+
function isBooleanLiteral(node, value) {
|
|
2624
|
+
return node.type === import_utils18.AST_NODE_TYPES.Literal && typeof node.value === "boolean" && node.value === value;
|
|
2625
|
+
}
|
|
2626
|
+
function isZeroLiteral(node) {
|
|
2627
|
+
return node.type === import_utils18.AST_NODE_TYPES.Literal && typeof node.value === "number" && node.value === 0;
|
|
2628
|
+
}
|
|
2629
|
+
function isEmptyStringLiteral(node) {
|
|
2630
|
+
return node.type === import_utils18.AST_NODE_TYPES.Literal && typeof node.value === "string" && node.value === "";
|
|
2631
|
+
}
|
|
2632
|
+
function isAlwaysTrue(node) {
|
|
2633
|
+
if (isBooleanLiteral(node, true)) return true;
|
|
2634
|
+
if (node.type === import_utils18.AST_NODE_TYPES.LogicalExpression && node.operator === "||") {
|
|
2635
|
+
const { left, right } = node;
|
|
2636
|
+
if (left.type === import_utils18.AST_NODE_TYPES.Identifier && right.type === import_utils18.AST_NODE_TYPES.UnaryExpression && right.operator === "!" && right.argument.type === import_utils18.AST_NODE_TYPES.Identifier && left.name === right.argument.name) {
|
|
2637
|
+
return true;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (node.type === import_utils18.AST_NODE_TYPES.BinaryExpression && (node.operator === "===" || node.operator === "==") && node.left.type === import_utils18.AST_NODE_TYPES.Identifier && node.right.type === import_utils18.AST_NODE_TYPES.Identifier && node.left.name === node.right.name) {
|
|
2641
|
+
return true;
|
|
2642
|
+
}
|
|
2643
|
+
return false;
|
|
2644
|
+
}
|
|
2645
|
+
function isAlwaysFalse(node) {
|
|
2646
|
+
if (isBooleanLiteral(node, false)) return true;
|
|
2647
|
+
if (isZeroLiteral(node)) return true;
|
|
2648
|
+
if (isEmptyStringLiteral(node)) return true;
|
|
2649
|
+
if (node.type === import_utils18.AST_NODE_TYPES.LogicalExpression && node.operator === "&&") {
|
|
2650
|
+
const { left, right } = node;
|
|
2651
|
+
if (left.type === import_utils18.AST_NODE_TYPES.Identifier && right.type === import_utils18.AST_NODE_TYPES.UnaryExpression && right.operator === "!" && right.argument.type === import_utils18.AST_NODE_TYPES.Identifier && left.name === right.argument.name) {
|
|
2652
|
+
return true;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
if (node.type === import_utils18.AST_NODE_TYPES.BinaryExpression && (node.operator === "!==" || node.operator === "!=") && node.left.type === import_utils18.AST_NODE_TYPES.Identifier && node.right.type === import_utils18.AST_NODE_TYPES.Identifier && node.left.name === node.right.name) {
|
|
2656
|
+
return true;
|
|
2657
|
+
}
|
|
2658
|
+
return false;
|
|
2659
|
+
}
|
|
2660
|
+
var noDeadBranch = createRule18({
|
|
2661
|
+
name: "no-dead-branch",
|
|
2662
|
+
meta: {
|
|
2663
|
+
type: "suggestion",
|
|
2664
|
+
docs: {
|
|
2665
|
+
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."
|
|
2666
|
+
},
|
|
2667
|
+
fixable: void 0,
|
|
2668
|
+
schema: [],
|
|
2669
|
+
messages: {
|
|
2670
|
+
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.",
|
|
2671
|
+
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.",
|
|
2672
|
+
whileFalse: "This `while (false)` loop never executes. AI tools sometimes generate this as a placeholder. Remove it or replace with the intended loop condition."
|
|
2673
|
+
}
|
|
2674
|
+
},
|
|
2675
|
+
defaultOptions: [],
|
|
2676
|
+
create(context) {
|
|
2677
|
+
function checkCondition(testNode, reportNode, isBranchStatement) {
|
|
2678
|
+
if (!testNode) return;
|
|
2679
|
+
if (isAlwaysFalse(testNode)) {
|
|
2680
|
+
context.report({
|
|
2681
|
+
node: reportNode,
|
|
2682
|
+
messageId: isBranchStatement ? "alwaysFalse" : "whileFalse"
|
|
2683
|
+
});
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (isAlwaysTrue(testNode)) {
|
|
2687
|
+
if (reportNode.type === import_utils18.AST_NODE_TYPES.WhileStatement) return;
|
|
2688
|
+
context.report({
|
|
2689
|
+
node: reportNode,
|
|
2690
|
+
messageId: "alwaysTrue"
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
return {
|
|
2695
|
+
IfStatement(node) {
|
|
2696
|
+
checkCondition(node.test, node, true);
|
|
2697
|
+
},
|
|
2698
|
+
WhileStatement(node) {
|
|
2699
|
+
if (isAlwaysFalse(node.test)) {
|
|
2700
|
+
context.report({ node, messageId: "whileFalse" });
|
|
2701
|
+
}
|
|
2702
|
+
},
|
|
2703
|
+
DoWhileStatement(node) {
|
|
2704
|
+
if (isAlwaysFalse(node.test)) {
|
|
2705
|
+
context.report({ node, messageId: "whileFalse" });
|
|
2706
|
+
}
|
|
2707
|
+
},
|
|
2708
|
+
ConditionalExpression(node) {
|
|
2709
|
+
checkCondition(node.test, node, true);
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
|
|
2399
2715
|
// src/rules/index.ts
|
|
2400
2716
|
var allRules = {
|
|
2717
|
+
// Reliability
|
|
2401
2718
|
"no-empty-catch": noEmptyCatch,
|
|
2402
2719
|
"no-broad-exception": noBroadException,
|
|
2403
2720
|
"no-catch-log-rethrow": noCatchLogRethrow,
|
|
2404
2721
|
"no-catch-without-use": noCatchWithoutUse,
|
|
2722
|
+
// Async
|
|
2405
2723
|
"no-async-array-callback": noAsyncArrayCallback,
|
|
2406
2724
|
"no-floating-promise": noFloatingPromise,
|
|
2407
2725
|
"no-await-in-loop": noAwaitInLoop,
|
|
2408
2726
|
"no-async-without-await": noAsyncWithoutAwait,
|
|
2409
2727
|
"no-redundant-await": noRedundantAwait,
|
|
2728
|
+
// Security
|
|
2410
2729
|
"no-hardcoded-secret": noHardcodedSecret,
|
|
2411
2730
|
"no-eval-dynamic": noEvalDynamic,
|
|
2412
2731
|
"no-sql-string-concat": noSqlStringConcat,
|
|
2413
2732
|
"no-unsafe-deserialize": noUnsafeDeserialize,
|
|
2414
2733
|
"require-auth-middleware": requireAuthMiddleware,
|
|
2415
2734
|
"require-authz-check": requireAuthzCheck,
|
|
2735
|
+
// AI Patterns
|
|
2416
2736
|
"no-console-in-handler": noConsoleInHandler,
|
|
2417
|
-
"no-duplicate-logic-block": noDuplicateLogicBlock
|
|
2737
|
+
"no-duplicate-logic-block": noDuplicateLogicBlock,
|
|
2738
|
+
"no-dead-branch": noDeadBranch
|
|
2418
2739
|
};
|
|
2419
2740
|
|
|
2420
2741
|
// src/configs/recommended.ts
|
|
@@ -2424,27 +2745,35 @@ var recommended = {
|
|
|
2424
2745
|
// Adoption-first default:
|
|
2425
2746
|
// Keep only high-confidence, high-impact rules at error so first run is actionable,
|
|
2426
2747
|
// not overwhelming. Context-sensitive rules are warn/off by design.
|
|
2427
|
-
//
|
|
2748
|
+
// ── Reliability ────────────────────────────────────────────────────────────
|
|
2749
|
+
// Critical (error): low-noise, high-impact correctness failures.
|
|
2428
2750
|
"ai-guard/no-empty-catch": "error",
|
|
2429
|
-
|
|
2430
|
-
"ai-guard/no-hardcoded-secret": "error",
|
|
2431
|
-
"ai-guard/no-eval-dynamic": "error",
|
|
2432
|
-
// Important but noisier/context-dependent (warn): useful guidance without blocking.
|
|
2751
|
+
// Important but context-dependent (warn).
|
|
2433
2752
|
"ai-guard/no-broad-exception": "warn",
|
|
2434
|
-
|
|
2753
|
+
// Off in recommended — too noisy in mixed codebases.
|
|
2754
|
+
"ai-guard/no-catch-without-use": "off",
|
|
2755
|
+
"ai-guard/no-catch-log-rethrow": "off",
|
|
2756
|
+
// ── Async ──────────────────────────────────────────────────────────────────
|
|
2757
|
+
"ai-guard/no-floating-promise": "error",
|
|
2435
2758
|
"ai-guard/no-await-in-loop": "warn",
|
|
2436
2759
|
"ai-guard/no-async-without-await": "warn",
|
|
2437
|
-
"ai-guard/no-sql-string-concat": "warn",
|
|
2438
|
-
// Kept as warn in recommended to reduce false positives in mixed codebases.
|
|
2439
2760
|
"ai-guard/no-async-array-callback": "warn",
|
|
2440
|
-
//
|
|
2441
|
-
// Intentional for trust and gradual adoption.
|
|
2442
|
-
"ai-guard/no-console-in-handler": "off",
|
|
2761
|
+
// Off in recommended — reduces noise in common patterns.
|
|
2443
2762
|
"ai-guard/no-redundant-await": "off",
|
|
2444
|
-
|
|
2763
|
+
// ── Security ───────────────────────────────────────────────────────────────
|
|
2764
|
+
// Critical (error): direct security vulnerabilities.
|
|
2765
|
+
"ai-guard/no-hardcoded-secret": "error",
|
|
2766
|
+
"ai-guard/no-eval-dynamic": "error",
|
|
2767
|
+
// Context-sensitive (warn): useful but require project context.
|
|
2768
|
+
"ai-guard/no-sql-string-concat": "warn",
|
|
2445
2769
|
"ai-guard/no-unsafe-deserialize": "warn",
|
|
2446
|
-
"ai-guard/
|
|
2770
|
+
"ai-guard/require-auth-middleware": "warn",
|
|
2447
2771
|
"ai-guard/require-authz-check": "warn",
|
|
2772
|
+
// ── AI Patterns ────────────────────────────────────────────────────────────
|
|
2773
|
+
// Warn: useful signal, but may fire in intentional dead-code removal flows.
|
|
2774
|
+
"ai-guard/no-dead-branch": "warn",
|
|
2775
|
+
// Off in recommended — contextual rules for teams opting into stricter checks.
|
|
2776
|
+
"ai-guard/no-console-in-handler": "off",
|
|
2448
2777
|
"ai-guard/no-duplicate-logic-block": "off"
|
|
2449
2778
|
}
|
|
2450
2779
|
};
|
|
@@ -2474,9 +2803,10 @@ var strict = {
|
|
|
2474
2803
|
"ai-guard/no-unsafe-deserialize": "error",
|
|
2475
2804
|
"ai-guard/require-auth-middleware": "error",
|
|
2476
2805
|
"ai-guard/require-authz-check": "error",
|
|
2477
|
-
//
|
|
2806
|
+
// AI Patterns - all at error
|
|
2478
2807
|
"ai-guard/no-console-in-handler": "error",
|
|
2479
|
-
"ai-guard/no-duplicate-logic-block": "error"
|
|
2808
|
+
"ai-guard/no-duplicate-logic-block": "error",
|
|
2809
|
+
"ai-guard/no-dead-branch": "error"
|
|
2480
2810
|
}
|
|
2481
2811
|
};
|
|
2482
2812
|
var strict_default = strict;
|
|
@@ -2501,7 +2831,7 @@ var security_default = security;
|
|
|
2501
2831
|
var plugin = {
|
|
2502
2832
|
meta: {
|
|
2503
2833
|
name: "eslint-plugin-ai-guard",
|
|
2504
|
-
version: "1.
|
|
2834
|
+
version: "1.2.0"
|
|
2505
2835
|
},
|
|
2506
2836
|
rules: allRules,
|
|
2507
2837
|
configs: {
|