Skip to content

Commit 3382e8a

Browse files
crisbetoleonsenft
authored andcommitted
refactor(compiler-cli): generalize out of band diagnostics recorder
Currently the `OutOfBandDiagnosticRecorder` is tied to producing TypeScript diagnostics, however some of our use cases might call for a different form. These changes decouple the recorder from TypeScript and make the diagnostic type generic.
1 parent 1938054 commit 3382e8a

10 files changed

Lines changed: 317 additions & 299 deletions

File tree

packages/compiler-cli/private/hybrid_analysis.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ export type {
1818
TemplateDiagnostic,
1919
TcbReferenceMetadata,
2020
SourceMapping,
21+
OutOfBandDiagnosticRecorder,
22+
OutOfBadDiagnosticCategory,
2123
} from '../src/ngtsc/typecheck/api';
2224
export {DomSchemaChecker, RegistryDomSchemaChecker} from '../src/ngtsc/typecheck/src/dom';
2325
export {Environment} from '../src/ngtsc/typecheck/src/environment';
24-
export {OutOfBandDiagnosticRecorder} from '../src/ngtsc/typecheck/src/oob';
2526
export {TcbGenericContextBehavior} from '../src/ngtsc/typecheck/src/ops/context';
2627
export {ImportManager} from '../src/ngtsc/translator';
2728
export type {ReferenceEmitter} from '../src/ngtsc/imports';

