8000 Type checking support for host bindings by crisbeto · Pull Request #60267 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

Type checking support for host bindings #60267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from

Conversation

crisbeto
Copy link
Member
@crisbeto crisbeto commented Mar 7, 2025

Historically Angular's type checking only extended to templates, however host bindings can contain expressions as well which can have type checking issues of their own (example and example). These changes add the ability to type check the host object literal, @HostBinding and @HostListener decorators of @Directive and @Component classes. The new functionality is hidden behind the typeCheckHostBindings compiler flag, but we plan to enable it by default in a future version.

This also allows us to enable language service features in host bindings like renaming and hover information. Here's demo showing how it's integrated:

host-bindings-demo

Note: the language service integration for this is done, but I'll send it out in a separate PR to make this one easier to review.

@crisbeto crisbeto added action: review The PR is still awaiting reviews from at least one requested reviewer target: major This PR is targeted for the next major release labels Mar 7, 2025
@angular-robot angular-robot bot added detected: feature PR contains a feature commit area: compiler Issues related to `ngc`, Angular's template compiler labels Mar 7, 2025
@ngbot ngbot bot added this to the Backlog milestone Mar 7, 2025
@crisbeto crisbeto force-pushed the type-check-host-compiler branch from 6832d7d to 7aa4001 Compare March 7, 2025 10:33
@crisbeto crisbeto requested a review from alxhub March 7, 2025 10:33
@crisbeto crisbeto marked this pull request as ready for review March 7, 2025 10:33
@pullapprove pullapprove bot requested a review from devversion March 7, 2025 10:33
@crisbeto crisbeto force-pushed the type-check-host-compiler branch from 7aa4001 to 0f4911a Compare March 8, 2025 13:08
@Harpush
Copy link
Harpush commented Mar 8, 2025

Is there a way to support syntax highlighting? From the provided image there is none

@crisbeto
Copy link
Member Author
crisbeto commented Mar 8, 2025

Yeah I have it on my TODO to look into syntax highlighting. It's in the vscode extension repo though.

@Harpush
Copy link
Harpush commented Mar 8, 2025

Yeah I have it on my TODO to look into syntax highlighting. It's in the vscode extension repo though.

The vscode extension generally needs more love and care 😁

@sheikalthaf
Copy link
Contributor

@crisbeto i really like to have tailwind support for class names in host binding. Whether it will enabled by this PR?

Copy link
Member
@JoostK JoostK left a comment

Choose a reason for hiding this comment

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

Excited for this feature! 💯


The commit message of the feat commit mentions:

Note that initially the new functionality is hidden behind the typeCheckHostBindings compiler flag

Since the option is public, I'd rephrase that to

Note that initially the new functionality is disabled by default and has to be enabled using the typeCheckHostBindings compiler flag.

return diag.file!.text.slice(diag.start!, diag.start! + diag.length!);
}

