Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/engine/diff_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ pub fn detect_language(path: &str) -> &'static str {
"html" | "htm" => "html",
"css" | "scss" | "sass" | "less" => "css",
"dart" => "dart",
"svelte" => "svelte",
"lua" => "lua",
"zig" => "zig",
"ex" | "exs" => "ex",
Expand Down
229 changes: 229 additions & 0 deletions src/engine/language_analyzer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
//! Language-specific analyzers — enriched prompts for LLM to understand language idioms.
//!
//! Not a static analyzer. Injects language-specific context into the review prompt
//! so the LLM understands language-specific patterns and anti-patterns.

use crate::engine::diff_parser;

/// Build language-specific context string from detected languages in the diff.
///
/// Detects which languages are present and injects relevant rules/patterns
/// into the LLM prompt.
pub fn build_language_context(diff_text: &str) -> String {
let chunks = diff_parser::parse_diff(diff_text);
let languages = detect_languages(&chunks);

if languages.is_empty() {
return String::new();
}

let mut parts = Vec::new();
parts.push("LANGUAGE-SPECIFIC REVIEW GUIDANCE:".to_string());

for lang in &languages {
if let Some(rules) = get_language_rules(lang) {
parts.push(rules.to_string());
}
}

parts.join("\n\n")
}

/// Detect unique languages present in diff chunks.
fn detect_languages(chunks: &[diff_parser::FileChunk]) -> Vec<String> {
let mut langs = std::collections::HashSet::new();
for chunk in chunks {
let path = chunk
.new_path
.as_deref()
.or(chunk.old_path.as_deref())
.unwrap_or("");
let lang = diff_parser::detect_language(path);
if lang != "unknown" && lang != "image" && lang != "document" && lang != "lock" {
langs.insert(lang.to_string());
}
}
let mut result: Vec<String> = langs.into_iter().collect();
result.sort();
result
}

/// Get language-specific review rules.
fn get_language_rules(lang: &str) -> Option<&'static str> {
match lang {
"dart" => Some(DART_RULES),
"svelte" => Some(SVELTE_RULES),
"ts" | "js" => Some(TYPESCRIPT_RULES),
"go" => Some(GO_RULES),
"rs" => Some(RUST_RULES),
"py" => Some(PYTHON_RULES),
_ => None,
}
}

// ─── Language-specific rules ───

const DART_RULES: &str = r#"## Dart/Flutter Review Focus
- **Widget lifecycle**: Check for `initState`/`dispose` mismatches, missing `super` calls
- **State management**: Flag direct `setState` in large widgets (prefer BLoC/Riverpod)
- **Async patterns**: Check for missing `await`, uncaught `Future` errors
- **Null safety**: Flag `!` operator misuse, prefer null-aware operators (`?.`, `??`)
- **Memory leaks**: Check for uncancelled timers, unclosed streams, missing `dispose`
- **Performance**: Flag `const` constructors that could be used, unnecessary rebuilds"#;

const SVELTE_RULES: &str = r#"## Svelte/SvelteKit Review Focus
- **Reactive declarations**: Check `$:` label correctness, dependency tracking issues
- **Store patterns**: Flag direct store mutation (use `$store` for reads, `store.set()`/`store.update()` for writes)
- **SSR considerations**: Check for browser-only APIs (`window`, `document`) in load functions
- **Lifecycle**: Check `onMount`/`onDestroy` cleanup, missing `onDestroy` for subscriptions
- **Type safety**: Flag `any` types in `.svelte` files, prefer typed props
- **Performance**: Flag unnecessary reactive statements, prefer `{#each}` with `key`"#;

const TYPESCRIPT_RULES: &str = r#"## TypeScript Review Focus
- **Type safety**: Flag `any` types, prefer generics and union types
- **Null handling**: Check for missing null checks, prefer optional chaining (`?.`)
- **Error handling**: Flag uncaught promises, prefer `try/catch` with proper typing
- **Async patterns**: Check for missing `await`, floating promises
- **Performance**: Flag unnecessary type assertions, prefer `const` assertions
- **Best practices**: Prefer interfaces over type aliases for objects, use `unknown` over `any`"#;

const GO_RULES: &str = r#"## Go Review Focus
- **Error handling**: Flag ignored errors (`_ = riskyCall()`), prefer explicit error check
- **Goroutine safety**: Check for race conditions, missing mutex, channel leaks
- **Resource cleanup**: Flag missing `defer Close()`, unclosed response bodies
- **Context propagation**: Check for missing `context.Context` in function signatures
- **Slice/map safety**: Flag concurrent access without synchronization
- **Error wrapping**: Prefer `fmt.Errorf("...: %w", err)` for error chains"#;

const RUST_RULES: &str = r#"## Rust Review Focus
- **Unsafe usage**: Flag `unsafe` blocks, prefer safe alternatives
- **Error handling**: Flag `unwrap()`/`expect()` in non-test code, prefer `?` operator
- **Lifetime issues**: Check for unnecessary lifetime annotations, lifetime elision
- **Ownership**: Flag unnecessary `clone()`, prefer borrowing
- **Concurrency**: Check for `Send`/`Sync` violations, deadlock patterns
- **Performance**: Flag `String` where `&str` suffices, unnecessary allocations"#;

const PYTHON_RULES: &str = r#"## Python Review Focus
- **Type hints**: Flag missing return type hints, `Any` overuse
- **Exception handling**: Flag bare `except:`, prefer specific exception types
- **Resource management**: Flag missing `with` statement for file/connection handling
- **Security**: Flag `pickle.loads` on untrusted data, `subprocess` with shell=True
- **Async patterns**: Check for missing `await`, blocking calls in async functions
- **Best practices**: Prefer `pathlib` over `os.path`, f-strings over `.format()`"#;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn detect_languages_from_diff() {
let diff = "\
diff --git a/src/main.rs b/src/main.rs
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,1 +1,1 @@
-fn old() {}
+fn new() {}
";
let langs = detect_languages(&diff_parser::parse_diff(diff));
assert!(langs.contains(&"rs".to_string()));
}

#[test]
fn detect_multiple_languages() {
let diff = "\
diff --git a/main.go b/main.go
--- a/main.go
+++ b/main.go
@@ -1,1 +1,1 @@
-old
+new
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -1,1 +1,1 @@
-old
+new
";
let langs = detect_languages(&diff_parser::parse_diff(diff));
assert!(langs.contains(&"go".to_string()));
assert!(langs.contains(&"py".to_string()));
assert_eq!(langs.len(), 2);
}

#[test]
fn build_context_not_empty_for_rs() {
let diff = "\
diff --git a/main.rs b/main.rs
--- a/main.rs
+++ b/main.rs
@@ -1,1 +1,1 @@
-old
+new
";
let ctx = build_language_context(diff);
assert!(ctx.contains("Rust"));
assert!(ctx.contains("unsafe"));
}

#[test]
fn build_context_for_dart() {
let diff = "\
diff --git a/lib/main.dart b/lib/main.dart
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,1 +1,1 @@
-old
+new
";
let ctx = build_language_context(diff);
assert!(ctx.contains("Flutter"));
}

#[test]
fn build_context_for_svelte() {
let diff = "\
diff --git a/src/App.svelte b/src/App.svelte
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -1,1 +1,1 @@
-old
+new
";
let ctx = build_language_context(diff);
assert!(ctx.contains("Svelte"));
}

#[test]
fn build_context_for_go() {
let diff = "\
diff --git a/main.go b/main.go
--- a/main.go
+++ b/main.go
@@ -1,1 +1,1 @@
-old
+new
";
let ctx = build_language_context(diff);
assert!(ctx.contains("Go"));
assert!(ctx.contains("Error handling"));
}

#[test]
fn empty_diff_no_context() {
let ctx = build_language_context("");
assert!(ctx.is_empty());
}

#[test]
fn all_rules_available() {
assert!(get_language_rules("dart").is_some());
assert!(get_language_rules("svelte").is_some());
assert!(get_language_rules("ts").is_some());
assert!(get_language_rules("js").is_some());
assert!(get_language_rules("go").is_some());
assert!(get_language_rules("rs").is_some());
assert!(get_language_rules("py").is_some());
assert!(get_language_rules("unknown").is_none());
}
}
1 change: 1 addition & 0 deletions src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod cache;
pub mod chunker;
pub mod context;
pub mod diff_parser;
pub mod language_analyzer;
pub mod llm;
pub mod profiles;
pub mod quality_gate;
Expand Down
11 changes: 11 additions & 0 deletions src/engine/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,17 @@ async fn review_diff_inner(
combined_context
};

// Inject language-specific context
let lang_context = crate::engine::language_analyzer::build_language_context(diff);
let final_context = if !lang_context.is_empty() {
match final_context {
Some(ctx) => Some(format!("{lang_context}\n\n{ctx}")),
None => Some(lang_context),
}
} else {
final_context
};

// Inject profile instructions into the context
let final_context = match (&config.profile, final_context) {
(Some(profile), Some(ctx)) => {
Expand Down
Loading