Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions goldens/public-api/compiler-cli/error_code.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum ErrorCode {
COMPONENT_NOT_STANDALONE = 2010,
COMPONENT_RESOURCE_NOT_FOUND = 2008,
COMPONENT_UNKNOWN_IMPORT = 2012,
COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE = 8109,
// (undocumented)
CONFIG_EXTENDED_DIAGNOSTICS_IMPLIES_STRICT_TEMPLATES = 4003,
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

// @public
export enum ExtendedTemplateDiagnosticName {
// (undocumented)
COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE = "ComponentVariableShadowsTemplateReference",
// (undocumented)
INVALID_BANANA_IN_BOX = "invalidBananaInBox",
// (undocumented)
Expand Down
15 changes: 15 additions & 0 deletions packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,21 @@ export enum ErrorCode {
*/
SKIP_HYDRATION_NOT_STATIC = 8108,

/**
* A template reference shadows a component class property or method.
* For example:
*
* ```
* @Component({
* template: '<div #var1></div>'
* })
* export class FooComponent {
* var1: string;
* }
* ```
*/
COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE = 8109,

/**
* The template type-checking engine would need to generate an inline type check block for a
* component, but the current type-checking environment doesn't support it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ export enum ExtendedTemplateDiagnosticName {
MISSING_NGFOROF_LET = 'missingNgForOfLet',
SUFFIX_NOT_SUPPORTED = 'suffixNotSupported',
SKIP_HYDRATION_NOT_STATIC = 'skipHydrationNotStatic',
COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE = 'ComponentVariableShadowsTemplateReference',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "component_variable_shadows_template_reference",
srcs = ["index.ts"],
visibility = [
"//packages/compiler-cli/src/ngtsc:__subpackages__",
"//packages/compiler-cli/test/ngtsc:__pkg__",
],
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"@npm//typescript",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {AST, TmplAstNode as Node, TmplAstReference as Reference, TmplAstVariable as Variable} from '@angular/compiler';
import ts from 'typescript';

import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic} from '../../../api';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';

/**
* Ensures component variables don't shadow any template reference.
* Will return diagnostic information when any shadow is found.
*/
class ComponentVariableShadowsTemplateReference extends
TemplateCheckWithVisitor<ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE> {
override code = ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE as const;

override visitNode(
ctx: TemplateContext<ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE>,
component: ts.ClassDeclaration,
node: Node|AST,
): NgTemplateDiagnostic<ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE>[] {
if (!(node instanceof Reference || node instanceof Variable)) return [];

const templateReference = node.name;
const parent = component.parent as unknown as
{classifiableNames: Set<string>, identifiers: Map<string, string>};
const componentVariables = new Set<string>();
for (let id of parent.identifiers.keys()) {
if (!parent.classifiableNames.has(id)) {
componentVariables.add(id);
}
}

if (!componentVariables.has(templateReference)) {
return [];
}

const diagnostic = ctx.makeTemplateDiagnostic(
node.sourceSpan,
`you have a component variable and template reference with the same name "${
templateReference}"`);

return [diagnostic];
}
}
export const factory: TemplateCheckFactory<
ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE,
ExtendedTemplateDiagnosticName.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE> = {
code: ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE,
name: ExtendedTemplateDiagnosticName.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE,
create: () => new ComponentVariableShadowsTemplateReference(),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")

ts_library(
name = "test_lib",
testonly = True,
srcs = ["component_variable_shadows_template_reference_spec.ts"],
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/typecheck/extended",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/component_variable_shadows_template_reference",
"//packages/compiler-cli/src/ngtsc/typecheck/testing",
"@npm//typescript",
],
)

jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
deps = [
":test_lib",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import ts from 'typescript';

import {ErrorCode, ExtendedTemplateDiagnosticName, ngErrorCode} from '../../../../../diagnostics';
import {absoluteFrom, getSourceFileOrError} from '../../../../../file_system';
import {runInEachFileSystem} from '../../../../../file_system/testing';
import {getSourceCodeForDiagnostic} from '../../../../../testing';
import {getClass, setup} from '../../../../testing';
import {factory as componentVariableShadowsTemplateReference} from '../../../checks/component_variable_shadows_template_reference';
import {ExtendedTemplateCheckerImpl} from '../../../src/extended_template_checker';

runInEachFileSystem(() => {
describe('TemplateChecks', () => {
it('binds the error code to its extended template diagnostic name', () => {
expect(componentVariableShadowsTemplateReference.code)
.toBe(ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE);
expect(componentVariableShadowsTemplateReference.name)
.toBe(ExtendedTemplateDiagnosticName.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE);
});

it('should produce component variable shadow template reference warning', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'TestCmp': '<div #var1> </div> <div #var2> </div>',
},
source: `export class TestCmp { var3 = 'text'; var2: string = 'text'; }`
}]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker, program.getTypeChecker(),
[componentVariableShadowsTemplateReference], {} /* options */);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
expect(diags.length).toBe(1);
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
expect(diags[0].code)
.toBe(ngErrorCode(ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE));
expect(getSourceCodeForDiagnostic(diags[0])).toBe('#var2');
});

it('should not produce component variable shadow template reference warning if written correctly',
() => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'TestCmp': '<div #var1> </div> <div #var2> </div>',
},
source: `export class TestCmp { var3 = 'text'; var4 = 'text'; }`
}]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker, program.getTypeChecker(),
[componentVariableShadowsTemplateReference], {} /* options */);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
expect(diags.length).toBe(0);
});

