Skip to content

Commit

Permalink
fix(compiler-cli): interpolatedSignalNotInvoked diagnostic for model …
Browse files Browse the repository at this point in the history
…signals (#54338)

The new `model()` signal introduces a `ModelSignal` type that needs to be handled by the interpolatedSignalNotInvoked diagnostic to catch issues like:

```
<div>{{ myModel }}</div>
```

PR Close #54338
  • Loading branch information
cexbrayat authored and thePunderWoman committed Feb 8, 2024
1 parent ef55d48 commit 63a9027
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 1 deletion.
Expand Up @@ -40,7 +40,8 @@ class InterpolatedSignalCheck extends
function isSignal(symbol: ts.Symbol|undefined): boolean {
return (symbol?.escapedName === 'WritableSignal' || symbol?.escapedName === 'Signal' ||
symbol?.escapedName === 'InputSignal' ||
symbol?.escapedName === 'InputSignalWithTransform') &&
symbol?.escapedName === 'InputSignalWithTransform' ||
symbol?.escapedName === 'ModelSignal') &&
(symbol as any).parent.escapedName.includes('@angular/core');
}

Expand Down
Expand Up @@ -194,6 +194,88 @@ runInEachFileSystem(() => {
expect(getSourceCodeForDiagnostic(diags[0])).toBe(`myRequiredInput`);
});

it('should produce a warning when a model signal isn\'t invoked', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
{
fileName,
templates: {
'TestCmp': `<div>{{ myModel }}</div>`,
},
source: `
import {model} from '@angular/core';
export class TestCmp {
myModel = model(0);
}`,
},
]);
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(`myModel`);
});

it('should produce a warning when a required model signal isn\'t invoked', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
{
fileName,
templates: {
'TestCmp': `<div>{{ myRequiredModel }}</div>`,
},
source: `
import {model} from '@angular/core';
export class TestCmp {
myRequiredModel = model.required(0);
}`,
},
]);
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(`myRequiredModel`);
});

it('should not produce a warning when model signals are invoked', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
{
fileName,
templates: {
'TestCmp': `<div>{{ myModel() }} - {{ myRequiredModel() }}</div>`,
},
source: `
import {model} from '@angular/core';
export class TestCmp {
myModel = model(0);
myRequiredModel = model.required(0);
}`,
},
]);
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 not produce a warning when a computed signal is invoked', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
Expand Down

0 comments on commit 63a9027

Please sign in to comment.