packages/compiler-cli/src/ngtsc/typecheck/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * from './completion';
1212
export * from './context';
1313
export * from './scope';
1414
export * from './symbols';
15+
export * from './oob';
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
AST,
11+
BindingPipe,
12+
PropertyRead,
13+
TmplAstBoundAttribute,
14+
TmplAstBoundEvent,
15+
TmplAstComponent,
16+
TmplAstDirective,
17+
TmplAstElement,
18+
TmplAstForLoopBlock,
19+
TmplAstForLoopBlockEmpty,
20+
TmplAstHoverDeferredTrigger,
21+
TmplAstIfBlockBranch,
22+
TmplAstInteractionDeferredTrigger,
23+
TmplAstLetDeclaration,
24+
TmplAstReference,
25+
TmplAstSwitchBlockCase,
26+
TmplAstTemplate,
27+
TmplAstTextAttribute,
28+
TmplAstVariable,
29+
TmplAstViewportDeferredTrigger,
30+
} from '@angular/compiler';
31+
32+
import {TcbDirectiveMetadata, TypeCheckId} from './api';
33+
34+
/** Categories of diagnostics that can be reported by a `OutOfBandDiagnosticRecorder`. */
35+
export enum OutOfBadDiagnosticCategory {
36+
Error,
37+
Warning,
38+
}
39+
40+
/**
41+
* Collects diagnostics on problems which occur in the template which aren't directly sourced
42+
* from type check blocks.
43+
*
44+
* During the creation of a type check block, the template is traversed and the
45+
* `OutOfBandDiagnosticRecorder` is called to record cases when a correct interpretation for the
46+
* template cannot be found. These operations create diagnostics which are stored by the
47+
* recorder for later display.
48+
*/
49+
export interface OutOfBandDiagnosticRecorder<T> {
50+
readonly diagnostics: ReadonlyArray<T>;
51+
52+
/**
53+
* Reports a `#ref="target"` expression in the template for which a target directive could not be
54+
* found.
55+
*
56+
* @param id the type-checking ID of the template which contains the broken reference.
57+
* @param ref the `TmplAstReference` which could not be matched to a directive.
58+
*/
59+
missingReferenceTarget(id: TypeCheckId, ref: TmplAstReference): void;
60+
61+
/**
62+
* Reports usage of a `| pipe` expression in the template for which the named pipe could not be
63+
* found.
64+
*
65+
* @param id the type-checking ID of the template which contains the unknown pipe.
66+
* @param ast the `BindingPipe` invocation of the pipe which could not be found.
67+
* @param isStandalone whether the host component is standalone.
68+
*/
69+
missingPipe(id: TypeCheckId, ast: BindingPipe, isStandalone: boolean): void;
70+
71+
/**
72+
* Reports usage of a pipe imported via `@Component.deferredImports` outside
73+
* of a `@defer` block in a template.
74+
*
75+
* @param id the type-checking ID of the template which contains the unknown pipe.
76+
* @param ast the `BindingPipe` invocation of the pipe which could not be found.
77+
*/
78+
deferredPipeUsedEagerly(id: TypeCheckId, ast: BindingPipe): void;
79+
80+
/**
81+
* Reports usage of a component/directive imported via `@Component.deferredImports` outside
82+
* of a `@defer` block in a template.
83+
*
84+
* @param id the type-checking ID of the template which contains the unknown pipe.
85+
* @param element the element which hosts a component that was defer-loaded.
86+
*/
87+
deferredComponentUsedEagerly(id: TypeCheckId, element: TmplAstElement): void;
88+
89+
/**
90+
* Reports a duplicate declaration of a template variable.
91+
*
92+
* @param id the type-checking ID of the template which contains the duplicate
93+
* declaration.
94+
* @param variable the `TmplAstVariable` which duplicates a previously declared variable.
95+
* @param firstDecl the first variable declaration which uses the same name as `variable`.
96+
*/
97+
duplicateTemplateVar(
98+
id: TypeCheckId,
99+
variable: TmplAstVariable,
100+
firstDecl: TmplAstVariable,
101+
): void;
102+
103+
/**
104+
* Report a warning when structural directives support context guards, but the current
105+
* type-checking configuration prohibits their usage.
106+
*/
107+
suboptimalTypeInference(id: TypeCheckId, variables: TmplAstVariable[]): void;
108+
109+
/**
110+
* Reports a split two way binding error message.
111+
*/
112+
splitTwoWayBinding(
113+
id: TypeCheckId,
114+
input: TmplAstBoundAttribute,
115+
output: TmplAstBoundEvent,
116+
inputConsumer: Pick<TcbDirectiveMetadata, 'name' | 'isComponent' | 'ref'>,
117+
outputConsumer: Pick<TcbDirectiveMetadata, 'name' | 'isComponent' | 'ref'> | TmplAstElement,
118+
): void;
119+
120+
/** Reports required inputs that haven't been bound. */
121+
missingRequiredInputs(
122+
id: TypeCheckId,
123+
element: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
124+
directiveName: string,
125+
isComponent: boolean,
126+
inputAliases: string[],
127+
): void;
128+
129+
/**
130+
* Reports accesses of properties that aren't available in a `for` block's tracking expression.
131+
*/
132+
illegalForLoopTrackAccess(
133+
id: TypeCheckId,
134+
block: TmplAstForLoopBlock,
135+
access: PropertyRead,
136+
): void;
137+
138+
/**
139+
* Reports deferred triggers that cannot access the element they're referring to.
140+
*/
141+
inaccessibleDeferredTriggerElement(
142+
id: TypeCheckId,
143+
trigger:
144+
| TmplAstHoverDeferredTrigger
145+
| TmplAstInteractionDeferredTrigger
146+
| TmplAstViewportDeferredTrigger,
147+
): void;
148+
149+
/**
150+
* Reports cases where control flow nodes prevent content projection.
151+
*/
152+
controlFlowPreventingContentProjection(
153+
id: TypeCheckId,
154+
category: OutOfBadDiagnosticCategory,
155+
projectionNode: TmplAstElement | TmplAstTemplate,
156+
componentName: string,
157+
slotSelector: string,
158+
controlFlowNode:
159+
| TmplAstIfBlockBranch
160+
| TmplAstSwitchBlockCase
161+
| TmplAstForLoopBlock
162+
| TmplAstForLoopBlockEmpty,
163+
preservesWhitespaces: boolean,
164+
): void;
165+
166+
/** Reports cases where users are writing to `@let` declarations. */
167+
illegalWriteToLetDeclaration(id: TypeCheckId, node: AST, target: TmplAstLetDeclaration): void;
168+
169+
/** Reports cases where users are accessing an `@let` before it is defined.. */
170+
letUsedBeforeDefinition(id: TypeCheckId, node: PropertyRead, target: TmplAstLetDeclaration): void;
171+
172+
/**
173+
* Reports a `@let` declaration that conflicts with another symbol in the same scope.
174+
*
175+
* @param id the type-checking ID of the template which contains the declaration.
176+
* @param current the `TmplAstLetDeclaration` which is invalid.
177+
*/
178+
conflictingDeclaration(id: TypeCheckId, current: TmplAstLetDeclaration): void;
179+
180+
/**
181+
* Reports that a named template dependency (e.g. `<Missing/>`) is not available.
182+
* @param id Type checking ID of the template in which the dependency is declared.
183+
* @param node Node that declares the dependency.
184+
*/
185+
missingNamedTemplateDependency(id: TypeCheckId, node: TmplAstComponent | TmplAstDirective): void;
186+
187+
/**
188+
* Reports that a templace dependency of the wrong kind has been referenced at a specific position
189+
* (e.g. `<SomeDirective/>`).
190+
* @param id Type checking ID of the template in which the dependency is declared.
191+
* @param node Node that declares the dependency.
192+
*/
193+
incorrectTemplateDependencyType(id: TypeCheckId, node: TmplAstComponent | TmplAstDirective): void;
194+
195+
/**
196+
* Reports a binding inside directive syntax that does not match any of the inputs/outputs of
197+
* the directive.
198+
* @param id Type checking ID of the template in which the directive was defined.
199+
* @param directive Directive that contains the binding.
200+
* @param node Node declaring the binding.
201+
*/
202+
unclaimedDirectiveBinding(
203+
id: TypeCheckId,
204+
directive: TmplAstDirective,
205+
node: TmplAstBoundAttribute | TmplAstTextAttribute | TmplAstBoundEvent,
206+
): void;
207+
208+
/**
209+
* Reports that an implicit deferred trigger is set on a block that does not have a placeholder.
210+
*/
211+
deferImplicitTriggerMissingPlaceholder(
212+
id: TypeCheckId,
213+
trigger:
214+
| TmplAstHoverDeferredTrigger
215+
| TmplAstInteractionDeferredTrigger
216+
| TmplAstViewportDeferredTrigger,
217+
): void;
218+
219+
/**
220+
* Reports that an implicit deferred trigger is set on a block whose placeholder is not set up
221+
* correctly (e.g. more than one root node).
222+
*/
223+
deferImplicitTriggerInvalidPlaceholder(
224+
id: TypeCheckId,
225+
trigger:
226+
| TmplAstHoverDeferredTrigger
227+
| TmplAstInteractionDeferredTrigger
228+
| TmplAstViewportDeferredTrigger,
229+
): void;
230+
231+
/**
232+
* Reports an unsupported binding on a form `FormField` node.
233+
*/
234+
formFieldUnsupportedBinding(
235+
id: TypeCheckId,
236+
node: TmplAstBoundAttribute | TmplAstTextAttribute,
237+
): void;
238+
239+
/**
240+
* Reports that multiple components in the compilation scope match a given element.
241+
*/
242+
multipleMatchingComponents(
243+
id: TypeCheckId,
244+
element: TmplAstElement,
245+
componentNames: string[],
246+
): void;
247+
}

