Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ivy): type-checking error for duplicate variables in templates #35674

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
Expand Up @@ -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.
*/
Expand Down
30 changes: 30 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts
Expand Up @@ -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(
alxhub marked this conversation as resolved.
Show resolved Hide resolved
templateId: TemplateId, variable: TmplAstVariable, firstDecl: TmplAstVariable): void;
}

export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder {
Expand Down Expand Up @@ -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,
}));
}
}
10 changes: 10 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts
Expand Up @@ -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);
}
Expand Down
Expand Up @@ -400,4 +400,5 @@ export class NoopOobRecorder implements OutOfBandDiagnosticRecorder {
missingReferenceTarget(): void {}
missingPipe(): void {}
illegalAssignmentToTemplateVar(): void {}
duplicateTemplateVar(): void {}
}
31 changes: 31 additions & 0 deletions packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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;');
alxhub marked this conversation as resolved.
Show resolved Hide resolved
});

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
Expand Down
1 change: 1 addition & 0 deletions tools/public_api_guard/error_code.d.ts
Expand Up @@ -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
}