codeguardian-mcp 1.3.6 → 1.3.8
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/api-contract/extractors/python.d.ts +10 -1
- package/dist/api-contract/extractors/python.d.ts.map +1 -1
- package/dist/api-contract/extractors/python.js +41 -13
- package/dist/api-contract/extractors/python.js.map +1 -1
- package/dist/api-contract/validators/index.d.ts.map +1 -1
- package/dist/api-contract/validators/index.js +190 -89
- package/dist/api-contract/validators/index.js.map +1 -1
- package/dist/context/apiContractContext.d.ts.map +1 -1
- package/dist/context/apiContractContext.js +86 -25
- package/dist/context/apiContractContext.js.map +1 -1
- package/dist/context/apiContractExtraction.d.ts +7 -1
- package/dist/context/apiContractExtraction.d.ts.map +1 -1
- package/dist/context/apiContractExtraction.js +58 -14
- package/dist/context/apiContractExtraction.js.map +1 -1
- package/dist/context/projectContext.js +7 -6
- package/dist/context/projectContext.js.map +1 -1
- package/dist/tools/validation/deadCode.d.ts.map +1 -1
- package/dist/tools/validation/deadCode.js +20 -0
- package/dist/tools/validation/deadCode.js.map +1 -1
- package/dist/tools/validation/extractors/python.d.ts.map +1 -1
- package/dist/tools/validation/extractors/python.js +179 -54
- package/dist/tools/validation/extractors/python.js.map +1 -1
- package/dist/tools/validation/validation.d.ts.map +1 -1
- package/dist/tools/validation/validation.js +454 -94
- package/dist/tools/validation/validation.js.map +1 -1
- package/package.json +2 -2
|
@@ -19,10 +19,10 @@ import * as path from "path";
|
|
|
19
19
|
import * as fsSync from "fs";
|
|
20
20
|
import { globSync } from "glob";
|
|
21
21
|
import { resolveImport } from "../../context/projectContext.js";
|
|
22
|
-
import { extractSymbolsAST, collectLocalDefinitionsAST } from "./extractors/index.js";
|
|
22
|
+
import { extractSymbolsAST, collectLocalDefinitionsAST, } from "./extractors/index.js";
|
|
23
23
|
import { parseCodeCached } from "./parser.js";
|
|
24
24
|
import { suggestSimilar, extractSimilarSymbols } from "./scoring.js";
|
|
25
|
-
import { isPythonSymbolExported, getPythonPipNameForImport } from "./manifest.js";
|
|
25
|
+
import { isPythonSymbolExported, getPythonPipNameForImport, } from "./manifest.js";
|
|
26
26
|
import { isJSBuiltin, isPythonBuiltin, isTSBuiltinType, NODE_BUILTIN_MODULES, } from "./builtins.js";
|
|
27
27
|
import { usagePatternAnalyzer } from "../../analyzers/usagePatterns.js";
|
|
28
28
|
import { isContextuallyValid, } from "./contextualNaming.js";
|
|
@@ -50,7 +50,12 @@ function findPrismaSchemaPathSync(projectPath) {
|
|
|
50
50
|
// (best-effort; guarded by caching so it won't run repeatedly).
|
|
51
51
|
try {
|
|
52
52
|
const matches = globSync(path.join(projectPath, "**/prisma/schema.prisma"), {
|
|
53
|
-
ignore: [
|
|
53
|
+
ignore: [
|
|
54
|
+
"**/node_modules/**",
|
|
55
|
+
"**/dist/**",
|
|
56
|
+
"**/build/**",
|
|
57
|
+
"**/.git/**",
|
|
58
|
+
],
|
|
54
59
|
nodir: true,
|
|
55
60
|
absolute: true,
|
|
56
61
|
});
|
|
@@ -124,8 +129,8 @@ export async function validateManifest(imports, manifest, newCode, language = "t
|
|
|
124
129
|
}
|
|
125
130
|
// Check if package is in manifest
|
|
126
131
|
if (!manifest.all.has(pkgName)) {
|
|
127
|
-
const scopedName = imp.module.startsWith("@")
|
|
128
|
-
imp.module.split("/").slice(0, 2).join("/")
|
|
132
|
+
const scopedName = imp.module.startsWith("@")
|
|
133
|
+
? imp.module.split("/").slice(0, 2).join("/")
|
|
129
134
|
: pkgName;
|
|
130
135
|
if (!manifest.all.has(scopedName)) {
|
|
131
136
|
// For Python, check if this import name is a known alias of a pip package
|
|
@@ -141,7 +146,7 @@ export async function validateManifest(imports, manifest, newCode, language = "t
|
|
|
141
146
|
if (unknownPackages.length === 0)
|
|
142
147
|
return issues;
|
|
143
148
|
// Phase 2: Batch registry lookups — deduplicate and check in parallel
|
|
144
|
-
const uniquePkgNames = [...new Set(unknownPackages.map(u => u.pkgName))];
|
|
149
|
+
const uniquePkgNames = [...new Set(unknownPackages.map((u) => u.pkgName))];
|
|
145
150
|
const REGISTRY_BATCH_SIZE = 10;
|
|
146
151
|
const registryResults = new Map();
|
|
147
152
|
for (let i = 0; i < uniquePkgNames.length; i += REGISTRY_BATCH_SIZE) {
|
|
@@ -487,8 +492,8 @@ typeReferences = []) {
|
|
|
487
492
|
for (const sym of newCodeSymbols) {
|
|
488
493
|
const projectSym = {
|
|
489
494
|
name: sym.name,
|
|
490
|
-
type: sym.type === "interface" || sym.type === "type"
|
|
491
|
-
"variable"
|
|
495
|
+
type: sym.type === "interface" || sym.type === "type"
|
|
496
|
+
? "variable"
|
|
492
497
|
: sym.type,
|
|
493
498
|
file: "(new code)",
|
|
494
499
|
params: sym.params,
|
|
@@ -606,7 +611,10 @@ typeReferences = []) {
|
|
|
606
611
|
file: resolvedFile || imp.module,
|
|
607
612
|
};
|
|
608
613
|
validVariables.set(name.local, namespaceSym);
|
|
609
|
-
validFunctions.set(name.local, {
|
|
614
|
+
validFunctions.set(name.local, {
|
|
615
|
+
...namespaceSym,
|
|
616
|
+
type: "function",
|
|
617
|
+
});
|
|
610
618
|
validClasses.set(name.local, { ...namespaceSym, type: "class" });
|
|
611
619
|
}
|
|
612
620
|
continue;
|
|
@@ -623,7 +631,8 @@ typeReferences = []) {
|
|
|
623
631
|
// Double check against all symbols in file marked as exported
|
|
624
632
|
// (sometimes exports are implicit in simple extractors)
|
|
625
633
|
// For Python: ALL module-level names are importable (no export keyword)
|
|
626
|
-
let symExport = fileInfo.symbols.find((s) => (language === "python" || s.exported) &&
|
|
634
|
+
let symExport = fileInfo.symbols.find((s) => (language === "python" || s.exported) &&
|
|
635
|
+
s.name === name.imported);
|
|
627
636
|
// For Python: check if the symbol is imported at module level in the target file.
|
|
628
637
|
// In Python, any name imported at module level is part of the module's namespace
|
|
629
638
|
// and is importable by other modules (e.g., `from app.api.cli_tokens import CLIToken`
|
|
@@ -633,7 +642,12 @@ typeReferences = []) {
|
|
|
633
642
|
modImp.defaultImport === name.imported);
|
|
634
643
|
if (isImportedInModule) {
|
|
635
644
|
// Treat as found — symbol is a valid Python namespace member via re-import
|
|
636
|
-
symExport = {
|
|
645
|
+
symExport = {
|
|
646
|
+
name: name.imported,
|
|
647
|
+
kind: "variable",
|
|
648
|
+
line: 0,
|
|
649
|
+
exported: true,
|
|
650
|
+
};
|
|
637
651
|
}
|
|
638
652
|
}
|
|
639
653
|
if (!symExport) {
|
|
@@ -645,7 +659,8 @@ typeReferences = []) {
|
|
|
645
659
|
: path.dirname(resolvedFile);
|
|
646
660
|
const subModPy = path.join(resolvedDir, `${name.imported}.py`);
|
|
647
661
|
const subModInit = path.join(resolvedDir, name.imported, "__init__.py");
|
|
648
|
-
if (context.files.has(subModPy) ||
|
|
662
|
+
if (context.files.has(subModPy) ||
|
|
663
|
+
context.files.has(subModInit)) {
|
|
649
664
|
// Valid sub-module import
|
|
650
665
|
continue;
|
|
651
666
|
}
|
|
@@ -759,7 +774,9 @@ typeReferences = []) {
|
|
|
759
774
|
if (language === "python" && projectSym) {
|
|
760
775
|
// First check if it's a sub-module file (sub-modules don't need __all__)
|
|
761
776
|
if (context) {
|
|
762
|
-
const moduleDir = imp.module
|
|
777
|
+
const moduleDir = imp.module
|
|
778
|
+
.replace(/^\.+/, "")
|
|
779
|
+
.replace(/\./g, "/");
|
|
763
780
|
const basePath = context.projectPath;
|
|
764
781
|
let subModFound = false;
|
|
765
782
|
const subModPy = path.join(basePath, moduleDir, `${name.imported}.py`);
|
|
@@ -821,7 +838,9 @@ typeReferences = []) {
|
|
|
821
838
|
// For Python: check if the imported name is a sub-module file
|
|
822
839
|
// e.g., "from app.api import auth" where app/api/auth.py exists
|
|
823
840
|
if (language === "python" && context) {
|
|
824
|
-
const modulePath = imp.module
|
|
841
|
+
const modulePath = imp.module
|
|
842
|
+
.replace(/^\.+/, "")
|
|
843
|
+
.replace(/\./g, "/");
|
|
825
844
|
const basePath = context.projectPath;
|
|
826
845
|
let resolvedSubMod = null;
|
|
827
846
|
const subModulePy = path.join(basePath, modulePath, `${name.imported}.py`);
|
|
@@ -946,7 +965,8 @@ typeReferences = []) {
|
|
|
946
965
|
const fn = valueNode.childForFieldName("function");
|
|
947
966
|
if (fn?.type === "member_expression") {
|
|
948
967
|
const prop = fn.childForFieldName("property");
|
|
949
|
-
if (prop &&
|
|
968
|
+
if (prop &&
|
|
969
|
+
newCode.slice(prop.startIndex, prop.endIndex) === "json") {
|
|
950
970
|
return true;
|
|
951
971
|
}
|
|
952
972
|
}
|
|
@@ -1045,8 +1065,8 @@ typeReferences = []) {
|
|
|
1045
1065
|
issues.push({
|
|
1046
1066
|
type: "nonExistentFunction",
|
|
1047
1067
|
severity: "critical",
|
|
1048
|
-
message: existsInProject
|
|
1049
|
-
`Function '${used.name}' exists in your project but is not imported in this file`
|
|
1068
|
+
message: existsInProject
|
|
1069
|
+
? `Function '${used.name}' exists in your project but is not imported in this file`
|
|
1050
1070
|
: `Function '${used.name}' does not exist in project`,
|
|
1051
1071
|
line: used.line,
|
|
1052
1072
|
file: filePath,
|
|
@@ -1071,9 +1091,7 @@ typeReferences = []) {
|
|
|
1071
1091
|
existsInProject: true,
|
|
1072
1092
|
strictMode,
|
|
1073
1093
|
});
|
|
1074
|
-
const expectedRange = min === max
|
|
1075
|
-
? `${max}`
|
|
1076
|
-
: `${min}-${max}`;
|
|
1094
|
+
const expectedRange = min === max ? `${max}` : `${min}-${max}`;
|
|
1077
1095
|
issues.push({
|
|
1078
1096
|
type: "wrongParamCount",
|
|
1079
1097
|
severity: "high",
|
|
@@ -1081,8 +1099,8 @@ typeReferences = []) {
|
|
|
1081
1099
|
line: used.line,
|
|
1082
1100
|
file: filePath,
|
|
1083
1101
|
code: used.code,
|
|
1084
|
-
suggestion: func.params
|
|
1085
|
-
`Expected params: ${func.params.join(", ")}`
|
|
1102
|
+
suggestion: func.params
|
|
1103
|
+
? `Expected params: ${func.params.join(", ")}`
|
|
1086
1104
|
: `Check the function signature in ${func.file}`,
|
|
1087
1105
|
confidence,
|
|
1088
1106
|
reasoning,
|
|
@@ -1108,7 +1126,7 @@ typeReferences = []) {
|
|
|
1108
1126
|
// Example: `${sop.code}-${sop.number}`.toLowerCase().includes(q)
|
|
1109
1127
|
// Returning a pseudo-root like `${sop` causes false undefinedVariable findings.
|
|
1110
1128
|
if (trimmedObj.startsWith("`") ||
|
|
1111
|
-
trimmedObj.startsWith("
|
|
1129
|
+
trimmedObj.startsWith('"') ||
|
|
1112
1130
|
trimmedObj.startsWith("'")) {
|
|
1113
1131
|
return "";
|
|
1114
1132
|
}
|
|
@@ -1139,7 +1157,9 @@ typeReferences = []) {
|
|
|
1139
1157
|
// Not a type assertion — check if it's an arithmetic/logical/nullish expression
|
|
1140
1158
|
// e.g., (i + 1), (value / 1000), (milestones || []), (budgetUsedPercentage || 0)
|
|
1141
1159
|
// These are NOT object references — skip method validation entirely.
|
|
1142
|
-
if (/[+\-*/%|&<>=!~^]/.test(innerExpr) ||
|
|
1160
|
+
if (/[+\-*/%|&<>=!~^]/.test(innerExpr) ||
|
|
1161
|
+
/\s\|\|\s/.test(innerExpr) ||
|
|
1162
|
+
/\s\?\?\s/.test(innerExpr)) {
|
|
1143
1163
|
return ""; // Not a valid object reference
|
|
1144
1164
|
}
|
|
1145
1165
|
// Otherwise extract from the inner expression (e.g., (myObj).method())
|
|
@@ -1177,15 +1197,20 @@ typeReferences = []) {
|
|
|
1177
1197
|
if (!first || first.toUpperCase() === first)
|
|
1178
1198
|
return "";
|
|
1179
1199
|
const candidate = first.toUpperCase() + rootObject.slice(1);
|
|
1180
|
-
return projectClasses.has(candidate) || hasContextClass(candidate)
|
|
1200
|
+
return projectClasses.has(candidate) || hasContextClass(candidate)
|
|
1201
|
+
? candidate
|
|
1202
|
+
: "";
|
|
1181
1203
|
})();
|
|
1182
1204
|
const scopesToMatch = new Set([used.object, rootObject, inferredClassName].filter(Boolean));
|
|
1183
1205
|
// CRITICAL FIX: Skip ALL 'this'/'self'/'cls' method calls - we can't validate class scope
|
|
1184
1206
|
// The 'this' keyword (JS/TS) and 'self'/'cls' (Python) are always in scope within a class context
|
|
1185
1207
|
// TypeScript/ESLint/mypy handle class method validation, not CodeGuardian
|
|
1186
|
-
if (used.object === "this" ||
|
|
1187
|
-
used.object
|
|
1188
|
-
used.object === "
|
|
1208
|
+
if (used.object === "this" ||
|
|
1209
|
+
used.object?.startsWith("this.") ||
|
|
1210
|
+
used.object === "self" ||
|
|
1211
|
+
used.object?.startsWith("self.") ||
|
|
1212
|
+
used.object === "cls" ||
|
|
1213
|
+
used.object?.startsWith("cls.")) {
|
|
1189
1214
|
continue; // Trust class scope - this is not a hallucination risk
|
|
1190
1215
|
}
|
|
1191
1216
|
// Skip other whitelisted global objects and common patterns
|
|
@@ -1315,9 +1340,7 @@ typeReferences = []) {
|
|
|
1315
1340
|
// that `<model>` exists as a Prisma delegate derived from schema.prisma.
|
|
1316
1341
|
// This catches hallucinations like `prisma.ghostItems.findMany()`.
|
|
1317
1342
|
if (rootObject === "prisma" && typeof used.object === "string") {
|
|
1318
|
-
const expr = used.object
|
|
1319
|
-
.replace(/\s+/g, "")
|
|
1320
|
-
.replace(/\?\./g, ".");
|
|
1343
|
+
const expr = used.object.replace(/\s+/g, "").replace(/\?\./g, ".");
|
|
1321
1344
|
// Only handle the delegate form. Client-level methods like `prisma.$transaction()`
|
|
1322
1345
|
// have used.object === "prisma" and should not be checked here.
|
|
1323
1346
|
const parts = expr.split(".").filter(Boolean);
|
|
@@ -1373,9 +1396,18 @@ typeReferences = []) {
|
|
|
1373
1396
|
// NOT for generic whitelisted objects (db, prisma, etc.) where `as any` may
|
|
1374
1397
|
// be a legitimate workaround for missing type definitions.
|
|
1375
1398
|
const STEALTH_HALLUCINATION_GLOBALS = new Set([
|
|
1376
|
-
"window",
|
|
1377
|
-
"
|
|
1378
|
-
"
|
|
1399
|
+
"window",
|
|
1400
|
+
"navigator",
|
|
1401
|
+
"document",
|
|
1402
|
+
"location",
|
|
1403
|
+
"history",
|
|
1404
|
+
"localStorage",
|
|
1405
|
+
"sessionStorage",
|
|
1406
|
+
"console",
|
|
1407
|
+
"process",
|
|
1408
|
+
"global",
|
|
1409
|
+
"globalThis",
|
|
1410
|
+
"Intl",
|
|
1379
1411
|
]);
|
|
1380
1412
|
if (STEALTH_HALLUCINATION_GLOBALS.has(rootObject)) {
|
|
1381
1413
|
const codeLine = used.code || "";
|
|
@@ -1403,7 +1435,8 @@ typeReferences = []) {
|
|
|
1403
1435
|
// Check for @ts-ignore or @ts-expect-error which indicates intentional bypass
|
|
1404
1436
|
const codeLines = newCode.split("\n");
|
|
1405
1437
|
const prevLine = codeLines[used.line - 2]; // line is 1-indexed
|
|
1406
|
-
if (prevLine?.includes("@ts-ignore") ||
|
|
1438
|
+
if (prevLine?.includes("@ts-ignore") ||
|
|
1439
|
+
prevLine?.includes("@ts-expect-error")) {
|
|
1407
1440
|
issues.push({
|
|
1408
1441
|
type: "nonExistentMethod",
|
|
1409
1442
|
severity: "high",
|
|
@@ -1422,17 +1455,42 @@ typeReferences = []) {
|
|
|
1422
1455
|
// - prisma.pantryItem.create() - 'pantryItem' is a model name
|
|
1423
1456
|
// We should validate that the METHOD (findMany, create, etc.) is a known ORM method
|
|
1424
1457
|
// and flag hallucinated methods like hallucinateMissingFunction()
|
|
1425
|
-
const ORM_OBJECTS = new Set([
|
|
1458
|
+
const ORM_OBJECTS = new Set([
|
|
1459
|
+
"prisma",
|
|
1460
|
+
"db",
|
|
1461
|
+
"entityManager",
|
|
1462
|
+
"repository",
|
|
1463
|
+
"queryBuilder",
|
|
1464
|
+
]);
|
|
1426
1465
|
if (ORM_OBJECTS.has(rootObject)) {
|
|
1427
1466
|
// Known Prisma methods
|
|
1428
1467
|
const KNOWN_PRISMA_METHODS = new Set([
|
|
1429
1468
|
// Model-level methods (called on prisma.model)
|
|
1430
|
-
"findMany",
|
|
1431
|
-
"
|
|
1432
|
-
"
|
|
1469
|
+
"findMany",
|
|
1470
|
+
"findUnique",
|
|
1471
|
+
"findFirst",
|
|
1472
|
+
"findFirstOrThrow",
|
|
1473
|
+
"findUniqueOrThrow",
|
|
1474
|
+
"create",
|
|
1475
|
+
"createMany",
|
|
1476
|
+
"update",
|
|
1477
|
+
"updateMany",
|
|
1478
|
+
"upsert",
|
|
1479
|
+
"delete",
|
|
1480
|
+
"deleteMany",
|
|
1481
|
+
"count",
|
|
1482
|
+
"aggregate",
|
|
1483
|
+
"groupBy",
|
|
1433
1484
|
// Client-level methods (called on prisma)
|
|
1434
|
-
"$connect",
|
|
1435
|
-
"$
|
|
1485
|
+
"$connect",
|
|
1486
|
+
"$disconnect",
|
|
1487
|
+
"$transaction",
|
|
1488
|
+
"$queryRaw",
|
|
1489
|
+
"$executeRaw",
|
|
1490
|
+
"$use",
|
|
1491
|
+
"$on",
|
|
1492
|
+
"$extends",
|
|
1493
|
+
"$extends",
|
|
1436
1494
|
]);
|
|
1437
1495
|
// Check if this is a model-level call (prisma.modelName.method())
|
|
1438
1496
|
// or a client-level call (prisma.$method())
|
|
@@ -1443,7 +1501,8 @@ typeReferences = []) {
|
|
|
1443
1501
|
// Check for @ts-ignore or @ts-expect-error which indicates intentional bypass
|
|
1444
1502
|
const codeLines = newCode.split("\n");
|
|
1445
1503
|
const prevLine = codeLines[used.line - 2]; // line is 1-indexed
|
|
1446
|
-
if (prevLine?.includes("@ts-ignore") ||
|
|
1504
|
+
if (prevLine?.includes("@ts-ignore") ||
|
|
1505
|
+
prevLine?.includes("@ts-expect-error")) {
|
|
1447
1506
|
issues.push({
|
|
1448
1507
|
type: "nonExistentMethod",
|
|
1449
1508
|
severity: "high",
|
|
@@ -1482,7 +1541,8 @@ typeReferences = []) {
|
|
|
1482
1541
|
// Check if the object is a locally-defined variable (function params, assignments, loop vars, etc.)
|
|
1483
1542
|
localDefinitions.has(rootObject) ||
|
|
1484
1543
|
// Python: Check if rootObject is the base of a dotted import (e.g., `import concurrent.futures` → `concurrent`)
|
|
1485
|
-
(language === "python" &&
|
|
1544
|
+
(language === "python" &&
|
|
1545
|
+
imports.some((imp) => imp.names.some((n) => n.local.startsWith(rootObject + ".")))) ||
|
|
1486
1546
|
// Always trust common short variable names in non-strict mode
|
|
1487
1547
|
(!strictMode &&
|
|
1488
1548
|
[
|
|
@@ -1630,7 +1690,8 @@ typeReferences = []) {
|
|
|
1630
1690
|
line: used.line,
|
|
1631
1691
|
file: filePath,
|
|
1632
1692
|
code: used.code,
|
|
1633
|
-
suggestion: suggestion ||
|
|
1693
|
+
suggestion: suggestion ||
|
|
1694
|
+
"Use a real React API (e.g., useState, useEffect, memo, forwardRef).",
|
|
1634
1695
|
confidence: 86,
|
|
1635
1696
|
reasoning: `The object '${objectName}' is imported from 'react'. React's public API is stable; '${used.name}' is not in a conservative allowlist of common React namespace methods/hooks.`,
|
|
1636
1697
|
});
|
|
@@ -1647,7 +1708,9 @@ typeReferences = []) {
|
|
|
1647
1708
|
else if (!imp.isExternal) {
|
|
1648
1709
|
// For internal imports, check if we have CLASS info
|
|
1649
1710
|
const objClass = projectClasses.get(objectName) ||
|
|
1650
|
-
(inferredClassName
|
|
1711
|
+
(inferredClassName
|
|
1712
|
+
? projectClasses.get(inferredClassName)
|
|
1713
|
+
: undefined);
|
|
1651
1714
|
if (objClass || inferredClassName) {
|
|
1652
1715
|
shouldCheck = true; // We have class info, so we can validate methods
|
|
1653
1716
|
}
|
|
@@ -1683,8 +1746,11 @@ typeReferences = []) {
|
|
|
1683
1746
|
// ONLY check if we have class info, otherwise we don't know the type enough to flag it
|
|
1684
1747
|
// Use projectClasses (actual class definitions), not validClasses (which includes all imports)
|
|
1685
1748
|
const objClass = projectClasses.get(objectName) ||
|
|
1686
|
-
(inferredClassName
|
|
1687
|
-
|
|
1749
|
+
(inferredClassName
|
|
1750
|
+
? projectClasses.get(inferredClassName)
|
|
1751
|
+
: undefined);
|
|
1752
|
+
if ((objClass || inferredClassName) &&
|
|
1753
|
+
objClass?.file !== "(new code)") {
|
|
1688
1754
|
shouldCheck = true;
|
|
1689
1755
|
}
|
|
1690
1756
|
// Local literal inference: method calls on obvious built-in types (e.g., array literals)
|
|
@@ -1731,30 +1797,260 @@ typeReferences = []) {
|
|
|
1731
1797
|
// and won't appear in the project symbol table
|
|
1732
1798
|
const FRAMEWORK_METHODS = new Set([
|
|
1733
1799
|
// Pydantic BaseModel methods (v1 + v2)
|
|
1734
|
-
"model_validate",
|
|
1735
|
-
"
|
|
1736
|
-
"
|
|
1737
|
-
"
|
|
1738
|
-
"
|
|
1739
|
-
"
|
|
1740
|
-
|
|
1741
|
-
"
|
|
1742
|
-
"
|
|
1743
|
-
"
|
|
1744
|
-
"
|
|
1745
|
-
"
|
|
1800
|
+
"model_validate",
|
|
1801
|
+
"model_dump",
|
|
1802
|
+
"model_json_schema",
|
|
1803
|
+
"model_copy",
|
|
1804
|
+
"model_validate_json",
|
|
1805
|
+
"model_dump_json",
|
|
1806
|
+
"model_fields_set",
|
|
1807
|
+
"model_construct",
|
|
1808
|
+
"model_post_init",
|
|
1809
|
+
"model_rebuild",
|
|
1810
|
+
"dict",
|
|
1811
|
+
"json",
|
|
1812
|
+
"parse_obj",
|
|
1813
|
+
"parse_raw",
|
|
1814
|
+
"parse_file",
|
|
1815
|
+
"from_orm",
|
|
1816
|
+
"schema",
|
|
1817
|
+
"schema_json",
|
|
1818
|
+
"validate",
|
|
1819
|
+
"update_forward_refs",
|
|
1820
|
+
"copy",
|
|
1821
|
+
"construct",
|
|
1822
|
+
// SQLAlchemy Model/Query methods (legacy 1.x style)
|
|
1823
|
+
"query",
|
|
1824
|
+
"filter",
|
|
1825
|
+
"filter_by",
|
|
1826
|
+
"all",
|
|
1827
|
+
"first",
|
|
1828
|
+
"one",
|
|
1829
|
+
"one_or_none",
|
|
1830
|
+
"get",
|
|
1831
|
+
"count",
|
|
1832
|
+
"delete",
|
|
1833
|
+
"update",
|
|
1834
|
+
"order_by",
|
|
1835
|
+
"limit",
|
|
1836
|
+
"offset",
|
|
1837
|
+
"join",
|
|
1838
|
+
"outerjoin",
|
|
1839
|
+
"group_by",
|
|
1840
|
+
"having",
|
|
1841
|
+
"distinct",
|
|
1842
|
+
"subquery",
|
|
1843
|
+
"scalar",
|
|
1844
|
+
"scalars",
|
|
1845
|
+
"execute",
|
|
1846
|
+
"add",
|
|
1847
|
+
"flush",
|
|
1848
|
+
"commit",
|
|
1849
|
+
"rollback",
|
|
1850
|
+
"refresh",
|
|
1851
|
+
"expire",
|
|
1852
|
+
"expunge",
|
|
1853
|
+
"merge",
|
|
1854
|
+
"close",
|
|
1855
|
+
// SQLAlchemy 2.0 Core / Select / Result methods
|
|
1856
|
+
// These are called on select(...) / session.execute(...) results and are
|
|
1857
|
+
// extremely common in async FastAPI/SQLAlchemy 2.0 codebases.
|
|
1858
|
+
"where",
|
|
1859
|
+
"select_from",
|
|
1860
|
+
"scalar_one_or_none",
|
|
1861
|
+
"scalar_one",
|
|
1862
|
+
"fetchall",
|
|
1863
|
+
"fetchone",
|
|
1864
|
+
"fetchmany",
|
|
1865
|
+
"mappings",
|
|
1866
|
+
"partitions",
|
|
1867
|
+
"unique",
|
|
1868
|
+
"tuples",
|
|
1869
|
+
"columns",
|
|
1870
|
+
"returning",
|
|
1871
|
+
"with_for_update",
|
|
1872
|
+
"correlate",
|
|
1873
|
+
"correlate_except",
|
|
1874
|
+
"execution_options",
|
|
1875
|
+
"options",
|
|
1876
|
+
"populate_existing",
|
|
1877
|
+
"yield_per",
|
|
1878
|
+
// SQLAlchemy event system (e.g., @event.listens_for(Model, 'after_insert'))
|
|
1879
|
+
"listens_for",
|
|
1880
|
+
"listen",
|
|
1881
|
+
"remove",
|
|
1746
1882
|
// SQLAlchemy Column expression methods (called on Model.column attributes)
|
|
1747
|
-
"desc",
|
|
1748
|
-
"
|
|
1749
|
-
"
|
|
1750
|
-
"
|
|
1751
|
-
"
|
|
1883
|
+
"desc",
|
|
1884
|
+
"asc",
|
|
1885
|
+
"in_",
|
|
1886
|
+
"notin_",
|
|
1887
|
+
"not_in",
|
|
1888
|
+
"isnot",
|
|
1889
|
+
"is_",
|
|
1890
|
+
"is_not",
|
|
1891
|
+
"like",
|
|
1892
|
+
"ilike",
|
|
1893
|
+
"not_like",
|
|
1894
|
+
"not_ilike",
|
|
1895
|
+
"contains",
|
|
1896
|
+
"startswith",
|
|
1897
|
+
"endswith",
|
|
1898
|
+
"between",
|
|
1899
|
+
"any_",
|
|
1900
|
+
"has",
|
|
1901
|
+
"label",
|
|
1902
|
+
"cast",
|
|
1903
|
+
"op",
|
|
1904
|
+
"collate",
|
|
1905
|
+
"nullsfirst",
|
|
1906
|
+
"nullslast",
|
|
1907
|
+
"regexp_match",
|
|
1908
|
+
"regexp_replace",
|
|
1909
|
+
"concat",
|
|
1910
|
+
"nulls_first",
|
|
1911
|
+
"nulls_last",
|
|
1752
1912
|
// Django ORM methods
|
|
1753
|
-
"objects",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1756
|
-
"
|
|
1757
|
-
"
|
|
1913
|
+
"objects",
|
|
1914
|
+
"create",
|
|
1915
|
+
"get_or_create",
|
|
1916
|
+
"update_or_create",
|
|
1917
|
+
"bulk_create",
|
|
1918
|
+
"bulk_update",
|
|
1919
|
+
"values",
|
|
1920
|
+
"values_list",
|
|
1921
|
+
"annotate",
|
|
1922
|
+
"aggregate",
|
|
1923
|
+
"exists",
|
|
1924
|
+
"exclude",
|
|
1925
|
+
"select_related",
|
|
1926
|
+
"prefetch_related",
|
|
1927
|
+
"defer",
|
|
1928
|
+
"only",
|
|
1929
|
+
"using",
|
|
1930
|
+
"raw",
|
|
1931
|
+
"save",
|
|
1932
|
+
"full_clean",
|
|
1933
|
+
"clean",
|
|
1934
|
+
"clean_fields",
|
|
1935
|
+
// Python stdlib – datetime / date / time
|
|
1936
|
+
// These are classmethods or instance methods on datetime/date/timedelta objects.
|
|
1937
|
+
// They are imported directly (from datetime import datetime) and their
|
|
1938
|
+
// type is not in the project symbol table, so the method check fires falsely.
|
|
1939
|
+
"now",
|
|
1940
|
+
"utcnow",
|
|
1941
|
+
"today",
|
|
1942
|
+
"fromisoformat",
|
|
1943
|
+
"fromtimestamp",
|
|
1944
|
+
"utcfromtimestamp",
|
|
1945
|
+
"fromordinal",
|
|
1946
|
+
"combine",
|
|
1947
|
+
"strptime",
|
|
1948
|
+
"strftime",
|
|
1949
|
+
"isoformat",
|
|
1950
|
+
"timetuple",
|
|
1951
|
+
"toordinal",
|
|
1952
|
+
"weekday",
|
|
1953
|
+
"isoweekday",
|
|
1954
|
+
"isocalendar",
|
|
1955
|
+
"ctime",
|
|
1956
|
+
"timestamp",
|
|
1957
|
+
"utctimetuple",
|
|
1958
|
+
"astimezone",
|
|
1959
|
+
"dst",
|
|
1960
|
+
"tzname",
|
|
1961
|
+
"utcoffset",
|
|
1962
|
+
// Python stdlib – timedelta
|
|
1963
|
+
"total_seconds",
|
|
1964
|
+
// Python stdlib – pathlib.Path / os.path operations
|
|
1965
|
+
"resolve",
|
|
1966
|
+
"absolute",
|
|
1967
|
+
"expanduser",
|
|
1968
|
+
"iterdir",
|
|
1969
|
+
"glob",
|
|
1970
|
+
"rglob",
|
|
1971
|
+
"mkdir",
|
|
1972
|
+
"rmdir",
|
|
1973
|
+
"unlink",
|
|
1974
|
+
"rename",
|
|
1975
|
+
"stat",
|
|
1976
|
+
"lstat",
|
|
1977
|
+
"open",
|
|
1978
|
+
"read_text",
|
|
1979
|
+
"write_text",
|
|
1980
|
+
"read_bytes",
|
|
1981
|
+
"write_bytes",
|
|
1982
|
+
"is_file",
|
|
1983
|
+
"is_dir",
|
|
1984
|
+
"is_symlink",
|
|
1985
|
+
"exists",
|
|
1986
|
+
"samefile",
|
|
1987
|
+
"with_name",
|
|
1988
|
+
"with_suffix",
|
|
1989
|
+
"with_stem",
|
|
1990
|
+
"relative_to",
|
|
1991
|
+
// Python stdlib – itertools / functools
|
|
1992
|
+
"group", // itertools.groupby group object
|
|
1993
|
+
"chain",
|
|
1994
|
+
// Jinja2 Template rendering
|
|
1995
|
+
"render",
|
|
1996
|
+
"render_async",
|
|
1997
|
+
"generate",
|
|
1998
|
+
"stream",
|
|
1999
|
+
"make_module",
|
|
2000
|
+
"get_template",
|
|
2001
|
+
"select_template",
|
|
2002
|
+
"get_or_select_template",
|
|
2003
|
+
// Prometheus / metrics clients (prometheus-client, opentelemetry-sdk)
|
|
2004
|
+
"inc",
|
|
2005
|
+
"dec",
|
|
2006
|
+
"observe",
|
|
2007
|
+
"set_to_current_time",
|
|
2008
|
+
"labels",
|
|
2009
|
+
"remove",
|
|
2010
|
+
// OpenTelemetry instrumentation methods
|
|
2011
|
+
"instrument",
|
|
2012
|
+
"uninstrument",
|
|
2013
|
+
"start_span",
|
|
2014
|
+
"use_span",
|
|
2015
|
+
"set_attribute",
|
|
2016
|
+
"add_event",
|
|
2017
|
+
"record_exception",
|
|
2018
|
+
"set_status",
|
|
2019
|
+
// Cloudinary / file storage SDK methods
|
|
2020
|
+
"upload",
|
|
2021
|
+
"destroy",
|
|
2022
|
+
"rename",
|
|
2023
|
+
"explicit",
|
|
2024
|
+
"generate_url",
|
|
2025
|
+
"add_tag",
|
|
2026
|
+
"remove_tag",
|
|
2027
|
+
"replace_tag",
|
|
2028
|
+
"remove_all_tags",
|
|
2029
|
+
// NATS / message broker methods
|
|
2030
|
+
"connect",
|
|
2031
|
+
"subscribe",
|
|
2032
|
+
"publish",
|
|
2033
|
+
"unsubscribe",
|
|
2034
|
+
"drain",
|
|
2035
|
+
"flush",
|
|
2036
|
+
// Alembic migration helpers
|
|
2037
|
+
"op",
|
|
2038
|
+
"batch_alter_table",
|
|
2039
|
+
"add_column",
|
|
2040
|
+
"drop_column",
|
|
2041
|
+
"create_table",
|
|
2042
|
+
"drop_table",
|
|
2043
|
+
"create_index",
|
|
2044
|
+
"drop_index",
|
|
2045
|
+
"alter_column",
|
|
2046
|
+
"bulk_insert",
|
|
2047
|
+
// Authlib / OAuth methods
|
|
2048
|
+
"authorize_redirect",
|
|
2049
|
+
"authorize_access_token",
|
|
2050
|
+
"parse_id_token",
|
|
2051
|
+
"userinfo",
|
|
2052
|
+
"revoke_token",
|
|
2053
|
+
"introspect_token",
|
|
1758
2054
|
]);
|
|
1759
2055
|
if (FRAMEWORK_METHODS.has(used.name))
|
|
1760
2056
|
continue;
|
|
@@ -1766,25 +2062,80 @@ typeReferences = []) {
|
|
|
1766
2062
|
if (language === "python") {
|
|
1767
2063
|
const PYTHON_DYNAMIC_METHODS = new Set([
|
|
1768
2064
|
// HTTP client methods (httpx, requests, aiohttp, etc.)
|
|
1769
|
-
"post",
|
|
2065
|
+
"post",
|
|
2066
|
+
"put",
|
|
2067
|
+
"patch",
|
|
2068
|
+
"request",
|
|
2069
|
+
"head",
|
|
2070
|
+
"options",
|
|
1770
2071
|
// Python string methods called on model attribute chains (e.g., user.email.split())
|
|
1771
|
-
"split",
|
|
1772
|
-
"
|
|
2072
|
+
"split",
|
|
2073
|
+
"strip",
|
|
2074
|
+
"lstrip",
|
|
2075
|
+
"rstrip",
|
|
2076
|
+
"lower",
|
|
2077
|
+
"upper",
|
|
2078
|
+
"replace",
|
|
2079
|
+
"encode",
|
|
2080
|
+
"decode",
|
|
2081
|
+
"format",
|
|
2082
|
+
"capitalize",
|
|
1773
2083
|
// Storage/external client methods (Supabase, S3, GCS, etc.)
|
|
1774
|
-
"from_",
|
|
1775
|
-
"
|
|
2084
|
+
"from_",
|
|
2085
|
+
"upload",
|
|
2086
|
+
"download",
|
|
2087
|
+
"create_signed_url",
|
|
2088
|
+
"get_public_url",
|
|
2089
|
+
"get_bucket",
|
|
2090
|
+
"create_bucket",
|
|
2091
|
+
"remove",
|
|
2092
|
+
"list",
|
|
1776
2093
|
// Redis client methods
|
|
1777
|
-
"setex",
|
|
1778
|
-
"
|
|
1779
|
-
"
|
|
1780
|
-
"
|
|
2094
|
+
"setex",
|
|
2095
|
+
"setnx",
|
|
2096
|
+
"getex",
|
|
2097
|
+
"hset",
|
|
2098
|
+
"hget",
|
|
2099
|
+
"hdel",
|
|
2100
|
+
"hgetall",
|
|
2101
|
+
"lpush",
|
|
2102
|
+
"rpush",
|
|
2103
|
+
"lpop",
|
|
2104
|
+
"rpop",
|
|
2105
|
+
"lrange",
|
|
2106
|
+
"sadd",
|
|
2107
|
+
"srem",
|
|
2108
|
+
"smembers",
|
|
2109
|
+
"zadd",
|
|
2110
|
+
"zrem",
|
|
2111
|
+
"zrange",
|
|
2112
|
+
"zrangebyscore",
|
|
2113
|
+
"expire",
|
|
2114
|
+
"ttl",
|
|
2115
|
+
"pttl",
|
|
2116
|
+
"publish",
|
|
2117
|
+
"subscribe",
|
|
2118
|
+
"unsubscribe",
|
|
2119
|
+
"pipeline",
|
|
1781
2120
|
// Webhook/integration client methods
|
|
1782
|
-
"create_webhook",
|
|
1783
|
-
"
|
|
2121
|
+
"create_webhook",
|
|
2122
|
+
"delete_webhook",
|
|
2123
|
+
"grant_access",
|
|
2124
|
+
"revoke_access",
|
|
2125
|
+
"get_commit_details",
|
|
2126
|
+
"get_commits",
|
|
2127
|
+
"get_branches",
|
|
1784
2128
|
// Task/job object attribute access
|
|
1785
|
-
"func",
|
|
2129
|
+
"func",
|
|
2130
|
+
"args",
|
|
2131
|
+
"kwargs",
|
|
2132
|
+
"result",
|
|
2133
|
+
"status",
|
|
1786
2134
|
// Celery task methods
|
|
1787
|
-
"delay",
|
|
2135
|
+
"delay",
|
|
2136
|
+
"apply_async",
|
|
2137
|
+
"retry",
|
|
2138
|
+
"revoke",
|
|
1788
2139
|
]);
|
|
1789
2140
|
if (PYTHON_DYNAMIC_METHODS.has(used.name))
|
|
1790
2141
|
continue;
|
|
@@ -1866,8 +2217,8 @@ typeReferences = []) {
|
|
|
1866
2217
|
issues.push({
|
|
1867
2218
|
type: "nonExistentClass",
|
|
1868
2219
|
severity: "critical",
|
|
1869
|
-
message: existsInProject
|
|
1870
|
-
`Class '${used.name}' exists in your project but is not imported in this file`
|
|
2220
|
+
message: existsInProject
|
|
2221
|
+
? `Class '${used.name}' exists in your project but is not imported in this file`
|
|
1871
2222
|
: `Class '${used.name}' does not exist in project`,
|
|
1872
2223
|
line: used.line,
|
|
1873
2224
|
file: filePath,
|
|
@@ -1881,9 +2232,12 @@ typeReferences = []) {
|
|
|
1881
2232
|
else if (used.type === "reference") {
|
|
1882
2233
|
// CRITICAL FIX: Skip property access on 'this'/'self'/'cls' (e.g., this.ws, self.data, cls._client)
|
|
1883
2234
|
// These are class properties and should not be validated as standalone variables
|
|
1884
|
-
if (used.object === "this" ||
|
|
1885
|
-
used.object
|
|
1886
|
-
used.object === "
|
|
2235
|
+
if (used.object === "this" ||
|
|
2236
|
+
used.object?.startsWith("this.") ||
|
|
2237
|
+
used.object === "self" ||
|
|
2238
|
+
used.object?.startsWith("self.") ||
|
|
2239
|
+
used.object === "cls" ||
|
|
2240
|
+
used.object?.startsWith("cls.")) {
|
|
1887
2241
|
continue; // Trust class scope - properties are validated by TypeScript/mypy
|
|
1888
2242
|
}
|
|
1889
2243
|
const func = validFunctions.get(used.name);
|
|
@@ -1965,8 +2319,8 @@ typeReferences = []) {
|
|
|
1965
2319
|
issues.push({
|
|
1966
2320
|
type: "undefinedVariable",
|
|
1967
2321
|
severity: "critical",
|
|
1968
|
-
message: existsInProject
|
|
1969
|
-
`Variable '${used.name}' exists in your project but is not imported in this file`
|
|
2322
|
+
message: existsInProject
|
|
2323
|
+
? `Variable '${used.name}' exists in your project but is not imported in this file`
|
|
1970
2324
|
: `Variable '${used.name}' is not defined or imported`,
|
|
1971
2325
|
line: used.line,
|
|
1972
2326
|
file: filePath,
|
|
@@ -1996,8 +2350,14 @@ typeReferences = []) {
|
|
|
1996
2350
|
let currentModuleAllExports = null;
|
|
1997
2351
|
if (language === "python" && filePath && pythonExports.size > 0) {
|
|
1998
2352
|
const basePath = context?.projectPath || "";
|
|
1999
|
-
const relPath = filePath.startsWith(basePath)
|
|
2000
|
-
|
|
2353
|
+
const relPath = filePath.startsWith(basePath)
|
|
2354
|
+
? filePath.slice(basePath.length + 1)
|
|
2355
|
+
: filePath;
|
|
2356
|
+
const pyModPath = relPath
|
|
2357
|
+
.replace(/__init__\.py$/, "")
|
|
2358
|
+
.replace(/\.py$/, "")
|
|
2359
|
+
.replace(/\//g, ".")
|
|
2360
|
+
.replace(/\.$/, "");
|
|
2001
2361
|
currentModuleAllExports = pythonExports.get(pyModPath) || null;
|
|
2002
2362
|
}
|
|
2003
2363
|
// For Python: Build a set of symbol names that appear in the code text as word boundaries
|
|
@@ -2083,10 +2443,10 @@ typeReferences = []) {
|
|
|
2083
2443
|
continue; // Only check type-only imports
|
|
2084
2444
|
for (const name of imp.names) {
|
|
2085
2445
|
// Check if this type-imported symbol is used as a value (not just in type positions)
|
|
2086
|
-
const usages = usedSymbols.filter(u => u.name === name.local);
|
|
2087
|
-
const typeUsages = typeReferences.filter(t => t.name === name.local);
|
|
2446
|
+
const usages = usedSymbols.filter((u) => u.name === name.local);
|
|
2447
|
+
const typeUsages = typeReferences.filter((t) => t.name === name.local);
|
|
2088
2448
|
// If used in runtime contexts (call, instantiation, etc.) but imported as type
|
|
2089
|
-
const runtimeUsages = usages.filter(u => u.type === "call" ||
|
|
2449
|
+
const runtimeUsages = usages.filter((u) => u.type === "call" ||
|
|
2090
2450
|
u.type === "instantiation" ||
|
|
2091
2451
|
u.type === "methodCall");
|
|
2092
2452
|
if (runtimeUsages.length > 0) {
|