Skip to content

Commit d702438

Browse files
authored
rust/glint: Add fix mode (#2813)
Signed-off-by: Ryan Northey <[email protected]>
1 parent cdd033c commit d702438

14 files changed

+421
-8
lines changed

rust/glint/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ serde_json = "1.0"
1313

1414
[dev-dependencies]
1515
serde_json = "1.0"
16+
tempfile = "3.0"

rust/glint/src/args.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ use std::path::PathBuf;
55
#[command(name = "glint")]
66
#[command(about = "Lint files for whitespace issues", long_about = None)]
77
pub struct Args {
8+
#[arg(short, long)]
9+
pub fix: bool,
10+
811
#[arg(required = true)]
912
pub paths: Vec<PathBuf>,
1013
}

rust/glint/src/fix.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use anyhow::{Context, Result};
2+
use std::fs;
3+
use std::path::Path;
4+
5+
#[derive(Debug)]
6+
pub struct FixResult {
7+
pub trailing_whitespace_fixed: usize,
8+
pub tabs_converted: usize,
9+
pub final_newline_added: bool,
10+
}
11+
12+
pub fn fix_file(path: &Path) -> Result<Option<FixResult>> {
13+
let content = fs::read_to_string(path)
14+
.with_context(|| format!("Failed to read file: {}", path.display()))?;
15+
16+
let mut fixed_lines = Vec::new();
17+
let mut trailing_whitespace_fixed = 0;
18+
let mut tabs_converted = 0;
19+
let mut changed = false;
20+
21+
for line in content.lines() {
22+
let mut fixed_line = line.to_string();
23+
24+
let tab_count = fixed_line.chars().filter(|&c| c == '\t').count();
25+
26+
if tab_count > 0 {
27+
fixed_line = fixed_line.replace('\t', " ");
28+
tabs_converted += tab_count;
29+
changed = true;
30+
}
31+
32+
let trimmed = fixed_line.trim_end();
33+
if trimmed.len() < fixed_line.len() {
34+
fixed_line = trimmed.to_string();
35+
trailing_whitespace_fixed += 1;
36+
changed = true;
37+
}
38+
39+
fixed_lines.push(fixed_line);
40+
}
41+
42+
let needs_final_newline = !content.is_empty() && !content.ends_with('\n');
43+
let mut fixed_content = fixed_lines.join("\n");
44+
45+
if !content.is_empty() && content.ends_with('\n') {
46+
fixed_content.push('\n');
47+
} else if needs_final_newline {
48+
fixed_content.push('\n');
49+
changed = true;
50+
}
51+
52+
if changed {
53+
fs::write(path, fixed_content)
54+
.with_context(|| format!("Failed to write file: {}", path.display()))?;
55+
56+
Ok(Some(FixResult {
57+
trailing_whitespace_fixed,
58+
tabs_converted,
59+
final_newline_added: needs_final_newline,
60+
}))
61+
} else {
62+
Ok(None)
63+
}
64+
}

rust/glint/src/main.rs

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod args;
22
mod check;
33
mod files;
4+
mod fix;
45

56
use anyhow::Result;
67
use clap::Parser;
@@ -13,7 +14,8 @@ use std::sync::{
1314
};
1415

1516
use crate::args::Args;
16-
use crate::files::{FileIssues, find_files, process_file};
17+
use crate::files::{FileIssues, find_files, process_file, should_exclude};
18+
use crate::fix::fix_file;
1719

1820
#[derive(Debug, Serialize, Deserialize)]
1921
struct LintResult {
@@ -28,8 +30,29 @@ struct Summary {
2830
total_issues: usize,
2931
}
3032

33+
#[derive(Debug, Serialize, Deserialize)]
34+
struct FixResult {
35+
files: HashMap<String, FixedFileInfo>,
36+
summary: FixSummary,
37+
}
38+
39+
#[derive(Debug, Clone, Serialize, Deserialize)]
40+
struct FixedFileInfo {
41+
trailing_whitespace_fixed: usize,
42+
tabs_converted: usize,
43+
final_newline_added: bool,
44+
}
45+
46+
#[derive(Debug, Serialize, Deserialize)]
47+
struct FixSummary {
48+
total_files: usize,
49+
files_fixed: usize,
50+
total_fixes: usize,
51+
}
52+
3153
fn main() -> Result<()> {
3254
let args = Args::parse();
55+
3356
let mut all_files = Vec::new();
3457
for path in &args.paths {
3558
if !path.exists() {
@@ -44,8 +67,72 @@ fn main() -> Result<()> {
4467
return Ok(());
4568
}
4669

70+
if args.fix {
71+
let mut fixed_files = HashMap::new();
72+
let mut error_count = 0;
73+
let mut total_files_checked = 0;
74+
75+
for file in &all_files {
76+
if should_exclude(file) {
77+
continue;
78+
}
79+
80+
total_files_checked += 1;
81+
82+
match fix_file(file) {
83+
Ok(Some(result)) => {
84+
fixed_files.insert(
85+
file.display().to_string(),
86+
FixedFileInfo {
87+
trailing_whitespace_fixed: result.trailing_whitespace_fixed,
88+
tabs_converted: result.tabs_converted,
89+
final_newline_added: result.final_newline_added,
90+
},
91+
);
92+
}
93+
Ok(None) => {
94+
// No fixes needed for this file
95+
}
96+
Err(e) => {
97+
eprintln!("Error fixing {}: {}", file.display(), e);
98+
error_count += 1;
99+
}
100+
}
101+
}
102+
103+
if error_count > 0 {
104+
std::process::exit(1);
105+
}
106+
107+
if !fixed_files.is_empty() {
108+
let files_fixed = fixed_files.len();
109+
let total_fixes: usize = fixed_files
110+
.values()
111+
.map(|f| {
112+
f.trailing_whitespace_fixed
113+
+ f.tabs_converted
114+
+ if f.final_newline_added { 1 } else { 0 }
115+
})
116+
.sum();
117+
118+
let result = FixResult {
119+
files: fixed_files,
120+
summary: FixSummary {
121+
total_files: total_files_checked,
122+
files_fixed,
123+
total_fixes,
124+
},
125+
};
126+
127+
println!("{}", serde_json::to_string_pretty(&result)?);
128+
}
129+
130+
return Ok(());
131+
}
132+
47133
let file_issues = Mutex::new(HashMap::new());
48134
let had_error = AtomicBool::new(false);
135+
49136
all_files
50137
.par_iter()
51138
.for_each(|file| match process_file(file) {
@@ -61,8 +148,6 @@ fn main() -> Result<()> {
61148
});
62149

63150
let files = file_issues.into_inner().unwrap();
64-
65-
// Calculate summary
66151
let total_files = all_files.len();
67152
let files_with_issues = files.len();
68153
let total_issues: usize = files.values().map(|f| f.total_issues()).sum();
@@ -76,13 +161,9 @@ fn main() -> Result<()> {
76161
},
77162
};
78163

79-
// Output JSON
80164
println!("{}", serde_json::to_string_pretty(&result)?);
81-
82-
// Exit with non-zero status if there were issues
83165
if files_with_issues > 0 || had_error.load(Ordering::Relaxed) {
84166
std::process::exit(1);
85167
}
86-
87168
Ok(())
88169
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"files": {
3+
"tests/fixtures/bad.txt": {
4+
"trailing_whitespace_fixed": 7,
5+
"tabs_converted": 12,
6+
"final_newline_added": true
7+
}
8+
},
9+
"summary": {
10+
"total_files": 1,
11+
"files_fixed": 1,
12+
"total_fixes": 20
13+
}
14+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
This is line 1 with trailing spaces
2+
This line 2 starts with a tab
3+
This line 3 starts with a space
4+
Line 4 is normal
5+
Line 5 has trailing tabs
6+
Another tab-indented line (6)
7+
Line 7 is clean
8+
Space-indented line 8
9+
Line 9 is normal
10+
Line 10 ends with mixed whitespace
11+
Tab line 11
12+
Line 12 is clean
13+
Line 13 is normal
14+
Space line 14
15+
Line 15 has a trailing space
16+
Line 16 is clean
17+
Tab line 17
18+
Line 18 is normal
19+
Line 19 is clean
20+
Line 20 with trailing whitespace
21+
Space line 21
22+
Line 22 is normal
23+
Tab line 23
24+
Line 24 is clean
25+
Line 25 is normal
26+
Line 26 is clean
27+
Space line 27
28+
Line 28 is normal
29+
Line 29 is clean
30+
Line 30 has many trailing spaces
31+
Tab line 31
32+
Line 32 is normal
33+
Space line 33
34+
Line 34 is clean
35+
Line 35 is normal
36+
Line 36 is clean
37+
Tab line 37
38+
Line 38 is normal
39+
Line 39 is clean
40+
Line 40 with subtle trailing space
41+
Space line 41
42+
Line 42 is normal
43+
Tab line 43
44+
Line 44 is clean
45+
Line 45 is normal
46+
Line 46 is clean
47+
Space line 47
48+
Line 48 is normal
49+
Line 49 is clean
50+
Line 50 is the last line with no newline
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"files": {
3+
"tests/fixtures/bad2.txt": {
4+
"trailing_whitespace_fixed": 2,
5+
"tabs_converted": 3,
6+
"final_newline_added": true
7+
}
8+
},
9+
"summary": {
10+
"total_files": 1,
11+
"files_fixed": 1,
12+
"total_fixes": 6
13+
}
14+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Line 1 is clean
2+
Line 2 has trailing space
3+
Tab indented line 3
4+
Line 4 is normal
5+
Line 5 ends with tabs
6+
Space indented line 6
7+
Line 7 is the last line without newline
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"files": {
3+
"tests/fixtures/bad3.txt": {
4+
"trailing_whitespace_fixed": 4,
5+
"tabs_converted": 2,
6+
"final_newline_added": false
7+
}
8+
},
9+
"summary": {
10+
"total_files": 1,
11+
"files_fixed": 1,
12+
"total_fixes": 6
13+
}
14+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
This file only has trailing whitespace issues
2+
Line 2 also has trailing spaces
3+
Line 3 is clean
4+
Line 4 has a tab at the end
5+
Line 5 is clean
6+
Line 6 has mixed trailing whitespace
7+
Final line with newline

0 commit comments

Comments
 (0)