feat(ivy): better error messages for unknown components (#33064)
For elements in a template that look like custom elements, i.e. containing a dash in their name, the template type checker will now issue an error with instructions on how the resolve the issue. Additionally, a property binding to a non-existent property will also produce a more descriptive error message. Resolves FW-1597 PR Close #33064
This commit is contained in:
		
							parent
							
								
									c88305d2eb
								
							
						
					
					
						commit
						d8249d1230
					
				| @ -61,7 +61,7 @@ export interface DomSchemaChecker { | ||||
|  * Checks non-Angular elements and properties against the `DomElementSchemaRegistry`, a schema | ||||
|  * maintained by the Angular team via extraction from a browser IDL. | ||||
|  */ | ||||
| export class RegistryDomSchemaChecker { | ||||
| export class RegistryDomSchemaChecker implements DomSchemaChecker { | ||||
|   private _diagnostics: ts.Diagnostic[] = []; | ||||
| 
 | ||||
|   get diagnostics(): ReadonlyArray<ts.Diagnostic> { return this._diagnostics; } | ||||
| @ -71,9 +71,21 @@ export class RegistryDomSchemaChecker { | ||||
|   checkElement(id: string, element: TmplAstElement, schemas: SchemaMetadata[]): void { | ||||
|     if (!REGISTRY.hasElement(element.name, schemas)) { | ||||
|       const mapping = this.resolver.getSourceMapping(id); | ||||
| 
 | ||||
|       let errorMsg = `'${element.name}' is not a known element:\n`; | ||||
|       errorMsg += | ||||
|           `1. If '${element.name}' is an Angular component, then verify that it is part of this module.\n`; | ||||
|       if (element.name.indexOf('-') > -1) { | ||||
|         errorMsg += | ||||
|             `2. If '${element.name}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.`; | ||||
|       } else { | ||||
|         errorMsg += | ||||
|             `2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`; | ||||
|       } | ||||
| 
 | ||||
|       const diag = makeTemplateDiagnostic( | ||||
|           mapping, element.sourceSpan, ts.DiagnosticCategory.Error, | ||||
|           ErrorCode.SCHEMA_INVALID_ELEMENT, `'${element.name}' is not a valid HTML element.`); | ||||
|           ErrorCode.SCHEMA_INVALID_ELEMENT, errorMsg); | ||||
|       this._diagnostics.push(diag); | ||||
|     } | ||||
|   } | ||||
| @ -83,9 +95,22 @@ export class RegistryDomSchemaChecker { | ||||
|       schemas: SchemaMetadata[]): void { | ||||
|     if (!REGISTRY.hasProperty(element.name, name, schemas)) { | ||||
|       const mapping = this.resolver.getSourceMapping(id); | ||||
| 
 | ||||
|       let errorMsg = | ||||
|           `Can't bind to '${name}' since it isn't a known property of '${element.name}'.`; | ||||
|       if (element.name.startsWith('ng-')) { | ||||
|         errorMsg += | ||||
|             `\n1. If '${name}' is an Angular directive, then add 'CommonModule' to the '@NgModule.imports' of this component.` + | ||||
|             `\n2. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`; | ||||
|       } else if (element.name.indexOf('-') > -1) { | ||||
|         errorMsg += | ||||
|             `\n1. If '${element.name}' is an Angular component and it has '${name}' input, then verify that it is part of this module.` + | ||||
|             `\n2. If '${element.name}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.` + | ||||
|             `\n3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`; | ||||
|       } | ||||
| 
 | ||||
|       const diag = makeTemplateDiagnostic( | ||||
|           mapping, span, ts.DiagnosticCategory.Error, ErrorCode.SCHEMA_INVALID_ATTRIBUTE, | ||||
|           `'${name}' is not a valid property of <${element.name}>.`); | ||||
|           mapping, span, ts.DiagnosticCategory.Error, ErrorCode.SCHEMA_INVALID_ATTRIBUTE, errorMsg); | ||||
|       this._diagnostics.push(diag); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -150,7 +150,7 @@ runInEachFileSystem(() => { | ||||
| 
 | ||||
|       expect(messages).toEqual([ | ||||
|         `synthetic.html(1, 29): Property 'heihgt' does not exist on type 'TestComponent'. Did you mean 'height'?`, | ||||
|         `synthetic.html(1, 6): 'srcc' is not a valid property of <img>.`, | ||||
|         `synthetic.html(1, 6): Can't bind to 'srcc' since it isn't a known property of 'img'.`, | ||||
|       ]); | ||||
|     }); | ||||
| 
 | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
| 
 | ||||
| import * as ts from 'typescript'; | ||||
| 
 | ||||
| import {absoluteFrom as _} from '../../src/ngtsc/file_system'; | ||||
| import {absoluteFrom as _, getFileSystem} from '../../src/ngtsc/file_system'; | ||||
| import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; | ||||
| import {loadStandardTestFiles} from '../helpers/src/mock_file_loading'; | ||||
| 
 | ||||
| @ -365,7 +365,29 @@ export declare class CommonModule { | ||||
|       `);
 | ||||
|         const diags = env.driveDiagnostics(); | ||||
|         expect(diags.length).toBe(1); | ||||
|         expect(diags[0].messageText).toBe(`'foo' is not a valid HTML element.`); | ||||
|         expect(diags[0].messageText).toBe(`'foo' is not a known element:
 | ||||
| 1. If 'foo' is an Angular component, then verify that it is part of this module. | ||||
| 2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`);
 | ||||
|       }); | ||||
| 
 | ||||
|       it('should have a descriptive error for unknown elements that contain a dash', () => { | ||||
|         env.write('test.ts', ` | ||||
|         import {Component, NgModule} from '@angular/core'; | ||||
|         @Component({ | ||||
|           selector: 'blah', | ||||
|           template: '<my-foo>test</my-foo>', | ||||
|         }) | ||||
|         export class FooCmp {} | ||||
|         @NgModule({ | ||||
|           declarations: [FooCmp], | ||||
|         }) | ||||
|         export class FooModule {} | ||||
|       `);
 | ||||
|         const diags = env.driveDiagnostics(); | ||||
|         expect(diags.length).toBe(1); | ||||
|         expect(diags[0].messageText).toBe(`'my-foo' is not a known element:
 | ||||
| 1. If 'my-foo' is an Angular component, then verify that it is part of this module. | ||||
| 2. If 'my-foo' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.`);
 | ||||
|       }); | ||||
| 
 | ||||
|       it('should check for unknown properties', () => { | ||||
| @ -383,7 +405,27 @@ export declare class CommonModule { | ||||
|       `);
 | ||||
|         const diags = env.driveDiagnostics(); | ||||
|         expect(diags.length).toBe(1); | ||||
|         expect(diags[0].messageText).toBe(`'foo' is not a valid property of <div>.`); | ||||
|         expect(diags[0].messageText) | ||||
|             .toBe(`Can't bind to 'foo' since it isn't a known property of 'div'.`); | ||||
|       }); | ||||
| 
 | ||||
|       it('should have a descriptive error for unknown properties with an "ng-" prefix', () => { | ||||
|         env.write('test.ts', ` | ||||
|         import {Component, NgModule} from '@angular/core'; | ||||
|         @Component({ | ||||
|           selector: 'blah', | ||||
|           template: '<div [foo]="1">test</div>', | ||||
|         }) | ||||
|         export class FooCmp {} | ||||
|         @NgModule({ | ||||
|           declarations: [FooCmp], | ||||
|         }) | ||||
|         export class FooModule {} | ||||
|       `);
 | ||||
|         const diags = env.driveDiagnostics(); | ||||
|         expect(diags.length).toBe(1); | ||||
|         expect(diags[0].messageText) | ||||
|             .toBe(`Can't bind to 'foo' since it isn't a known property of 'div'.`); | ||||
|       }); | ||||
| 
 | ||||
|       it('should convert property names when binding special properties', () => { | ||||
| @ -423,8 +465,14 @@ export declare class CommonModule { | ||||
|       `);
 | ||||
|            const diags = env.driveDiagnostics(); | ||||
|            expect(diags.length).toBe(2); | ||||
|            expect(diags[0].messageText).toBe(`'custom-element' is not a valid HTML element.`); | ||||
|            expect(diags[1].messageText).toBe(`'foo' is not a valid property of <custom-element>.`); | ||||
|            expect(diags[0].messageText).toBe(`'custom-element' is not a known element:
 | ||||
| 1. If 'custom-element' is an Angular component, then verify that it is part of this module. | ||||
| 2. If 'custom-element' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.`);
 | ||||
|            expect(diags[1].messageText) | ||||
|                .toBe(`Can't bind to 'foo' since it isn't a known property of 'custom-element'.
 | ||||
| 1. If 'custom-element' is an Angular component and it has 'foo' input, then verify that it is part of this module. | ||||
| 2. If 'custom-element' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. | ||||
| 3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`);
 | ||||
|          }); | ||||
| 
 | ||||
|       it('should not produce diagnostics for custom-elements-style elements when using the CUSTOM_ELEMENTS_SCHEMA', | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user