Skip to content

Commit 336fe6e

Browse files
bumahkib7claude
andcommitted
fix(typestate): Add context-awareness to database rule
The database typestate rule now: 1. **Requires database context**: Only runs if file has database-related imports (mysql, pg, prisma, sequelize, mongoose, etc.) 2. **Skips API client calls**: Recognizes patterns like `cartApi.update()`, `userService.create()`, `httpClient.post()` as HTTP calls, not DB queries 3. **Variable naming detection**: Uses regex to detect camelCase API client patterns like `someApi.method()` or `someService.method()` This prevents false positives where frontend API client code was incorrectly flagged as "Query executed without establishing connection". Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 4232e83 commit 336fe6e

File tree

1 file changed

+91
-0
lines changed

1 file changed

+91
-0
lines changed

crates/analyzer/src/security/typestate_rules.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3310,6 +3310,86 @@ impl DatabaseTypestateRule {
33103310
sm.is_safe_pattern(content)
33113311
}
33123312

3313+
/// Check if file has database-related imports/requires
3314+
fn has_database_context(content: &str, language: Language) -> bool {
3315+
let db_indicators = match language {
3316+
Language::JavaScript | Language::TypeScript => &[
3317+
// Database drivers
3318+
"mysql", "mysql2", "pg", "postgres", "mongodb", "mongoose",
3319+
"sequelize", "prisma", "typeorm", "knex", "drizzle",
3320+
"better-sqlite3", "sql.js", "sqlite3",
3321+
// Database-specific imports
3322+
"PrismaClient", "MongoClient", "createConnection", "createPool",
3323+
// ORM indicators
3324+
"@prisma/client", "@nestjs/typeorm", "mikro-orm",
3325+
][..],
3326+
Language::Python => &[
3327+
"psycopg2", "pymysql", "mysql.connector", "sqlite3", "sqlalchemy",
3328+
"asyncpg", "databases", "tortoise", "peewee", "mongoengine",
3329+
"pymongo", "motor", "django.db", "flask_sqlalchemy",
3330+
][..],
3331+
Language::Go => &[
3332+
"database/sql", "gorm", "sqlx", "pgx", "mongo-driver",
3333+
"go-redis", "ent", "sql.Open", "gorm.Open",
3334+
][..],
3335+
Language::Java => &[
3336+
"java.sql", "javax.sql", "jdbc", "hibernate", "jpa",
3337+
"spring.data", "mybatis", "mongodb", "EntityManager",
3338+
][..],
3339+
Language::Rust => &[
3340+
"sqlx", "diesel", "sea-orm", "mongodb", "tokio-postgres",
3341+
"rusqlite", "postgres", "mysql_async",
3342+
][..],
3343+
_ => &[][..],
3344+
};
3345+
3346+
db_indicators.iter().any(|indicator| content.contains(indicator))
3347+
}
3348+
3349+
/// Check if line looks like an API client call (not a database call)
3350+
fn is_api_client_call(line: &str) -> bool {
3351+
// Patterns that indicate HTTP API clients, not database operations
3352+
let api_patterns = [
3353+
// Common API client naming patterns (case-insensitive check)
3354+
"api.", "Api.", "API.",
3355+
"service.", "Service.",
3356+
"client.", "Client.", // Only when followed by HTTP-like methods
3357+
"http.", "Http.", "HTTP.",
3358+
"axios.", "fetch(", "request.",
3359+
// React Query / TanStack patterns
3360+
"useMutation", "useQuery",
3361+
// Common API method patterns
3362+
".get(", ".post(", ".put(", ".patch(", ".delete(",
3363+
];
3364+
3365+
// Check for API client naming convention: variableApi.method() or variableService.method()
3366+
let trimmed = line.trim();
3367+
3368+
// Skip lines that are clearly API calls
3369+
if api_patterns.iter().any(|p| trimmed.contains(p)) {
3370+
return true;
3371+
}
3372+
3373+
// Check for camelCase API client patterns: someApi.update(), cartApi.update()
3374+
// Match pattern: word ending in Api/Service/Client followed by .method(
3375+
let api_var_pattern = regex::Regex::new(r"\b\w+(Api|Service|Client)\.(create|update|delete|get|post|put|patch|fetch|send|request)\(").unwrap();
3376+
if api_var_pattern.is_match(trimmed) {
3377+
return true;
3378+
}
3379+
3380+
// Check for await with API patterns
3381+
if trimmed.contains("await") && (
3382+
trimmed.contains("Api.") ||
3383+
trimmed.contains("Service.") ||
3384+
trimmed.contains("api.") ||
3385+
trimmed.contains("service.")
3386+
) {
3387+
return true;
3388+
}
3389+
3390+
false
3391+
}
3392+
33133393
/// Detect potential connection leak
33143394
fn check_connection_leak(
33153395
conn: &TrackedDbConnection,
@@ -3379,6 +3459,12 @@ impl Rule for DatabaseTypestateRule {
33793459
return Vec::new();
33803460
}
33813461

3462+
// Only run database checks if the file has database-related imports/code
3463+
// This prevents false positives on API client code
3464+
if !Self::has_database_context(&parsed.content, parsed.language) {
3465+
return Vec::new();
3466+
}
3467+
33823468
let sm = Self::state_machine(parsed.language);
33833469
let mut findings = Vec::new();
33843470
let mut connections: Vec<TrackedDbConnection> = Vec::new();
@@ -3389,6 +3475,11 @@ impl Rule for DatabaseTypestateRule {
33893475
for (line_num, line) in parsed.content.lines().enumerate() {
33903476
let line_num = line_num + 1;
33913477

3478+
// Skip lines that look like API client calls (not database operations)
3479+
if Self::is_api_client_call(line) {
3480+
continue;
3481+
}
3482+
33923483
// Check for safe patterns on this line
33933484
let line_has_safe = sm.is_safe_pattern(line);
33943485

0 commit comments

Comments
 (0)