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, | ||||
| 
 | ||||
|   /** | ||||
|    * 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. | ||||
|    */ | ||||
|  | ||||
| @ -50,6 +50,17 @@ export interface OutOfBandDiagnosticRecorder { | ||||
| 
 | ||||
|   illegalAssignmentToTemplateVar( | ||||
|       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 { | ||||
| @ -100,4 +111,23 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor | ||||
|           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.
 | ||||
|     if (templateOrNodes instanceof TmplAstTemplate) { | ||||
|       // The template's variable declarations need to be added as `TcbVariableOp`s.
 | ||||
|       const varMap = new Map<string, TmplAstVariable>(); | ||||
| 
 | ||||
|       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; | ||||
|         scope.varMap.set(v, opIndex); | ||||
|       } | ||||
|  | ||||
| @ -400,4 +400,5 @@ export class NoopOobRecorder implements OutOfBandDiagnosticRecorder { | ||||
|   missingReferenceTarget(): void {} | ||||
|   missingPipe(): void {} | ||||
|   illegalAssignmentToTemplateVar(): void {} | ||||
|   duplicateTemplateVar(): void {} | ||||
| } | ||||
|  | ||||
| @ -8,6 +8,7 @@ | ||||
| 
 | ||||
| import * as ts from 'typescript'; | ||||
| 
 | ||||
| import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics'; | ||||
| 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'; | ||||
| @ -1303,6 +1304,36 @@ export declare class AnimationEvent { | ||||
|       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', | ||||
|        () => { | ||||
|          // 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_PIPE = 8004, | ||||
|     WRITE_TO_READ_ONLY_VARIABLE = 8005, | ||||
|     DUPLICATE_VARIABLE_DECLARATION = 8006, | ||||
|     INJECTABLE_DUPLICATE_PROV = 9001 | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user