Skip to content

Commit 1f389b8

Browse files
JeanMechethePunderWoman
authored andcommitted
fix(compiler-cli): missingStructuralDirective diagnostic produces false negatives (angular#64579)
Fixes a bug in the missingStructuralDirective diagnostic where structural directives with missing imports were not reported when the element using the structural directive contained other directives Fixes angular#64467 co-authored-by: Matt Lewis <[email protected]> PR Close angular#64579
1 parent 9d48e53 commit 1f389b8

File tree

2 files changed

+129
-3
lines changed

2 files changed

+129
-3
lines changed

packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export const KNOWN_CONTROL_FLOW_DIRECTIVES = new Set([
2525
'ngForTrackBy',
2626
'ngSwitchCase',
2727
'ngSwitchDefault',
28+
'ngIfThen',
29+
'ngIfElse',
2830
]);
2931

3032
/**
@@ -65,9 +67,13 @@ class MissingStructuralDirectiveCheck extends TemplateCheckWithVisitor<ErrorCode
6567
if (!customStructuralDirective) return [];
6668

6769
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component);
68-
if (symbol?.directives.length) {
69-
return [];
70-
}
70+
// Check if there's a directive that matches the structural directive we're checking.
71+
// The structural directive *foo desugars to [foo], so we need to check if any
72+
// directive's selector would match the attribute [foo].
73+
const hasStructuralDirective = symbol?.directives.some((dir) =>
74+
dir.selector?.includes(`[${customStructuralDirective.name}]`),
75+
);
76+
if (hasStructuralDirective) return [];
7177

7278
const sourceSpan = customStructuralDirective.keySpan || customStructuralDirective.sourceSpan;
7379
const errorMessage =

packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/missing_structural_directive_spec.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,5 +337,125 @@ runInEachFileSystem(() => {
337337

338338
// What matters is that we don't get the missing structural directive diagnostic.
339339
});
340+
341+
it('should produce a warning when missing structural directive is used alongside an imported directive', () => {
342+
const fileName = absoluteFrom('/main.ts');
343+
const {program, templateTypeChecker} = setup([
344+
{
345+
fileName,
346+
templates: {
347+
'TestCmp': `
348+
<div *foo="bar" [bar]></div>
349+
`,
350+
},
351+
source: `
352+
export class TestCmp {}
353+
export class BarDirective {}
354+
`,
355+
declarations: [
356+
{
357+
type: 'directive',
358+
name: 'BarDirective',
359+
selector: '[bar]',
360+
inputs: {bar: 'bar'},
361+
},
362+
{
363+
name: 'TestCmp',
364+
type: 'directive',
365+
selector: `test-cmp`,
366+
isStandalone: true,
367+
},
368+
],
369+
},
370+
]);
371+
const sf = getSourceFileOrError(program, fileName);
372+
const component = getClass(sf, 'TestCmp');
373+
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
374+
templateTypeChecker,
375+
program.getTypeChecker(),
376+
[missingStructuralDirectiveCheck],
377+
{strictNullChecks: true} /* options */,
378+
);
379+
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
380+
381+
expect(diags.length).toBe(1);
382+
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
383+
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.MISSING_STRUCTURAL_DIRECTIVE));
384+
});
385+
386+
it('should *not* produce a warning when structural directive with more specific selector is imported', () => {
387+
const fileName = absoluteFrom('/main.ts');
388+
const {program, templateTypeChecker} = setup([
389+
{
390+
fileName,
391+
templates: {
392+
'TestCmp': `
393+
<div *bar></div>
394+
`,
395+
},
396+
source: `
397+
export class TestCmp {}
398+
export class BarDirective {}
399+
`,
400+
declarations: [
401+
{
402+
type: 'directive',
403+
name: 'BarDirective',
404+
selector: 'ng-template[bar]',
405+
},
406+
{
407+
name: 'TestCmp',
408+
type: 'directive',
409+
selector: `test-cmp`,
410+
isStandalone: true,
411+
},
412+
],
413+
},
414+
]);
415+
const sf = getSourceFileOrError(program, fileName);
416+
const component = getClass(sf, 'TestCmp');
417+
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
418+
templateTypeChecker,
419+
program.getTypeChecker(),
420+
[missingStructuralDirectiveCheck],
421+
{strictNullChecks: true} /* options */,
422+
);
423+
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
424+
425+
expect(diags.length).toBe(0);
426+
});
427+
428+
it('should *not* produce a warning for ngIf with template references using then/else', () => {
429+
const fileName = absoluteFrom('/main.ts');
430+
const {program, templateTypeChecker} = setup([
431+
{
432+
fileName,
433+
templates: {
434+
'TestCmp': `
435+
<ng-container *ngIf="foo; then bar; else bam"></ng-container>
436+
`,
437+
},
438+
declarations: [
439+
{
440+
name: 'TestCmp',
441+
type: 'directive',
442+
selector: `test-cmp`,
443+
isStandalone: true,
444+
},
445+
],
446+
},
447+
]);
448+
const sf = getSourceFileOrError(program, fileName);
449+
const component = getClass(sf, 'TestCmp');
450+
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
451+
templateTypeChecker,
452+
program.getTypeChecker(),
453+
[missingStructuralDirectiveCheck],
454+
{strictNullChecks: true} /* options */,
455+
);
456+
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
457+
458+
expect(diags.length).toBe(0);
459+
});
340460
});
341461
});

0 commit comments

Comments
 (0)