Skip to content

Commit 9aea8a0

Browse files
crisbetothePunderWoman
authored andcommitted
refactor(compiler-cli): add diagnostic for duplicate let declarations (#56199)
Adds a template diagnostic that will flag cases where multiple `@let` declarations use the same name. PR Close #56199
1 parent cb16592 commit 9aea8a0

File tree

6 files changed

+70
-6
lines changed

6 files changed

+70
-6
lines changed

goldens/public-api/compiler-cli/error_code.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export enum ErrorCode {
4242
DIRECTIVE_INHERITS_UNDECORATED_CTOR = 2006,
4343
// (undocumented)
4444
DIRECTIVE_MISSING_SELECTOR = 2004,
45+
DUPLICATE_LET_DECLARATION = 8017,
4546
DUPLICATE_VARIABLE_DECLARATION = 8006,
4647
HOST_BINDING_PARSE_ERROR = 5001,
4748
HOST_DIRECTIVE_COMPONENT = 2015,

packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,9 @@ export enum ErrorCode {
364364
/** An expression is trying to read an `@let` before it has been defined. */
365365
LET_USED_BEFORE_DEFINITION = 8016,
366366

367+
/** Multiple `@let` declarations were defined with the same name within a scope. */
368+
DUPLICATE_LET_DECLARATION = 8017,
369+
367370
/**
368371
* A two way binding in a template has an incorrect syntax,
369372
* parentheses outside brackets. For example:

packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,15 @@ export interface OutOfBandDiagnosticRecorder {
185185
node: PropertyRead,
186186
target: TmplAstLetDeclaration,
187187
): void;
188+
189+
/**
190+
* Reports a duplicate `@let` declaration within the same scope.
191+
*
192+
* @param templateId the template type-checking ID of the template which contains the duplicate
193+
* declaration.
194+
* @param current the `TmplAstLetDeclaration` which duplicates a previous declaration.
195+
*/
196+
duplicateLetDeclaration(templateId: TemplateId, current: TmplAstLetDeclaration): void;
188197
}
189198

190199
export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder {
@@ -640,6 +649,22 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
640649
),
641650
);
642651
}
652+
653+
duplicateLetDeclaration(templateId: TemplateId, current: TmplAstLetDeclaration): void {
654+
const mapping = this.resolver.getSourceMapping(templateId);
655+
const errorMsg = `Cannot declare @let called '${current.name}' as there is another @let declaration with the same name.`;
656+
657+
this._diagnostics.push(
658+
makeTemplateDiagnostic(
659+
templateId,
660+
mapping,
661+
current.sourceSpan,
662+
ts.DiagnosticCategory.Error,
663+
ngErrorCode(ErrorCode.DUPLICATE_LET_DECLARATION),
664+
errorMsg,
665+
),
666+
);
667+
}
643668
}
644669

645670
function makeInlineDiagnostic(

packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2021,9 +2021,11 @@ class Scope {
20212021
private varMap = new Map<TmplAstVariable, number | ts.Identifier>();
20222022

20232023
/**
2024-
* A map of `TmplAstLetDeclaration`s to the index of their op in the `opQueue`.
2024+
* A map of the names of `TmplAstLetDeclaration`s to the index of their op in the `opQueue`.
2025+
*
2026+
* Assumes that there won't be duplicated `@let` declarations within the same scope.
20252027
*/
2026-
private letDeclOpMap = new Map<TmplAstLetDeclaration, number>();
2028+
private letDeclOpMap = new Map<string, number>();
20272029

20282030
/**
20292031
* Statements for this template.
@@ -2132,7 +2134,11 @@ class Scope {
21322134

21332135
if (node instanceof TmplAstLetDeclaration) {
21342136
const opIndex = scope.opQueue.push(new TcbLetDeclarationOp(tcb, scope, node)) - 1;
2135-
scope.letDeclOpMap.set(node, opIndex);
2137+
if (scope.letDeclOpMap.has(node.name)) {
2138+
tcb.oobRecorder.duplicateLetDeclaration(tcb.id, node);
2139+
} else {
2140+
scope.letDeclOpMap.set(node.name, opIndex);
2141+
}
21362142
}
21372143
}
21382144
return scope;
@@ -2255,7 +2261,7 @@ class Scope {
22552261
return this.varMap.has(node);
22562262
}
22572263
if (node instanceof TmplAstLetDeclaration) {
2258-
return this.letDeclOpMap.has(node);
2264+
return this.letDeclOpMap.has(node.name);
22592265
}
22602266
return this.referenceOpMap.has(node);
22612267
}
@@ -2266,8 +2272,8 @@ class Scope {
22662272
): ts.Expression | null {
22672273
if (ref instanceof TmplAstReference && this.referenceOpMap.has(ref)) {
22682274
return this.resolveOp(this.referenceOpMap.get(ref)!);
2269-
} else if (ref instanceof TmplAstLetDeclaration && this.letDeclOpMap.has(ref)) {
2270-
return this.resolveOp(this.letDeclOpMap.get(ref)!);
2275+
} else if (ref instanceof TmplAstLetDeclaration && this.letDeclOpMap.has(ref.name)) {
2276+
return this.resolveOp(this.letDeclOpMap.get(ref.name)!);
22712277
} else if (ref instanceof TmplAstVariable && this.varMap.has(ref)) {
22722278
// Resolving a context variable for this template.
22732279
// Execute the `TcbVariableOp` associated with the `TmplAstVariable`.

packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,4 +1039,5 @@ export class NoopOobRecorder implements OutOfBandDiagnosticRecorder {
10391039
node: PropertyRead,
10401040
target: TmplAstLetDeclaration,
10411041
): void {}
1042+
duplicateLetDeclaration(templateId: TemplateId, current: TmplAstLetDeclaration): void {}
10421043
}

packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6876,6 +6876,34 @@ suppress
68766876
);
68776877
});
68786878

6879+
it('should not allow multiple @let declarations with the same name within a scope', () => {
6880+
env.write(
6881+
'test.ts',
6882+
`
6883+
import {Component} from '@angular/core';
6884+
6885+
@Component({
6886+
template: \`
6887+
@if (true) {
6888+
@let value = 1;
6889+
@let value = 'one';
6890+
{{value}}
6891+
}
6892+
\`,
6893+
standalone: true,
6894+
})
6895+
export class Main {
6896+
}
6897+
`,
6898+
);
6899+
6900+
const diags = env.driveDiagnostics();
6901+
expect(diags.length).toBe(1);
6902+
expect(diags[0].messageText).toBe(
6903+
`Cannot declare @let called 'value' as there is another @let declaration with the same name.`,
6904+
);
6905+
});
6906+
68796907
it('should not allow a let declaration to be referenced before it is defined', () => {
68806908
env.write(
68816909
'test.ts',

0 commit comments

Comments
 (0)