From 8efb4fe29ced0a425f743a51a56773ea72bf5a8e Mon Sep 17 00:00:00 2001 From: Alan Zimmerman Date: Wed, 19 Nov 2025 03:56:10 -0800 Subject: [PATCH] BE: track related info in declarative tests Summary: Our declarative tests allow us to add annotations for expected diagnostics, so we can clearly see and check what is generated. But at present there is no way to check any related information for a diagnostic. This diff adds that capability, showing related information together with the FileId and range each refers to. Reviewed By: robertoaloi Differential Revision: D87316965 fbshipit-source-id: ea7db2980b11950c3552befec7b666665ceef84b --- .llms/rules/elp_development.md | 6 ++++++ crates/ide/src/diagnostics.rs | 5 +++++ crates/ide/src/diagnostics/head_mismatch.rs | 6 ++++++ .../src/diagnostics/misspelled_attribute.rs | 3 +++ crates/ide/src/diagnostics/unused_include.rs | 1 + crates/ide/src/tests.rs | 17 +++++++++++++++++ crates/project_model/src/test_fixture.rs | 18 ++++++++++++++++-- 7 files changed, 54 insertions(+), 2 deletions(-) diff --git a/.llms/rules/elp_development.md b/.llms/rules/elp_development.md index 0a024728a1..efd132ac4b 100644 --- a/.llms/rules/elp_development.md +++ b/.llms/rules/elp_development.md @@ -168,6 +168,12 @@ directly in test code: - **Left-margin annotation**: `%%<^^^ text` - Annotation starts at `%%` position instead of first `^` - **Multiline annotations**: Use continuation lines with `%% | next line` + - Continuation lines are particularly useful for diagnostics with related information: + ```erlang + foo() -> syntax error oops. + %% ^^^^^ error: P1711: syntax error before: error + %% | Related info: 0:45-50 function foo/0 undefined + ``` #### Example Test Fixture diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index 4f8a9f8732..5e25b9d385 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs @@ -3433,6 +3433,9 @@ baz(1)->4. -spec foo() -> ok. foo( -> ok. %% %% ^ error: W0004: Missing ')' + %% | Related info: 0:21-43 function foo/0 undefined + %% | Related info: 0:74-79 function foo/0 undefined + %% | Related info: 0:82-99 spec for undefined function foo/0 "#, ); } @@ -3463,6 +3466,7 @@ baz(1)->4. foo() -> syntax error oops. %% ^^^^^ error: P1711: syntax error before: error + %% | Related info: 0:25-30 function foo/0 undefined "#, ); } @@ -3766,6 +3770,7 @@ baz(1)->4. \~"\"\\ยตA\"" = \~/"\\ยตA"/ X = 3. %% ^ error: P1711: syntax error before: X + %% | Related info: 0:32-37 function foo/0 undefined "#, ); } diff --git a/crates/ide/src/diagnostics/head_mismatch.rs b/crates/ide/src/diagnostics/head_mismatch.rs index b548b84dff..3a92d6f931 100644 --- a/crates/ide/src/diagnostics/head_mismatch.rs +++ b/crates/ide/src/diagnostics/head_mismatch.rs @@ -396,6 +396,7 @@ mod tests { foo(0) -> 1; boo(1) -> 2. %% ^^^ ๐Ÿ’ก error: P1700: head mismatch 'boo' vs 'foo' + %% | Related info: 0:21-24 Mismatched clause name "#, ); check_fix( @@ -421,6 +422,7 @@ mod tests { ok; fooX(_X) -> %% ^^^^ ๐Ÿ’ก error: P1700: head mismatch 'fooX' vs 'food' + %% | Related info: 0:21-25 Mismatched clause name no. bar() -> @@ -450,6 +452,7 @@ mod tests { -module(main). foo(0) -> 1; %% ^^^ ๐Ÿ’ก error: P1700: head mismatch 'foo' vs 'boo' + %% | Related info: 0:37-40 Mismatched clause name boo(1) -> 2; boo(2) -> 3. "#, @@ -478,6 +481,7 @@ mod tests { foo(0) -> 1; foo(1,0) -> 2. %% ^^^^^^^^^^^^^ error: P1700: head arity mismatch 2 vs 1 + %% | Related info: 0:21-32 Mismatched clause "#, ); } @@ -490,6 +494,7 @@ mod tests { foo(2,0) -> 3; foo(0) -> 1; %% ^^^^^^^^^^^ error: P1700: head arity mismatch 1 vs 2 + %% | Related info: 0:21-34 Mismatched clause foo(1,0) -> 2. "#, ); @@ -516,6 +521,7 @@ mod tests { (0) -> ok; A(N) -> ok %% ^ ๐Ÿ’ก error: P1700: head mismatch 'A' vs '' + %% | Related info: 0:44-53 Mismatched clause name end, F(). "#, diff --git a/crates/ide/src/diagnostics/misspelled_attribute.rs b/crates/ide/src/diagnostics/misspelled_attribute.rs index 9dc5868879..6ba95c8ef9 100644 --- a/crates/ide/src/diagnostics/misspelled_attribute.rs +++ b/crates/ide/src/diagnostics/misspelled_attribute.rs @@ -164,6 +164,7 @@ mod tests { -module(main). -dyalizer({nowarn_function, f/0}). %%% ^^^^^^^^ ๐Ÿ’ก error: W0013: misspelled attribute, saw 'dyalizer' but expected 'dialyzer' + %%% | Related info: 0:22-30 Misspelled attribute "#, ); check_fix( @@ -224,6 +225,7 @@ mod tests { -module(main). -module_doc """ %%% ^^^^^^^^^^ ๐Ÿ’ก error: W0013: misspelled attribute, saw 'module_doc' but expected 'moduledoc' +%%% | Related info: 0:24-34 Misspelled attribute Hola """. "#, @@ -237,6 +239,7 @@ mod tests { -module(main). -docs """ %%% ^^^^ ๐Ÿ’ก error: W0013: misspelled attribute, saw 'docs' but expected 'doc' +%%% | Related info: 0:24-28 Misspelled attribute Hola """. foo() -> ok. diff --git a/crates/ide/src/diagnostics/unused_include.rs b/crates/ide/src/diagnostics/unused_include.rs index 3a91377338..959181a328 100644 --- a/crates/ide/src/diagnostics/unused_include.rs +++ b/crates/ide/src/diagnostics/unused_include.rs @@ -581,6 +581,7 @@ foo() -> ok. %% The following shows up as a wild attribute, which we regard as being used. -defin e(X, 1). %% ^^^^^ ๐Ÿ’ก error: W0013: misspelled attribute, saw 'defin' but expected 'define' +%% | Related info: 1:82-87 Misspelled attribute -def ine(Y, 2). "#, diff --git a/crates/ide/src/tests.rs b/crates/ide/src/tests.rs index 71125f5094..8d71b65dad 100644 --- a/crates/ide/src/tests.rs +++ b/crates/ide/src/tests.rs @@ -401,6 +401,23 @@ fn convert_diagnostics_to_annotations(diagnostics: Vec) -> Vec<(Text annotation.push_str(&d.code.as_code()); annotation.push_str(": "); annotation.push_str(&convert_diagnostic_message(&d)); + + // Append related info to the annotation + if let Some(related_info) = &d.related_info { + // Sort related info alphabetically by message for consistent test output + let mut sorted_info = related_info.clone(); + sorted_info.sort_by(|a, b| a.message.cmp(&b.message)); + for info in sorted_info { + annotation.push_str(&format!( + "\nRelated info: {}:{}-{} {}", + info.file_id.index(), + u32::from(info.range.start()), + u32::from(info.range.end()), + info.message + )); + } + } + (d.range, annotation) }) .collect::>(); diff --git a/crates/project_model/src/test_fixture.rs b/crates/project_model/src/test_fixture.rs index 769891190a..ac60d3b7c2 100644 --- a/crates/project_model/src/test_fixture.rs +++ b/crates/project_model/src/test_fixture.rs @@ -472,6 +472,14 @@ pub fn extract_tags(mut text: &str, tag: &str) -> (Vec<(TextRange, Option syntax error oops. +/// %% ^^^^^ error: P1711: syntax error before: error +/// %% | Related info: 0:25-30 function foo/0 undefined +/// ``` +/// /// Annotations point to the last line that actually was long enough for the /// range, not counting annotations themselves. So overlapping annotations are /// possible: @@ -551,10 +559,16 @@ pub fn extract_annotations(text: &str) -> (Vec<(TextRange, String)>, String) { if !res.is_empty() { offset += annotation_offset; this_line_annotations.push((offset, res.len() - 1)); - let &(_, idx) = prev_line_annotations + // Try to find a previous annotation at the same offset + let idx = if let Some(&(_, idx)) = prev_line_annotations .iter() .find(|&&(off, _idx)| off == offset) - .unwrap(); + { + idx + } else { + // If no exact offset match, append to the most recent annotation + res.len() - 1 + }; res[idx].1.push('\n'); res[idx].1.push_str(&content); }