Skip to content
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

Type Constructors for Generic Directives #41043

Closed
wants to merge 2 commits into from

Conversation

zarend
Copy link
Contributor

@zarend zarend commented Mar 2, 2021

Background Knowledge Required

This requires a bit of background knowledge

  • How to use bound generics in typescript
  • How to create a Directive in angular
  • type-check-block (tcb) generation
    -- type constructors
    -- TcbOps
    -- inlining, specifically of type constructors

Motivation

This PR came from an edge case with generic directives – specifically directives with bound generic parameters.

Creating a generic directive with a bound parameters creates an edge case for TCB generation. Let's see the behavior in VSCode for this situation.

In this screenshot, we have a two confusing error messages.

  1. ng serve reports that we cannot bind to notAnInput because that property does not exist. This error is expected, but it doesn't show up in the IDE.

  2. the language service threw an exception with This component requires inline type checking which is not available in this environment.

Screen Shot 2021-03-05 at 9 31 03 AM

The type generation code cannot inline the type constructor for this component, which causes it to throw an exception before finishing type checking the template. This means that part of the template will not be typechecked and VSCode will not be able to present diagnostics in the skipped sections.

Why bound generics are a "no-go" for compiling TCBs

Bound generics can reference private members which are impossible for the compiler to import into the tcb. They can also contain other things such as symbols which would be impossible to import.

Application Code

interface PrivateInterface{}

@Componet({ selector: 'bad' })
export class BadComponent <T extends PrivateInterface>{}

TCB bad.component.typecheck.ts

  var ctor1: ()<T extends PrivateInterface> => BadComponent<T> = () => (null!);
  // but wait, how do we import PrivateInterface?

The solutions approach is to assign any to generic arguments for constructors with bound parameters.

var ctor1: BadComponent<any> = (null!);

Why inline type constructors impact performance.

When we compile a program that requires inlining type constructors, we change add generated code to the component file that the user originally wrote.

bad.component.ts

...
export class BadComponent <T extends PrivateInterface>{
...
// type constructor will look something like this, but not exactly like this
ngTypeConstructor: ()<T> => BadComponent<T> = () => (null!) 
}

Changing this file can trigger a second compile, which increases the total compilation time.

Also, we cannot change a user's file while it's been edited, which is why inlining is not available for the language service. It's only available when we compile a build target for the program.

Solution

This is a 2-part PR.

The first commit refactors our tests, which allows use to use TDD in the next commit, which fixes the bug.

Part 1: refactor tests

For the tests in //packages/compiler-cli/src/ngtsc/typecheck, this
commits uses a TypeCheckFile for the environment, rather than a
FakeEnvironment. Using a real environment gives us more flexibility
with testing.

Part 2: implement fix for generic directives with bound parameters

This commit fixes the behavior when creating a type constructor for a directive when the following conditions are met.

  1. The directive has bound generic parameters.
  2. Inlining is not available. (This happens for language service compiles).

Previously, we would throw an error saying 'Inlining is not supported in this environment.' The compiler would stop type checking, and the developer could lose out on getting errors after the compiler gives up.

This commit adds a useInlineTypeConstructors to the type check config. When set to false, we use any type for bound generic parameters to avoid crashing. When set to true, we inline the type constructor when inlining is required.

@google-cla google-cla bot added the cla: yes label Mar 2, 2021
@pullapprove pullapprove bot requested a review from JoostK March 2, 2021 00:32
@zarend zarend added the area: compiler Issues related to `ngc`, Angular's template compiler label Mar 2, 2021
@ngbot ngbot bot added this to the Backlog milestone Mar 2, 2021
@zarend zarend force-pushed the type-constructors branch 2 times, most recently from d4dff54 to a0fba5a Compare March 4, 2021 22:45
@zarend zarend changed the title progress on milestone 1 Directive Type Constructors and Inlining Mar 4, 2021
@zarend zarend changed the title Directive Type Constructors and Inlining Type Constructors for Generic Directives Mar 4, 2021
@zarend zarend force-pushed the type-constructors branch 2 times, most recently from 7cd920d to 6acd6d9 Compare March 4, 2021 23:06
@zarend zarend added the action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews label Mar 4, 2021
@zarend zarend force-pushed the type-constructors branch 4 times, most recently from 4c42512 to 6b2d963 Compare March 4, 2021 23:59
@zarend zarend added action: review The PR is still awaiting reviews from at least one requested reviewer and removed action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews labels Mar 5, 2021
@zarend zarend requested review from atscott and alxhub March 5, 2021 18:12
Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

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

You have a pretty good overview of the issue in the comment. Please add a similar description to the commit message. The commit messages should contain all the same information about why a change is being made - it shouldn't only be in the PR comment.

