Skip to content

Commit 0182d1e

Browse files
committed
Revert attempting to format any code that contains any parse errors,
introduce formatter errors and return code enums Close #323
1 parent 37019d1 commit 0182d1e

9 files changed

Lines changed: 141 additions & 76 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This file documents the changes made to the formatter with each release.
1212

1313
- Removed space between lambda function name and parameter list
1414
- Force @export and @onready annotations to stay on the same line as a variable but keep other annotations separate
15+
- Stop trying to format any code containing parse errors. Until now we tried to still format definitions around the code with errors, but this can lead to cases where the formatter produces invalid code
1516

1617
### Fixed
1718

src/bin/benchmark.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
//! cargo run --bin benchmark --release >> benchmark_results.txt
2020
//! git checkout -
2121
//! ```
22-
use gdscript_formatter::{FormatterConfiguration, RenderElement, format_gdscript_with_buffers};
22+
use gdscript_formatter::{
23+
FormatErrors, FormatterConfiguration, RenderElement, format_gdscript_with_buffers,
24+
};
2325
use std::{
2426
env, fs,
2527
hint::black_box,
@@ -51,7 +53,11 @@ impl BenchmarkRunner {
5153
}
5254
}
5355

54-
fn format(&mut self, source: &str, config: &FormatterConfiguration) -> Result<(), String> {
56+
fn format(
57+
&mut self,
58+
source: &str,
59+
config: &FormatterConfiguration,
60+
) -> Result<(), FormatErrors> {
5561
format_gdscript_with_buffers(
5662
black_box(source),
5763
black_box(config),
@@ -66,7 +72,7 @@ impl BenchmarkRunner {
6672
&mut self,
6773
source: &str,
6874
config: &FormatterConfiguration,
69-
) -> Result<BenchmarkMeasurement, String> {
75+
) -> Result<BenchmarkMeasurement, FormatErrors> {
7076
let warmup_start = Instant::now();
7177
while warmup_start.elapsed() < WARMUP_DURATION {
7278
self.format(source, config)?;

src/formatter.rs

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,7 @@ fn process_source(
980980
// pull code into or out of a # fmt: off disabled region. For now we
981981
// skip reordering for disabled regions, but in the future we may want
982982
// to reorder code around disabled regions as well?
983-
if input.reorder_code && !input.has_parse_errors {
983+
if input.reorder_code {
984984
if input.disabled_regions.is_empty() {
985985
process_source_reorder(input, node, render_elements);
986986
return;
@@ -1010,12 +1010,6 @@ fn process_source(
10101010
};
10111011

10121012
let kind = GDScriptNodeKind::get_kind_from_ast_node(child);
1013-
// Tree-sitter can recover from syntax errors by wrapping a declaration
1014-
// in an ERROR node. Formatting inside that subtree would rely on AST
1015-
// relationships that may no longer describe the source. Keep the
1016-
// malformed declaration intact while formatting its valid siblings.
1017-
let contains_parse_error = input.has_parse_errors && child.has_error();
1018-
10191013
// This code is similar to the one in process_body(). See comments
10201014
// there for some explanation of what this does and why it's needed.
10211015
match classify_disabled_region_overlap(input, node, child, current_index) {
@@ -1047,16 +1041,7 @@ fn process_source(
10471041
continue;
10481042
}
10491043
DisabledRegionOverlapKind::PartiallyCovered => {
1050-
if contains_parse_error {
1051-
render_elements.push(RenderElement::UnformattedSource {
1052-
range: RangeSourceBytes {
1053-
start_byte: child.start_byte(),
1054-
end_byte: child.end_byte(),
1055-
},
1056-
});
1057-
} else {
1058-
process_node(input, child, render_elements);
1059-
}
1044+
process_node(input, child, render_elements);
10601045
spacing_context.last_output_end = Some(child.end_byte());
10611046
spacing_context.last_declaration_end = Some(child.end_byte());
10621047
spacing_context.last_declaration_kind = Some(kind);
@@ -1098,16 +1083,7 @@ fn process_source(
10981083
&spacing_context,
10991084
child,
11001085
);
1101-
if contains_parse_error {
1102-
render_elements.push(RenderElement::UnformattedSource {
1103-
range: RangeSourceBytes {
1104-
start_byte: child.start_byte(),
1105-
end_byte: child.end_byte(),
1106-
},
1107-
});
1108-
} else {
1109-
process_node(input, child, render_elements);
1110-
}
1086+
process_node(input, child, render_elements);
11111087
spacing_context.last_output_end = Some(child.end_byte());
11121088
spacing_context.last_declaration_end = Some(child.end_byte());
11131089
spacing_context.last_declaration_kind = Some(kind);

src/lib.rs

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,34 @@ pub mod verify_structure;
2525

2626
pub use renderer::{PrinterConfiguration, RenderElement};
2727

28+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29+
pub enum FormatErrors {
30+
FailedToParseInput,
31+
ParseErrors,
32+
FailedToParseFormattedOutput,
33+
StructureChanged,
34+
}
35+
36+
impl std::fmt::Display for FormatErrors {
37+
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38+
let message = match self {
39+
Self::FailedToParseInput => {
40+
"We failed to parse input, the parser did not produce a valid AST"
41+
}
42+
Self::ParseErrors => "The input GDScript code contains parse errors",
43+
Self::FailedToParseFormattedOutput => {
44+
"The parser could not parse the formatted output, it did not produce a valid AST"
45+
}
46+
Self::StructureChanged => {
47+
"The formatted output is structurally different from the input. Please report this to the bug tracker with a copy of the input code: https://github.com/GDQuest/GDScript-formatter/issues/"
48+
}
49+
};
50+
formatter.write_str(message)
51+
}
52+
}
53+
54+
impl std::error::Error for FormatErrors {}
55+
2856
/// Selects which delimiters the formatter prefers for string literals.
2957
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3058
pub enum QuoteStyle {
@@ -81,7 +109,10 @@ impl Default for FormatterConfiguration {
81109
///
82110
/// For formatting multiple files, prefer [format_gdscript_with_buffers] to
83111
/// reuse pre-allocated buffers across multiple calls.
84-
pub fn format_gdscript(source: &str, config: &FormatterConfiguration) -> Result<String, String> {
112+
pub fn format_gdscript(
113+
source: &str,
114+
config: &FormatterConfiguration,
115+
) -> Result<String, FormatErrors> {
85116
let mut render_elements = Vec::new();
86117
let mut output = String::new();
87118
format_gdscript_with_buffers(source, config, &mut render_elements, &mut output)?;
@@ -96,9 +127,11 @@ pub fn format_gdscript_with_buffers(
96127
config: &FormatterConfiguration,
97128
render_elements: &mut Vec<RenderElement>,
98129
output: &mut String,
99-
) -> Result<(), String> {
100-
let parsed = parser::ParseInput::new(source, config)
101-
.ok_or_else(|| "Failed to parse input".to_string())?;
130+
) -> Result<(), FormatErrors> {
131+
let parsed = parser::ParseInput::new(source, config).ok_or(FormatErrors::FailedToParseInput)?;
132+
if parsed.has_parse_errors {
133+
return Err(FormatErrors::ParseErrors);
134+
}
102135
formatter::build_formatter_intermediate_representation(&parsed, render_elements);
103136

104137
// The renderer clamps every blank-line run to `maximum_blank_lines`. If a
@@ -114,17 +147,13 @@ pub fn format_gdscript_with_buffers(
114147

115148
if config.safe {
116149
let reparsed = parser::ParseInput::new(output, config)
117-
.ok_or_else(|| "Verify structure: formatted output does not parse".to_string())?;
118-
if !verify_structure::trees_structurally_equal(
150+
.ok_or(FormatErrors::FailedToParseFormattedOutput)?;
151+
if !verify_structure::are_syntax_trees_structurally_equal(
119152
&parsed.tree,
120153
&reparsed.tree,
121154
parsed.kind_lookup,
122155
) {
123-
return Err(
124-
"Verify structure: formatted output is structurally different from input. \
125-
Keeping original source."
126-
.to_string(),
127-
);
156+
return Err(FormatErrors::StructureChanged);
128157
}
129158
}
130159

src/main.rs

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,31 @@ use gdscript_formatter::linter::rule_config::{
2727
get_all_rule_names, parse_disabled_rules, validate_rule_names,
2828
};
2929
use gdscript_formatter::{
30-
FormatterConfiguration, QuoteStyle, RenderElement, format_gdscript,
30+
FormatErrors, FormatterConfiguration, QuoteStyle, RenderElement, format_gdscript,
3131
format_gdscript_with_buffers, linter::LinterConfig,
3232
};
3333
use std::collections::HashSet;
3434

3535
use cli::{Command, parse_args};
3636

37-
const ERROR_CODE_NOT_FORMATTED: i32 = 1;
37+
#[repr(i32)]
38+
enum FormatterExitCodes {
39+
NotFormatted = 1,
40+
ParseErrors = 2,
41+
}
3842

39-
#[derive(Debug, Clone)]
4043
struct FormatterOutput {
4144
index: usize,
4245
file_path: PathBuf,
4346
formatted_content: String,
4447
is_formatted: bool,
4548
}
4649

50+
enum FormatterFileProcessingResult {
51+
SkippedParseErrors { index: usize, file_path: PathBuf },
52+
Formatted(FormatterOutput),
53+
}
54+
4755
#[derive(Clone, Copy)]
4856
struct FormatterConfigOverrides {
4957
/// Explicitly requested tab or space indentation.
@@ -160,7 +168,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
160168
&current_directory.join("stdin.gd"),
161169
config_overrides,
162170
);
163-
let formatted_content = format_gdscript(&input_content, &stdin_config)?;
171+
let formatted_content = match format_gdscript(&input_content, &stdin_config) {
172+
Ok(formatted_content) => formatted_content,
173+
Err(FormatErrors::ParseErrors) => {
174+
eprintln!(
175+
"Skipping formatting stdin: the input GDScript code contains parse errors"
176+
);
177+
std::process::exit(FormatterExitCodes::ParseErrors as i32);
178+
}
179+
Err(error) => return Err(error.into()),
180+
};
164181

165182
if do_check_formatted_only {
166183
if input_content != formatted_content {
@@ -197,17 +214,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
197214
let _ = io::stdout().flush();
198215
}
199216

200-
let mut sorted_outputs: Vec<Result<FormatterOutput, String>> =
217+
let mut sorted_outputs: Vec<Result<FormatterFileProcessingResult, String>> =
201218
format_files_parallel(&input_gdscript_files, &config, config_overrides);
202219

203220
sorted_outputs.sort_by(compare_output_index);
204221

205222
let mut all_formatted = true;
206223
let mut modified_file_count = 0;
207224
let mut unformatted_files = Vec::new();
225+
let mut skipped_parse_error_files = Vec::new();
208226
for output in sorted_outputs {
209227
match output {
210-
Ok(output) => {
228+
Ok(FormatterFileProcessingResult::SkippedParseErrors { file_path, .. }) => {
229+
skipped_parse_error_files.push(file_path);
230+
}
231+
Ok(FormatterFileProcessingResult::Formatted(output)) => {
211232
if do_check_formatted_only {
212233
if use_verbose_output {
213234
eprintln!(
@@ -267,6 +288,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
267288
}
268289
}
269290

291+
if !skipped_parse_error_files.is_empty() {
292+
for file_path in skipped_parse_error_files {
293+
eprintln!(
294+
"Skipped formatting file {}: the input GDScript code contains parse errors",
295+
file_path.display()
296+
);
297+
}
298+
std::process::exit(FormatterExitCodes::ParseErrors as i32);
299+
}
300+
270301
if do_check_formatted_only {
271302
if !use_verbose_output {
272303
terminal_clear_line();
@@ -282,7 +313,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
282313
for file_path in unformatted_files {
283314
eprintln!("{}", file_path.display());
284315
}
285-
std::process::exit(ERROR_CODE_NOT_FORMATTED);
316+
std::process::exit(FormatterExitCodes::NotFormatted as i32);
286317
}
287318
} else if !do_print_to_stdout {
288319
if !use_verbose_output {
@@ -356,25 +387,39 @@ fn format_one_file(
356387
config_overrides: FormatterConfigOverrides,
357388
render_elements: &mut Vec<RenderElement>,
358389
output: &mut String,
359-
) -> Result<FormatterOutput, String> {
390+
) -> Result<FormatterFileProcessingResult, String> {
360391
let input_content = fs::read_to_string(file_path)
361392
.map_err(|error| format!("Failed to read file {}: {}", file_path.display(), error))?;
362393

363394
// We need to clone that config because files in nested directories can
364395
// match different EditorConfig files and rules.
365396
let mut file_config = config.clone();
366397
config_apply_editorconfig_then_cli_overrides(&mut file_config, file_path, config_overrides);
367-
format_gdscript_with_buffers(&input_content, &file_config, render_elements, output)
368-
.map_err(|error| format!("Failed to format file {}: {}", file_path.display(), error))?;
398+
match format_gdscript_with_buffers(&input_content, &file_config, render_elements, output) {
399+
Ok(()) => {}
400+
Err(FormatErrors::ParseErrors) => {
401+
return Ok(FormatterFileProcessingResult::SkippedParseErrors {
402+
index,
403+
file_path: file_path.clone(),
404+
});
405+
}
406+
Err(error) => {
407+
return Err(format!(
408+
"Failed to format file {}: {}",
409+
file_path.display(),
410+
error
411+
));
412+
}
413+
}
369414

370415
let is_formatted = input_content == *output;
371416

372-
Ok(FormatterOutput {
417+
Ok(FormatterFileProcessingResult::Formatted(FormatterOutput {
373418
index,
374419
file_path: file_path.clone(),
375420
formatted_content: output.clone(),
376421
is_formatted,
377-
})
422+
}))
378423
}
379424

380425
/// Applies project editorconfig settings first and CLI settings second.
@@ -415,7 +460,7 @@ fn format_files_parallel(
415460
files: &[PathBuf],
416461
config: &FormatterConfiguration,
417462
config_overrides: FormatterConfigOverrides,
418-
) -> Vec<Result<FormatterOutput, String>> {
463+
) -> Vec<Result<FormatterFileProcessingResult, String>> {
419464
if files.is_empty() {
420465
return Vec::new();
421466
}
@@ -450,7 +495,7 @@ fn format_chunk(
450495
chunk_size: usize,
451496
config: &FormatterConfiguration,
452497
config_overrides: FormatterConfigOverrides,
453-
) -> Vec<Result<FormatterOutput, String>> {
498+
) -> Vec<Result<FormatterFileProcessingResult, String>> {
454499
let mut results = Vec::with_capacity(chunk.len());
455500
let mut render_elements: Vec<RenderElement> = Vec::new();
456501
let mut output = String::new();
@@ -553,15 +598,17 @@ fn is_path_excluded(path: &Path, excluded_paths: &[PathBuf]) -> bool {
553598
}
554599

555600
fn compare_output_index(
556-
left: &Result<FormatterOutput, String>,
557-
right: &Result<FormatterOutput, String>,
601+
left: &Result<FormatterFileProcessingResult, String>,
602+
right: &Result<FormatterFileProcessingResult, String>,
558603
) -> std::cmp::Ordering {
559604
let left_index = match left {
560-
Ok(formatter_output) => formatter_output.index,
605+
Ok(FormatterFileProcessingResult::Formatted(formatter_output)) => formatter_output.index,
606+
Ok(FormatterFileProcessingResult::SkippedParseErrors { index, .. }) => *index,
561607
Err(_) => usize::MAX,
562608
};
563609
let right_index = match right {
564-
Ok(formatter_output) => formatter_output.index,
610+
Ok(FormatterFileProcessingResult::Formatted(formatter_output)) => formatter_output.index,
611+
Ok(FormatterFileProcessingResult::SkippedParseErrors { index, .. }) => *index,
565612
Err(_) => usize::MAX,
566613
};
567614
left_index.cmp(&right_index)

src/verify_structure.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ struct NormalizedNode {
3939

4040
/// Entry point: compare two tree-sitter trees for structural equivalence,
4141
/// accounting for formatting-induced CST changes.
42-
pub fn trees_structurally_equal(
42+
pub fn are_syntax_trees_structurally_equal(
4343
input_tree: &tree_sitter::Tree,
4444
output_tree: &tree_sitter::Tree,
4545
lookup: &[GDScriptNodeKind; 256],
@@ -265,7 +265,7 @@ mod tests {
265265
fn structurally_equal(a: &str, b: &str) -> bool {
266266
let ta = parse(a);
267267
let tb = parse(b);
268-
trees_structurally_equal(&ta, &tb, lookup())
268+
are_syntax_trees_structurally_equal(&ta, &tb, lookup())
269269
}
270270

271271
#[test]

tests/expected/parse_error.gd

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
# Valid declarations around declarations with parse errors should still be
2-
# formatted.
1+
# If there is any parse error in the file, it should be left untouched.
32
var valid = 1
43
var name.bla = value
54
var also_valid = 2

0 commit comments

Comments
 (0)