helix_core/compiler/tools/
lint.rs1use 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}