UNPKG

21.9 kBJavaScriptView Raw
1/*
2 * Licensed under the Apache License, Version 2.0 (the "License");
3 * you may not use this file except in compliance with the License.
4 * You may obtain a copy of the License at
5 *
6 * http://www.apache.org/licenses/LICENSE-2.0
7 *
8 * Unless required by applicable law or agreed to in writing, software
9 * distributed under the License is distributed on an "AS IS" BASIS,
10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 * See the License for the specific language governing permissions and
12 * limitations under the License.
13 */
14
15'use strict';
16
17const doctrine = require('doctrine');
18const acorn = require('acorn');
19
20/**
21 * Processes a single Javascript file (.js extension)
22 *
23 * @param {string} file - the file to process
24 * @param {Object} fileProcessor - the processor instance to use to generate code
25 * @private
26 * @class
27 * @memberof module:concerto
28 */
29class JavaScriptParser {
30
31 /**
32 * Create a JavaScriptParser.
33 *
34 * @param {string} fileContents - the text of the JS file to parse
35 * @param {boolean} [includePrivates] - if true methods tagged as private are also returned
36 * @param {number} [ecmaVersion] - the ECMAScript version to use
37 * @param {boolean} [engineMode] - true if being used by engine for TP/ACL function Parsing
38 */
39 constructor(fileContents, includePrivates, ecmaVersion, engineMode = true) {
40 let comments = [];
41 this.tokens = [];
42
43 // If the magic flag is set, don't collect tokens. They eat up a lot of
44 // memory and this is problematic for the HLFv1 runtime (the only place
45 // that this flag is set).
46 let noTokens = !!global.composerJavaScriptParserNoTokens;
47
48 let options = {
49 // collect ranges for each node
50 ranges: true,
51 // collect comments in Esprima's format
52 onComment: comments,
53 // collect token ranges
54 onToken: noTokens ? null : this.tokens,
55 // collect token locations
56 locations: true,
57 // locations: true,
58 plugins: {
59 'composereof': true
60 }
61 };
62
63
64 if (ecmaVersion) {
65 options.ecmaVersion = ecmaVersion;
66 }
67
68 acorn.plugins.composereof = function (parser) {
69
70 parser.extend('parseTopLevel', function (nextMethod) {
71 return function (node) {
72 let this$1 = this;
73
74 let exports = {};
75 if (!node.body) {
76 node.body = [];
77 }
78 while (this.type.label !== 'eof') {
79 let stmt = this$1.parseStatement(true, true, exports);
80 node.body.push(stmt);
81 }
82 this.next();
83 if (this.options.ecmaVersion >= 6) {
84 node.sourceType = this.options.sourceType;
85 }
86 return this.finishNode(node, 'Program');
87 };
88 });
89 };
90 let ast = acorn.parse(fileContents, options);
91 this.includes = [];
92 this.classes = [];
93 this.functions = [];
94
95 let nodesToProcess = ast.body;
96
97 // engine mode only wants to look at the top level function definitions
98 // but the js doc generator wants to look at everything so this parser
99 // needs to handle both requirements.
100 if (!engineMode) {
101 nodesToProcess = [];
102 const walk = require('acorn/dist/walk');
103 walk.simple(ast, {
104 FunctionDeclaration(node) {
105 if (node.id && node.id.name) {
106 nodesToProcess.push(node);
107 }
108 },
109 FunctionExpression(node) {
110 if (node.id && node.id.name) {
111 nodesToProcess.push(node);
112 }
113 },
114 ClassDeclaration(node) {
115 nodesToProcess.push(node);
116 }
117 });
118 }
119
120 for (let n = 0; n < nodesToProcess.length; n++) {
121 let statement = nodesToProcess[n];
122
123 // record the end of the previous node, required for engineMode only
124 let previousEnd = -1;
125 if (n !== 0) {
126 previousEnd = nodesToProcess[n - 1].end;
127 }
128
129 if (statement.type === 'VariableDeclaration') {
130 let variableDeclarations = statement.declarations;
131
132 for (let n = 0; n < variableDeclarations.length; n++) {
133 let variableDeclaration = variableDeclarations[n];
134
135 if (variableDeclaration.init && variableDeclaration.init.type === 'CallExpression' &&
136 variableDeclaration.init.callee.name === 'require') {
137 let requireName = variableDeclaration.init.arguments[0].value;
138 // we only care about the code we require with a relative path
139 if (requireName.startsWith('.')) {
140 this.includes.push(variableDeclaration.init.arguments[0].value);
141 }
142 }
143 }
144 } else if (statement.type === 'FunctionDeclaration' || (statement.type === 'FunctionExpression' && !engineMode)) {
145 let closestComment;
146 // different approaches to finding comments depending on mode as they are not compatible.
147 if (!engineMode) {
148 closestComment = JavaScriptParser.findCommentBefore(comments, statement.loc.start.line);
149 } else {
150 closestComment = JavaScriptParser.searchForComment(statement.start, statement.end, previousEnd, comments);
151 }
152 let returnType = '';
153 let visibility = '+';
154 let parameterTypes = [];
155 let parameterNames = [];
156 let decorators = [];
157 let throws = '';
158 let example = '';
159 let commentData;
160 if (closestComment >= 0) {
161 let comment = comments[closestComment].value;
162 commentData = doctrine.parse(comment, {
163 unwrap: true,
164 sloppy: true
165 });
166 returnType = JavaScriptParser.getReturnType(comment);
167 visibility = JavaScriptParser.getVisibility(comment);
168 parameterTypes = JavaScriptParser.getMethodArguments(comment);
169 throws = JavaScriptParser.getThrows(comment);
170 decorators = JavaScriptParser.getDecorators(comment);
171 example = JavaScriptParser.getExample(comment);
172 }
173
174 if (visibility === '+' || includePrivates) {
175 for (let n = 0; n < statement.params.length; n++) {
176 parameterNames.push(statement.params[n].name);
177 }
178 const func = {
179 visibility: visibility,
180 returnType: returnType,
181 name: statement.id.name,
182 parameterTypes: parameterTypes,
183 parameterNames: parameterNames,
184 throws: throws,
185 decorators: decorators,
186 functionText: JavaScriptParser.getText(statement.start, statement.end, fileContents),
187 example: example,
188 commentData: commentData
189 };
190 this.functions.push(func);
191 }
192 } else if (statement.type === 'ClassDeclaration') {
193 let closestComment;
194 if (!engineMode) {
195 closestComment = JavaScriptParser.findCommentBefore(comments, statement.loc.start.line);
196 } else {
197 closestComment = JavaScriptParser.searchForComment(statement.start, statement.end, previousEnd, comments);
198 }
199 let privateClass = false;
200 let d;
201 if (closestComment >= 0) {
202 let comment = comments[closestComment].value;
203 d = doctrine.parse(comment, {
204 unwrap: true,
205 sloppy: true
206 });
207 privateClass = JavaScriptParser.getVisibility(comment) === '-';
208 }
209
210 if (privateClass === false || includePrivates) {
211 d = d || [];
212 const clazz = {
213 name: statement.id.name,
214 commentData: d
215 };
216 clazz.methods = [];
217
218 for (let n = 0; n < statement.body.body.length; n++) {
219 let thing = statement.body.body[n];
220
221 if (thing.type === 'MethodDefinition') {
222 let closestComment;
223 if (!engineMode) {
224 closestComment = JavaScriptParser.findCommentBefore(comments, thing.loc.start.line);
225 } else {
226 // previousEnd is the end of the node before the ClassDeclaration
227 let previousThingEnd = previousEnd;
228 if (n !== 0) {
229 // record the end of the previous thing inside the ClassDeclaration
230 previousThingEnd = statement.body.body[n - 1].end;
231 }
232 closestComment = JavaScriptParser.searchForComment(thing.key.start, thing.key.end, previousThingEnd, comments);
233 }
234 let returnType = '';
235 let visibility = '+';
236 let methodArgs = [];
237 let throws = '';
238 let decorators = [];
239 let example = '';
240 let commentData;
241 if (closestComment >= 0) {
242 let comment = comments[closestComment].value;
243 commentData = doctrine.parse(comment, {
244 unwrap: true,
245 sloppy: true
246 });
247 returnType = JavaScriptParser.getReturnType(comment);
248 visibility = JavaScriptParser.getVisibility(comment);
249 methodArgs = JavaScriptParser.getMethodArguments(comment);
250 decorators = JavaScriptParser.getDecorators(comment);
251 throws = JavaScriptParser.getThrows(comment);
252 example = JavaScriptParser.getExample(comment);
253 }
254 let name = thing.key.name;
255 if(!thing.key.name && thing.key.property.name){
256 name = thing.key.property.name;
257 }
258
259 commentData = commentData || [];
260 if (visibility === '+' || visibility === '~' || includePrivates) {
261 const method = {
262 visibility: visibility,
263 returnType: returnType,
264 name: name,
265 methodArgs: methodArgs,
266 decorators: decorators,
267 throws: throws,
268 example: example,
269 commentData: commentData
270 };
271 clazz.methods.push(method);
272 }
273 }
274 }
275
276 if (statement.superClass) {
277 clazz.superClass = statement.superClass.name;
278 }
279
280 this.classes.push(clazz);
281 }
282 }
283 }
284 }
285
286 /**
287 * Return the includes that were extracted from the JS file.
288 *
289 * @return {Object[]} information about each include
290 */
291 getIncludes() {
292 return this.includes;
293 }
294
295 /**
296 * Return the classes that were extracted from the JS file.
297 *
298 * @return {Object[]} information about each class
299 */
300 getClasses() {
301 return this.classes;
302 }
303
304 /**
305 * Return the methods that were extracted from the JS file.
306 *
307 * @return {Object[]} information about each method
308 */
309 getFunctions() {
310 return this.functions;
311 }
312
313 /**
314 * Return the tokens that were extracted from the JS file.
315 *
316 * @return {Object[]} information about each tokens
317 */
318 getTokens() {
319 return this.tokens;
320 }
321
322 /**
323 * Grab the text between a range
324 *
325 * @param {integer} rangeStart - the start of the range
326 * @param {integer} rangeEnd - the end of the range
327 * @param {string} source - the source text
328 * @return {string} the text between start and end
329 * @private
330 */
331 static getText(rangeStart, rangeEnd, source) {
332 return source.substring(rangeStart, rangeEnd);
333 }
334
335 /**
336 * Find the comments that are directly above a specific line number.
337 * This is used when order of the nodes cannot be guaranteed but
338 * limitation is that all comments must directly precede what they
339 * are commenting (ie no blank lines)
340 *
341 * @param {string[]} comments - the end of the range
342 * @param {integer} lineNumber - current linenumber
343 * @return {integer} the comment index or -1 if there are no comments
344 * @private
345 */
346 static findCommentBefore(comments, lineNumber) {
347 let foundIndex = -1;
348
349 for (let n = 0; n < comments.length; n++) {
350 let comment = comments[n];
351 let endComment = parseInt(comment.loc.end.line);
352
353 if ((lineNumber - endComment) === 1) {
354 foundIndex = n;
355 break;
356 }
357
358 }
359 return foundIndex;
360 }
361
362 /**
363 * Find the comments that are above and closest to the start of the range.
364 * This is used in engineMode and supports locating comments that aren't
365 * directly before a TP function. It assumes that nodes will be in order
366 *
367 * @param {integer} rangeStart - the start of the range
368 * @param {integer} rangeEnd - the end of the range
369 * @param {integer} stopPoint - the point to stop searching for previous comments
370 * @param {string[]} comments - the end of the range
371 * @return {integer} the comment index or -1 if there are no comments
372 * @private
373 */
374 static searchForComment(rangeStart, rangeEnd, stopPoint, comments) {
375 let foundIndex = -1;
376 let distance = -1;
377
378 for (let n = 0; n < comments.length; n++) {
379 let comment = comments[n];
380 let endComment = comment.end;
381 if (rangeStart > endComment && comment.start > stopPoint) {
382
383 if (distance === -1 || rangeStart - endComment < distance) {
384 distance = rangeStart - endComment;
385 foundIndex = n;
386 }
387 }
388 }
389 return foundIndex;
390 }
391
392 /**
393 * Grabs all the @ prefixed decorators from a comment block.
394 * @param {string} comment - the comment block
395 * @return {string[]} the @ prefixed decorators within the comment block
396 * @private
397 */
398 static getDecorators(comment) {
399 const re = /(?:^|\W)@(\w+)/g;
400 let match;
401 const matches = [];
402 match = re.exec(comment);
403 while (match) {
404 matches.push(match[1]);
405 match = re.exec(comment);
406 }
407 return matches;
408 }
409
410 /**
411 * Extracts the visibilty from a comment block
412 * @param {string} comment - the comment block
413 * @return {string} the return visibility (either + for public, ~ for protected, or - for private)
414 * @private
415 */
416 static getVisibility(comment) {
417 const PRIVATE = 'private';
418 const PROTECTED = 'protected';
419
420 let parsedComment = doctrine.parse(comment, {
421 unwrap: true,
422 sloppy: true,
423 tags: [PRIVATE, PROTECTED]
424 });
425 const tags = parsedComment.tags;
426
427 if (tags.length > 0) {
428 switch (tags[0].title) {
429 case PRIVATE:
430 return '-';
431 case PROTECTED:
432 return '~';
433 default:
434 return '+';
435 }
436 }
437 return '+';
438 }
439
440 /**
441 * Extracts the return type from a comment block.
442 * @param {string} comment - the comment block
443 * @return {string} the return type of the comment
444 * @private
445 */
446 static getReturnType(comment) {
447 const RETURN = 'return';
448 const RETURNS = 'returns';
449
450 let result = 'void';
451 let parsedComment = doctrine.parse(comment, {
452 unwrap: true,
453 sloppy: true,
454 tags: [RETURN, RETURNS]
455 });
456
457 const tags = parsedComment.tags;
458
459 if (tags.length > 1) {
460 throw new Error('Malformed JSDoc comment. More than one returns: ' + comment);
461 }
462
463 tags.forEach((tag) => {
464 if (tag.type) {
465 if (!tag.type.name && !tag.type) {
466 throw new Error('Malformed JSDoc comment. ' + comment);
467 }
468
469 if (tag.type.name) {
470 result = tag.type.name;
471 } else if (tag.type.applications) {
472 if(!tag.type.applications[0].name && tag.type.applications[0].type === 'RecordType'){
473 result = 'Object[]';
474 } else {
475 result = tag.type.applications[0].name + '[]';
476 }
477 } else if (tag.type.expression) {
478 result = tag.type.expression.name;
479
480 }
481 } else {
482 throw new Error('Malformed JSDoc comment. ' + comment);
483 }
484 });
485 return result;
486 }
487
488 /**
489 * Extracts the return type from a comment block.
490 * @param {string} comment - the comment block
491 * @return {string} the return type of the comment
492 * @private
493 */
494 static getThrows(comment) {
495 const THROWS = 'throws';
496 const EXCEPTION = 'exception';
497 let result = '';
498 let parsedComment = doctrine.parse(comment, {
499 unwrap: true,
500 sloppy: true,
501 tags: [THROWS, EXCEPTION]
502 });
503
504 const tags = parsedComment.tags;
505
506 if (tags.length > 1) {
507 throw new Error('Malformed JSDoc comment. More than one throws/exception: ' + comment);
508 }
509
510 tags.forEach((tag) => {
511 if (tag.type) {
512 if (!tag.type.type || !tag.type.name) {
513 throw new Error('Malformed JSDoc comment. ' + comment);
514 }
515 result = tag.type.name;
516 } else {
517 throw new Error('Malformed JSDoc comment. ' + comment);
518 }
519 });
520
521 return result;
522 }
523
524 /**
525 * Extracts the method arguments from a comment block.
526 * @param {string} comment - the comment block
527 * @return {string} the the argument types
528 * @private
529 */
530 static getMethodArguments(comment) {
531 const TAG = 'param';
532 let paramTypes = [];
533 let parsedComment = doctrine.parse(comment, {
534 unwrap: true,
535 sloppy: true,
536 tags: [TAG]
537 });
538
539 const tags = parsedComment.tags;
540
541 // param is mentioned but not picked up by parser
542 if (comment.indexOf('@' + TAG) !== -1 && tags.length === 0) {
543 throw new Error('Malformed JSDoc comment: ' + comment);
544 }
545
546 tags.forEach((tag) => {
547 if (tag.description) {
548 //If description starts with }
549 if (tag.description.trim().indexOf('}') === 0 ||
550 !tag.type ||
551 !tag.name) {
552 throw new Error('Malformed JSDoc comment: ' + comment);
553 }
554 }
555 if (tag.type.name) {
556 if (tag.type.name.indexOf(' ') !== -1) {
557 throw new Error('Malformed JSDoc comment: ' + comment);
558 }
559 }
560
561 if (tag.type.name) {
562 paramTypes.push(tag.type.name);
563 } else if (tag.type.applications) {
564 paramTypes.push(tag.type.applications[0].name + '[]');
565 } else if (tag.type.expression) {
566 paramTypes.push(tag.type.expression.name);
567
568 }
569 });
570 return paramTypes;
571 }
572
573 /**
574 * Extracts the example tag from a comment block.
575 * @param {string} comment - the comment block
576 * @return {string} the the argument types
577 * @private
578 */
579 static getExample(comment) {
580 const EXAMPLE = 'example';
581 let result = '';
582 let parsedComment = doctrine.parse(comment, {
583 unwrap: true,
584 sloppy: true,
585 tags: [EXAMPLE]
586 });
587
588 const tags = parsedComment.tags;
589
590 if (tags.length > 0) {
591 result = tags[0].description;
592 }
593
594 return result;
595 }
596}
597
598module.exports = JavaScriptParser;