it('should produce component variable shadow template reference warning for inherited properties as well',
() => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'TestCmp': '<div #var1> </div>',
},
source:
`export abstract class TestAbsCls { var1 = 'text'; } export class TestCmp extends TestAbsCls { var2: string = 'text'; }`
}]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker, program.getTypeChecker(),
[componentVariableShadowsTemplateReference], {} /* options */);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
expect(diags.length).toBe(1);
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
expect(diags[0].code)
.toBe(ngErrorCode(ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE));
expect(getSourceCodeForDiagnostic(diags[0])).toBe('#var1');
});

it('should not produce component variable shadow template reference warning if used class name as variable',
() => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'TestCmp': '<div #TestAbsCls> </div> <div #TestCmp> </div>',
},
source:
`export abstract class TestAbsCls { var1 = 'text'; } export class TestCmp extends TestAbsCls { var2: string = 'text'; }`
}]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker, program.getTypeChecker(),
[componentVariableShadowsTemplateReference], {} /* options */);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
expect(diags.length).toBe(0);
});

it('should produce component variable shadow template reference warning for ngFor variable',
() => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'TestCmp': '<div *ngFor="let item of array"> </div>',
},
source:
`export class TestCmp extends TestAbsCls { item = 'text'; array: string[] = []; }`
}]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker, program.getTypeChecker(),
[componentVariableShadowsTemplateReference], {} /* options */);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
expect(diags.length).toBe(1);
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
expect(diags[0].code)
.toBe(ngErrorCode(ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE));
expect(getSourceCodeForDiagnostic(diags[0]).trim()).toBe('let item');
});

it('should produce component variable shadow template reference warning for ngIf as variable',
() => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'TestCmp': '<div *ngIf="result$ | async as result"> </div>',
},
source:
`export class TestCmp extends TestAbsCls { result = 'text'; result$: Observable<string>; }`
}]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker, program.getTypeChecker(),
[componentVariableShadowsTemplateReference], {} /* options */);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
expect(diags.length).toBe(1);
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
expect(diags[0].code)
.toBe(ngErrorCode(ErrorCode.COMPONENT_VARIABLE_SHADOWS_TEMPLATE_REFERENCE));
expect(getSourceCodeForDiagnostic(diags[0]).trim())
.toBe('ngIf="result$ | async as result');
});
});
});