runInEachFileSystem(() => {
Copy link
Member

Choose a reason for hiding this comment

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

I havent't looked at all tests in detail, but I realised there weren't any cases for window: and document: prefixes and I haven't seen those being handled specially so they're probably incorrectly targeting the host element instead of window/document.

Copy link
Member Author

Choose a reason for hiding this comment

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

There's a document:click in the should check host event listeners with a target test and it seems to get inferred as a MouseEvent correctly.

Copy link
Member

Choose a reason for hiding this comment

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

I suppose that is because the host element for button[foo] also supports the click event, You'd likely see issues for host bindings for properties that are only available on document/window.

Copy link
Member Author

Choose a reason for hiding this comment

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

How can you access document or window in the handler expression though? The only way I can think of is $event.target, but even then TS infers it to be EventTarget rather than the node type.

Copy link
Member
@JoostK JoostK Mar 11, 2025

Choose a reason for hiding this comment

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

@Directive({
  host: {
    '(window:beforeunload)': 'beforeUnload($event)',
  },
})
export class MyCmp {
  beforeUnload(event: BeforeUnloadEvent) {}
}

Since the beforeunload event is mapped to BeforeUnloadEvent in WindowEventHandlersEventMap, it is specific to Window.addEventListener. I expect that we're currently targeting the host element itself, which likely has a different addEventListener that doesn't use WindowEventHandlersEventMap and thus doesn't map to the BeforeUnloadEvent.

Copy link
Member Author
@crisbeto crisbeto Mar 11, 2025

Choose a reason for hiding this comment

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

Okay I see what you mean. In that case the event gets inferred as Event, rather than BeforeUnloadEvent. That being said, we don't seem to handle this in the template TCB either. E.g. here's the same setup in a template:

image

Copy link
Member

Choose a reason for hiding this comment

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

I'd expect that the target prefix is designed to be used in host listeners, but it could be that they also (unintentionally, maybe?) work in templates?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a bit counter-intuitive, but they work in templates too. E.g. you can do <button (window:scroll)="..."> to listen to the global scroll event "through" some random button.

Copy link
Member
@devversion devversion left a comment

Choose a reason for hiding this comment

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

Looks pretty good to me. Nice work!

I agree with a few comments from Joost, and will defer further to him, but couldn't spot anything else on my own.

// back to the source. E.g. `@HostListener('foo') handleFoo() {}` in source code would look
// something like `(foo)="handleFoo()"` in the AST. Instead we construct the expressions
// manually here. Note that we use a dummy span with -1/-1 as offsets, because it isn't
// used for type checking and constructing it accurately would take some effort.
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity: since you are saying the source spans here aren't used for type checking, wouldn't it be possible to use a fake expression here with a "synthetic" source file?

Copy link
Member Author

Choose a reason for hiding this comment

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

For context, these are the spans within the expression itself, rather than the source file. E.g. the span for 'foo' would be start: 0, end: 6. It's fairly straightforward to count the number of characters for each expression, but I figured it's not worth the hassle considering that it's not used anywhere.

1E0A
Copy link
Member Author
@crisbeto crisbeto left a comment

Choose a reason for hiding this comment

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

Feedback has been addressed.

// back to the source. E.g. `@HostListener('foo') handleFoo() {}` in source code would look
// something like `(foo)="handleFoo()"` in the AST. Instead we construct the expressions
// manually here. Note that we use a dummy span with -1/-1 as offsets, because it isn't
// used for type checking and constructing it accurately would take some effort.
Copy link
Member Author

Choose a reason for hiding this comment

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

For context, these are the spans within the expression itself, rather than the source file. E.g. the span for 'foo' would be start: 0, end: 6. It's fairly straightforward to count the number of characters for each expression, but I figured it's not worth the hassle considering that it's not used anywhere.

Comment on lines +422 to +486
// 4. Constructing some sort of string like `<host ${name.getText()}=${initializer.getText()}/>`,
// passing it through the HTML parser and extracting the first attribute from it - wasn't explored
// much, but likely has the same issues as approach #3.
Copy link
Member Author

Choose a reason for hiding this comment

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

Ah interesting. When I was looking for a way to handle this, I was comparing with templates and noticed that it was dealing with it correctly. Too bad escapedString is in the ML parser, rather than the expression parser. In theory we could go with approach #4 so we can take advantage of escapedString, but it'll also come with a bit more overhead.

return diag.file!.text.slice(diag.start!, diag.start! + diag.length!);
}

runInEachFileSystem(() => {
Copy link
Member Author

Choose a reason for hiding this comment

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

There's a document:click in the should check host event listeners with a target test and it seems to get inferred as a MouseEvent correctly.

@crisbeto crisbeto force-pushed the type-check-host-compiler branch from 0f4911a to 845a13c Compare March 10, 2025 10:40
return diag.file!.text.slice(diag.start!, diag.start! + diag.length!);
}

runInEachFileSystem(() => {
Copy link
Member

Choose a reason for hiding this comment

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

I suppose that is because the host element for button[foo] also supports the click event, You'd likely see issues for host bindings for properties that are only available on document/window.

@crisbeto crisbeto force-pushed the type-check-host-compiler branch from 36de1f6 to 58fce16 Compare March 11, 2025 09:33
Copy link
Member
@pkozlowski-opensource pkozlowski-opensource left a comment

Choose a reason for hiding this comment

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

LGTM

Reviewed-for: public-api

@pullapprove pullapprove bot requested a review from atscott March 11, 2025 12:03
Copy link
Member
@devversion devversion left a comment

Choose a reason for hiding this comment

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

Reviewed-for: public-api

crisbeto added a commit to crisbeto/material2 that referenced this pull request Mar 14, 2025
Following up on angular#30626 (comment), these changes move the button's shared host bindings into the base class and turn some classes that are added manually into host bindings.

The previous approach was only necessary with ViewEngine, because it wasn't doing inheritance of host bindings correctly and wasn't merging classes. These are no longer issues in Ivy.

Finally, these changes allow us to take advantage of an upcoming framework feature: angular/angular#60267
crisbeto added a commit to crisbeto/material2 that referenced this pull request Mar 14, 2025
Following up on angular#30626 (comment), these changes move the button's shared host bindings into the base class and turn some classes that are added manually into host bindings.

The previous approach was only necessary with ViewEngine, because it wasn't doing inheritance of host bindings correctly and wasn't merging classes. These are no longer issues in Ivy.

Finally, these changes allow us to take advantage of an upcoming framework feature: angular/angular#60267
crisbeto added a commit to crisbeto/vscode-ng-language-service that referenced this pull request Mar 14, 2025
Makes the necessary changes on the vscode extension side to enable angular/angular#60267, including:
* Looking for completions, quick info etc inside the `host` property.
* Syntax highlighting for the `host` object literal.
Moves the `typeCheckHostBindings` option into `StrictTemplateOptions` and renames the latter.
@crisbeto crisbeto force-pushed the type-check-host-compiler branch from 58fce16 to eff1b1f Compare March 17, 2025 05:50
crisbeto added a commit to angular/components that referenced this pull request Mar 17, 2025
Following up on #30626 (comment), these changes move the button's shared host bindings into the base class and turn some classes that are added manually into host bindings.

The previous approach was only necessary with ViewEngine, because it wasn't doing inheritance of host bindings correctly and wasn't merging classes. These are no longer issues in Ivy.

Finally, these changes allow us to take advantage of an upcoming framework feature: angular/angular#60267
@crisbeto crisbeto added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Mar 17, 2025
crisbeto added a commit to crisbeto/vscode-ng-language-service that referenced this pull request Mar 17, 2025
Makes the necessary changes on the vscode extension side to enable angular/angular#60267, including:
* Looking for completions, quick info etc inside the `host` property.
* Syntax highlighting for the `host` object literal.
crisbeto added a commit to angular/vscode-ng-language-service that referenced this pull request Mar 17, 2025
Makes the necessary changes on the vscode extension side to enable angular/angular#60267, including:
* Looking for completions, quick info etc inside the `host` property.
* Syntax highlighting for the `host` object literal.
@pkozlowski-opensource
Copy link
Member

This PR was merged into the repository by commit bf2b025.

The changes were merged into the following branches: main

pkozlowski-opensource pushed a commit that referenced this pull request Mar 17, 2025
Updates the `R3TargetBinder` to ingest host element nodes.

PR Close #60267
pkozlowski-opensource pushed a commit that referenced this pull request Mar 17, 2025
Sets up the infrastructure to track the host bindings of directives for type checking purposes.

PR Close #60267
pkozlowski-opensource pushed a commit that referenced this pull request Mar 17, 2025
Adds the logic that creates a `HostElement` AST node based on the host bindings of a directive.

PR Close #60267
pkozlowski-opensource pushed a commit that referenced this pull request Mar 17, 2025
…n for components (#60267)

Sets up the logic that produces the information necessary to type check host bindings of a component. Also introduces a compiler flag for toggling checking of host bindings.

PR Close #60267
pkozlowski-opensource pushed a commit that referenced this pull request Mar 17, 2025
…0267)

Updates the directive handler to enable type checking of directives. Currently this only applies to checking of host bindings.

PR Close #60267
pkozlowski-opensource pushed a commit that referenced this pull request Mar 17, 2025
Historically Angular's type checking only extended to templates, however host bindings can contain expressions as well which can have type checking issues of their own. These changes expand the type checking infrastructure to cover the `host` object literal, `@HostBinding` decorators and `@HostListener` with full language service support coming in future commits.

Note that initially the new functionality is disabled by default and has to be enabled using the `typeCheckHostBindings` compiler flag.

PR Close #60267
pkozlowski-opensource pushed a commit that referenced this pull request Mar 17, 2025
Moves the `typeCheckHostBindings` option into `StrictTemplateOptions` and renames the latter.

PR Close #60267
crisbeto added a commit to crisbeto/angular that referenced this pull request Mar 24, 2025
The `TemplateLiteralElementExpr` has some logic where it tries to estimate the `rawText` if one isn't provided by looking at the node's source span. The problem with this approach is that we have some long-standing issues with our expression AST parser (see angular#60267 (comment)) where it might not produce accurate spans if escape sequences are involved. This in turn can lead to unrecoverable errors, because TypeScript will throw an error if the raw string doesn't match the cooked one when constructing a TypeScript AST node.

These changes remove the logic that depends on the source span and relies purely on the secondary fallback that inserts escaped characters manually.

It's also worth noting that the `rawText` doesn't seem to matter much at this point, because the main usage of it is when downlevelling template literals to ES5 which we no longer support.

Fixes angular#60528.
alxhub pushed a commit that referenced this pull request Mar 27, 2025
The `TemplateLiteralElementExpr` has some logic where it tries to estimate the `rawText` if one isn't provided by looking at the node's source span. The problem with this approach is that we have some long-standing issues with our expression AST parser (see #60267 (comment)) where it might not produce accurate spans if escape sequences are involved. This in turn can lead to unrecoverable errors, because TypeScript will throw an error if the raw string doesn't match the cooked one when constructing a TypeScript AST node.

These changes remove the logic that depends on the source span and relies purely on the secondary fallback that inserts escaped characters manually.

It's also worth noting that the `rawText` doesn't seem to matter much at this point, because the main usage of it is when downlevelling template literals to ES5 which we no longer support.

Fixes #60528.

PR Close #60529
crisbeto added a commit to crisbeto/angular that referenced this pull request Apr 5, 2025
)

The `TemplateLiteralElementExpr` has some logic where it tries to estimate the `rawText` if one isn't provided by looking at the node's source span. The problem with this approach is that we have some long-standing issues with our expression AST parser (see angular#60267 (comment)) where it might not produce accurate spans if escape sequences are involved. This in turn can lead to unrecoverable errors, because TypeScript will throw an error if the raw string doesn't match the cooked one when constructing a TypeScript AST node.

These changes remove the logic that depends on the source span and relies purely on the secondary fallback that inserts escaped characters manually.

It's also worth noting that the `rawText` doesn't seem to matter much at this point, because the main usage of it is when downlevelling template literals to ES5 which we no longer support.

Fixes angular#60528.

PR Close angular#60529
atscott pushed a commit that referenced this pull request Apr 7, 2025
…60753)

The `TemplateLiteralElementExpr` has some logic where it tries to estimate the `rawText` if one isn't provided by looking at the node's source span. The problem with this approach is that we have some long-standing issues with our expression AST parser (see #60267 (comment)) where it might not produce accurate spans if escape sequences are involved. This in turn can lead to unrecoverable errors, because TypeScript will throw an error if the raw string doesn't match the cooked one when constructing a TypeScript AST node.

These changes remove the logic that depends on the source span and relies purely on the secondary fallback that inserts escaped characters manually.

It's also worth noting that the `rawText` doesn't seem to matter much at this point, because the main usage of it is when downlevelling template literals to ES5 which we no longer support.

Fixes #60528.

PR Close #60529

PR Close #60753
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Apr 17, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: compiler Issues related to `ngc`, Angular's template compiler detected: feature PR contains a feature commit target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants
0