UNPKG

7.64 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 // Filter node modules.
73 if ( typeof arg.value !== 'string' || arg.value.startsWith( "node://" ) ) return;
74 Util.pushUnique( output.requires, arg.value );
75}
76
77
78/**
79 * Look for the exports of the module with their comments.
80 * This will be used for documentation.
81 * @param {object} ast - AST of the module.
82 * @returns {array} `{ id, type, comments }`
83 */
84function findExports( ast ) {
85 try {
86 const
87 publics = {},
88 privates = {},
89 body = ast.program.body[ 0 ].expression.arguments[ 1 ].body.body;
90
91 body.forEach( function ( item ) {
92 if ( parseExports( item, publics ) ) return;
93 if ( parseModuleExports( item, publics ) ) return;
94 parseFunctionDeclaration( item, privates );
95 } );
96
97 const output = [];
98 Object.keys( publics ).forEach( function ( exportName ) {
99 const
100 exportValue = publics[ exportName ],
101 target = privates[ exportValue.target ];
102 if ( target ) {
103 exportValue.comments = exportValue.comments || target.comments;
104 exportValue.type = target.type;
105 exportValue.params = target.params;
106 }
107 output.push( {
108 id: exportName,
109 type: exportValue.type,
110 params: exportValue.params,
111 comments: exportValue.comments
112 } );
113 } );
114
115 return output;
116 } catch ( ex ) {
117 throw new Error( `${ex}\n...in module-analyser/findExports()` );
118 }
119}
120
121/**
122 * @param {object} ast - AST for an Expression Statement.
123 * @param {object} publics - Dictionary of exported elements.
124 * @returns {boolean} `true` if `ast.type === "ExpressionStatement"`
125 */
126function parseExports( ast, publics ) {
127 try {
128 if ( ast.type !== "ExpressionStatement" ) return false;
129 const expression = ast.expression;
130 if ( expression.operator !== '=' ) return false;
131
132 const left = expression.left;
133 if ( left.type !== "MemberExpression" ) return false;
134 const object = left.object;
135 if ( object.type !== "Identifier" && object.name !== "exports" ) return false;
136
137 const right = expression.right;
138 if ( right.type !== "Identifier" ) return false;
139
140 publics[ right.name ] = {
141 type: "function",
142 comments: parseComments( ast.leadingComments )
143 };
144 } catch ( ex ) {
145 throw new Error( `${ex}\n...in module-analyser/parseExports(${JSON.stringify(ast)})` );
146 }
147 return true;
148}
149
150/**
151 * @param {object} ast - AST for an Expression Statement.
152 * @param {object} publics - Dictionary of exported elements.
153 * @returns {boolean} `true` if `ast.type === "ExpressionStatement"`
154 */
155function parseModuleExports( ast, publics ) {
156 try {
157 if ( ast.type !== "ExpressionStatement" ) return false;
158 const expression = ast.expression;
159 if ( expression.operator !== '=' ) return false;
160
161 const left = expression.left;
162 if ( left.type !== "MemberExpression" ) return false;
163 const object = left.object;
164 if ( object.type !== "Identifier" && object.name !== "exports" ) return false;
165
166 const right = expression.right;
167 if ( right.type !== "ObjectExpression" ) return false;
168
169 const properties = right.properties;
170 properties.forEach( function ( property ) {
171 const name = property.key.name;
172 publics[ name ] = {
173 target: name,
174 comments: parseComments( property.leadingComments )
175 };
176
177 } );
178 } catch ( ex ) {
179 throw new Error( `${ex}\n...in module-analyser/parseExports(${JSON.stringify(ast)})` );
180 }
181 return true;
182}
183
184/**
185 * @param {object} ast - AST for an Expression Statement.
186 * @param {object} privates - Dictionary of delcarations.
187 * @returns {boolean} `true` if `ast.type === "ExpressionStatement"`
188 */
189function parseFunctionDeclaration( ast, privates ) {
190 if ( ast.type !== "FunctionDeclaration" ) return false;
191 const name = ast.id.name;
192
193 privates[ name ] = {
194 type: "function",
195 params: ast.params.map( mapParam ),
196 comments: parseComments( ast.leadingComments )
197 };
198 return true;
199}
200
201/**
202 * @param {array} comments - Array of AST describing comments.
203 * @returns {string} Join of all comments.
204 */
205function parseComments( comments ) {
206 if ( !Array.isArray( comments ) ) return "";
207
208 let output = "";
209 comments.forEach( function ( commentAst ) {
210 const comment = commentAst.value;
211 output += comment.trim().split( "\n" )
212 .map( ( line ) => line.trim() )
213 .map( ( line ) => removeLeadingStar( line ) )
214 .join( "\n" )
215 .trim();
216 } );
217 return JsDoc.parse( output );
218}
219
220/**
221 * @param {string} line - A comment line.
222 * @return {string} Same line without any leading star.
223 */
224function removeLeadingStar( line ) {
225 return line.charAt( 0 ) === '*' ? line.substr( 1 ) : line;
226}
227
228/**
229 * @param {object} param - AST describing a function parameter.
230 * @returns {object|string} Name of the param, of object such as `{ name: "loop", value: "false"}`.
231 */
232function mapParam( param ) {
233 try {
234 switch ( param.type ) {
235 case "Identifier":
236 return param.name;
237 case "AssignmentPattern":
238 return {
239 name: param.left.name,
240 value: param.right.value
241 };
242 default:
243 return "?";
244 }
245 } catch ( ex ) {
246 throw new Error( `${ex}\n...in module-analyser/mapParam(${JSON.stringify(param)})` );
247 }
248}
249// https://astexplorer.net/
\No newline at end of file