-
Notifications
You must be signed in to change notification settings - Fork 24.8k
/
diagnostic.ts
178 lines (161 loc) Β· 6.66 KB
/
diagnostic.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
/**
* @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 {ParseSourceSpan} from '@angular/compiler';
import ts from 'typescript';
import {addDiagnosticChain, makeDiagnosticChain} from '../../../diagnostics';
import {ExternalTemplateSourceMapping, IndirectTemplateSourceMapping, TemplateDiagnostic, TemplateId, TemplateSourceMapping} from '../../api';
/**
* Constructs a `ts.Diagnostic` for a given `ParseSourceSpan` within a template.
*/
export function makeTemplateDiagnostic(
templateId: TemplateId, mapping: TemplateSourceMapping, span: ParseSourceSpan,
category: ts.DiagnosticCategory, code: number, messageText: string|ts.DiagnosticMessageChain,
relatedMessages?: {
text: string,
start: number,
end: number,
sourceFile: ts.SourceFile,
}[]): TemplateDiagnostic {
if (mapping.type === 'direct') {
let relatedInformation: ts.DiagnosticRelatedInformation[]|undefined = undefined;
if (relatedMessages !== undefined) {
relatedInformation = [];
for (const relatedMessage of relatedMessages) {
relatedInformation.push({
category: ts.DiagnosticCategory.Message,
code: 0,
file: relatedMessage.sourceFile,
start: relatedMessage.start,
length: relatedMessage.end - relatedMessage.start,
messageText: relatedMessage.text,
});
}
}
// For direct mappings, the error is shown inline as ngtsc was able to pinpoint a string
// constant within the `@Component` decorator for the template. This allows us to map the error
// directly into the bytes of the source file.
return {
source: 'ngtsc',
code,
category,
messageText,
file: mapping.node.getSourceFile(),
componentFile: mapping.node.getSourceFile(),
templateId,
start: span.start.offset,
length: span.end.offset - span.start.offset,
relatedInformation,
};
} else if (mapping.type === 'indirect' || mapping.type === 'external') {
// For indirect mappings (template was declared inline, but ngtsc couldn't map it directly
// to a string constant in the decorator), the component's file name is given with a suffix
// indicating it's not the TS file being displayed, but a template.
// For external temoplates, the HTML filename is used.
const componentSf = mapping.componentClass.getSourceFile();
const componentName = mapping.componentClass.name.text;
// TODO(alxhub): remove cast when TS in g3 supports this narrowing.
const fileName = mapping.type === 'indirect' ?
`${componentSf.fileName} (${componentName} template)` :
(mapping as ExternalTemplateSourceMapping).templateUrl;
let relatedInformation: ts.DiagnosticRelatedInformation[] = [];
if (relatedMessages !== undefined) {
for (const relatedMessage of relatedMessages) {
relatedInformation.push({
category: ts.DiagnosticCategory.Message,
code: 0,
file: relatedMessage.sourceFile,
start: relatedMessage.start,
length: relatedMessage.end - relatedMessage.start,
messageText: relatedMessage.text,
});
}
}
let sf: ts.SourceFile;
try {
sf = getParsedTemplateSourceFile(fileName, mapping);
} catch (e) {
const failureChain = makeDiagnosticChain(
`Failed to report an error in '${fileName}' at ${span.start.line + 1}:${
span.start.col + 1}`,
[
makeDiagnosticChain((e as Error)?.stack ?? `${e}`),
]);
return {
source: 'ngtsc',
category,
code,
messageText: addDiagnosticChain(messageText, [failureChain]),
file: componentSf,
componentFile: componentSf,
templateId,
// mapping.node represents either the 'template' or 'templateUrl' expression. getStart()
// and getEnd() are used because they don't include surrounding whitespace.
start: mapping.node.getStart(),
length: mapping.node.getEnd() - mapping.node.getStart(),
relatedInformation,
};
}
relatedInformation.push({
category: ts.DiagnosticCategory.Message,
code: 0,
file: componentSf,
// mapping.node represents either the 'template' or 'templateUrl' expression. getStart()
// and getEnd() are used because they don't include surrounding whitespace.
start: mapping.node.getStart(),
length: mapping.node.getEnd() - mapping.node.getStart(),
messageText: `Error occurs in the template of component ${componentName}.`,
});
return {
source: 'ngtsc',
category,
code,
messageText,
file: sf,
componentFile: componentSf,
templateId,
start: span.start.offset,
length: span.end.offset - span.start.offset,
// Show a secondary message indicating the component whose template contains the error.
relatedInformation,
};
} else {
throw new Error(`Unexpected source mapping type: ${(mapping as {type: string}).type}`);
}
}
const TemplateSourceFile = Symbol('TemplateSourceFile');
type TemplateSourceMappingWithSourceFile =
(ExternalTemplateSourceMapping|IndirectTemplateSourceMapping)&{
[TemplateSourceFile]?: ts.SourceFile;
};
function getParsedTemplateSourceFile(
fileName: string, mapping: TemplateSourceMappingWithSourceFile): ts.SourceFile {
if (mapping[TemplateSourceFile] !== undefined) {
mapping[TemplateSourceFile] = parseTemplateAsSourceFile(fileName, mapping.template);
}
return mapping[TemplateSourceFile]!;
}
let parseTemplateAsSourceFileForTest: typeof parseTemplateAsSourceFile|null = null;
export function setParseTemplateAsSourceFileForTest(fn: typeof parseTemplateAsSourceFile): void {
parseTemplateAsSourceFileForTest = fn;
}
export function resetParseTemplateAsSourceFileForTest(): void {
parseTemplateAsSourceFileForTest = null;
}
function parseTemplateAsSourceFile(fileName: string, template: string): ts.SourceFile {
if (parseTemplateAsSourceFileForTest !== null) {
return parseTemplateAsSourceFileForTest(fileName, template);
}
// TODO(alxhub): investigate creating a fake `ts.SourceFile` here instead of invoking the TS
// parser against the template (HTML is just really syntactically invalid TypeScript code ;).
return ts.createSourceFile(
fileName, template, ts.ScriptTarget.Latest, /* setParentNodes */ false, ts.ScriptKind.JSX);
}
export function isTemplateDiagnostic(diagnostic: ts.Diagnostic): diagnostic is TemplateDiagnostic {
return diagnostic.hasOwnProperty('componentFile') &&
ts.isSourceFile((diagnostic as any).componentFile);
}