* Executing this operation returns a reference to the directive instance variable with its generic
* type parameters set to `any`.
*/
class TcbDirectiveTypeOpWithGenericParams extends TcbOp {
Copy link
Member

@alxhub alxhub Mar 5, 2021

Choose a reason for hiding this comment

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

All of our TCB operations have names of the form Tcb*Op, and so should this one.

I'm thinking TcbGenericDirectiveTypeWithAnyParamsOp (it's kind of unfortunate that "any" has a confusing meaning in a short name like this).

We could also rename TcbDirectiveTypeOp to TcbNonGenericDirectiveTypeOp to more cleanly separate the two.

@@ -690,6 +690,7 @@ export class NgCompiler {
useContextGenericType: strictTemplates,
strictLiteralTypes: true,
enableTemplateTypeChecker: this.enableTemplateTypeChecker,
useInlineTypeConstructors: false,
Copy link
Member

Choose a reason for hiding this comment

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

This should definitely not be false. Inline type constructors are required for how the compiler normally operates.

This should probably be set based on whether the current TypeCheckingProgramStrategy supports inline operations (it exposes a supportsInlineOperations property to report this).

Copy link
Contributor Author

@zarend zarend Mar 9, 2021

Choose a reason for hiding this comment

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

🤦 , that is correct.

For strict mode, I made it
useInlineTypeConstructors: this.typeCheckingProgramStrategy.supportsInlineOperations,
That's because inlining gives us strict type checking with bound generics because we do not have to set the generic params to any.

For non-strict mode, I set it to false. That's because we want to be as lenient as possible in non-strict mode. When useInlineTypeConstructors is false, we use any for the generic params of bound generics instead of trying to infer the types. It's better to avoid inlining unless we have to because it's problematic ( perf issues, complications with editing user's files).

@zarend zarend added action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Mar 8, 2021
@zarend zarend force-pushed the type-constructors branch 5 times, most recently from 513df2f to e87111c Compare March 9, 2021 16:53
class TcbGenericDirectiveTypeWithAnyParamsOp extends TcbDirectiveTypeBaseOp {
execute(): ts.Identifier {
const dirRef = this.dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
if (dirRef.node.typeParameters === undefined) {
Copy link
Member

Choose a reason for hiding this comment

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

Should we check !this.dir.isGeneric too?

@zarend zarend force-pushed the type-constructors branch 2 times, most recently from d8deebd to d959c06 Compare March 17, 2021 16:29
@zarend zarend added action: presubmit The PR is in need of a google3 presubmit target: patch This PR is targeted for the next patch release and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Mar 17, 2021
For the tests in //packages/compiler-cli/src/ngtsc/typecheck, this
commits uses a `TypeCheckFile` for the environment, rather than a
`FakeEnvironment`. Using a real environment gives us more flexibility
with testing.
This commit fixes the behavior when creating a type constructor for a directive when the following
conditions are met.
1. The directive has bound generic parameters.
2. Inlining is not available. (This happens for language service compiles).

Previously, we would throw an error saying 'Inlining is not supported in this environment.' The
compiler would stop type checking, and the developer could lose out on getting errors after the
compiler gives up.

This commit adds a useInlineTypeConstructors to the type check config. When set to false, we use
`any` type for bound generic parameters to avoid crashing. When set to true, we inline the type
constructor when inlining is required.

Addresses angular#40963
@zarend zarend removed the request for review from JoostK March 17, 2021 17:07
@zarend zarend added action: merge The PR is ready for merge by the caretaker and removed action: presubmit The PR is in need of a google3 presubmit labels Mar 17, 2021
@mhevery mhevery added target: major This PR is targeted for the next major release target: patch This PR is targeted for the next patch release and removed target: patch This PR is targeted for the next patch release target: major This PR is targeted for the next major release labels Mar 18, 2021
@mhevery mhevery closed this in c267c68 Mar 18, 2021
mhevery pushed a commit that referenced this pull request Mar 18, 2021
)

This commit fixes the behavior when creating a type constructor for a directive when the following
conditions are met.
1. The directive has bound generic parameters.
2. Inlining is not available. (This happens for language service compiles).

Previously, we would throw an error saying 'Inlining is not supported in this environment.' The
compiler would stop type checking, and the developer could lose out on getting errors after the
compiler gives up.

This commit adds a useInlineTypeConstructors to the type check config. When set to false, we use
`any` type for bound generic parameters to avoid crashing. When set to true, we inline the type
constructor when inlining is required.

Addresses #40963

PR Close #41043
@mhevery
Copy link
Contributor

mhevery commented Mar 18, 2021

@zarend This had a merge conflict on patch branch. Please create a new PR and label accordingly

@zarend
Copy link
Contributor Author

zarend commented Mar 18, 2021

@zarend This had a merge conflict on patch branch. Please create a new PR and label accordingly

Thanks, here's a PR to merge it into patch branch.
#41268

@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 18, 2021
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 cla: yes target: patch This PR is targeted for the next patch release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants