UNPKG

11.3 kBPlain TextView Raw
1import * as _ from 'lodash';
2import { ts, SyntaxKind } from 'ts-morph';
3
4import * as _ts from './ts-internal';
5
6import { JSDocParameterTagExt } from '../app/nodes/jsdoc-parameter-tag.node';
7
8export class JsdocParserUtil {
9 public isVariableLike(node: ts.Node): node is ts.VariableLikeDeclaration {
10 if (node) {
11 switch (node.kind) {
12 case SyntaxKind.BindingElement:
13 case SyntaxKind.EnumMember:
14 case SyntaxKind.Parameter:
15 case SyntaxKind.PropertyAssignment:
16 case SyntaxKind.PropertyDeclaration:
17 case SyntaxKind.PropertySignature:
18 case SyntaxKind.ShorthandPropertyAssignment:
19 case SyntaxKind.VariableDeclaration:
20 return true;
21 }
22 }
23 return false;
24 }
25
26 isTopmostModuleDeclaration(node: ts.ModuleDeclaration): boolean {
27 if (node.nextContainer && node.nextContainer.kind === ts.SyntaxKind.ModuleDeclaration) {
28 let next = <ts.ModuleDeclaration>node.nextContainer;
29 if (node.name.end + 1 === next.name.pos) {
30 return false;
31 }
32 }
33
34 return true;
35 }
36
37 getRootModuleDeclaration(node: ts.ModuleDeclaration): ts.Node {
38 while (node.parent && node.parent.kind === ts.SyntaxKind.ModuleDeclaration) {
39 let parent = <ts.ModuleDeclaration>node.parent;
40 if (node.name.pos === parent.name.end + 1) {
41 node = parent;
42 } else {
43 break;
44 }
45 }
46
47 return node;
48 }
49
50 public getMainCommentOfNode(node: ts.Node, sourceFile?: ts.SourceFile): string {
51 let description: string = '';
52
53 if (node.parent && node.parent.kind === ts.SyntaxKind.VariableDeclarationList) {
54 node = node.parent.parent;
55 } else if (node.kind === ts.SyntaxKind.ModuleDeclaration) {
56 if (!this.isTopmostModuleDeclaration(<ts.ModuleDeclaration>node)) {
57 return null;
58 } else {
59 node = this.getRootModuleDeclaration(<ts.ModuleDeclaration>node);
60 }
61 }
62
63 const comments = _ts.getJSDocCommentRanges(node, sourceFile.text);
64 if (comments && comments.length) {
65 let comment: ts.CommentRange;
66 if (node.kind === ts.SyntaxKind.SourceFile) {
67 if (comments.length === 1) {
68 return null;
69 }
70 comment = comments[0];
71 } else {
72 comment = comments[comments.length - 1];
73 }
74
75 description = sourceFile.text.substring(comment.pos, comment.end);
76 }
77 return description;
78 }
79
80 public parseComment(text: string): string {
81 let comment = '';
82 let shortText = 0;
83
84 function readBareLine(line: string) {
85 comment += '\n' + line;
86 if (line === '' && shortText === 0) {
87 // Ignore
88 } else if (line === '' && shortText === 1) {
89 shortText = 2;
90 } else {
91 if (shortText === 2) {
92 comment += (comment === '' ? '' : '\n') + line;
93 }
94 }
95 }
96
97 const CODE_FENCE = /^\s*```(?!.*```)/;
98 let inCode = false;
99 let inExample = false; // first line with @example, end line with empty string or string or */
100 let nbLines = 0;
101 function readLine(line: string, index: number) {
102 line = line.replace(/^\s*\*? ?/, '');
103 line = line.replace(/\s*$/, '');
104
105 if (CODE_FENCE.test(line)) {
106 inCode = !inCode;
107 }
108
109 if (line.indexOf('@example') !== -1) {
110 inExample = true;
111 line = '```html';
112 }
113
114 if (inExample && line === '') {
115 inExample = false;
116 line = '```';
117 }
118
119 if (!inCode) {
120 const tag = /^@(\S+)/.exec(line);
121 const SeeTag = /^@see/.exec(line);
122
123 if (SeeTag) {
124 line = line.replace(/^@see/, 'See');
125 }
126
127 if (tag && !SeeTag) {
128 return;
129 }
130 }
131
132 readBareLine(line);
133 }
134
135 text = text.replace(/^\s*\/\*+/, '');
136 text = text.replace(/\*+\/\s*$/, '');
137
138 nbLines = text.split(/\r\n?|\n/).length;
139
140 text.split(/\r\n?|\n/).forEach(readLine);
141
142 return comment;
143 }
144
145 private getJSDocTags(node: ts.Node, kind: SyntaxKind): ts.JSDocTag[] {
146 const docs = this.getJSDocs(node);
147 if (docs) {
148 const result: ts.JSDocTag[] = [];
149 for (const doc of docs) {
150 if (ts.isJSDocParameterTag(doc)) {
151 if (doc.kind === kind) {
152 result.push(doc);
153 }
154 } else if (ts.isJSDoc(doc)) {
155 result.push(..._.filter(doc.tags, tag => tag.kind === kind));
156 } else {
157 throw new Error('Unexpected type');
158 }
159 }
160 return result;
161 }
162 }
163
164 public getJSDocs(node: ts.Node): ReadonlyArray<ts.JSDoc | ts.JSDocTag> {
165 // TODO: jsDocCache is internal, see if there's a way around it
166 let cache: ReadonlyArray<ts.JSDoc | ts.JSDocTag> = (node as any).jsDocCache;
167 if (!cache) {
168 cache = this.getJSDocsWorker(node, []).filter(x => x);
169 (node as any).jsDocCache = cache;
170 }
171 return cache;
172 }
173
174 // Try to recognize this pattern when node is initializer
175 // of variable declaration and JSDoc comments are on containing variable statement.
176 // /**
177 // * @param {number} name
178 // * @returns {number}
179 // */
180 // var x = function(name) { return name.length; }
181 private getJSDocsWorker(node: ts.Node, cache): ReadonlyArray<any> {
182 const parent = node.parent;
183 const isInitializerOfVariableDeclarationInStatement =
184 this.isVariableLike(parent) &&
185 parent.initializer === node &&
186 ts.isVariableStatement(parent.parent.parent);
187 const isVariableOfVariableDeclarationStatement =
188 this.isVariableLike(node) && ts.isVariableStatement(parent.parent);
189 const variableStatementNode = isInitializerOfVariableDeclarationInStatement
190 ? parent.parent.parent
191 : isVariableOfVariableDeclarationStatement
192 ? parent.parent
193 : undefined;
194 if (variableStatementNode) {
195 cache = this.getJSDocsWorker(variableStatementNode, cache);
196 }
197
198 // Also recognize when the node is the RHS of an assignment expression
199 const isSourceOfAssignmentExpressionStatement =
200 parent &&
201 parent.parent &&
202 ts.isBinaryExpression(parent) &&
203 parent.operatorToken.kind === SyntaxKind.EqualsToken &&
204 ts.isExpressionStatement(parent.parent);
205 if (isSourceOfAssignmentExpressionStatement) {
206 cache = this.getJSDocsWorker(parent.parent, cache);
207 }
208
209 const isModuleDeclaration =
210 ts.isModuleDeclaration(node) && parent && ts.isModuleDeclaration(parent);
211 const isPropertyAssignmentExpression = parent && ts.isPropertyAssignment(parent);
212 if (isModuleDeclaration || isPropertyAssignmentExpression) {
213 cache = this.getJSDocsWorker(parent, cache);
214 }
215
216 // Pull parameter comments from declaring function as well
217 if (ts.isParameter(node)) {
218 cache = _.concat(cache, this.getJSDocParameterTags(node));
219 }
220
221 if (this.isVariableLike(node) && node.initializer) {
222 cache = _.concat(cache, node.initializer.jsDoc);
223 }
224
225 cache = _.concat(cache, node.jsDoc);
226
227 return cache;
228 }
229
230 private getJSDocParameterTags(
231 param: ts.ParameterDeclaration
232 ): ReadonlyArray<ts.JSDocParameterTag> {
233 const func = param.parent as ts.FunctionLikeDeclaration;
234 const tags = this.getJSDocTags(
235 func,
236 SyntaxKind.JSDocParameterTag
237 ) as ts.JSDocParameterTag[];
238
239 if (!param.name) {
240 // this is an anonymous jsdoc param from a `function(type1, type2): type3` specification
241 const i = func.parameters.indexOf(param);
242 const paramTags = _.filter(tags, tag => ts.isJSDocParameterTag(tag));
243
244 if (paramTags && 0 <= i && i < paramTags.length) {
245 return [paramTags[i]];
246 }
247 } else if (ts.isIdentifier(param.name)) {
248 const name = param.name.text;
249 return _.filter(tags, tag => {
250 if (ts && ts.isJSDocParameterTag(tag)) {
251 let t: JSDocParameterTagExt = tag;
252 if (typeof t.parameterName !== 'undefined') {
253 return t.parameterName.text === name;
254 } else if (typeof t.name !== 'undefined') {
255 if (typeof t.name.escapedText !== 'undefined') {
256 return t.name.escapedText === name;
257 }
258 }
259 }
260 });
261 } else {
262 // TODO: it's a destructured parameter, so it should look up an "object type" series of multiple lines
263 // But multi-line object types aren't supported yet either
264 return undefined;
265 }
266 }
267
268 public parseJSDocNode(node): string {
269 let rawDescription = '';
270
271 if (typeof node.comment === 'string') {
272 rawDescription += node.comment;
273 } else {
274 if (node.comment) {
275 const len = node.comment.length;
276
277 for (let i = 0; i < len; i++) {
278 const JSDocNode = node.comment[i];
279 switch (JSDocNode.kind) {
280 case SyntaxKind.JSDocComment:
281 rawDescription += JSDocNode.comment;
282 break;
283 case SyntaxKind.JSDocText:
284 rawDescription += JSDocNode.text;
285 break;
286 case SyntaxKind.JSDocLink:
287 if (JSDocNode.name) {
288 let text = JSDocNode.name.escapedText;
289 if (
290 text === undefined &&
291 JSDocNode.name.left &&
292 JSDocNode.name.right
293 ) {
294 text =
295 JSDocNode.name.left.escapedText +
296 '.' +
297 JSDocNode.name.right.escapedText;
298 }
299 rawDescription += JSDocNode.text + '{@link ' + text + '}';
300 }
301 break;
302 default:
303 break;
304 }
305 }
306 }
307 }
308
309 return rawDescription;
310 }
311}