1 | import * as _ from 'lodash';
|
2 | import { ts, SyntaxKind } from 'ts-morph';
|
3 |
|
4 | import * as _ts from './ts-internal';
|
5 |
|
6 | import { JSDocParameterTagExt } from '../app/nodes/jsdoc-parameter-tag.node';
|
7 |
|
8 | export 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 |
|
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;
|
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 |
|
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 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
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 |
|
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 |
|
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 |
|
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 |
|
263 |
|
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 | }
|