From ca1f1e11d95965cc8eb03f14f77788ee77282175 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Thu, 19 May 2022 22:48:18 -0700 Subject: [PATCH] fix(core): produce proper error message for unknown props on ``s Currently for cases when an unknown structural directive is applied to ``s, an error message thrown by the framework doesn't contain a tag name, for example: ``` NG0303: Can't bind to 'unknownDir' since it isn't a known property of 'null'. ``` The underlying reason is that the tag name for the `` is not produced (`null` is useed as a value) by the compiler in case of inline templates and runtime logic relies on this effect. This commit handles this situation when an error message is thrown, as the fastest way to improve the error message. More refactoring would be needed to avoid relying on the mentioned effect at runtime. --- .../core/src/render3/instructions/shared.ts | 17 +++- .../core/test/acceptance/ng_module_spec.ts | 84 +++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 951097697469d4..7d0c0bf4e05f52 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -1034,7 +1034,7 @@ export function elementPropertyInternal( validateAgainstEventProperties(propName); if (!validateProperty(element, tNode.value, propName, tView.schemas)) { // Return here since we only log warnings for unknown properties. - handleUnknownPropertyError(propName, tNode.value); + handleUnknownPropertyError(propName, tNode); return; } ngDevMode.rendererSetProperty++; @@ -1053,7 +1053,7 @@ export function elementPropertyInternal( // If the node is a container and the property didn't // match any of the inputs or schemas we should throw. if (ngDevMode && !matchingSchemas(tView.schemas, tNode.value)) { - handleUnknownPropertyError(propName, tNode.value); + handleUnknownPropertyError(propName, tNode); } } } @@ -1170,7 +1170,18 @@ export function matchingSchemas(schemas: SchemaMetadata[]|null, tagName: string| * @param propName Name of the invalid property. * @param tagName Name of the node on which we encountered the property. */ -function handleUnknownPropertyError(propName: string, tagName: string): void { +function handleUnknownPropertyError(propName: string, tNode: TNode): void { + let tagName = tNode.value; + + // Special-case a situation when a structural directive is applied to + // an `` element, for example: ``. + // In this case the compiler generates the `ɵɵtemplate` instruction with + // the `null` as the tagName. The directive matching logic at runtime relies + // on this effect (see `isInlineTemplate`), thus using the 'ng-template' as + // a default value of the `tNode.value` is not feasible at this moment. + if (!tagName && tNode.type === TNodeType.Container) { + tagName = 'ng-template'; + } const message = `Can't bind to '${propName}' since it isn't a known property of '${tagName}'.`; if (shouldThrowErrorOnUnknownProperty) { throw new RuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message); diff --git a/packages/core/test/acceptance/ng_module_spec.ts b/packages/core/test/acceptance/ng_module_spec.ts index 768f5c8e09dedf..e346602efa2b53 100644 --- a/packages/core/test/acceptance/ng_module_spec.ts +++ b/packages/core/test/acceptance/ng_module_spec.ts @@ -255,6 +255,90 @@ describe('NgModule', () => { expect(spy.calls.mostRecent().args[0]) .toMatch(/Can't bind to 'unknown-prop' since it isn't a known property of 'div'/); }); + + it('should log an error on unknown props of `ng-template` if NO_ERRORS_SCHEMA is absent', + () => { + @Component({ + selector: 'my-comp', + template: ` + + `, + }) + class MyComp { + condition = true; + } + + @NgModule({ + declarations: [MyComp], + }) + class MyModule { + } + + TestBed.configureTestingModule({imports: [MyModule]}); + + const spy = spyOn(console, 'error'); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(spy.calls.mostRecent().args[0]) + .toMatch(/Can't bind to 'ngIf' since it isn't a known property of 'ng-template'/); + }); + + it('should log an error on unknown props of `ng-container` if NO_ERRORS_SCHEMA is absent', + () => { + @Component({ + selector: 'my-comp', + template: ` + + `, + }) + class MyComp { + condition = true; + } + + @NgModule({ + declarations: [MyComp], + }) + class MyModule { + } + + TestBed.configureTestingModule({imports: [MyModule]}); + + const spy = spyOn(console, 'error'); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(spy.calls.mostRecent().args[0]) + .toMatch(/Can't bind to 'ngIf' since it isn't a known property of 'ng-container'/); + }); + + it('should log an error on unknown props of `ng-content` if NO_ERRORS_SCHEMA is absent', () => { + @Component({ + selector: 'my-comp', + template: ` + + `, + }) + class MyComp { + condition = true; + } + + @NgModule({ + declarations: [MyComp], + }) + class MyModule { + } + + TestBed.configureTestingModule({imports: [MyModule]}); + + const spy = spyOn(console, 'error'); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(spy.calls.mostRecent().args[0]) + .toMatch(/Can't bind to 'ngIf' since it isn't a known property of 'ng-content'/); + }); + it('should throw an error with errorOnUnknownProperties on unknown props if NO_ERRORS_SCHEMA is absent', () => { @Component({