forked from angular/angular
/
oob.ts
390 lines (334 loc) · 17.2 KB
/
oob.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
/**
* @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 {BindingPipe, PropertyRead, PropertyWrite, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstForLoopBlock, TmplAstHoverDeferredTrigger, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstReference, TmplAstTemplate, TmplAstVariable, TmplAstViewportDeferredTrigger} from '@angular/compiler';
import ts from 'typescript';
import {ErrorCode, makeDiagnostic, makeRelatedInformation, ngErrorCode} from '../../diagnostics';
import {ClassDeclaration} from '../../reflection';
import {TemplateDiagnostic, TemplateId} from '../api';
import {makeTemplateDiagnostic} from '../diagnostics';
import {TemplateSourceResolver} from './tcb_util';
/**
* Collects `ts.Diagnostic`s on problems which occur in the template which aren't directly sourced
* from Type Check Blocks.
*
* During the creation of a Type Check Block, the template is traversed and the
* `OutOfBandDiagnosticRecorder` is called to record cases when a correct interpretation for the
* template cannot be found. These operations create `ts.Diagnostic`s which are stored by the
* recorder for later display.
*/
export interface OutOfBandDiagnosticRecorder {
readonly diagnostics: ReadonlyArray<TemplateDiagnostic>;
/**
* Reports a `#ref="target"` expression in the template for which a target directive could not be
* found.
*
* @param templateId the template type-checking ID of the template which contains the broken
* reference.
* @param ref the `TmplAstReference` which could not be matched to a directive.
*/
missingReferenceTarget(templateId: TemplateId, ref: TmplAstReference): void;
/**
* Reports usage of a `| pipe` expression in the template for which the named pipe could not be
* found.
*
* @param templateId the template type-checking ID of the template which contains the unknown
* pipe.
* @param ast the `BindingPipe` invocation of the pipe which could not be found.
*/
missingPipe(templateId: TemplateId, ast: BindingPipe): void;
illegalAssignmentToTemplateVar(
templateId: TemplateId, assignment: PropertyWrite, target: TmplAstVariable): void;
/**
* Reports a duplicate declaration of a template variable.
*
* @param templateId the template type-checking ID of the template which contains the duplicate
* declaration.
* @param variable the `TmplAstVariable` which duplicates a previously declared variable.
* @param firstDecl the first variable declaration which uses the same name as `variable`.
*/
duplicateTemplateVar(
templateId: TemplateId, variable: TmplAstVariable, firstDecl: TmplAstVariable): void;
requiresInlineTcb(templateId: TemplateId, node: ClassDeclaration): void;
requiresInlineTypeConstructors(
templateId: TemplateId, node: ClassDeclaration, directives: ClassDeclaration[]): void;
/**
* Report a warning when structural directives support context guards, but the current
* type-checking configuration prohibits their usage.
*/
suboptimalTypeInference(templateId: TemplateId, variables: TmplAstVariable[]): void;
/**
* Reports a split two way binding error message.
*/
splitTwoWayBinding(
templateId: TemplateId, input: TmplAstBoundAttribute, output: TmplAstBoundEvent,
inputConsumer: ClassDeclaration, outputConsumer: ClassDeclaration|TmplAstElement): void;
/** Reports required inputs that haven't been bound. */
missingRequiredInputs(
templateId: TemplateId, element: TmplAstElement|TmplAstTemplate, directiveName: string,
isComponent: boolean, inputAliases: string[]): void;
/**
* Reports accesses of properties that aren't available in a `for` block's tracking expression.
*/
illegalForLoopTrackAccess(
templateId: TemplateId, block: TmplAstForLoopBlock, access: PropertyRead): void;
/**
* Reports deferred triggers that cannot access the element they're referring to.
*/
inaccessibleDeferredTriggerElement(
templateId: TemplateId,
trigger: TmplAstHoverDeferredTrigger|TmplAstInteractionDeferredTrigger|
TmplAstViewportDeferredTrigger): void;
/**
* Reports cases where control flow nodes prevent content projection.
*/
controlFlowPreventingContentProjection(
templateId: TemplateId, projectionNode: TmplAstElement|TmplAstTemplate, componentName: string,
slotSelector: string, controlFlowNode: TmplAstIfBlockBranch|TmplAstForLoopBlock,
preservesWhitespaces: boolean): void;
}
export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder {
private _diagnostics: TemplateDiagnostic[] = [];
/**
* Tracks which `BindingPipe` nodes have already been recorded as invalid, so only one diagnostic
* is ever produced per node.
*/
private recordedPipes = new Set<BindingPipe>();
constructor(private resolver: TemplateSourceResolver) {}
get diagnostics(): ReadonlyArray<TemplateDiagnostic> {
return this._diagnostics;
}
missingReferenceTarget(templateId: TemplateId, ref: TmplAstReference): void {
const mapping = this.resolver.getSourceMapping(templateId);
const value = ref.value.trim();
const errorMsg = `No directive found with exportAs '${value}'.`;
this._diagnostics.push(makeTemplateDiagnostic(
templateId, mapping, ref.valueSpan || ref.sourceSpan, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.MISSING_REFERENCE_TARGET), errorMsg));
}
missingPipe(templateId: TemplateId, ast: BindingPipe): void {
if (this.recordedPipes.has(ast)) {
return;
}
const mapping = this.resolver.getSourceMapping(templateId);
const errorMsg = `No pipe found with name '${ast.name}'.`;
const sourceSpan = this.resolver.toParseSourceSpan(templateId, ast.nameSpan);
if (sourceSpan === null) {
throw new Error(
`Assertion failure: no SourceLocation found for usage of pipe '${ast.name}'.`);
}
this._diagnostics.push(makeTemplateDiagnostic(
templateId, mapping, sourceSpan, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.MISSING_PIPE), errorMsg));
this.recordedPipes.add(ast);
}
illegalAssignmentToTemplateVar(
templateId: TemplateId, assignment: PropertyWrite, target: TmplAstVariable): void {
const mapping = this.resolver.getSourceMapping(templateId);
const errorMsg = `Cannot use variable '${
assignment
.name}' as the left-hand side of an assignment expression. Template variables are read-only.`;
const sourceSpan = this.resolver.toParseSourceSpan(templateId, assignment.sourceSpan);
if (sourceSpan === null) {
throw new Error(`Assertion failure: no SourceLocation found for property binding.`);
}
this._diagnostics.push(makeTemplateDiagnostic(
templateId, mapping, sourceSpan, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.WRITE_TO_READ_ONLY_VARIABLE), errorMsg, [{
text: `The variable ${assignment.name} is declared here.`,
start: target.valueSpan?.start.offset || target.sourceSpan.start.offset,
end: target.valueSpan?.end.offset || target.sourceSpan.end.offset,
sourceFile: mapping.node.getSourceFile(),
}]));
}
duplicateTemplateVar(
templateId: TemplateId, variable: TmplAstVariable, firstDecl: TmplAstVariable): void {
const mapping = this.resolver.getSourceMapping(templateId);
const errorMsg = `Cannot redeclare variable '${
variable.name}' as it was previously declared elsewhere for the same template.`;
// The allocation of the error here is pretty useless for variables declared in microsyntax,
// since the sourceSpan refers to the entire microsyntax property, not a span for the specific
// variable in question.
//
// TODO(alxhub): allocate to a tighter span once one is available.
this._diagnostics.push(makeTemplateDiagnostic(
templateId, mapping, variable.sourceSpan, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.DUPLICATE_VARIABLE_DECLARATION), errorMsg, [{
text: `The variable '${firstDecl.name}' was first declared here.`,
start: firstDecl.sourceSpan.start.offset,
end: firstDecl.sourceSpan.end.offset,
sourceFile: mapping.node.getSourceFile(),
}]));
}
requiresInlineTcb(templateId: TemplateId, node: ClassDeclaration): void {
this._diagnostics.push(makeInlineDiagnostic(
templateId, ErrorCode.INLINE_TCB_REQUIRED, node.name,
`This component requires inline template type-checking, which is not supported by the current environment.`));
}
requiresInlineTypeConstructors(
templateId: TemplateId, node: ClassDeclaration, directives: ClassDeclaration[]): void {
let message: string;
if (directives.length > 1) {
message =
`This component uses directives which require inline type constructors, which are not supported by the current environment.`;
} else {
message =
`This component uses a directive which requires an inline type constructor, which is not supported by the current environment.`;
}
this._diagnostics.push(makeInlineDiagnostic(
templateId, ErrorCode.INLINE_TYPE_CTOR_REQUIRED, node.name, message,
directives.map(
dir => makeRelatedInformation(dir.name, `Requires an inline type constructor.`))));
}
suboptimalTypeInference(templateId: TemplateId, variables: TmplAstVariable[]): void {
const mapping = this.resolver.getSourceMapping(templateId);
// Select one of the template variables that's most suitable for reporting the diagnostic. Any
// variable will do, but prefer one bound to the context's $implicit if present.
let diagnosticVar: TmplAstVariable|null = null;
for (const variable of variables) {
if (diagnosticVar === null || (variable.value === '' || variable.value === '$implicit')) {
diagnosticVar = variable;
}
}
if (diagnosticVar === null) {
// There is no variable on which to report the diagnostic.
return;
}
let varIdentification = `'${diagnosticVar.name}'`;
if (variables.length === 2) {
varIdentification += ` (and 1 other)`;
} else if (variables.length > 2) {
varIdentification += ` (and ${variables.length - 1} others)`;
}
const message =
`This structural directive supports advanced type inference, but the current compiler configuration prevents its usage. The variable ${
varIdentification} will have type 'any' as a result.\n\nConsider enabling the 'strictTemplates' option in your tsconfig.json for better type inference within this template.`;
this._diagnostics.push(makeTemplateDiagnostic(
templateId, mapping, diagnosticVar.keySpan, ts.DiagnosticCategory.Suggestion,
ngErrorCode(ErrorCode.SUGGEST_SUBOPTIMAL_TYPE_INFERENCE), message));
}
splitTwoWayBinding(
templateId: TemplateId, input: TmplAstBoundAttribute, output: TmplAstBoundEvent,
inputConsumer: ClassDeclaration, outputConsumer: ClassDeclaration|TmplAstElement): void {
const mapping = this.resolver.getSourceMapping(templateId);
const errorMsg = `The property and event halves of the two-way binding '${
input.name}' are not bound to the same target.
Find more at https://angular.io/guide/two-way-binding#how-two-way-binding-works`;
const relatedMessages: {text: string; start: number; end: number;
sourceFile: ts.SourceFile;}[] = [];
relatedMessages.push({
text: `The property half of the binding is to the '${inputConsumer.name.text}' component.`,
start: inputConsumer.name.getStart(),
end: inputConsumer.name.getEnd(),
sourceFile: inputConsumer.name.getSourceFile(),
});
if (outputConsumer instanceof TmplAstElement) {
let message = `The event half of the binding is to a native event called '${
input.name}' on the <${outputConsumer.name}> DOM element.`;
if (!mapping.node.getSourceFile().isDeclarationFile) {
message += `\n \n Are you missing an output declaration called '${output.name}'?`;
}
relatedMessages.push({
text: message,
start: outputConsumer.sourceSpan.start.offset + 1,
end: outputConsumer.sourceSpan.start.offset + outputConsumer.name.length + 1,
sourceFile: mapping.node.getSourceFile(),
});
} else {
relatedMessages.push({
text: `The event half of the binding is to the '${outputConsumer.name.text}' component.`,
start: outputConsumer.name.getStart(),
end: outputConsumer.name.getEnd(),
sourceFile: outputConsumer.name.getSourceFile(),
});
}
this._diagnostics.push(makeTemplateDiagnostic(
templateId, mapping, input.keySpan, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.SPLIT_TWO_WAY_BINDING), errorMsg, relatedMessages));
}
missingRequiredInputs(
templateId: TemplateId, element: TmplAstElement|TmplAstTemplate, directiveName: string,
isComponent: boolean, inputAliases: string[]): void {
const message = `Required input${inputAliases.length === 1 ? '' : 's'} ${
inputAliases.map(n => `'${n}'`).join(', ')} from ${
isComponent ? 'component' : 'directive'} ${directiveName} must be specified.`;
this._diagnostics.push(makeTemplateDiagnostic(
templateId, this.resolver.getSourceMapping(templateId), element.startSourceSpan,
ts.DiagnosticCategory.Error, ngErrorCode(ErrorCode.MISSING_REQUIRED_INPUTS), message));
}
illegalForLoopTrackAccess(
templateId: TemplateId, block: TmplAstForLoopBlock, access: PropertyRead): void {
const sourceSpan = this.resolver.toParseSourceSpan(templateId, access.sourceSpan);
if (sourceSpan === null) {
throw new Error(`Assertion failure: no SourceLocation found for property read.`);
}
const message =
`Cannot access '${access.name}' inside of a track expression. ` +
`Only '${block.item.name}', '${
block.contextVariables.$index
.name}' and properties on the containing component are available to this expression.`;
this._diagnostics.push(makeTemplateDiagnostic(
templateId, this.resolver.getSourceMapping(templateId), sourceSpan,
ts.DiagnosticCategory.Error, ngErrorCode(ErrorCode.ILLEGAL_FOR_LOOP_TRACK_ACCESS),
message));
}
inaccessibleDeferredTriggerElement(
templateId: TemplateId,
trigger: TmplAstHoverDeferredTrigger|TmplAstInteractionDeferredTrigger|
TmplAstViewportDeferredTrigger): void {
let message: string;
if (trigger.reference === null) {
message = `Trigger cannot find reference. Make sure that the @defer block has a ` +
`@placeholder with at least one root element node.`;
} else {
message =
`Trigger cannot find reference "${trigger.reference}".\nCheck that an element with #${
trigger.reference} exists in the same template and it's accessible from the ` +
`@defer block.\nDeferred blocks can only access triggers in same view, a parent ` +
`embedded view or the root view of the @placeholder block.`;
}
this._diagnostics.push(makeTemplateDiagnostic(
templateId, this.resolver.getSourceMapping(templateId), trigger.sourceSpan,
ts.DiagnosticCategory.Error, ngErrorCode(ErrorCode.INACCESSIBLE_DEFERRED_TRIGGER_ELEMENT),
message));
}
controlFlowPreventingContentProjection(
templateId: TemplateId, projectionNode: TmplAstElement|TmplAstTemplate, componentName: string,
slotSelector: string, controlFlowNode: TmplAstIfBlockBranch|TmplAstForLoopBlock,
preservesWhitespaces: boolean): void {
const blockName = controlFlowNode instanceof TmplAstIfBlockBranch ? '@if' : '@for';
const lines = [
`Node matches the "${slotSelector}" slot of the "${
componentName}" component, but will not be projected because the surrounding ${
blockName} has more than one node at its root. To project the node in the right slot, you can:\n`,
`1. Wrap the content of the ${blockName} block in an <ng-container/> that matches the "${
slotSelector}" selector.`,
`2. Split the content of the ${blockName} block into across multiple ${
blockName} blocks such that each one only has a single projectable node at its root.`,
`3. Remove all content from the ${blockName} block, except for the node being projected.`
];
if (preservesWhitespaces) {
lines.push(
`Note: the host component has \`preserveWhitespaces: true\` which may ` +
`cause whitespace to affect content projection.`);
}
this._diagnostics.push(makeTemplateDiagnostic(
templateId, this.resolver.getSourceMapping(templateId), projectionNode.startSourceSpan,
ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION), lines.join('\n')));
}
}
function makeInlineDiagnostic(
templateId: TemplateId, code: ErrorCode.INLINE_TCB_REQUIRED|ErrorCode.INLINE_TYPE_CTOR_REQUIRED,
node: ts.Node, messageText: string|ts.DiagnosticMessageChain,
relatedInformation?: ts.DiagnosticRelatedInformation[]): TemplateDiagnostic {
return {
...makeDiagnostic(code, node, messageText, relatedInformation),
componentFile: node.getSourceFile(),
templateId,
};
}