/
diagnostic.ts
134 lines (123 loc) Β· 5.14 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
/**
* @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 {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;
// 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 ;).
const sf = getParsedTemplateSourceFile(fileName, mapping);
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,
});
}
}
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] = ts.createSourceFile(
fileName, mapping.template, ts.ScriptTarget.Latest, false, ts.ScriptKind.JSX);
}
return mapping[TemplateSourceFile];
}
export function isTemplateDiagnostic(diagnostic: ts.Diagnostic): diagnostic is TemplateDiagnostic {
return diagnostic.hasOwnProperty('componentFile') &&
ts.isSourceFile((diagnostic as any).componentFile);
}