-
Notifications
You must be signed in to change notification settings - Fork 189
/
CommentNodeParser.ts
331 lines (273 loc) · 12.6 KB
/
CommentNodeParser.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
import { errors, getSyntaxKindName, StringUtils, ts, SyntaxKind } from "@ts-morph/common";
import { CompilerCommentNode, CompilerCommentStatement, CompilerCommentClassElement, CompilerCommentTypeElement, CompilerCommentObjectLiteralElement,
CompilerCommentEnumMember, CommentNodeKind } from "../comment/CompilerComments";
enum CommentKind {
SingleLine,
MultiLine,
JsDoc
}
export type StatementContainerNodes = ts.SourceFile
| ts.Block
| ts.ModuleBlock
| ts.CaseClause
| ts.DefaultClause;
export type ContainerNodes = StatementContainerNodes
| ts.ClassDeclaration
| ts.InterfaceDeclaration
| ts.EnumDeclaration
| ts.ClassExpression
| ts.TypeLiteralNode
| ts.ObjectLiteralExpression;
type CommentSyntaxKinds = SyntaxKind.SingleLineCommentTrivia | SyntaxKind.MultiLineCommentTrivia;
const childrenSaver = new WeakMap<ContainerNodes, (ts.Node | CompilerCommentNode)[]>();
const commentNodeParserKinds = new Set<SyntaxKind>([
SyntaxKind.SourceFile,
SyntaxKind.Block,
SyntaxKind.ModuleBlock,
SyntaxKind.CaseClause,
SyntaxKind.DefaultClause,
SyntaxKind.ClassDeclaration,
SyntaxKind.InterfaceDeclaration,
SyntaxKind.EnumDeclaration,
SyntaxKind.ClassExpression,
SyntaxKind.TypeLiteral,
SyntaxKind.ObjectLiteralExpression
]);
export class CommentNodeParser {
private constructor() {
}
static getOrParseChildren(container: ContainerNodes | ts.SyntaxList, sourceFile: ts.SourceFile) {
// always store the syntax list result on the parent so that a second array isn't created
if (isSyntaxList(container))
container = container.parent as ContainerNodes;
// cache the result
let children = childrenSaver.get(container);
if (children == null) {
children = Array.from(getNodes(container, sourceFile));
childrenSaver.set(container, children);
}
return children;
}
static shouldParseChildren(container: ts.Node): container is ContainerNodes {
// this needs to be really fast because it's used whenever getting the children, so use a map
return commentNodeParserKinds.has(container.kind)
// Ignore zero length nodes... for some reason this might happen when parsing
// jsx in non-jsx files.
&& container.pos !== container.end;
}
static hasParsedChildren(container: ContainerNodes | ts.SyntaxList) {
if (isSyntaxList(container))
container = container.parent as ContainerNodes;
return childrenSaver.has(container);
}
static isCommentStatement(node: ts.Node): node is CompilerCommentStatement {
return (node as CompilerCommentNode)._commentKind === CommentNodeKind.Statement;
}
static isCommentClassElement(node: ts.Node): node is CompilerCommentClassElement {
return (node as CompilerCommentNode)._commentKind === CommentNodeKind.ClassElement;
}
static isCommentTypeElement(node: ts.Node): node is CompilerCommentTypeElement {
return (node as CompilerCommentNode)._commentKind === CommentNodeKind.TypeElement;
}
static isCommentObjectLiteralElement(node: ts.Node): node is CompilerCommentObjectLiteralElement {
return (node as CompilerCommentNode)._commentKind === CommentNodeKind.ObjectLiteralElement;
}
static isCommentEnumMember(node: ts.Node): node is CompilerCommentEnumMember {
return (node as CompilerCommentNode)._commentKind === CommentNodeKind.EnumMember;
}
static getContainerBodyPos(container: ContainerNodes, sourceFile: ts.SourceFile) {
if (ts.isSourceFile(container))
return 0;
if (ts.isClassDeclaration(container)
|| ts.isEnumDeclaration(container)
|| ts.isInterfaceDeclaration(container)
|| ts.isTypeLiteralNode(container)
|| ts.isClassExpression(container)
|| ts.isBlock(container)
|| ts.isModuleBlock(container)
|| ts.isObjectLiteralExpression(container))
{
// this function is only used when there are no statements or members, so only do this
return getTokenEnd(container, SyntaxKind.OpenBraceToken);
}
if (ts.isCaseClause(container) || ts.isDefaultClause(container))
return getTokenEnd(container, SyntaxKind.ColonToken);
return errors.throwNotImplementedForNeverValueError(container);
function getTokenEnd(node: ts.Node, kind: SyntaxKind.OpenBraceToken | SyntaxKind.ColonToken) {
// @code-fence-allow(getChildren): Ok, not searching for comments.
const token = node.getChildren(sourceFile).find(c => c.kind === kind);
if (token == null)
throw new errors.NotImplementedError(`Unexpected scenario where a(n) ${getSyntaxKindName(kind)} was not found.`);
return token.end;
}
}
}
function* getNodes(container: ContainerNodes, sourceFile: ts.SourceFile): IterableIterator<ts.Node | CompilerCommentNode> {
const sourceFileText = sourceFile.text;
const childNodes = getContainerChildren();
const createComment = getCreationFunction();
if (childNodes.length === 0) {
const bodyStartPos = CommentNodeParser.getContainerBodyPos(container, sourceFile);
yield* getCommentNodes(bodyStartPos, false); // do not skip js docs because they won't have a node to be attached to
}
else {
for (const childNode of childNodes) {
yield* getCommentNodes(childNode.pos, true);
yield childNode;
}
// get the comments on a newline after the last node
const lastChild = childNodes[childNodes.length - 1];
yield* getCommentNodes(lastChild.end, false); // parse any jsdocs afterwards
}
function* getCommentNodes(pos: number, stopAtJsDoc: boolean) {
const fullStart = pos;
skipTrailingLine();
const leadingComments = Array.from(getLeadingComments());
// `pos` will be at the first significant token of the next node or at the source file length.
// At this point, allow comments that end at the end of the source file or on the same line as the close brace token
const maxEnd = sourceFileText.length === pos || sourceFileText[pos] === "}" ? pos : StringUtils.getLineStartFromPos(sourceFileText, pos);
for (const leadingComment of leadingComments) {
if (leadingComment.end <= maxEnd)
yield leadingComment;
}
function skipTrailingLine() {
// skip first line of the block as the comment there is likely to describe the header
if (pos === 0)
return;
let lineEnd = StringUtils.getLineEndFromPos(sourceFileText, pos);
while (pos < lineEnd) {
const commentKind = getCommentKind();
if (commentKind != null) {
const comment = parseForComment(commentKind);
if (comment.kind === SyntaxKind.SingleLineCommentTrivia)
return;
else
lineEnd = StringUtils.getLineEndFromPos(sourceFileText, pos);
}
// skip any trailing comments too
else if (!StringUtils.isWhitespace(sourceFileText[pos]) && sourceFileText[pos] !== ",")
return;
else
pos++;
}
while (StringUtils.startsWithNewLine(sourceFileText[pos]))
pos++;
}
function* getLeadingComments() {
while (pos < sourceFileText.length) {
const commentKind = getCommentKind();
if (commentKind != null) {
const isJsDoc = commentKind === CommentKind.JsDoc;
if (isJsDoc && stopAtJsDoc)
return;
else
yield parseForComment(commentKind);
// treat comments on same line as trailing
skipTrailingLine();
}
else if (!StringUtils.isWhitespace(sourceFileText[pos]))
return;
else
pos++;
}
}
function parseForComment(commentKind: CommentKind) {
if (commentKind === CommentKind.SingleLine)
return parseSingleLineComment();
const isJsDoc = commentKind === CommentKind.JsDoc;
return parseMultiLineComment(isJsDoc);
}
function getCommentKind() {
const currentChar = sourceFileText[pos];
if (currentChar !== "/")
return undefined;
const nextChar = sourceFileText[pos + 1];
if (nextChar === "/")
return CommentKind.SingleLine;
if (nextChar !== "*")
return undefined;
const nextNextChar = sourceFileText[pos + 2];
return nextNextChar === "*" ? CommentKind.JsDoc : CommentKind.MultiLine;
}
function parseSingleLineComment() {
const start = pos;
skipSingleLineComment();
const end = pos;
return createComment(fullStart, start, end, SyntaxKind.SingleLineCommentTrivia);
}
function skipSingleLineComment() {
pos += 2; // skip the slash slash
while (pos < sourceFileText.length && sourceFileText[pos] !== "\n" && sourceFileText[pos] !== "\r")
pos++;
}
function parseMultiLineComment(isJsDoc: boolean) {
const start = pos;
skipSlashStarComment(isJsDoc);
const end = pos;
return createComment(fullStart, start, end, SyntaxKind.MultiLineCommentTrivia);
}
function skipSlashStarComment(isJsDoc: boolean) {
pos += isJsDoc ? 3 : 2; // skip slash star star or slash star
while (pos < sourceFileText.length) {
if (sourceFileText[pos] === "*" && sourceFileText[pos + 1] === "/") {
pos += 2; // skip star slash
break;
}
pos++;
}
}
}
function getContainerChildren() {
if (ts.isSourceFile(container) || ts.isBlock(container) || ts.isModuleBlock(container) || ts.isCaseClause(container) || ts.isDefaultClause(container))
return container.statements;
if (ts.isClassDeclaration(container)
|| ts.isClassExpression(container)
|| ts.isEnumDeclaration(container)
|| ts.isInterfaceDeclaration(container)
|| ts.isTypeLiteralNode(container)
|| ts.isClassExpression(container))
{
return container.members;
}
if (ts.isObjectLiteralExpression(container))
return container.properties;
return errors.throwNotImplementedForNeverValueError(container);
}
function getCreationFunction(): (fullStart: number, pos: number, end: number, kind: CommentSyntaxKinds) => CompilerCommentNode {
const ctor = getCtor();
return (fullStart: number, pos: number, end: number, kind: CommentSyntaxKinds) => new ctor(fullStart, pos, end, kind, sourceFile, container);
function getCtor() {
if (isStatementContainerNode(container))
return CompilerCommentStatement;
if (ts.isClassLike(container))
return CompilerCommentClassElement;
if (ts.isInterfaceDeclaration(container) || ts.isTypeLiteralNode(container))
return CompilerCommentTypeElement;
if (ts.isObjectLiteralExpression(container))
return CompilerCommentObjectLiteralElement;
if (ts.isEnumDeclaration(container))
return CompilerCommentEnumMember;
throw new errors.NotImplementedError(`Not implemented comment node container type: ${getSyntaxKindName(container.kind)}`);
}
}
}
function isSyntaxList(node: ts.Node): node is ts.SyntaxList {
return node.kind === SyntaxKind.SyntaxList;
}
function isStatementContainerNode(node: ts.Node) {
return getStatementContainerNode() != null;
function getStatementContainerNode(): StatementContainerNodes | undefined {
// this is a bit of a hack so the type checker ensures this is correct
const container = node as any as StatementContainerNodes;
if (ts.isSourceFile(container)
|| ts.isBlock(container)
|| ts.isModuleBlock(container)
|| ts.isCaseClause(container)
|| ts.isDefaultClause(container))
{
return container;
}
const assertNever: never = container;
return undefined;
}
}