Interfaces errors emit better notes - #12772
Conversation
… missing member error The error is already generated in `property_matches_interface`. DRY.
`Element::lookup_property` walks the base type chain, but the node that declares a member was only reachable for local declarations. Add a lookup that mirrors how `lookup_property` is split across `Element` and `ElementType`, and replace the two hand-rolled walks in the LSP with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…conflicting statements Previously this function only added notes for local declarations. Now we can use Element::property_declaration_node to get the declaration on the inherited node that causes the problem.
This is a private function that is used by both public functions `validate_self_implement_statements` and `apply_child_implement_statements`. This means that the interface validation logic for an element is consolidated into one path. As a consequence, the `apply_child_implement_statements` path now emits notes for declarations that conflict with an interface.
…conflicts on base components Emit a note on the base component when a declaration in the base component does not satisfy the interface in an `implement Interface <=> child-id;` statement.
…ented interface
Add a note that highlights where the conflict comes from. We end up with
multiple notes because we want one per "Cannot override" error:
```
error: Cannot override 'reset' from 'ValidInterface'
--> internal/compiler/tests/syntax/interfaces/implement_child.slint:163:26
|
163 | public pure function reset() {}
| ^^^^^
note: 'ValidInterface' declares 'pure public function reset() { }'
--> internal/compiler/tests/syntax/interfaces/implement_child.slint:153:15
|
153 | implement ValidInterface <=> base;
| ^^^^^^^^^^^^^^^
error: Cannot override 'speak' from 'ValidInterface'
--> internal/compiler/tests/syntax/interfaces/implement_child.slint:161:14
|
161 | callback speak();
| ^^^^^
note: 'ValidInterface' declares 'callback speak();'
--> internal/compiler/tests/syntax/interfaces/implement_child.slint:153:15
|
153 | implement ValidInterface <=> base;
| ^^^^^^^^^^^^^^^
error: Cannot override 'value' from 'ValidInterface'
--> internal/compiler/tests/syntax/interfaces/implement_child.slint:159:27
|
159 | in-out property <int> value;
| ^^^^^
note: 'ValidInterface' declares 'in-out property <int> value;'
--> internal/compiler/tests/syntax/interfaces/implement_child.slint:153:15
|
153 | implement ValidInterface <=> base;
|
```
The source now highlights the syntax that is incorrect, and the note
says what the interface declares. Coupled with the actual error message,
this should give the user the full picture of what the error is, where
the error originates and where to apply the fix.
In order to find the correct source location for the diagnostic we have
to hunt through the syntax nodes to find an identifier with the expected
keyword, as the SyntaxNodes do not provide enough granularity to extract
visibility or purity directly. If we can't find the expected anchor we
try to fall back to the name identifier, and worst case we refer to the
whole declaration (which does not happen in practise).
One side effect is that duplicate notes can be a bit repetitive:
```
error: Cannot implement 'OutOnlyInterface'.
- 'count' must be a 'int' property (found a 'float' property)
- 'count' must be 'out' (found 'in-out')
--> internal/compiler/tests/syntax/interfaces/implement_self.slint:286:15
|
286 | implement OutOnlyInterface <=> self;
| ^^^^^^^^^^^^^^^^^
note: 'OutOnlyInterface' declares 'count' as 'out property <int> count;'
--> internal/compiler/tests/syntax/interfaces/implement_self.slint:289:22
|
289 | in-out property <float> count;
| ^^^^^
note: 'OutOnlyInterface' declares 'count' as 'out property <int> count;'
--> internal/compiler/tests/syntax/interfaces/implement_self.slint:289:5
|
289 | in-out property <float> count;
| ^^^^^^
```
We considered having one "'Interface' declares 'member' as" error and
having multiple more specific notes, but the notes are not currently
visible in the IDE.
This allows us to add `syntax_for_lookup_result` in a future commit. We want to compare the syntax for PropertyDeclaration and PropertyLookupResult. This allows us to re-use the display code.
… functions and callbacks This means we don't get the `function() -> void` output for void returning callbacks/functions.
`-> void` in diagnostics can be unexpected because Slint provides the void return type by default.
…or Function Now that it doesn't print the void return type we can use it directly, instead of re-implementing it.
5ebc3a1 to
63461e9
Compare
LeonMatthes
left a comment
There was a problem hiding this comment.
The more detailed spans are great, but I find the new note diagnostics somewhat misleading.
But we can probably fix that with a bit of rewording.
| } else { | ||
| format!(" -> {}", self.return_type) | ||
| }; | ||
| write!(formatter, "){}", return_type) |
There was a problem hiding this comment.
| write!(formatter, "){}", return_type) | |
| write!(formatter, "){return_type}") |
nit
| let joined_errors = | ||
| |violations: &[MemberViolation]| violations.iter().map(|v| v.error.as_str()).join("\n"); | ||
|
|
||
| if !lookup_result.is_valid() { | ||
| return Some(InterfaceMemberDiagnostics::from(joined_errors(&violations))); | ||
| } | ||
|
|
||
| let mut conflicts = InterfaceMemberDiagnostics::from(conflicts); | ||
| let source = element | ||
| .property_declarations | ||
| .get(member_name) | ||
| .and_then(|declaration| declaration.node.clone()); | ||
| let base_conflict = (!lookup_result.is_local_to_component && child_id.is_none()) | ||
| .then(|| check_property_declaration_conflicts(&lookup_result, &element.base_type).err()) | ||
| .flatten(); |
There was a problem hiding this comment.
Actually, why is this code here? Checking the lookup result should really be done in property_matches_interface, no?
Why do we have to treat this specially anyway? Can't this just fall through to the source anchoring loop?
There was a problem hiding this comment.
Good point, I think this is has just remained through. I'll see if I can move it without it affecting the error outputs.
|
|
||
| component IncorrectType { | ||
| in-out property <float> value; | ||
| // > <note{'ValidInterface' declares 'value' as 'in-out property <int> value;'} |
There was a problem hiding this comment.
Hm, I find this phrasing somewhat confusing.
This sounds like the diagnostic is pointing at the declaration within ValidInterface, but it is instead pointing to the conflicting implementation that we are checking against.
That is the right thing to point at, but the message should not suggest that it is showing the ValidInterface declaration.
Maybe a rewording could be:
| // > <note{'ValidInterface' declares 'value' as 'in-out property <int> value;'} | |
| // > <note{'value' declared here conflicts with 'ValidInterface' (expected 'in-out property <int> value;')} |
I'm still not entirely happy with it, so suggestions appreciated :)
There was a problem hiding this comment.
I wasn't super happy with this either, and agree that the wording probably could be improved. In particular I wondered if the note should also say what the problem is. However, the error diagnostic is what is visible to the user in the IDE, and must contain the error. The note is only visible in the compiler output (AFAIK) and always relates to the error, so we get:
- error: there is an error here, caused by the
implementkeyword (normally) and these are the problems; - notes: this is where the declaration that conflicts with the interface is, it is probably this particular part of the declaration, and this is what the interface declares/actually expects.
Maybe the note should say something more like: 'Foo' declares 'bar' here. 'Interface' expects 'in-out' property (found 'in'). Previously you said you wanted the code that the interface expects, but I'm not sure how best to fit that in as well. It could be part of the error.
| const VISIBILITY_KEYWORDS: &[&str] = | ||
| &["in", "out", "in-out", "in_out", "private", "public", "protected"]; |
There was a problem hiding this comment.
Hm, this list will become outdated if we ever add another property visibility 🤔
Instead, we can make the Visibility variant Visibility(PropertyVisibility) which stores the actual visibility we found.
Then we can just check against the Display of it, which is an exhaustive match over the enum.
(we just need to make sure to replace _ with -)
When an interface is not satisfied by the implementing component we emit notes that attempt to highlight the particular syntax that is causing the problem.
In some cases, it is not possible to highlight the particular syntax causing the issue (e.g. missing
purekeyword) so we fall back to emitting a note on the name identifier of the declarations:In some cases we may emit multiple notes for the same error. We aggregate errors for each
implementstatement into one error string, and emit notes for each part of the syntax that is incorrect:This might appear a bit repetitive, but the error text must contain the information needed to identify the error - the note does not appear in the IDE (otherwise we might emit one generic error for the
implementstatement, with specific notes for the problematic declarations).