|
1 # |
|
2 # Jasy - Web Tooling Framework |
|
3 # Copyright 2010-2012 Zynga Inc. |
|
4 # Copyright 2013-2014 Sebastian Werner |
|
5 # |
|
6 |
|
7 from __future__ import unicode_literals |
|
8 |
|
9 from jasy.script.output.Compressor import Compressor |
|
10 |
|
11 # Shared instance |
|
12 compressor = Compressor() |
|
13 |
|
14 pseudoTypes = set(["any", "var", "undefined", "null", "true", "false", "this", "arguments"]) |
|
15 builtinTypes = set(["Object", "String", "Number", "Boolean", "Array", "Function", "RegExp", "Date"]) |
|
16 |
|
17 # Basic user friendly node type to human type |
|
18 nodeTypeToDocType = { |
|
19 |
|
20 # Primitives |
|
21 "string": "String", |
|
22 "number": "Number", |
|
23 "not": "Boolean", |
|
24 "true": "Boolean", |
|
25 "false": "Boolean", |
|
26 |
|
27 # Literals |
|
28 "function": "Function", |
|
29 "regexp": "RegExp", |
|
30 "object_init": "Map", |
|
31 "array_init": "Array", |
|
32 |
|
33 # We could figure out the real class automatically - at least that's the case quite often |
|
34 "new": "Object", |
|
35 "new_with_args": "Object", |
|
36 |
|
37 # Comparisons |
|
38 "eq" : "Boolean", |
|
39 "ne" : "Boolean", |
|
40 "strict_eq" : "Boolean", |
|
41 "strict_ne" : "Boolean", |
|
42 "lt" : "Boolean", |
|
43 "le" : "Boolean", |
|
44 "gt" : "Boolean", |
|
45 "ge" : "Boolean", |
|
46 "in" : "Boolean", |
|
47 "instanceof" : "Boolean", |
|
48 |
|
49 # Numbers |
|
50 "lsh": "Number", |
|
51 "rsh": "Number", |
|
52 "ursh": "Number", |
|
53 "minus": "Number", |
|
54 "mul": "Number", |
|
55 "div": "Number", |
|
56 "mod": "Number", |
|
57 "bitwise_and": "Number", |
|
58 "bitwise_xor": "Number", |
|
59 "bitwise_or": "Number", |
|
60 "bitwise_not": "Number", |
|
61 "increment": "Number", |
|
62 "decrement": "Number", |
|
63 "unary_minus": "Number", |
|
64 "unary_plus": "Number", |
|
65 |
|
66 # This is not 100% correct, but I don't like to introduce a BooleanLike type. |
|
67 # If the author likes something different he is still able to override it via API docs |
|
68 "and": "Boolean", |
|
69 "or": "Boolean", |
|
70 |
|
71 # Operators/Built-ins |
|
72 "void": "undefined", |
|
73 "null": "null", |
|
74 "typeof": "String", |
|
75 "delete": "Boolean", |
|
76 "this": "This", |
|
77 |
|
78 # These are not real types, we try to figure out the real value behind automatically |
|
79 "call": "Call", |
|
80 "hook": "Hook", |
|
81 "assign": "Assign", |
|
82 "plus": "Plus", |
|
83 "identifier" : "Identifier", |
|
84 "dot": "Object", |
|
85 "index": "var" |
|
86 } |
|
87 |
|
88 |
|
89 def getVisibility(name): |
|
90 """ |
|
91 Returns the visibility of the given name by convention |
|
92 """ |
|
93 |
|
94 if name.startswith("__"): |
|
95 return "private" |
|
96 elif name.startswith("_"): |
|
97 return "internal" |
|
98 else: |
|
99 return "public" |
|
100 |
|
101 |
|
102 def requiresDocumentation(name): |
|
103 """ |
|
104 Whether the given name suggests that documentation is required |
|
105 """ |
|
106 |
|
107 return not name.startswith("_") |
|
108 |
|
109 |
|
110 def getKeyValue(dict, key): |
|
111 """ |
|
112 Returns the value node of the given key inside the given object initializer. |
|
113 """ |
|
114 |
|
115 for propertyInit in dict: |
|
116 if propertyInit[0].value == key: |
|
117 return propertyInit[1] |
|
118 |
|
119 |
|
120 def findAssignments(name, node): |
|
121 """ |
|
122 Returns a list of assignments which might have impact on the value used in the given node. |
|
123 """ |
|
124 |
|
125 # Looking for all script blocks |
|
126 scripts = [] |
|
127 parent = node |
|
128 while parent: |
|
129 if parent.type == "script": |
|
130 scope = getattr(parent, "scope", None) |
|
131 if scope and name in scope.modified: |
|
132 scripts.append(parent) |
|
133 |
|
134 parent = getattr(parent, "parent", None) |
|
135 |
|
136 def assignMatcher(node): |
|
137 if node.type == "assign" and node[0].type == "identifier" and node[0].value == name: |
|
138 return True |
|
139 |
|
140 if node.type == "declaration" and node.name == name and getattr(node, "initializer", None): |
|
141 return True |
|
142 |
|
143 if node.type == "function" and node.functionForm == "declared_form" and node.name == name: |
|
144 return True |
|
145 |
|
146 return False |
|
147 |
|
148 # Query all relevant script nodes |
|
149 assignments = [] |
|
150 for script in scripts: |
|
151 queryResult = queryAll(script, assignMatcher, False) |
|
152 assignments.extend(queryResult) |
|
153 |
|
154 # Collect assigned values |
|
155 values = [] |
|
156 for assignment in assignments: |
|
157 if assignment.type == "function": |
|
158 values.append(assignment) |
|
159 elif assignment.type == "assign": |
|
160 values.append(assignment[1]) |
|
161 else: |
|
162 values.append(assignment.initializer) |
|
163 |
|
164 return assignments, values |
|
165 |
|
166 |
|
167 def findFunction(node): |
|
168 """ |
|
169 Returns the first function inside the given node |
|
170 """ |
|
171 |
|
172 return query(node, lambda node: node.type == "function") |
|
173 |
|
174 |
|
175 def findCommentNode(node): |
|
176 """ |
|
177 Finds the first doc comment node inside the given node |
|
178 """ |
|
179 |
|
180 def matcher(node): |
|
181 comments = getattr(node, "comments", None) |
|
182 if comments: |
|
183 for comment in comments: |
|
184 if comment.variant == "doc": |
|
185 return True |
|
186 |
|
187 return query(node, matcher) |
|
188 |
|
189 |
|
190 def getDocComment(node): |
|
191 """ |
|
192 Returns the first doc comment of the given node. |
|
193 """ |
|
194 |
|
195 comments = getattr(node, "comments", None) |
|
196 if comments: |
|
197 for comment in comments: |
|
198 if comment.variant == "doc": |
|
199 return comment |
|
200 |
|
201 return None |
|
202 |
|
203 |
|
204 def findReturn(node): |
|
205 """ |
|
206 Finds the first return inside the given node |
|
207 """ |
|
208 |
|
209 return query(node, lambda node: node.type == "return", True) |
|
210 |
|
211 |
|
212 |
|
213 def valueToString(node): |
|
214 """ |
|
215 Converts the value of the given node into something human friendly |
|
216 """ |
|
217 |
|
218 if node.type in ("number", "string", "false", "true", "regexp", "null"): |
|
219 return compressor.compress(node) |
|
220 elif node.type in nodeTypeToDocType: |
|
221 if node.type == "plus": |
|
222 return detectPlusType(node) |
|
223 elif node.type in ("new", "new_with_args", "dot"): |
|
224 return detectObjectType(node) |
|
225 else: |
|
226 return nodeTypeToDocType[node.type] |
|
227 else: |
|
228 return "Other" |
|
229 |
|
230 |
|
231 |
|
232 def queryAll(node, matcher, deep=True, inner=False, result=None): |
|
233 """ |
|
234 Recurses the tree starting with the given node and returns a list of nodes |
|
235 matched by the given matcher method |
|
236 |
|
237 - node: any node |
|
238 - matcher: function which should return a truish value when node matches |
|
239 - deep: whether inner scopes should be scanned, too |
|
240 - inner: used internally to differentiate between current and inner nodes |
|
241 - result: can be used to extend an existing list, otherwise a new list is created and returned |
|
242 """ |
|
243 |
|
244 if result == None: |
|
245 result = [] |
|
246 |
|
247 # Don't do in closure functions |
|
248 if inner and node.type == "script" and not deep: |
|
249 return None |
|
250 |
|
251 if matcher(node): |
|
252 result.append(node) |
|
253 |
|
254 for child in node: |
|
255 queryAll(child, matcher, deep, True, result) |
|
256 |
|
257 return result |
|
258 |
|
259 |
|
260 |
|
261 def query(node, matcher, deep=True, inner=False): |
|
262 """ |
|
263 Recurses the tree starting with the given node and returns the first node |
|
264 which is matched by the given matcher method. |
|
265 |
|
266 - node: any node |
|
267 - matcher: function which should return a truish value when node matches |
|
268 - deep: whether inner scopes should be scanned, too |
|
269 - inner: used internally to differentiate between current and inner nodes |
|
270 """ |
|
271 |
|
272 # Don't do in closure functions |
|
273 if inner and node.type == "script" and not deep: |
|
274 return None |
|
275 |
|
276 if matcher(node): |
|
277 return node |
|
278 |
|
279 for child in node: |
|
280 result = query(child, matcher, deep, True) |
|
281 if result is not None: |
|
282 return result |
|
283 |
|
284 return None |
|
285 |
|
286 |
|
287 def findCall(node, methodName): |
|
288 """ |
|
289 Recurses the tree starting with the given node and returns the first node |
|
290 which calls the given method name (supports namespaces, too) |
|
291 """ |
|
292 |
|
293 if type(methodName) is str: |
|
294 methodName = set([methodName]) |
|
295 |
|
296 def matcher(node): |
|
297 call = getCallName(node) |
|
298 if call and call in methodName: |
|
299 return call |
|
300 |
|
301 return query(node, matcher) |
|
302 |
|
303 |
|
304 def getCallName(node): |
|
305 if node.type == "call": |
|
306 if node[0].type == "dot": |
|
307 return assembleDot(node[0]) |
|
308 elif node[0].type == "identifier": |
|
309 return node[0].value |
|
310 |
|
311 return None |
|
312 |
|
313 |
|
314 def getParameterFromCall(call, index=0): |
|
315 """ |
|
316 Returns a parameter node by index on the call node |
|
317 """ |
|
318 |
|
319 try: |
|
320 return call[1][index] |
|
321 except: |
|
322 return None |
|
323 |
|
324 |
|
325 def getParamNamesFromFunction(func): |
|
326 """ |
|
327 Returns a human readable list of parameter names (sorted by their order in the given function) |
|
328 """ |
|
329 |
|
330 params = getattr(func, "params", None) |
|
331 if params: |
|
332 return [identifier.value for identifier in params] |
|
333 else: |
|
334 return None |
|
335 |
|
336 |
|
337 def detectPlusType(plusNode): |
|
338 """ |
|
339 Analyses the given "plus" node and tries to figure out if a "string" or "number" result is produced. |
|
340 """ |
|
341 |
|
342 if plusNode[0].type == "string" or plusNode[1].type == "string": |
|
343 return "String" |
|
344 elif plusNode[0].type == "number" and plusNode[1].type == "number": |
|
345 return "Number" |
|
346 elif plusNode[0].type == "plus" and detectPlusType(plusNode[0]) == "String": |
|
347 return "String" |
|
348 else: |
|
349 return "var" |
|
350 |
|
351 |
|
352 def detectObjectType(objectNode): |
|
353 """ |
|
354 Returns a human readable type information of the given node |
|
355 """ |
|
356 |
|
357 if objectNode.type in ("new", "new_with_args"): |
|
358 construct = objectNode[0] |
|
359 else: |
|
360 construct = objectNode |
|
361 |
|
362 # Only support built-in top level constructs |
|
363 if construct.type == "identifier" and construct.value in ("Array", "Boolean", "Date", "Function", "Number", "Object", "String", "RegExp"): |
|
364 return construct.value |
|
365 |
|
366 # And namespaced custom classes |
|
367 elif construct.type == "dot": |
|
368 assembled = assembleDot(construct) |
|
369 if assembled: |
|
370 return assembled |
|
371 |
|
372 return "Object" |
|
373 |
|
374 |
|
375 |
|
376 def resolveIdentifierNode(identifierNode): |
|
377 assignNodes, assignValues = findAssignments(identifierNode.value, identifierNode) |
|
378 if assignNodes: |
|
379 |
|
380 assignCommentNode = None |
|
381 |
|
382 # Find first relevant assignment with comment! Otherwise just first one. |
|
383 for assign in assignNodes: |
|
384 |
|
385 # The parent is the relevant doc comment container |
|
386 # It's either a "var" (declaration) or "semicolon" (assignment) |
|
387 if getDocComment(assign): |
|
388 assignCommentNode = assign |
|
389 break |
|
390 elif getDocComment(assign.parent): |
|
391 assignCommentNode = assign.parent |
|
392 break |
|
393 |
|
394 return assignValues[0], assignCommentNode or assignValues[0] |
|
395 |
|
396 return None, None |
|
397 |
|
398 |
|
399 |
|
400 def assembleDot(node, result=None): |
|
401 """ |
|
402 Joins a dot node (cascaded supported, too) into a single string like "foo.bar.Baz" |
|
403 """ |
|
404 |
|
405 if result == None: |
|
406 result = [] |
|
407 |
|
408 for child in node: |
|
409 if child.type == "identifier": |
|
410 result.append(child.value) |
|
411 elif child.type == "dot": |
|
412 assembleDot(child, result) |
|
413 else: |
|
414 return None |
|
415 |
|
416 return ".".join(result) |