Skip to content

Commit

Permalink
refactor(compiler-cli): interpolatedSignalNotInvoked diagnostic for i…
Browse files Browse the repository at this point in the history
…nput signals (#53883)

This updates the extended diagnotic to handle input signals as well.

PR Close #53883
  • Loading branch information
cexbrayat authored and dylhunn committed Jan 17, 2024
1 parent bb8a5a4 commit 18fa6d7
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class InterpolatedSignalCheck extends
}

function isSignal(symbol: ts.Symbol|undefined): boolean {
return (symbol?.escapedName === 'WritableSignal' || symbol?.escapedName === 'Signal') &&
return (symbol?.escapedName === 'WritableSignal' || symbol?.escapedName === 'Signal' ||
symbol?.escapedName === 'InputSignal') &&
(symbol as any).parent.escapedName.includes('@angular/core');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,6 @@ import {getClass, setup} from '../../../../testing';
import {factory as interpolatedSignalFactory} from '../../../checks/interpolated_signal_not_invoked';
import {ExtendedTemplateCheckerImpl} from '../../../src/extended_template_checker';

function coreDtsWithSignals() {
return {
fileName: absoluteFrom('/node_modules/@angular/core/index.d.ts'),
source: `
export declare const SIGNAL: unique symbol;
export declare type Signal<T> = (() => T) & {
[SIGNAL]: unknown;
};
export declare function signal<T>(initialValue: T): WritableSignal<T>;
export declare function computed<T>(computation: () => T): Signal<T>;
export interface WritableSignal<T> extends Signal<T> {
asReadonly(): Signal<T>;
}
`,
templates: {},
};
}

runInEachFileSystem(() => {
describe('Interpolated Signal ', () => {
it('binds the error code to its extended template diagnostic name', () => {
Expand All @@ -46,7 +27,6 @@ runInEachFileSystem(() => {
it('should not produce a warning when a signal getter is invoked', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
Expand Down Expand Up @@ -74,7 +54,6 @@ runInEachFileSystem(() => {
it('should produce a warning when a signal isn\'t invoked', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
Expand Down Expand Up @@ -105,7 +84,6 @@ runInEachFileSystem(() => {
it('should produce a warning when a readonly signal isn\'t invoked', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
Expand Down Expand Up @@ -134,11 +112,10 @@ runInEachFileSystem(() => {
it('should produce a warning when a computed signal isn\'t invoked', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
'TestCmp': `<div> {{ mySignal2 }}</div>`,
'TestCmp': `<div>{{ mySignal2 }}</div>`,
},
source: `
import {signal, Signal, computed} from '@angular/core';
Expand All @@ -161,14 +138,69 @@ runInEachFileSystem(() => {
expect(getSourceCodeForDiagnostic(diags[0])).toBe(`mySignal2`);
});

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

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

it('should not produce a warning when a computed signal is invoked', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
'TestCmp': `<div> {{ mySignal2() }}</div>`,
'TestCmp': `<div>{{ mySignal2() }}</div>`,
},
source: `
import {signal, Signal, computed} from '@angular/core';
Expand All @@ -188,10 +220,35 @@ runInEachFileSystem(() => {
expect(diags.length).toBe(0);
});

it('should not produce a warning when input signals are invoked', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
{
fileName,
templates: {
'TestCmp': `<div>{{ myInput() }} - {{ myRequiredInput() }}</div>`,
},
source: `
import {input} from '@angular/core';
export class TestCmp {
myInput = input(0);
myRequiredInput = input.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 produce a warning when signal isn\'t invoked on interpolated binding', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
Expand Down Expand Up @@ -220,7 +277,6 @@ runInEachFileSystem(() => {
it('should not produce a warning when signal is invoked on interpolated binding', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
Expand All @@ -246,7 +302,6 @@ runInEachFileSystem(() => {
it('should produce a warning when signal is invoked in attribute binding interpolation ', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
Expand Down Expand Up @@ -276,7 +331,6 @@ runInEachFileSystem(() => {
() => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
Expand All @@ -303,7 +357,6 @@ runInEachFileSystem(() => {
it('should produce a warning when nested signal isn\'t invoked on interpolated binding', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
Expand Down Expand Up @@ -332,7 +385,6 @@ runInEachFileSystem(() => {
it('should not produce a warning with other Signal type', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
Expand All @@ -359,7 +411,6 @@ runInEachFileSystem(() => {
it('should not produce a warning with other Signal type', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
coreDtsWithSignals(),
{
fileName,
templates: {
Expand Down
76 changes: 70 additions & 6 deletions packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,80 @@ export function angularCoreDts(): TestFile {
export declare type NgIterable<T> = Array<T> | Iterable<T>;
export declare const ɵINPUT_SIGNAL_BRAND_READ_TYPE: unique symbol;
export declare const ɵINPUT_SIGNAL_BRAND_WRITE_TYPE: unique symbol;
/**
* -------
* Signals
* ------
*/
export declare const SIGNAL: unique symbol;
export declare type Signal<T> = (() => T) & {
[SIGNAL]: unknown;
};
export declare function signal<T>(initialValue: T): WritableSignal<T>;
export declare function computed<T>(computation: () => T): Signal<T>;
export interface WritableSignal<T> extends Signal<T> {
asReadonly(): Signal<T>;
}
export declare type InputSignal<ReadT, WriteT = ReadT> = (() => ReadT)&{
/**
* -------
* Signal inputs
* ------
*/
export interface InputOptions<ReadT, WriteT> {
alias?: string;
transform?: (v: WriteT) => ReadT;
}
export type InputOptionsWithoutTransform<ReadT> =
InputOptions<ReadT, ReadT>&{transform?: undefined};
export type InputOptionsWithTransform<ReadT, WriteT> =
Required<Pick<InputOptions<ReadT, WriteT>, 'transform'>>&InputOptions<ReadT, WriteT>;
const ɵINPUT_SIGNAL_BRAND_READ_TYPE: unique symbol;
export const ɵINPUT_SIGNAL_BRAND_WRITE_TYPE: unique symbol;
export interface InputSignal<ReadT, WriteT = ReadT> extends Signal<ReadT> {
[ɵINPUT_SIGNAL_BRAND_READ_TYPE]: ReadT;
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: WriteT;
};
}
export function inputFunction<ReadT>(): InputSignal<ReadT|undefined>;
export function inputFunction<ReadT>(
initialValue: ReadT, opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
export function inputFunction<ReadT, WriteT>(
initialValue: ReadT,
opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
export function inputFunction<ReadT, WriteT>(
_initialValue?: ReadT,
_opts?: InputOptions<ReadT, WriteT>): InputSignal<ReadT|undefined, WriteT> {
return null!;
}
export function inputRequiredFunction<ReadT>(opts?: InputOptionsWithoutTransform<ReadT>):
InputSignal<ReadT>;
export function inputRequiredFunction<ReadT, WriteT>(
opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
export function inputRequiredFunction<ReadT, WriteT>(_opts?: InputOptions<ReadT, WriteT>):
InputSignal<ReadT, WriteT> {
return null!;
}
export type InputFunction = typeof inputFunction&{required: typeof inputRequiredFunction};
export type ɵUnwrapInputSignalWriteType<Field> = Field extends InputSignal<unknown, infer WriteT>? WriteT : never;
export type ɵUnwrapDirectiveSignalInputs<Dir, Fields extends keyof Dir> = {[P in Fields]: ɵUnwrapInputSignalWriteType<Dir[P]>};
export const input: InputFunction = (() => {
(inputFunction as any).required = inputRequiredFunction;
return inputFunction as InputFunction;
})();
export type ɵUnwrapInputSignalWriteType<Field> =
Field extends InputSignal<unknown, infer WriteT>? WriteT : never;
export type ɵUnwrapDirectiveSignalInputs<Dir, Fields extends keyof Dir> = {
[P in Fields]: ɵUnwrapInputSignalWriteType<Dir[P]>
};
`
};
}
Expand Down

0 comments on commit 18fa6d7

Please sign in to comment.