Skip to content

Interfaces errors emit better notes - #12772

Open
0x6e wants to merge 11 commits into
masterfrom
nathan/interface-notes
Open

Interfaces errors emit better notes#12772
0x6e wants to merge 11 commits into
masterfrom
nathan/interface-notes

Conversation

@0x6e

@0x6e 0x6e commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

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.

error: Cannot implement 'ValidInterface'.
       - 'value' must be a 'int' property (found a 'float' property)
   --> internal/compiler/tests/syntax/interfaces/implement_self.slint:92:15
    |
 92 |     implement ValidInterface <=> self;
    |               ^^^^^^^^^^^^^^^
note: 'ValidInterface' declares 'value' as 'in-out property <int> value;'
   --> internal/compiler/tests/syntax/interfaces/implement_self.slint:95:22
    |
 95 |     in-out property <float> value;
    |                      ^^^^^

In some cases, it is not possible to highlight the particular syntax causing the issue (e.g. missing pure keyword) so we fall back to emitting a note on the name identifier of the declarations:

error: Cannot implement 'Inverter'.
       - 'invert' must be 'pure'
   --> internal/compiler/tests/syntax/interfaces/implement_self.slint:125:15
    |
125 |     implement Inverter <=> self;
    |               ^^^^^^^^^
note: 'Inverter' declares 'invert' as 'pure callback invert(bool) -> bool;'
   --> internal/compiler/tests/syntax/interfaces/implement_self.slint:129:14
    |
129 |     callback invert(bool) -> bool;
    |              ^^^^^^

In some cases we may emit multiple notes for the same error. We aggregate errors for each implement statement into one error string, and emit notes for each part of the syntax that is incorrect:

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;
    |     ^^^^^^

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 implement statement, with specific notes for the problematic declarations).

@0x6e
0x6e requested a review from LeonMatthes August 3, 2026 16:39
0x6e and others added 11 commits August 5, 2026 08:14
… 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.

@LeonMatthes LeonMatthes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
write!(formatter, "){}", return_type)
write!(formatter, "){return_type}")

nit

Comment on lines +380 to +389
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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;'}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
// > <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 :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 implement keyword (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.

Comment on lines +258 to +259
const VISIBILITY_KEYWORDS: &[&str] =
&["in", "out", "in-out", "in_out", "private", "public", "protected"];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 -)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants