Skip to content

feat(compiler-cli): detect missing structural directive imports#59443

Closed
manbearwiz wants to merge 7 commits intoangular:mainfrom
manbearwiz:check-missing-structural-directives
Closed

feat(compiler-cli): detect missing structural directive imports#59443
manbearwiz wants to merge 7 commits intoangular:mainfrom
manbearwiz:check-missing-structural-directives

Conversation

@manbearwiz
Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Feature

What is the current behavior?

Issue Number: #37322

Currently, the compiler will warn about missing directive imports if they are one of the "known" structural directives from the CommonModule or use regular binding syntax. This extends that to include custom, user-defined, structural directives. Currently, missing an import for these custom structural directives just fails silently which is a pain point when doing standalone migrations.

What is the new behavior?

The compiler will now warn on missing custom structural directives.

Does this PR introduce a breaking change?

  • Yes
  • No

I made minimal changes and left existing logic in order to ensure the changes are not breaking. I am definitely open to taking another approach, adding more test cases, etc.

@pullapprove pullapprove bot requested a review from devversion January 9, 2025 07:02
@angular-robot angular-robot bot added the detected: feature PR contains a feature commit label Jan 9, 2025
Copy link
Copy Markdown
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.

LGTM, thanks for the PR.

The diagnostic is named control flow, but might be fine. Also adding @thePunderWoman as the original author IIRC

@devversion devversion added the action: review The PR is still awaiting reviews from at least one requested reviewer label Jan 9, 2025
@manbearwiz
Copy link
Copy Markdown
Contributor Author

The diagnostic is named control flow, but might be fine.

Yeah that was one of the things I was unsure about. Would it be better as a separate diagnostic? Would renaming this diagnostic be considered a breaking change?

@thePunderWoman
Copy link
Copy Markdown
Contributor

@manbearwiz Since this extended diagnostic is specifically about control flow, this seems out of scope to me. I think it would be better to have a separate optional template diagnostic for this since it doesn't apply to everyone. What do you think?

@kirjs kirjs added the area: compiler Issues related to `ngc`, Angular's template compiler label Jan 9, 2025
@ngbot ngbot bot added this to the Backlog milestone Jan 9, 2025
@manbearwiz
Copy link
Copy Markdown
Contributor Author

manbearwiz commented Jan 9, 2025

That works for me. I'll update it to be a separate diagnostic. Any other suggestions while I'm in there? This will definitely touch a lot more files

@angular-robot angular-robot bot removed the area: compiler Issues related to `ngc`, Angular's template compiler label Jan 9, 2025
@ngbot ngbot bot removed this from the Backlog milestone Jan 9, 2025
@manbearwiz manbearwiz force-pushed the check-missing-structural-directives branch 2 times, most recently from f0b2991 to 9cdc812 Compare January 9, 2025 20:25
@kirjs kirjs added the area: compiler Issues related to `ngc`, Angular's template compiler label Jan 9, 2025
@ngbot ngbot bot added this to the Backlog milestone Jan 9, 2025
@manbearwiz manbearwiz changed the title feat(compiler-cli): detect missing structural directive imports in standalone components feat(compiler-cli): detect missing structural directive imports Jan 9, 2025
@thePunderWoman thePunderWoman added area: compiler-cli area: compiler Issues related to `ngc`, Angular's template compiler target: rc This PR is targeted for the next release-candidate and removed area: compiler Issues related to `ngc`, Angular's template compiler area: compiler-cli labels Jan 10, 2025
Copy link
Copy Markdown
Contributor

@thePunderWoman thePunderWoman left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks!

@thePunderWoman thePunderWoman added action: presubmit The PR is in need of a google3 presubmit and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Jan 10, 2025
@thePunderWoman
Copy link
Copy Markdown
Contributor

@manbearwiz It looks like you have legitimate test failures. Can you take a look?

@thePunderWoman thePunderWoman 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: presubmit The PR is in need of a google3 presubmit labels Jan 10, 2025
@manbearwiz
Copy link
Copy Markdown
Contributor Author

Should be good to go

@JeanMeche
Copy link
Copy Markdown
Member

JeanMeche commented Mar 28, 2025

Running the diagnostic against google's codebase surfaced some interesting issues.

I could narrow it down as following :

@Component({ template: '' })
class UnrelatedComponent {} // unexported

@Directive({ selector: '[foo]' })
export class Foo {}

@Component({
  selector: 'app-root',
  imports: [Foo],
  template: `<div *foo></div>`,
})
export class AppComponent {}

If there is an unexported component, even if that component is unrelated, the diagnostic raises a false positive :

A structural directive `foo` was used in the template without a corresponding import in the component. 
Make sure that the directive is included in the `@Component.imports` array of this component

I pushed a (currently) broken test as a fixup, feel free to try to fix it.

@JoostK
Copy link
Copy Markdown
Member

JoostK commented Mar 28, 2025

I pushed a (currently) broken test as a fixup, feel free to try to fix it.

We've been discussing this a bit and we do understand why this is happening, which isn't too easy to address.

@manbearwiz
Copy link
Copy Markdown
Contributor Author

We've been discussing this a bit and we do understand why this is happening, which isn't too easy to address.

I pushed a commit before I read this. I didn't look that thoroughly, but it seems like it ends up doing a reference comparison on nodes from 2 different ASTs? I definitely don't know enough about the ng compiler to know the best solution. My commit just changes the reference check to a name comparison. That's enough to make all the tests pass, but I would defer to you whether there are additional fields to compare or if different nodes should be passed into that fn entirely.

@JoostK
Copy link
Copy Markdown
Member

JoostK commented Mar 28, 2025

Yeah that would pass the test but is unreliable; two different classes with equal name would be incorrectly matched. For this particular use-case that might be an acceptable trade-off for simplicity, but in the general sense it is incorrect and I think we need a better check.

The problem occurs because Angular's template type-checking approach operates in a separate TypeScript ts.Program from the ts.Program that is used for the primary compilation. The second ts.Program is mostly identical except for containing Angular-specific type-checking code (TCBs, for type-check blocks). These TCBs are typically emitted into separate .ngtypecheck.ts files that are adjacent to the original source files, but in the case of non-exported classes the compiler wouldn't be able to reference the backing type. Hence, it uses a different strategy where the TCBs are emitted into the original source file (but only within the second ts.Program that is specific to type-checking) and this means that the AST has to be recreated, creating new object identities for all AST nodes. Then, type-checking symbols use the AST from the type-checkingts.Program, but directive metadata that has been gathered is obtained from the original ts.Program, resulting in the reference equality to miss.

@manbearwiz
Copy link
Copy Markdown
Contributor Author

manbearwiz commented Mar 28, 2025

Thanks for the explanation! That makes a lot of sense and matches what I was seeing when I was debugging.
image
When the TCBs are emitted into the source files are they guaranteed to be at the end? In that case, position and file path might work to give us more confidence in a match?

Do we know if this affects any of the other diagnostics? I'm guessing the missing control flow check has this same issue.

edit: I checked and yeah, tests in missing_control_flow_directive_spec.ts do break when you add an unexported class.

@JoostK
Copy link
Copy Markdown
Member

JoostK commented Mar 28, 2025

The type-check file may be adjusted in various places:

/**
* Type check blocks are inserted immediately after the end of the directve class.
*/
get splitPoint(): number {
return this.ref.node.end + 1;
}

/**
* Type constructor operations are inserted immediately before the end of the directive class.
*/
get splitPoint(): number {
return this.ref.node.end - 1;
}

as well as additional import statements at the top.

My suggestion would be to search for declarations of the same name in both files, then cross-reference based on their index in this list. TCB-generated code shouldn't have conflicting names so the indices should be stable. We'd only need to do this if we know that the type-check declaration is within a source file that was updated in the type-checking ts.Program:

export const NgOriginalFile = Symbol('NgOriginalFile');
/**
* If an updated file has an associated original source file, then the original source file
* is attached to the updated file using the `NgOriginalFile` symbol.
*/
export interface MaybeSourceFileWithOriginalFile extends ts.SourceFile {
[NgOriginalFile]?: ts.SourceFile;
}

so the perf impact of this (realizing this approach is quite expensive) may not be too much of a concern?

@manbearwiz
Copy link
Copy Markdown
Contributor Author

I think that makes sense. I want to see if I can make a test case that makes the simple name comparison fail.

Since this is a bug that affects existing diagnostics, do we want to handle this in this feature PR or should it be a seperate bug fix PR that would go in first?

@JeanMeche
Copy link
Copy Markdown
Member

Having it in this PR could help us check if this actually fixes any false-positive we detected in Google's monorepo.

I think it would be fine as a separate commit.

manbearwiz and others added 2 commits April 25, 2025 17:06
Adds a new diagnostic that ensures that a standalone component using custom structural directives in a template has the necessary imports for those directives.

Fixes angular#37322
@JeanMeche
Copy link
Copy Markdown
Member

JeanMeche commented Apr 25, 2025

I found a final breakage within the Google codebase.

@Directive({ selector: '[foo]' })
export class FooDir {}

export function foo() {
  @Component({
    selector: 'error',
    imports: [FooDir],
    template: '<div *foo></div>',
  })
  class MyCmp {}
}

A function that wraps a component and which has a structural directive will report the import error despite having the correct import.

This pattern was found in 2 unit tests, so it's really a rare case.

Comment on lines 176 to 189
Copy link
Copy Markdown
Member

@JeanMeche JeanMeche Apr 26, 2025

Choose a reason for hiding this comment

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

This fixes the issue with Components declared inside functions, but I have a medium confidence that this is the right fix.

Copy link
Copy Markdown
Member

@JeanMeche JeanMeche Apr 27, 2025

Choose a reason for hiding this comment

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

Had to introduce this change as compilation broke for this file

it('should support two-way binding to a signal @for loop variable', () => {
@Directive({selector: '[dir]'})
class Dir {
value = model(0);
}
@Component({
template: `
@for (value of values; track $index) {
<div [(value)]="value" dir></div>
}
`,
imports: [Dir],
})
class App {
@ViewChild(Dir) dir!: Dir;
values = [signal(1)];
}
const fixture = TestBed.createComponent(App);

@JeanMeche
Copy link
Copy Markdown
Member

JeanMeche commented Apr 28, 2025

G3 has been cleaned up. We'll go through a re-review after the recent changes.

@ngbot
Copy link
Copy Markdown

ngbot bot commented Apr 29, 2025

I see that you just added the action: merge label, but the following checks are still failing:
    failure status "google-internal-tests" is failing

If you want your PR to be merged, it has to pass all the CI checks.

If you can't get the PR to a green state due to flakes or broken main, please try rebasing to main and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help.

@JeanMeche
Copy link
Copy Markdown
Member

Passing TGP, the broken target got fixed in between.

@mmalerba
Copy link
Copy Markdown
Contributor

This PR was merged into the repository by commit c889382.

The changes were merged into the following branches: main

@JoostK
Copy link
Copy Markdown
Member

JoostK commented Apr 29, 2025

@manbearwiz Thanks for your effort into this PR! 💯

@manbearwiz
Copy link
Copy Markdown
Contributor Author

manbearwiz commented Apr 30, 2025

Of course! Glad it got in. Thanks @JeanMeche for taking it the rest of the way!

@angular-automatic-lock-bot
Copy link
Copy Markdown

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.

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

Labels

action: global presubmit The PR is in need of a google3 global presubmit action: merge The PR is ready for merge by the caretaker area: compiler Issues related to `ngc`, Angular's template compiler compiler: extended diagnostics detected: feature PR contains a feature commit PullApprove: disable requires: TGP This PR requires a passing TGP before merging is allowed target: major This PR is targeted for the next major release

Projects

None yet

Development

Successfully merging this pull request may close these issues.