fix(compiler): type-checking error for duplicate variables in templates (#35674)
It's an error to declare a variable twice on a specific template: ```html <div *ngFor="let i of items; let i = index"> </div> ``` This commit introduces a template type-checking error which helps to detect and diagnose this problem. Fixes #35186 PR Close #35674
This commit is contained in:
		
							parent
							
								
									1f8a243b67
								
							
						
					
					
						commit
						2c41bb8490
					
				| @ -122,6 +122,16 @@ export enum ErrorCode { | |||||||
|    */ |    */ | ||||||
|   WRITE_TO_READ_ONLY_VARIABLE = 8005, |   WRITE_TO_READ_ONLY_VARIABLE = 8005, | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * A template variable was declared twice. For example: | ||||||
|  |    * | ||||||
|  |    * ```html
 | ||||||
|  |    * <div *ngFor="let i of items; let i = index"> | ||||||
|  |    * </div> | ||||||
|  |    * ``` | ||||||
|  |    */ | ||||||
|  |   DUPLICATE_VARIABLE_DECLARATION = 8006, | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * An injectable already has a `ɵprov` property. |    * An injectable already has a `ɵprov` property. | ||||||
|    */ |    */ | ||||||
|  | |||||||
| @ -50,6 +50,17 @@ export interface OutOfBandDiagnosticRecorder { | |||||||
| 
 | 
 | ||||||
|   illegalAssignmentToTemplateVar( |   illegalAssignmentToTemplateVar( | ||||||
|       templateId: TemplateId, assignment: PropertyWrite, target: TmplAstVariable): void; |       templateId: TemplateId, assignment: PropertyWrite, target: TmplAstVariable): void; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Reports a duplicate declaration of a template variable. | ||||||
|  |    * | ||||||
|  |    * @param templateId the template type-checking ID of the template which contains the duplicate | ||||||
|  |    * declaration. | ||||||
|  |    * @param variable the `TmplAstVariable` which duplicates a previously declared variable. | ||||||
|  |    * @param firstDecl the first variable declaration which uses the same name as `variable`. | ||||||
|  |    */ | ||||||
|  |   duplicateTemplateVar( | ||||||
|  |       templateId: TemplateId, variable: TmplAstVariable, firstDecl: TmplAstVariable): void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder { | export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder { | ||||||
| @ -100,4 +111,23 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor | |||||||
|           span: target.valueSpan || target.sourceSpan, |           span: target.valueSpan || target.sourceSpan, | ||||||
|         })); |         })); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   duplicateTemplateVar( | ||||||
|  |       templateId: TemplateId, variable: TmplAstVariable, firstDecl: TmplAstVariable): void { | ||||||
|  |     const mapping = this.resolver.getSourceMapping(templateId); | ||||||
|  |     const errorMsg = | ||||||
|  |         `Cannot redeclare variable '${variable.name}' as it was previously declared elsewhere for the same template.`; | ||||||
|  | 
 | ||||||
|  |     // The allocation of the error here is pretty useless for variables declared in microsyntax,
 | ||||||
|  |     // since the sourceSpan refers to the entire microsyntax property, not a span for the specific
 | ||||||
|  |     // variable in question.
 | ||||||
|  |     //
 | ||||||
|  |     // TODO(alxhub): allocate to a tighter span once one is available.
 | ||||||
|  |     this._diagnostics.push(makeTemplateDiagnostic( | ||||||
|  |         mapping, variable.sourceSpan, ts.DiagnosticCategory.Error, | ||||||
|  |         ngErrorCode(ErrorCode.DUPLICATE_VARIABLE_DECLARATION), errorMsg, { | ||||||
|  |           text: `The variable '${firstDecl.name}' was first declared here.`, | ||||||
|  |           span: firstDecl.sourceSpan, | ||||||
|  |         })); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -738,7 +738,17 @@ class Scope { | |||||||
|     // has.
 |     // has.
 | ||||||
|     if (templateOrNodes instanceof TmplAstTemplate) { |     if (templateOrNodes instanceof TmplAstTemplate) { | ||||||
|       // The template's variable declarations need to be added as `TcbVariableOp`s.
 |       // The template's variable declarations need to be added as `TcbVariableOp`s.
 | ||||||
|  |       const varMap = new Map<string, TmplAstVariable>(); | ||||||
|  | 
 | ||||||
|       for (const v of templateOrNodes.variables) { |       for (const v of templateOrNodes.variables) { | ||||||
|  |         // Validate that variables on the `TmplAstTemplate` are only declared once.
 | ||||||
|  |         if (!varMap.has(v.name)) { | ||||||
|  |           varMap.set(v.name, v); | ||||||
|  |         } else { | ||||||
|  |           const firstDecl = varMap.get(v.name) !; | ||||||
|  |           tcb.oobRecorder.duplicateTemplateVar(tcb.id, v, firstDecl); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         const opIndex = scope.opQueue.push(new TcbVariableOp(tcb, scope, templateOrNodes, v)) - 1; |         const opIndex = scope.opQueue.push(new TcbVariableOp(tcb, scope, templateOrNodes, v)) - 1; | ||||||
|         scope.varMap.set(v, opIndex); |         scope.varMap.set(v, opIndex); | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -400,4 +400,5 @@ export class NoopOobRecorder implements OutOfBandDiagnosticRecorder { | |||||||
|   missingReferenceTarget(): void {} |   missingReferenceTarget(): void {} | ||||||
|   missingPipe(): void {} |   missingPipe(): void {} | ||||||
|   illegalAssignmentToTemplateVar(): void {} |   illegalAssignmentToTemplateVar(): void {} | ||||||
|  |   duplicateTemplateVar(): void {} | ||||||
| } | } | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ | |||||||
| 
 | 
 | ||||||
| import * as ts from 'typescript'; | import * as ts from 'typescript'; | ||||||
| 
 | 
 | ||||||
|  | import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics'; | ||||||
| import {absoluteFrom as _, getFileSystem} from '../../src/ngtsc/file_system'; | import {absoluteFrom as _, getFileSystem} from '../../src/ngtsc/file_system'; | ||||||
| import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; | import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; | ||||||
| import {loadStandardTestFiles} from '../helpers/src/mock_file_loading'; | import {loadStandardTestFiles} from '../helpers/src/mock_file_loading'; | ||||||
| @ -1303,6 +1304,36 @@ export declare class AnimationEvent { | |||||||
|       expect(getSourceCodeForDiagnostic(diags[0])).toEqual('y = !y'); |       expect(getSourceCodeForDiagnostic(diags[0])).toEqual('y = !y'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     it('should detect a duplicate variable declaration', () => { | ||||||
|  |       env.write('test.ts', ` | ||||||
|  |         import {Component, NgModule} from '@angular/core'; | ||||||
|  |         import {CommonModule} from '@angular/common'; | ||||||
|  | 
 | ||||||
|  |         @Component({ | ||||||
|  |           selector: 'test', | ||||||
|  |           template: \` | ||||||
|  |             <div *ngFor="let i of items; let i = index"> | ||||||
|  |               {{i}} | ||||||
|  |             </div> | ||||||
|  |           \`,
 | ||||||
|  |         }) | ||||||
|  |         export class TestCmp { | ||||||
|  |           items!: string[]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @NgModule({ | ||||||
|  |           declarations: [TestCmp], | ||||||
|  |           imports: [CommonModule], | ||||||
|  |         }) | ||||||
|  |         export class Module {} | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |       const diags = env.driveDiagnostics(); | ||||||
|  |       expect(diags.length).toEqual(1); | ||||||
|  |       expect(diags[0].code).toEqual(ngErrorCode(ErrorCode.DUPLICATE_VARIABLE_DECLARATION)); | ||||||
|  |       expect(getSourceCodeForDiagnostic(diags[0])).toContain('let i of items;'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     it('should still type-check when fileToModuleName aliasing is enabled, but alias exports are not in the .d.ts file', |     it('should still type-check when fileToModuleName aliasing is enabled, but alias exports are not in the .d.ts file', | ||||||
|        () => { |        () => { | ||||||
|          // The template type-checking file imports directives/pipes in order to type-check their
 |          // The template type-checking file imports directives/pipes in order to type-check their
 | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								tools/public_api_guard/error_code.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								tools/public_api_guard/error_code.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -30,5 +30,6 @@ export declare enum ErrorCode { | |||||||
|     MISSING_REFERENCE_TARGET = 8003, |     MISSING_REFERENCE_TARGET = 8003, | ||||||
|     MISSING_PIPE = 8004, |     MISSING_PIPE = 8004, | ||||||
|     WRITE_TO_READ_ONLY_VARIABLE = 8005, |     WRITE_TO_READ_ONLY_VARIABLE = 8005, | ||||||
|  |     DUPLICATE_VARIABLE_DECLARATION = 8006, | ||||||
|     INJECTABLE_DUPLICATE_PROV = 9001 |     INJECTABLE_DUPLICATE_PROV = 9001 | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user