mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(compiler-cli): add support for if/switch to the non-invoked signal diagnostic (#63502)
This commit adds the support to the existing "interpolated_signal_not_invoked" diagnostic (even though it's not really a interpolation) PR Close #63502
This commit is contained in:
parent
b3d102b475
commit
35a1d5d7bf
2 changed files with 150 additions and 0 deletions
|
|
@ -14,7 +14,9 @@ import {
|
|||
PrefixNot,
|
||||
PropertyRead,
|
||||
TmplAstBoundAttribute,
|
||||
TmplAstIfBlock,
|
||||
TmplAstNode,
|
||||
TmplAstSwitchBlock,
|
||||
} from '@angular/compiler';
|
||||
import ts from 'typescript';
|
||||
|
||||
|
|
@ -80,6 +82,29 @@ class InterpolatedSignalCheck extends TemplateCheckWithVisitor<ErrorCode.INTERPO
|
|||
return buildDiagnosticForSignal(ctx, nodeAst, component);
|
||||
}
|
||||
}
|
||||
// if blocks like `@if(mySignal) { ... }`
|
||||
else if (node instanceof TmplAstIfBlock) {
|
||||
return node.branches
|
||||
.map((branch) => branch.expression)
|
||||
.filter((expr): expr is ASTWithSource => expr instanceof ASTWithSource)
|
||||
.map((expr) => {
|
||||
const ast = expr.ast;
|
||||
return ast instanceof PrefixNot ? ast.expression : ast;
|
||||
})
|
||||
.filter((ast): ast is PropertyRead => ast instanceof PropertyRead)
|
||||
.flatMap((item) => buildDiagnosticForSignal(ctx, item, component));
|
||||
}
|
||||
// switch blocks like `@switch(mySignal) { ... }`
|
||||
else if (node instanceof TmplAstSwitchBlock && node.expression instanceof ASTWithSource) {
|
||||
const expression =
|
||||
node.expression.ast instanceof PrefixNot
|
||||
? node.expression.ast.expression
|
||||
: node.expression.ast;
|
||||
if (expression instanceof PropertyRead) {
|
||||
return buildDiagnosticForSignal(ctx, expression, component);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -852,6 +852,131 @@ runInEachFileSystem(() => {
|
|||
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
|
||||
expect(diags.length).toBe(0);
|
||||
});
|
||||
|
||||
[false, true].forEach((negate) => {
|
||||
// Control flow
|
||||
it(`should produce a warning when a property named '${functionInstanceProperty}' of a not invoked signal is used in an @if control flow expression`, () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const {program, templateTypeChecker} = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {
|
||||
'TestCmp': `@if(${negate ? '!' : ''}myObject.mySignal.${functionInstanceProperty}) { <div>Show</div> }`,
|
||||
},
|
||||
source: `
|
||||
import {signal} from '@angular/core';
|
||||
|
||||
export class TestCmp {
|
||||
myObject = { mySignal: signal<{ ${functionInstanceProperty}: string }>({ ${functionInstanceProperty}: 'foo' }) };
|
||||
}`,
|
||||
},
|
||||
]);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const component = getClass(sf, 'TestCmp');
|
||||
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
|
||||
templateTypeChecker,
|
||||
program.getTypeChecker(),
|
||||
[interpolatedSignalFactory],
|
||||
{},
|
||||
/* 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.INTERPOLATED_SIGNAL_NOT_INVOKED));
|
||||
expect(getSourceCodeForDiagnostic(diags[0])).toBe(`mySignal`);
|
||||
});
|
||||
|
||||
it(`should not produce a warning when a property named '${functionInstanceProperty}' of an invoked signal is used in an @if control flow expression`, () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const {program, templateTypeChecker} = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {
|
||||
'TestCmp': `@if(${negate ? '!' : ''}myObject.mySignal().${functionInstanceProperty}) { <div>Show</div> }`,
|
||||
},
|
||||
source: `
|
||||
import {signal} from '@angular/core';
|
||||
|
||||
export class TestCmp {
|
||||
myObject = { mySignal: signal<{ ${functionInstanceProperty}: string }>({ ${functionInstanceProperty}: 'foo' }) };
|
||||
}`,
|
||||
},
|
||||
]);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const component = getClass(sf, 'TestCmp');
|
||||
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
|
||||
templateTypeChecker,
|
||||
program.getTypeChecker(),
|
||||
[interpolatedSignalFactory],
|
||||
{},
|
||||
/* options */
|
||||
);
|
||||
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
|
||||
expect(diags.length).toBe(0);
|
||||
});
|
||||
|
||||
it(`should produce a warning when a property named '${functionInstanceProperty}' of a not invoked signal is used in an @switch control flow expression`, () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const {program, templateTypeChecker} = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {
|
||||
'TestCmp': `@switch(${negate ? '!' : ''}myObject.mySignal.${functionInstanceProperty}) { }`,
|
||||
},
|
||||
source: `
|
||||
import {signal} from '@angular/core';
|
||||
|
||||
export class TestCmp {
|
||||
myObject = { mySignal: signal<{ ${functionInstanceProperty}: string }>({ ${functionInstanceProperty}: 'foo' }) };
|
||||
}`,
|
||||
},
|
||||
]);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const component = getClass(sf, 'TestCmp');
|
||||
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
|
||||
templateTypeChecker,
|
||||
program.getTypeChecker(),
|
||||
[interpolatedSignalFactory],
|
||||
{},
|
||||
/* 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.INTERPOLATED_SIGNAL_NOT_INVOKED));
|
||||
expect(getSourceCodeForDiagnostic(diags[0])).toBe(`mySignal`);
|
||||
});
|
||||
|
||||
it(`should not produce a warning when a property named '${functionInstanceProperty}' of an invoked signal is used in an @switch control flow expression`, () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const {program, templateTypeChecker} = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {
|
||||
'TestCmp': `@switch(${negate ? '!' : ''}myObject.mySignal().${functionInstanceProperty}) { }`,
|
||||
},
|
||||
source: `
|
||||
import {signal} from '@angular/core';
|
||||
|
||||
export class TestCmp {
|
||||
myObject = { mySignal: signal<{ ${functionInstanceProperty}: string }>({ ${functionInstanceProperty}: 'foo' }) };
|
||||
}`,
|
||||
},
|
||||
]);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const component = getClass(sf, 'TestCmp');
|
||||
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
|
||||
templateTypeChecker,
|
||||
program.getTypeChecker(),
|
||||
[interpolatedSignalFactory],
|
||||
{},
|
||||
/* options */
|
||||
);
|
||||
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
|
||||
expect(diags.length).toBe(0);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue