diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/index.ts index 26d5a4d31af..b0c6e1bf51e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/index.ts @@ -25,6 +25,8 @@ export const KNOWN_CONTROL_FLOW_DIRECTIVES = new Set([ 'ngForTrackBy', 'ngSwitchCase', 'ngSwitchDefault', + 'ngIfThen', + 'ngIfElse', ]); /** @@ -65,9 +67,13 @@ class MissingStructuralDirectiveCheck extends TemplateCheckWithVisitor + dir.selector?.includes(`[${customStructuralDirective.name}]`), + ); + if (hasStructuralDirective) return []; const sourceSpan = customStructuralDirective.keySpan || customStructuralDirective.sourceSpan; const errorMessage = diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/missing_structural_directive_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/missing_structural_directive_spec.ts index fa996884617..9664a9e73c0 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/missing_structural_directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/missing_structural_directive_spec.ts @@ -337,5 +337,125 @@ runInEachFileSystem(() => { // What matters is that we don't get the missing structural directive diagnostic. }); + + it('should produce a warning when missing structural directive is used alongside an imported directive', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': ` +
+ `, + }, + source: ` + export class TestCmp {} + export class BarDirective {} + `, + declarations: [ + { + type: 'directive', + name: 'BarDirective', + selector: '[bar]', + inputs: {bar: 'bar'}, + }, + { + name: 'TestCmp', + type: 'directive', + selector: `test-cmp`, + isStandalone: true, + }, + ], + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [missingStructuralDirectiveCheck], + {strictNullChecks: true} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + + expect(diags.length).toBe(1); + expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.MISSING_STRUCTURAL_DIRECTIVE)); + }); + + it('should *not* produce a warning when structural directive with more specific selector is imported', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': ` +
+ `, + }, + source: ` + export class TestCmp {} + export class BarDirective {} + `, + declarations: [ + { + type: 'directive', + name: 'BarDirective', + selector: 'ng-template[bar]', + }, + { + name: 'TestCmp', + type: 'directive', + selector: `test-cmp`, + isStandalone: true, + }, + ], + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [missingStructuralDirectiveCheck], + {strictNullChecks: true} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + + expect(diags.length).toBe(0); + }); + + it('should *not* produce a warning for ngIf with template references using then/else', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': ` + + `, + }, + declarations: [ + { + name: 'TestCmp', + type: 'directive', + selector: `test-cmp`, + isStandalone: true, + }, + ], + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [missingStructuralDirectiveCheck], + {strictNullChecks: true} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + + expect(diags.length).toBe(0); + }); }); });