|
| 1 | +use std::fs::File; |
| 2 | +use std::io::{self, Read}; |
| 3 | +use std::path::PathBuf; |
1 | 4 |
|
| 5 | +use clap::Parser; |
| 6 | +use miette::{Diagnostic, GraphicalReportHandler, Report, SourceSpan}; |
| 7 | +use regex::Regex; |
| 8 | + |
| 9 | +#[derive(Parser, Debug)] |
| 10 | +#[command(name = "hsdf", about = "HackerScript Diagnostic Files - Diagnoses .hcs files with pretty errors")] |
| 11 | +struct Args { |
| 12 | + /// Path to the .hcs file to diagnose |
| 13 | + #[arg(required = true)] |
| 14 | + file: PathBuf, |
| 15 | +} |
| 16 | + |
| 17 | +#[derive(Debug, thiserror::Error, Diagnostic)] |
| 18 | +enum HcsError { |
| 19 | + #[error("File not found or unable to read: {0}")] |
| 20 | + IoError(#[from] io::Error), |
| 21 | + |
| 22 | + #[error("Unclosed block comment")] |
| 23 | + #[diagnostic(code(hcs::unclosed_block_comment))] |
| 24 | + UnclosedBlockComment { |
| 25 | + #[source_code] |
| 26 | + src: String, |
| 27 | + #[label("Block comment started here but never closed")] |
| 28 | + span: SourceSpan, |
| 29 | + }, |
| 30 | + |
| 31 | + #[error("Unmatched closing bracket ']' without opening '['")] |
| 32 | + #[diagnostic(code(hcs::unmatched_closing_bracket))] |
| 33 | + UnmatchedClosingBracket { |
| 34 | + #[source_code] |
| 35 | + src: String, |
| 36 | + #[label("This ']' has no matching '['")] |
| 37 | + span: SourceSpan, |
| 38 | + }, |
| 39 | + |
| 40 | + #[error("Unclosed sh block")] |
| 41 | + #[diagnostic(code(hcs::unclosed_sh_block))] |
| 42 | + UnclosedShBlock { |
| 43 | + #[source_code] |
| 44 | + src: String, |
| 45 | + #[label("sh block started here but never closed")] |
| 46 | + span: SourceSpan, |
| 47 | + }, |
| 48 | + |
| 49 | + #[error("Unclosed block (indent level > 0 at EOF)")] |
| 50 | + #[diagnostic(code(hcs::unclosed_block))] |
| 51 | + UnclosedBlock { |
| 52 | + #[source_code] |
| 53 | + src: String, |
| 54 | + #[label("Block opened here but not closed")] |
| 55 | + span: SourceSpan, |
| 56 | + }, |
| 57 | + |
| 58 | + #[error("Invalid syntax: {message}")] |
| 59 | + #[diagnostic(code(hcs::invalid_syntax))] |
| 60 | + InvalidSyntax { |
| 61 | + message: String, |
| 62 | + #[source_code] |
| 63 | + src: String, |
| 64 | + #[label("Invalid syntax here")] |
| 65 | + span: SourceSpan, |
| 66 | + }, |
| 67 | + |
| 68 | + #[error("Multiple errors found")] |
| 69 | + MultipleErrors(Vec<Report>), |
| 70 | +} |
| 71 | + |
| 72 | +fn main() -> miette::Result<()> { |
| 73 | + let args = Args::parse(); |
| 74 | + |
| 75 | + let mut file = File::open(&args.file)?; |
| 76 | + let mut code = String::new(); |
| 77 | + file.read_to_string(&mut code)?; |
| 78 | + |
| 79 | + match diagnose_hcs(&code) { |
| 80 | + Ok(_) => { |
| 81 | + println!("No errors found in {}", args.file.display()); |
| 82 | + Ok(()) |
| 83 | + } |
| 84 | + Err(HcsError::MultipleErrors(errors)) => { |
| 85 | + let handler = GraphicalReportHandler::new(); |
| 86 | + for err in errors { |
| 87 | + handler.render_report(&mut io::stdout(), err.as_ref())?; |
| 88 | + } |
| 89 | + std::process::exit(1); |
| 90 | + } |
| 91 | + Err(err) => { |
| 92 | + let report = Report::new(err); |
| 93 | + let handler = GraphicalReportHandler::new(); |
| 94 | + handler.render_report(&mut io::stdout(), report.as_ref())?; |
| 95 | + std::process::exit(1); |
| 96 | + } |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +fn diagnose_hcs(code: &str) -> Result<(), HcsError> { |
| 101 | + let lines: Vec<&str> = code.lines().collect(); |
| 102 | + let mut errors: Vec<Report> = Vec::new(); |
| 103 | + |
| 104 | + let mut indent_level = 0; |
| 105 | + let mut in_sh_block = false; |
| 106 | + let mut in_block_comment = false; |
| 107 | + let mut block_comment_start: Option<usize> = None; |
| 108 | + let mut sh_block_start: Option<usize> = None; |
| 109 | + let mut last_open_block_pos: Option<usize> = None; |
| 110 | + |
| 111 | + let rust_re = Regex::new(r"<rust:([\w\-]+)(?:=([\d\.]+))?>").unwrap(); |
| 112 | + let c_re = Regex::new(r"<c:(.*)>").unwrap(); |
| 113 | + let comment_re = Regex::new(r"@.*").unwrap(); |
| 114 | + |
| 115 | + let mut pos = 0; |
| 116 | + for (line_num, line) in lines.iter().enumerate() { |
| 117 | + let line_start = pos; |
| 118 | + let mut raw_line = line.trim().to_string(); |
| 119 | + |
| 120 | + // Advance pos |
| 121 | + pos += line.len() + 1; // +1 for newline |
| 122 | + |
| 123 | + // Handle block comments |
| 124 | + if raw_line.contains("-/") { |
| 125 | + if in_block_comment { |
| 126 | + errors.push(Report::new(HcsError::InvalidSyntax { |
| 127 | + message: "Nested block comment start".to_string(), |
| 128 | + src: code.to_string(), |
| 129 | + span: (line_start, raw_line.len()).into(), |
| 130 | + })); |
| 131 | + } |
| 132 | + in_block_comment = true; |
| 133 | + block_comment_start = Some(line_start); |
| 134 | + continue; |
| 135 | + } |
| 136 | + if raw_line.contains("-\\") { |
| 137 | + if !in_block_comment { |
| 138 | + errors.push(Report::new(HcsError::UnmatchedClosingBracket { |
| 139 | + src: code.to_string(), |
| 140 | + span: (line_start, raw_line.len()).into(), |
| 141 | + })); |
| 142 | + } |
| 143 | + in_block_comment = false; |
| 144 | + block_comment_start = None; |
| 145 | + continue; |
| 146 | + } |
| 147 | + if in_block_comment { |
| 148 | + continue; |
| 149 | + } |
| 150 | + |
| 151 | + // Remove line comments |
| 152 | + raw_line = comment_re.replace(&raw_line, "").trim().to_string(); |
| 153 | + |
| 154 | + if raw_line.is_empty() && !in_sh_block { |
| 155 | + continue; |
| 156 | + } |
| 157 | + |
| 158 | + // Special imports |
| 159 | + if rust_re.is_match(&raw_line) { |
| 160 | + // Valid, skip |
| 161 | + continue; |
| 162 | + } |
| 163 | + if c_re.is_match(&raw_line) { |
| 164 | + // Valid, skip |
| 165 | + continue; |
| 166 | + } |
| 167 | + |
| 168 | + // Manual mode |
| 169 | + if raw_line.contains("--- manual ---") { |
| 170 | + // Valid |
| 171 | + continue; |
| 172 | + } |
| 173 | + |
| 174 | + // Numpy syntax |
| 175 | + if raw_line.starts_with("matrix ") || raw_line.starts_with("vector ") { |
| 176 | + // Assume valid for now |
| 177 | + continue; |
| 178 | + } |
| 179 | + |
| 180 | + // SH commands |
| 181 | + if raw_line == "sh [" { |
| 182 | + if in_sh_block { |
| 183 | + errors.push(Report::new(HcsError::InvalidSyntax { |
| 184 | + message: "Nested sh block".to_string(), |
| 185 | + src: code.to_string(), |
| 186 | + span: (line_start, raw_line.len()).into(), |
| 187 | + })); |
| 188 | + } |
| 189 | + in_sh_block = true; |
| 190 | + sh_block_start = Some(line_start); |
| 191 | + continue; |
| 192 | + } |
| 193 | + if in_sh_block { |
| 194 | + if raw_line == "]" { |
| 195 | + in_sh_block = false; |
| 196 | + sh_block_start = None; |
| 197 | + continue; |
| 198 | + } |
| 199 | + // Otherwise, sh content, assume valid |
| 200 | + continue; |
| 201 | + } |
| 202 | + if raw_line.starts_with("sh [") && raw_line.ends_with("]") { |
| 203 | + // Single line sh, valid |
| 204 | + continue; |
| 205 | + } |
| 206 | + |
| 207 | + // Block handling |
| 208 | + if raw_line.starts_with("] except") || raw_line.starts_with("] else") { |
| 209 | + if indent_level == 0 { |
| 210 | + errors.push(Report::new(HcsError::UnmatchedClosingBracket { |
| 211 | + src: code.to_string(), |
| 212 | + span: (line_start, raw_line.len()).into(), |
| 213 | + })); |
| 214 | + } else { |
| 215 | + indent_level -= 1; |
| 216 | + } |
| 217 | + continue; |
| 218 | + } |
| 219 | + if raw_line == "]" { |
| 220 | + if indent_level == 0 { |
| 221 | + errors.push(Report::new(HcsError::UnmatchedClosingBracket { |
| 222 | + src: code.to_string(), |
| 223 | + span: (line_start, raw_line.len()).into(), |
| 224 | + })); |
| 225 | + } else { |
| 226 | + indent_level -= 1; |
| 227 | + } |
| 228 | + continue; |
| 229 | + } |
| 230 | + |
| 231 | + // Keywords |
| 232 | + if raw_line.starts_with("func ") || raw_line.starts_with("log ") { |
| 233 | + // Valid |
| 234 | + } |
| 235 | + |
| 236 | + // Opening blocks |
| 237 | + if raw_line.ends_with("[") { |
| 238 | + indent_level += 1; |
| 239 | + last_open_block_pos = Some(line_start); |
| 240 | + continue; |
| 241 | + } |
| 242 | + |
| 243 | + // If we reach here and it's not recognized, flag as invalid |
| 244 | + if !raw_line.is_empty() { |
| 245 | + errors.push(Report::new(HcsError::InvalidSyntax { |
| 246 | + message: "Unrecognized syntax".to_string(), |
| 247 | + src: code.to_string(), |
| 248 | + span: (line_start, raw_line.len()).into(), |
| 249 | + })); |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + // Check for unclosed states |
| 254 | + if in_block_comment { |
| 255 | + if let Some(start) = block_comment_start { |
| 256 | + errors.push(Report::new(HcsError::UnclosedBlockComment { |
| 257 | + src: code.to_string(), |
| 258 | + span: (start, 2).into(), // Approximate span for "-/" |
| 259 | + })); |
| 260 | + } |
| 261 | + } |
| 262 | + if in_sh_block { |
| 263 | + if let Some(start) = sh_block_start { |
| 264 | + errors.push(Report::new(HcsError::UnclosedShBlock { |
| 265 | + src: code.to_string(), |
| 266 | + span: (start, 4).into(), // "sh [" |
| 267 | + })); |
| 268 | + } |
| 269 | + } |
| 270 | + if indent_level > 0 { |
| 271 | + if let Some(start) = last_open_block_pos { |
| 272 | + errors.push(Report::new(HcsError::UnclosedBlock { |
| 273 | + src: code.to_string(), |
| 274 | + span: (start, 1).into(), // "[" |
| 275 | + })); |
| 276 | + } |
| 277 | + } |
| 278 | + |
| 279 | + if !errors.is_empty() { |
| 280 | + Err(HcsError::MultipleErrors(errors)) |
| 281 | + } else { |
| 282 | + Ok(()) |
| 283 | + } |
| 284 | +} |
0 commit comments