packages/compiler-cli/src/ngtsc/typecheck/src/context.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
import MagicString from 'magic-string';
1919
import ts from 'typescript';
2020

21-
import {ErrorCode, ngErrorCode} from '../../../../src/ngtsc/diagnostics';
21+
import {ErrorCode, makeDiagnostic, ngErrorCode} from '../../../../src/ngtsc/diagnostics';
2222
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
2323
import {Reference, ReferenceEmitter} from '../../imports';
2424
import {PerfEvent, PerfRecorder} from '../../perf';
@@ -36,13 +36,14 @@ import {
3636
TypeCheckingConfig,
3737
TypeCtorMetadata,
3838
TemplateContext,
39+
OutOfBandDiagnosticRecorder,
3940
} from '../api';
4041
import {makeTemplateDiagnostic} from '../diagnostics';
4142

4243
import {adaptTypeCheckBlockMetadata} from './tcb_adapter';
4344
import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom';
4445
import {Environment} from './environment';
45-
import {OutOfBandDiagnosticRecorder, OutOfBandDiagnosticRecorderImpl} from './oob';
46+
import {OutOfBandDiagnosticRecorderImpl} from './oob';
4647
import {ReferenceEmitEnvironment} from './reference_emit_environment';
4748
import {TypeCheckShimGenerator} from './shim';
4849
import {DirectiveSourceManager} from './source';
@@ -128,7 +129,7 @@ export interface PendingShimData {
128129
/**
129130
* Recorder for out-of-band diagnostics which are raised during generation.
130131
*/
131-
oobRecorder: OutOfBandDiagnosticRecorder;
132+
oobRecorder: OutOfBandDiagnosticRecorder<TemplateDiagnostic>;
132133

133134
/**
134135
* The `DomSchemaChecker` in use for this template, which records any schema-related diagnostics.
@@ -144,6 +145,11 @@ export interface PendingShimData {
144145
* Map of `TypeCheckId` to information collected about the template as it's ingested.
145146
*/
146147
data: Map<TypeCheckId, TypeCheckData>;
148+
149+
/**
150+
* Diagnostics produced during shim creation.
151+
*/
152+
shimDiagnostics: TemplateDiagnostic[] | null;
147153
}
148154

