Skip to content

Commit 91311a9

Browse files
committed
feat: implement all remaining CodeState CLI features
1 parent 1e094c3 commit 91311a9

7 files changed

Lines changed: 1243 additions & 20 deletions

File tree

src/advanced.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use std::path::PathBuf;
2+
use std::fs;
3+
use regex::Regex;
4+
use std::sync::OnceLock;
5+
use serde_json::json;
6+
7+
pub fn style_check(paths: &[PathBuf]) {
8+
println!("\n[--style-check] Checking for style issues...");
9+
let mut total_issues = 0;
10+
11+
for path in paths {
12+
if let Ok(content) = fs::read_to_string(path) {
13+
let mut issues = Vec::new();
14+
let lines: Vec<&str> = content.lines().collect();
15+
16+
for (i, line) in lines.iter().enumerate() {
17+
let line_num = i + 1;
18+
19+
// Check trailing whitespace
20+
if line.ends_with(' ') || line.ends_with('\t') {
21+
issues.push(format!("Line {}: Trailing whitespace", line_num));
22+
}
23+
24+
// Check line length > 100
25+
if line.chars().count() > 100 {
26+
issues.push(format!("Line {}: Line exceeds 100 characters", line_num));
27+
}
28+
}
29+
30+
// Check EOF newline missing
31+
if !content.is_empty() && !content.ends_with('\n') {
32+
issues.push("EOF: Missing newline at end of file".to_string());
33+
}
34+
35+
if !issues.is_empty() {
36+
println!(" {:?}:", path);
37+
for issue in issues {
38+
println!(" - {}", issue);
39+
total_issues += 1;
40+
}
41+
}
42+
}
43+
}
44+
45+
if total_issues == 0 {
46+
println!(" No style issues found!");
47+
} else {
48+
println!(" Found {} style issues in total.", total_issues);
49+
}
50+
}
51+
52+
static ROUTE_REGEX: OnceLock<Regex> = OnceLock::new();
53+
54+
pub fn generate_openapi(paths: &[PathBuf]) {
55+
println!("\n[--openapi] Generating OpenAPI skeleton from Python files...");
56+
57+
let route_re = ROUTE_REGEX.get_or_init(|| {
58+
Regex::new(r#"@app\.(get|post|put|delete|patch)\(["']([^"']+)["']\)"#).unwrap()
59+
});
60+
61+
let mut paths_obj = serde_json::Map::new();
62+
63+
for path in paths {
64+
if path.extension().and_then(|e| e.to_str()) == Some("py") {
65+
if let Ok(content) = fs::read_to_string(path) {
66+
for cap in route_re.captures_iter(&content) {
67+
let method = cap.get(1).unwrap().as_str().to_lowercase();
68+
let route = cap.get(2).unwrap().as_str();
69+
70+
let route_entry = paths_obj.entry(route.to_string()).or_insert(json!({}));
71+
if let Some(route_map) = route_entry.as_object_mut() {
72+
route_map.insert(method, json!({
73+
"summary": format!("Auto-generated {} route", route),
74+
"responses": {
75+
"200": {
76+
"description": "Successful response"
77+
}
78+
}
79+
}));
80+
}
81+
}
82+
}
83+
}
84+
}
85+
86+
let openapi_json = json!({
87+
"openapi": "3.0.0",
88+
"info": {
89+
"title": "Auto-generated API",
90+
"version": "1.0.0"
91+
},
92+
"paths": paths_obj
93+
});
94+
95+
println!("{}", serde_json::to_string_pretty(&openapi_json).unwrap());
96+
}
97+
98+
static COVERAGE_REGEX: OnceLock<Regex> = OnceLock::new();
99+
100+
pub fn parse_test_coverage(coverage_path: &str) {
101+
println!("\n[--test-coverage] Parsing coverage file: {}", coverage_path);
102+
103+
if let Ok(content) = fs::read_to_string(coverage_path) {
104+
let coverage_re = COVERAGE_REGEX.get_or_init(|| {
105+
Regex::new(r#"<coverage[^>]*line-rate="([^"]+)""#).unwrap()
106+
});
107+
108+
if let Some(cap) = coverage_re.captures(&content) {
109+
if let Ok(line_rate) = cap.get(1).unwrap().as_str().parse::<f64>() {
110+
println!(" Overall Line Coverage: {:.2}%", line_rate * 100.0);
111+
} else {
112+
println!(" Could not parse line-rate as a number.");
113+
}
114+
} else {
115+
println!(" Could not find line-rate attribute in <coverage> tag.");
116+
}
117+
} else {
118+
println!(" Failed to read coverage file: {}", coverage_path);
119+
}
120+
}

src/analyzer.rs

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,26 @@ use rayon::prelude::*;
22
use aho_corasick::{AhoCorasick, MatchKind};
33
use std::fs;
44
use std::path::{Path, PathBuf};
5+
use regex::Regex;
6+
use std::sync::OnceLock;
7+
8+
static FUNC_REGEX: OnceLock<Regex> = OnceLock::new();
9+
static CLASS_REGEX: OnceLock<Regex> = OnceLock::new();
510

611
pub struct AnalyzerStats {
712
pub path: PathBuf,
813
pub complexity: f64,
14+
#[allow(dead_code)]
915
pub todo_count: usize,
16+
#[allow(dead_code)]
1017
pub functions_count: usize,
18+
#[allow(dead_code)]
1119
pub naming_violations: usize,
20+
pub naming_violations_details: Vec<String>,
1221
}
1322

1423
// Simplified analyzer that scans concurrently using Aho-Corasick for extreme speed
15-
pub fn analyze_files(paths: &[PathBuf]) -> Vec<AnalyzerStats> {
24+
pub fn analyze_files(paths: &[PathBuf], check_naming: bool) -> Vec<AnalyzerStats> {
1625
let patterns = &[
1726
// Complexity (0..10)
1827
" if ", " for ", " while ", " case ", " catch ", " try ", " except ", "&&", "||", "?:",
@@ -30,15 +39,15 @@ pub fn analyze_files(paths: &[PathBuf]) -> Vec<AnalyzerStats> {
3039

3140
paths
3241
.into_par_iter()
33-
.filter_map(|p| analyze_file(p, &ac))
42+
.filter_map(|p| analyze_file(p, &ac, check_naming))
3443
.collect()
3544
}
3645

3746
fn is_word_character(b: u8) -> bool {
3847
b.is_ascii_alphanumeric() || b == b'_'
3948
}
4049

41-
fn analyze_file(path: &Path, ac: &AhoCorasick) -> Option<AnalyzerStats> {
50+
fn analyze_file(path: &Path, ac: &AhoCorasick, check_naming: bool) -> Option<AnalyzerStats> {
4251
let content = fs::read_to_string(path).ok()?;
4352

4453
let mut complexity = 0.0;
@@ -77,13 +86,118 @@ fn analyze_file(path: &Path, ac: &AhoCorasick) -> Option<AnalyzerStats> {
7786
}
7887
}
7988

80-
let naming_violations = 0;
89+
let mut naming_violations_details = Vec::new();
90+
91+
if check_naming {
92+
let func_re = FUNC_REGEX.get_or_init(|| Regex::new(r"(?:fn|def|function)\s+([a-zA-Z0-9_]+)").unwrap());
93+
let class_re = CLASS_REGEX.get_or_init(|| Regex::new(r"class\s+([a-zA-Z0-9_]+)").unwrap());
94+
95+
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
96+
let is_rust_or_py = ext == "rs" || ext == "py";
97+
98+
for cap in func_re.captures_iter(&content) {
99+
if let Some(m) = cap.get(1) {
100+
let name = m.as_str();
101+
if is_rust_or_py {
102+
// Check snake_case (lowercase, digits, underscores)
103+
if !name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_') {
104+
naming_violations_details.push(format!("Function '{}' in {:?} should be snake_case", name, path));
105+
}
106+
}
107+
}
108+
}
109+
110+
for cap in class_re.captures_iter(&content) {
111+
if let Some(m) = cap.get(1) {
112+
let name = m.as_str();
113+
// Check PascalCase (starts with uppercase)
114+
if !name.chars().next().map_or(false, |c| c.is_ascii_uppercase()) {
115+
naming_violations_details.push(format!("Class '{}' in {:?} should be PascalCase", name, path));
116+
}
117+
}
118+
}
119+
}
120+
121+
let naming_violations = naming_violations_details.len();
81122

82123
Some(AnalyzerStats {
83124
path: path.to_path_buf(),
84125
complexity,
85126
todo_count,
86127
functions_count,
87128
naming_violations,
129+
naming_violations_details,
88130
})
89131
}
132+
133+
pub fn find_deadcode(paths: &[PathBuf]) -> Vec<String> {
134+
let func_re = FUNC_REGEX.get_or_init(|| Regex::new(r"(?:fn|def|function)\s+([a-zA-Z0-9_]+)").unwrap());
135+
136+
// Pass 1: Extract all function names
137+
let mut all_functions: Vec<String> = paths.into_par_iter()
138+
.filter_map(|path| {
139+
if let Ok(content) = fs::read_to_string(path) {
140+
let mut local_funcs = Vec::new();
141+
for cap in func_re.captures_iter(&content) {
142+
if let Some(m) = cap.get(1) {
143+
local_funcs.push(m.as_str().to_string());
144+
}
145+
}
146+
Some(local_funcs)
147+
} else {
148+
None
149+
}
150+
})
151+
.flatten()
152+
.collect();
153+
154+
all_functions.sort();
155+
all_functions.dedup();
156+
157+
if all_functions.is_empty() {
158+
return Vec::new();
159+
}
160+
161+
// Pass 2: Count occurrences
162+
let ac = aho_corasick::AhoCorasickBuilder::new()
163+
.match_kind(MatchKind::Standard)
164+
.build(&all_functions)
165+
.unwrap();
166+
167+
let counts = paths.into_par_iter().map(|path| {
168+
let mut local_counts = vec![0usize; all_functions.len()];
169+
if let Ok(content) = fs::read_to_string(path) {
170+
let bytes = content.as_bytes();
171+
for mat in ac.find_iter(&content) {
172+
let start = mat.start();
173+
let end = mat.end();
174+
175+
let prev_ok = start == 0 || !is_word_character(bytes[start - 1]);
176+
let next_ok = end == bytes.len() || !is_word_character(bytes[end]);
177+
178+
if prev_ok && next_ok {
179+
local_counts[mat.pattern().as_usize()] += 1;
180+
}
181+
}
182+
}
183+
local_counts
184+
}).reduce(
185+
|| vec![0usize; all_functions.len()],
186+
|mut a, b| {
187+
for (i, v) in b.iter().enumerate() {
188+
a[i] += v;
189+
}
190+
a
191+
}
192+
);
193+
194+
let mut deadcode = Vec::new();
195+
for (i, count) in counts.iter().enumerate() {
196+
if *count == 1 {
197+
deadcode.push(all_functions[i].clone());
198+
}
199+
}
200+
201+
deadcode
202+
}
203+

0 commit comments

Comments
 (0)