UNPKG

7.77 kBJavaScriptView Raw
1"use strict";
2
3/**
4 * Try to figure oout which modules are needed from this javascript code.
5 * @param {object} ast - Abstract Syntax Tree from Babel.
6 * @returns {object} `{ requires: [] }`.
7 */
8exports.extractInfoFromAst = extractInfoFromAst;
9
10
11const
12 JsDoc = require( "./js-doc" ),
13 Util = require( "./util" );
14
15
16/**
17 * Try to figure oout which modules are needed from this javascript code.
18 * @param {object} ast - Abstract Syntax Tree from Babel.
19 * @returns {object} `{ requires: [] }`.
20 */
21function extractInfoFromAst( ast ) {
22 const output = {
23 requires: [],
24 exports: findExports( ast )
25 };
26 recursivelyExtractInfoFromAst( ast.program.body, output );
27 return output;
28}
29
30
31/**
32 * @param {array} ast - Array of AST objects.
33 * @param {object} output - `{ requires: [] }`.
34 * @returns {undefined}
35 */
36function recursivelyExtractInfoFromAst( ast, output ) {
37 if ( !Array.isArray( ast ) ) return;
38 ast.forEach( ( item ) => {
39 if ( !item ) return;
40 if ( item.type === 'CallExpression' ) {
41 parseCallExpression( item, output );
42 }
43
44 // Now, we explore other branches.
45 Object.keys( item ).forEach( ( key ) => {
46 const child = item[ key ];
47 if ( Array.isArray( child ) ) {
48 recursivelyExtractInfoFromAst( child, output );
49 } else if ( typeof child === 'object' ) {
50 recursivelyExtractInfoFromAst( [ child ], output );
51 }
52 } );
53 } );
54}
55
56
57/**
58 * @param {object} item - AST object.
59 * @param {object} output - `{ requires: [] }`.
60 * @return {undefined}
61 */
62function parseCallExpression( item, output ) {
63 const
64 callee = item.callee,
65 args = item.arguments;
66 if ( !callee || !args ) return;
67 if ( callee.name !== 'require' ) return;
68 if ( args.length !== 1 ) return;
69
70 const arg = args[ 0 ];
71 if ( arg.type !== 'StringLiteral' ) return;
72 Util.pushUnique( output.requires, arg.value );
73}
74
75
76/**
77 * Look for the exports of the module with their comments.
78 * This will be used for documentation.
79 * @param {object} ast - AST of the module.
80 * @returns {array} `{ id, type, comments }`
81 */
82function findExports( ast ) {
83 try {
84 const
85 publics = {},
86 privates = {},
87 body = ast.program.body[ 0 ].expression.arguments[ 1 ].body.body;
88
89 body.forEach( function ( item ) {
90 if ( parseExports( item, publics ) ) return;
91 if ( parseModuleExports( item, publics ) ) return;
92 parseFunctionDeclaration( item, privates );
93 } );
94
95 const output = [];
96 Object.keys( publics ).forEach( function ( exportName ) {
97 const
98 exportValue = publics[ exportName ],
99 target = privates[ exportValue.target ];
100 if ( target ) {
101 exportValue.comments = exportValue.comments || target.comments;
102 exportValue.type = target.type;
103 exportValue.params = target.params;
104 }
105 output.push( {
106 id: exportName,
107 type: exportValue.type,
108 params: exportValue.params,
109 comments: exportValue.comments
110 } );
111 } );
112
113 return output;
114 } catch ( ex ) {
115 throw new Error( `${ex}\n...in module-analyser/findExports()` );
116 }
117}
118
119/**
120 * @param {object} ast - AST for an Expression Statement.
121 * @param {object} publics - Dictionary of exported elements.
122 * @returns {boolean} `true` if `ast.type === "ExpressionStatement"`
123 */
124function parseExports( ast, publics ) {
125 try {
126 if ( ast.type !== "ExpressionStatement" ) return false;
127 const expression = ast.expression;
128 if ( expression.operator !== '=' ) return false;
129
130 const left = expression.left;
131 if ( left.type !== "MemberExpression" ) return false;
132 const object = left.object;
133 if ( object.type !== "Identifier" && object.name !== "exports" ) return false;
134
135 const right = expression.right;
136 if ( right.type !== "Identifier" ) return false;
137
138 publics[ right.name ] = {
139 type: "function",
140 comments: parseComments( ast.leadingComments )
141 };
142 } catch ( ex ) {
143 throw new Error( `${ex}\n...in module-analyser/parseExports(${JSON.stringify(ast)})` );
144 }
145 return true;
146}
147
148/**
149 * @param {object} ast - AST for an Expression Statement.
150 * @param {object} publics - Dictionary of exported elements.
151 * @returns {boolean} `true` if `ast.type === "ExpressionStatement"`
152 */
153function parseModuleExports( ast, publics ) {
154 try {
155 if ( ast.type !== "ExpressionStatement" ) return false;
156 const expression = ast.expression;
157 if ( expression.operator !== '=' ) return false;
158
159 const left = expression.left;
160 if ( left.type !== "MemberExpression" ) return false;
161 const object = left.object;
162 if ( object.type !== "Identifier" && object.name !== "exports" ) return false;
163
164 const right = expression.right;
165 if ( right.type !== "ObjectExpression" ) return false;
166
167 const properties = right.properties;
168 properties.forEach( function ( property ) {
169 const name = property.key.name;
170 publics[ name ] = {
171 target: name,
172 comments: parseComments( property.leadingComments )
173 };
174
175 } );
176 } catch ( ex ) {
177 throw new Error( `${ex}\n...in module-analyser/parseExports(${JSON.stringify(ast)})` );
178 }
179 return true;
180}
181
182/**
183 * @param {object} ast - AST for an Expression Statement.
184 * @param {object} privates - Dictionary of delcarations.
185 * @returns {boolean} `true` if `ast.type === "ExpressionStatement"`
186 */
187function parseFunctionDeclaration( ast, privates ) {
188 if ( ast.type !== "FunctionDeclaration" ) return false;
189 const name = ast.id.name;
190
191 privates[ name ] = {
192 type: "function",
193 params: ast.params.map( mapParam ),
194 comments: parseComments( ast.leadingComments )
195 };
196 return true;
197}
198
199/**
200 * @param {array} comments - Array of AST describing comments.
201 * @returns {string} Join of all comments.
202 */
203function parseComments( comments ) {
204 if ( !Array.isArray( comments ) ) return "";
205
206 let output = "";
207 comments.forEach( function ( commentAst ) {
208 const comment = commentAst.value;
209 output += comment.trim().split( "\n" )
210 .map( ( line ) => line.trim() )
211 .map( ( line ) => removeLeadingStar( line ) )
212 .join( "\n" )
213 .trim();
214 } );
215 return JsDoc.parse( output );
216}
217
218/**
219 * @param {string} line - A comment line.
220 * @return {string} Same line without any leading star.
221 */
222function removeLeadingStar( line ) {
223 return line.charAt( 0 ) === '*' ? line.substr( 1 ) : line;
224}
225
226/**
227 * @param {object} param - AST describing a function parameter.
228 * @returns {object|string} Name of the param, of object such as `{ name: "loop", value: "false"}`.
229 */
230function mapParam( param ) {
231 try {
232 switch ( param.type ) {
233 case "Identifier":
234 return param.name;
235 case "AssignmentPattern":
236 return {
237 name: param.left.name,
238 value: param.right.value
239 };
240 default:
241 return "?";
242 }
243 } catch ( ex ) {
244 throw new Error( `${ex}\n...in module-analyser/mapParam(${JSON.stringify(param)})` );
245 }
246}
247// https://astexplorer.net/
\No newline at end of file