149155
/**
@@ -335,7 +341,16 @@ export class TypeCheckContextImpl implements TypeCheckContext {
335341
// and inlining would be required.
336342

337343
// Record diagnostics to indicate the issues with this template.
338-
shimData.oobRecorder.requiresInlineTcb(id, ref.node);
344+
shimData.shimDiagnostics ??= [];
345+
shimData.shimDiagnostics.push({
346+
...makeDiagnostic(
347+
ErrorCode.INLINE_TCB_REQUIRED,
348+
ref.node.name,
349+
`This component requires inline template type-checking, which is not supported by the current environment.`,
350+
),
351+
sourceFile: ref.node.getSourceFile(),
352+
typeCheckId: id,
353+
});
339354

340355
// Checking this template would be unsupported, so don't try.
341356
this.perf.eventCount(PerfEvent.SkipGenerateTcbNoInline);
@@ -516,11 +531,17 @@ export class TypeCheckContextImpl implements TypeCheckContext {
516531
for (const [sfPath, pendingFileData] of this.fileMap) {
517532
// For each input file, consider generation operations for each of its shims.
518533
for (const pendingShimData of pendingFileData.shimData.values()) {
534+
const genesisDiagnostics = [
535+
...pendingShimData.domSchemaChecker.diagnostics,
536+
...pendingShimData.oobRecorder.diagnostics,
537+
];
538+
539+
if (pendingShimData.shimDiagnostics !== null) {
540+
genesisDiagnostics.unshift(...pendingShimData.shimDiagnostics);
541+
}
542+
519543
this.host.recordShimData(sfPath, {
520-
genesisDiagnostics: [
521-
...pendingShimData.domSchemaChecker.diagnostics,
522-
...pendingShimData.oobRecorder.diagnostics,
523-
],
544+
genesisDiagnostics,
524545
hasInlines: pendingFileData.hasInlines,
525546
path: pendingShimData.file.fileName,
526547
data: pendingShimData.data,
@@ -579,6 +600,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
579600
this.compilerHost,
580601
),
581602
data: new Map<TypeCheckId, TypeCheckData>(),
603+
shimDiagnostics: null,
582604
});
583605
}
584606
return fileData.shimData.get(shimPath)!;
@@ -657,7 +679,7 @@ class InlineTcbOp implements Op {
657679
readonly config: TypeCheckingConfig,
658680
readonly reflector: ReflectionHost,
659681
readonly domSchemaChecker: DomSchemaChecker,
660-
readonly oobRecorder: OutOfBandDiagnosticRecorder,
682+
readonly oobRecorder: OutOfBandDiagnosticRecorder<unknown>,
661683
) {}
662684

663685
/**

0 commit comments

Comments
 (0)