helix_core/compiler/tools/
lint.rs

1use std::path::PathBuf;
2use std::fs;
3use anyhow::{Result, Context};
4pub fn lint_files(files: Vec<PathBuf>, verbose: bool) -> Result<()> {
5    if files.is_empty() {
6        lint_project(verbose)
7    } else {
8        lint_specific_files(files, verbose)
9    }
10}
11fn lint_project(verbose: bool) -> Result<()> {
12    let project_dir = find_project_root()?;
13    if verbose {
14        println!("šŸ” Linting HELIX project:");
15        println!("  Project: {}", project_dir.display());
16    }
17    let mut helix_files = Vec::new();
18    find_helix_files(&project_dir, &mut helix_files)?;
19    if helix_files.is_empty() {
20        println!("ā„¹ļø  No HELIX files found to lint.");
21        return Ok(());
22    }
23    println!("šŸ“‹ Found {} HELIX files to lint", helix_files.len());
24    let mut total_issues = 0;
25    let mut files_with_issues = 0;
26    for file in helix_files {
27        match lint_single_file(&file, verbose) {
28            Ok(issues) => {
29                if !issues.is_empty() {
30                    files_with_issues += 1;
31                    total_issues += issues.len();
32                    if !verbose {
33                        println!("āŒ {} issues in {}", issues.len(), file.display());
34                    }
35                } else if verbose {
36                    println!("āœ… No issues in {}", file.display());
37                }
38            }
39            Err(e) => {
40                eprintln!("āŒ Failed to lint {}: {}", file.display(), e);
41            }
42        }
43    }
44    if total_issues == 0 {
45        println!("āœ… No linting issues found!");
46    } else {
47        println!("\nšŸ“Š Linting Results:");
48        println!("  Total issues: {}", total_issues);
49        println!("  Files with issues: {}", files_with_issues);
50        std::process::exit(1);
51    }
52    Ok(())
53}
54fn lint_specific_files(files: Vec<PathBuf>, verbose: bool) -> Result<()> {
55    if verbose {
56        println!("šŸ” Linting specific files:");
57        println!("  Files: {}", files.len());
58    }
59    let mut total_issues = 0;
60    let mut files_with_issues = 0;
61    for file in files {
62        if !file.exists() {
63            eprintln!("āŒ File not found: {}", file.display());
64            continue;
65        }
66        if !file.extension().map_or(false, |ext| ext == "hlx") {
67            eprintln!("āš ļø  Skipping non-HELIX file: {}", file.display());
68            continue;
69        }
70        match lint_single_file(&file, verbose) {
71            Ok(issues) => {
72                if !issues.is_empty() {
73                    files_with_issues += 1;
74                    total_issues += issues.len();
75                    if !verbose {
76                        println!("āŒ {} issues in {}", issues.len(), file.display());
77                    }
78                } else if verbose {
79                    println!("āœ… No issues in {}", file.display());
80                }
81            }
82            Err(e) => {
83                eprintln!("āŒ Failed to lint {}: {}", file.display(), e);
84            }
85        }
86    }
87    if total_issues == 0 {
88        println!("āœ… No linting issues found!");
89    } else {
90        println!("\nšŸ“Š Linting Results:");
91        println!("  Total issues: {}", total_issues);
92        println!("  Files with issues: {}", files_with_issues);
93        std::process::exit(1);
94    }
95    Ok(())
96}
97#[derive(Debug)]
98struct LintIssue {
99    line: usize,
100    column: usize,
101    severity: LintSeverity,
102    message: String,
103    rule: String,
104}
105#[derive(Debug)]
106enum LintSeverity {
107    Error,
108    Warning,
109    #[allow(dead_code)]
110    Info,
111}
112fn lint_single_file(file: &PathBuf, verbose: bool) -> Result<Vec<LintIssue>> {
113    let content = fs::read_to_string(file).context("Failed to read file")?;
114    let mut issues = Vec::new();
115    let lines: Vec<&str> = content.lines().collect();
116    for (i, line) in lines.iter().enumerate() {
117        let line_num = i + 1;
118        issues.extend(check_line_issues(line, line_num));
119    }
120    issues.extend(check_file_issues(&content, file));
121    if verbose && !issues.is_empty() {
122        println!("  Issues in {}:", file.display());
123        for issue in &issues {
124            println!(
125                "    {}:{}:{}: {}: {} ({})", file.display(), issue.line, issue.column,
126                format!("{:?}", issue.severity) .to_lowercase(), issue.message, issue
127                .rule
128            );
129        }
130    }
131    Ok(issues)
132}
133fn check_line_issues(line: &str, line_num: usize) -> Vec<LintIssue> {
134    let mut issues = Vec::new();
135    if line.ends_with(' ') || line.ends_with('\t') {
136        issues
137            .push(LintIssue {
138                line: line_num,
139                column: line.len(),
140                severity: LintSeverity::Warning,
141                message: "Trailing whitespace".to_string(),
142                rule: "no-trailing-whitespace".to_string(),
143            });
144    }
145    if line.contains('\t') && line.contains(' ') {
146        issues
147            .push(LintIssue {
148                line: line_num,
149                column: 1,
150                severity: LintSeverity::Error,
151                message: "Mixed tabs and spaces".to_string(),
152                rule: "no-mixed-indentation".to_string(),
153            });
154    }
155    if line.len() > 100 {
156        issues
157            .push(LintIssue {
158                line: line_num,
159                column: 101,
160                severity: LintSeverity::Warning,
161                message: "Line too long (over 100 characters)".to_string(),
162                rule: "line-length".to_string(),
163            });
164    }
165    if line.contains("  ") && line.trim().starts_with('{') {
166        let indent = line.len() - line.trim_start().len();
167        if indent % 2 != 0 {
168            issues
169                .push(LintIssue {
170                    line: line_num,
171                    column: 1,
172                    severity: LintSeverity::Warning,
173                    message: "Inconsistent indentation".to_string(),
174                    rule: "indentation".to_string(),
175                });
176        }
177    }
178    issues
179}
180fn check_file_issues(content: &str, _file: &PathBuf) -> Vec<LintIssue> {
181    let mut issues = Vec::new();
182    if !content.ends_with('\n') {
183        issues
184            .push(LintIssue {
185                line: content.lines().count(),
186                column: 1,
187                severity: LintSeverity::Warning,
188                message: "Missing newline at end of file".to_string(),
189                rule: "newline-at-eof".to_string(),
190            });
191    }
192    if content.starts_with('\u{FEFF}') {
193        issues
194            .push(LintIssue {
195                line: 1,
196                column: 1,
197                severity: LintSeverity::Error,
198                message: "Byte order mark (BOM) detected".to_string(),
199                rule: "no-bom".to_string(),
200            });
201    }
202    if content.contains("\r\n") {
203        issues
204            .push(LintIssue {
205                line: 1,
206                column: 1,
207                severity: LintSeverity::Warning,
208                message: "CRLF line endings detected (use LF)".to_string(),
209                rule: "line-endings".to_string(),
210            });
211    }
212    issues
213}
214fn find_helix_files(dir: &PathBuf, files: &mut Vec<PathBuf>) -> Result<()> {
215    let entries = fs::read_dir(dir).context("Failed to read directory")?;
216    for entry in entries {
217        let entry = entry.context("Failed to read directory entry")?;
218        let path = entry.path();
219        if path.is_file() {
220            if let Some(extension) = path.extension() {
221                if extension == "hlx" {
222                    files.push(path);
223                }
224            }
225        } else if path.is_dir() {
226            if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
227                if dir_name == "target" || dir_name == "lib" {
228                    continue;
229                }
230            }
231            find_helix_files(&path, files)?;
232        }
233    }
234    Ok(())
235}
236fn find_project_root() -> Result<PathBuf> {
237    let mut current_dir = std::env::current_dir()
238        .context("Failed to get current directory")?;
239    loop {
240        let manifest_path = current_dir.join("project.hlx");
241        if manifest_path.exists() {
242            return Ok(current_dir);
243        }
244        if let Some(parent) = current_dir.parent() {
245            current_dir = parent.to_path_buf();
246        } else {
247            break;
248        }
249    }
250    Err(anyhow::anyhow!("No HELIX project found. Run 'helix init' first."))
251}