├── .gitignore ├── .nojekyll ├── LICENSE ├── Makefile ├── README.rst ├── index.html ├── rebar.config ├── rebar.lock ├── resources ├── css │ └── json.human.css └── js │ ├── data.js │ ├── erlplorer.js │ ├── json.human.js │ └── vivagraph.js └── src ├── erlplorer.app.src ├── erlplorer.fn └── erlplorer_util.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marianoguerra/erlplorer/8c0e5cc4c68d2f669a5dc562571b70e7d770ece7/.nojekyll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Mariano Guerra . 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * The names of its contributors may not be used to endorse or promote 16 | products derived from this software without specific prior written 17 | permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | build: 4 | rebar3 escriptize 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | erlplorer 2 | ========= 3 | 4 | erlplorer search 5 | ---------------- 6 | 7 | A command line tool to search Erlang and efene code by providing expressions 8 | with "holes". To search by structure, not by text. 9 | 10 | What I mean by "by structure"? 11 | 12 | You may want to search for stuff like: 13 | 14 | * Calls to function f1 on module m1 with 3 arguments 15 | 16 | + Same but where some argument has a specific value 17 | 18 | * Tuples with a specific number of items where the first item is a given atom 19 | 20 | I guess you get the idea, you want to match some expression's structure, but not 21 | the text, because of formatting, new lines, spacing and also because some of the 22 | values are not important, you would like to match any expression in some 23 | places. 24 | 25 | Examples 26 | ........ 27 | 28 | .. code-block:: sh 29 | 30 | erlplorer search "{{_@0, _@1, _@2}}" **/src/*.erl 31 | 32 | :: 33 | 34 | asn1/src/asn1ct.erl:1366 {{decode,{Module,Type,Value},Error}} 35 | dialyzer/src/dialyzer_plt.erl:675 {{M,_F,_A}} 36 | kernel/src/pg2.erl:300 {{local_member,Name,Pid}} 37 | kernel/src/pg2.erl:302 {{pid,Pid,Name}} 38 | ... 39 | 40 | The first argument `"{{_@0, _@1, _@2}}"` is an Erlang expression, in this case 41 | a one item tuple holding a 3 item tuple, the weird looking variables are a 42 | specially named variables that erlplorer interprets as "match any AST node 43 | here, I don't care", they start with `_@`, any other variable will match that variable name in the code. 44 | 45 | We can see that by searching for places that match a 3 item tuple ignoring the 3 places: 46 | 47 | .. code-block:: sh 48 | 49 | erlplorer search "{_, _, _}" **/src/*.erl 50 | 51 | :: 52 | 53 | asn1/src/asn1ct_check.erl:917 {_,_,_} 54 | ... to many results to show 55 | 56 | We can use this "meta variables" to do more pattern matching, let's search 57 | for 3 item tuples that have the same thing in the 3 places: 58 | 59 | .. code-block:: sh 60 | 61 | erlplorer search "{_@, _@, _@}" **/src/*.erl 62 | 63 | :: 64 | 65 | asn1/src/asn1ct_check.erl:917 {_,_,_} 66 | ... 67 | asn1/src/asn1ct_constructed_ber_bin_v2.erl:701 {[],[],[]} 68 | ... 69 | asn1/src/asn1ct_constructed_per.erl:401 {false,false,false} 70 | ... 71 | common_test/src/ct_framework.erl:1130 {_,_,_} 72 | ... 73 | common_test/src/ct_logs.erl:1492 {"","",""} 74 | ... 75 | 76 | Or search for identity functions: 77 | 78 | .. code-block:: sh 79 | 80 | erlplorer search "fun(_@) -> _@ end" **/src/*.erl 81 | 82 | :: 83 | 84 | asn1/src/asn1ct.erl:2099 fun(D) -> D end 85 | asn1/src/asn1ct_gen_ber_bin_v2.erl:650 fun(V) -> V end 86 | common_test/src/test_server.erl:2067 fun(T) -> T end 87 | compiler/src/beam_a.erl:147 fun(L) -> L end 88 | compiler/src/beam_jump.erl:289 fun(Old) -> Old end 89 | ... 90 | 91 | Places that add 0 to something: 92 | 93 | .. code-block:: sh 94 | 95 | erlplorer search "_@ + 0" **/src/*.erl 96 | 97 | :: 98 | 99 | asn1/src/asn1rtt_per_common.erl:187 N + 0 100 | asn1/src/asn1rtt_per_common.erl:197 N + 0 101 | stdlib/src/ms_transform.erl:90 16 + 0 102 | stdlib/src/ms_transform.erl:92 17 + 0 103 | stdlib/src/ms_transform.erl:97 22 + 0 104 | stdlib/src/ms_transform.erl:102 18 + 0 105 | stdlib/src/ms_transform.erl:106 23 + 0 106 | stdlib/src/ms_transform.erl:111 24 + 0 107 | stdlib/src/ms_transform.erl:167 20 + 0 108 | stdlib/src/ms_transform.erl:170 19 + 0 109 | stdlib/src/ms_transform.erl:174 21 + 0 110 | 111 | Places that add the same thing: 112 | 113 | .. code-block:: sh 114 | 115 | erlplorer search "_@ + _@" **/src/*.erl 116 | 117 | :: 118 | 119 | dialyzer/test/small_SUITE_data/src/maps_redef2.erl:18 A + A 120 | stdlib/src/dets_utils.erl:1138 1 + 1 121 | stdlib/src/dets_utils.erl:1226 1 + 1 122 | stdlib/src/rand.erl:1464 Y + Y 123 | stdlib/src/zip.erl:1315 Sz + Sz 124 | 125 | You get the idea... 126 | 127 | How to use it 128 | ............. 129 | 130 | You need Erlang and rebar3 installed and in your `$PATH` 131 | 132 | .. code-block:: sh 133 | 134 | git clone https://github.com/marianoguerra/erlplorer 135 | cd erlplorer 136 | rebar3 escriptize 137 | # ~/bin or any other folder in your $PATH 138 | cp _build/default/bin/erlplorer ~/bin 139 | 140 | Emacs integration 141 | ................. 142 | 143 | Check `facundoolano emacs integration `_ 144 | 145 | How is it implemented? 146 | ...................... 147 | 148 | Wrap the expression passed in a function [1]_ 149 | 150 | Compile the module to Erlang AST [2]_ 151 | 152 | Extract the AST body of the dummy function [3]_ 153 | 154 | "abstract the AST", that is, I take an AST as Erlang data and I generate an 155 | AST that when compiled will generate that Erlang AST, I need that because I 156 | will put that AST in a pattern match position to pattern match AST nodes as I 157 | walk Erlang ASTs [4]_. yeah, meta and too many AST references 158 | 159 | An example is worth many of my words: 160 | 161 | .. code-block:: erlang 162 | 163 | % the 42 is a fake "line" where the code was supposedly parsed 164 | 1> erl_parse:abstract({foo, 100, "hi"}, 42). 165 | {tuple,42,[{atom,42,foo},{integer,42,100},{string,42,"hi"}]} 166 | 167 | We have to take care of two things: 168 | 169 | * vars must match an AST node for a var with that name, not act as vars that will be bound on first match and pattern matched on successive matches 170 | * vars that start with _@ will be compiled to actual vars that behave as vars in a pattern match, that's how we can use them to pattern match 171 | 172 | Then compile the abstracted AST into a module with a function we can pass to `ast_walk/3` [5]_ 173 | 174 | Load the compiled module [6]_ 175 | 176 | Parse the files passed as last argument [7]_ 177 | 178 | And walk the parsed AST with our compiled matcher [8]_ 179 | 180 | For each match, since we have the AST, try to pretty print it [9]_ 181 | 182 | Give it a try and let me know what you think. 183 | 184 | PS: the code is a hack I did to use it when I needed it, don't judge efene by the 185 | code you see on that project :P 186 | 187 | .. [1] https://github.com/marianoguerra/erlplorer/blob/0bdd56057dfeb399b9961ae6322f74ccabc2cc5a/src/erlplorer.fn#L53 188 | 189 | .. [2] https://github.com/marianoguerra/erlplorer/blob/0bdd56057dfeb399b9961ae6322f74ccabc2cc5a/src/erlplorer.fn#L56 190 | 191 | .. [3] https://github.com/marianoguerra/erlplorer/blob/0bdd56057dfeb399b9961ae6322f74ccabc2cc5a/src/erlplorer.fn#L57 192 | 193 | .. [4] https://github.com/marianoguerra/erlplorer/blob/0bdd56057dfeb399b9961ae6322f74ccabc2cc5a/src/erlplorer.fn#L58 194 | 195 | .. [5] https://github.com/marianoguerra/erlplorer/blob/0bdd56057dfeb399b9961ae6322f74ccabc2cc5a/src/erlplorer.fn#L64 196 | 197 | .. [6] https://github.com/marianoguerra/erlplorer/blob/0bdd56057dfeb399b9961ae6322f74ccabc2cc5a/src/erlplorer.fn#L13 198 | 199 | .. [7] https://github.com/marianoguerra/erlplorer/blob/0bdd56057dfeb399b9961ae6322f74ccabc2cc5a/src/erlplorer.fn#L152 200 | 201 | .. [8] https://github.com/marianoguerra/erlplorer/blob/0bdd56057dfeb399b9961ae6322f74ccabc2cc5a/src/erlplorer.fn#L156 202 | 203 | .. [9] https://github.com/marianoguerra/erlplorer/blob/0bdd56057dfeb399b9961ae6322f74ccabc2cc5a/src/erlplorer.fn#L76 204 | 205 | erlplorer info 206 | -------------- 207 | 208 | Analyzes your erlang and/or efene modules and outputs JSON with information 209 | about them, you can use that information to feed it to any tool you create. 210 | 211 | Information the tool exports: 212 | 213 | * Module name 214 | * Modules referenced by current module with number of references 215 | * External functions called by current module with number of calls 216 | * Declared functions, location, number of lines, number of clauses 217 | * Variable names and lines per function 218 | * Record names and lines per function 219 | * Function call names and lines per function 220 | * Number of clauses (branches) in functions 221 | 222 | The tool can easily be extended to record more statistics. 223 | 224 | Run 225 | --- 226 | 227 | :: 228 | 229 | erlplorer info file1.erl [file2.fn ...] 230 | 231 | Or:: 232 | 233 | _build/default/bin/erlplorer info *.erl > myproject.json 234 | 235 | License 236 | ------- 237 | 238 | See LICENSE file 239 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Erlplorer 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

Erlplorer

22 |

Erlplorer is a script (escript) written in efene that analyzes your erlang 23 | and/or efene modules and outputs JSON with information about them, you can 24 | use that information to feed it to any tool you create

25 | 26 |

Information the tool exports: 27 |

37 |

38 | 39 |

The tool can easily be extended to record more statistics.

40 | 41 |

You can upload the result of running erlplorer on your files here and it 42 | will display some information about it, the data won't be uploaded anywhere, 43 | it's all processed in browser.

44 | 45 |

Check the project's readme to build and run github.com/marianoguerra/erlplorer

46 | 47 |

You can also download a pre compiled release from the Releases Page, check the last one and download the erlplorer binary, you will still need Erlang installed to run it

48 | 49 |

Try It!

50 | 51 | 52 | 53 | 54 |

See Information below, (there's information after the chart), you can scroll to zoom on the chart and move nodes around to make them look better

55 | 56 |
57 |

Check a short demo:

58 | 59 |
60 | 61 |

Formatted Example Output of cowboy.erl and cowboy_req.erl

62 | 63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [no_debug_info]}. 2 | {deps, [jsx, ast_walk, efene, aleppo]}. 3 | 4 | {escript_incl_apps, 5 | [erlplorer, efene, jsx, aleppo, ast_walk]}. 6 | {escript_top_level_app, erlplorer}. 7 | {escript_name, erlplorer}. 8 | {escript_emu_args, "%%! +sbtu +A0\n"}. 9 | 10 | {plugins, [rebar3_efene]}. 11 | 12 | {provider_hooks, [{pre, [{compile, {efene, compile}}]}]}. 13 | 14 | %% Profiles 15 | {profiles, [{test, [{erl_opts, [debug_info]} ]}]}. 16 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"aleppo">>,{pkg,<<"aleppo">>,<<"0.9.0">>},0}, 3 | {<<"ast_walk">>,{pkg,<<"ast_walk">>,<<"0.3.1">>},0}, 4 | {<<"efene">>,{pkg,<<"efene">>,<<"0.99.2">>},0}, 5 | {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0}]}. 6 | [ 7 | {pkg_hash,[ 8 | {<<"aleppo">>, <<"43B4EC269FA136E39DCECBD4323D4346E0EF069A62528A9154F1BB3ACD26E3E4">>}, 9 | {<<"ast_walk">>, <<"3E513446DCEF39ADB62DD578AEEA32E1DA6802482170A2E7500B2F0575E14ABF">>}, 10 | {<<"efene">>, <<"FC74F9506DBA3232B0C4D6A5494426FC6D1C73AB71CAD0D066E933E79BC1120E">>}, 11 | {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}]}, 12 | {pkg_hash_ext,[ 13 | {<<"aleppo">>, <<"2F360631D64DA53F40621714E157FD33805A95D0160D5C62FCFB3E132986CE71">>}, 14 | {<<"ast_walk">>, <<"9548DD7629FD9614F1520AFFDD05DB14E5A2103B7A91856EB4DACD84ED089C25">>}, 15 | {<<"efene">>, <<"4796846239DB628F4180848FFBDD0EACA54D5EA472AAC34EE621810EF91C34AC">>}, 16 | {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}]} 17 | ]. 18 | -------------------------------------------------------------------------------- /resources/css/json.human.css: -------------------------------------------------------------------------------- 1 | .jh-root, .jh-type-object, .jh-type-array, .jh-key, .jh-value, .jh-root tr{ 2 | -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ 3 | -moz-box-sizing: border-box; /* Firefox, other Gecko */ 4 | box-sizing: border-box; /* Opera/IE 8+ */ 5 | font-weight: bold; 6 | } 7 | 8 | .jh-key, .jh-value{ 9 | margin: 0; 10 | padding: 0.2em; 11 | font-weight: bold; 12 | } 13 | 14 | .jh-value{ 15 | border-left: 1px solid #ddd; 16 | } 17 | 18 | .jh-type-number{ 19 | text-align: center; 20 | color: #5286BC; 21 | } 22 | 23 | .jh-type-bool-true{ 24 | text-align: center; 25 | color: #5A811C; 26 | } 27 | 28 | .jh-type-bool-false{ 29 | text-align: center; 30 | color: #D45317; 31 | } 32 | 33 | .jh-type-bool-image { 34 | width: 20px; 35 | height: 20px; 36 | margin-right: 5px; 37 | vertical-align: bottom; 38 | } 39 | 40 | .jh-type-string{ 41 | font-style: italic; 42 | color: #6E6E6E; 43 | } 44 | 45 | .jh-array-key{ 46 | font-style: italic; 47 | font-size: small; 48 | text-align: center; 49 | } 50 | 51 | .jh-object-key, .jh-array-key{ 52 | color: #444; 53 | vertical-align: top; 54 | } 55 | 56 | .jh-type-object > tbody > tr:nth-child(odd), .jh-type-array > tbody > tr:nth-child(odd){ 57 | background-color: #f5f5f5; 58 | } 59 | 60 | .jh-type-object > tbody > tr:nth-child(even), .jh-type-array > tbody > tr:nth-child(even){ 61 | background-color: #fff; 62 | } 63 | 64 | .jh-type-object, .jh-type-array{ 65 | width: 100%; 66 | border-collapse: collapse; 67 | } 68 | 69 | .jh-root{ 70 | border: 1px solid #ccc; 71 | margin: 0.2em; 72 | } 73 | 74 | th.jh-key{ 75 | text-align: left; 76 | } 77 | 78 | .jh-type-object > tbody > tr, .jh-type-array > tbody > tr{ 79 | border: 1px solid #ddd; 80 | border-bottom: none; 81 | } 82 | 83 | .jh-type-object > tbody > tr:last-child, .jh-type-array > tbody > tr:last-child{ 84 | border-bottom: 1px solid #ddd; 85 | } 86 | 87 | .jh-type-object > tbody > tr:hover, .jh-type-array > tbody > tr:hover{ 88 | border: 1px solid #F99927; 89 | } 90 | 91 | .jh-empty{ 92 | font-style: italic; 93 | color: #999; 94 | font-size: small; 95 | } 96 | 97 | .jh-a { 98 | text-decoration: none; 99 | } 100 | 101 | .jh-a:hover{ 102 | text-decoration: underline; 103 | } 104 | 105 | .jh-a span.jh-type-string { 106 | text-decoration: none; 107 | color : #268ddd; 108 | font-style: normal; 109 | } 110 | 111 | -------------------------------------------------------------------------------- /resources/js/erlplorer.js: -------------------------------------------------------------------------------- 1 | /*globals Viva, window, console, document, MODULES, EXAMPLE_OUTPUT, JsonHuman*/ 2 | (function () { 3 | 'use strict'; 4 | var MODULES_OBJ = {}; 5 | 6 | (function () { 7 | var i, len, m = MODULES; 8 | for (i = 0, len = m.length; i < len; i += 1) { 9 | MODULES_OBJ[m[i]] = true; 10 | } 11 | }()); 12 | 13 | function incr(obj, key, count) { 14 | if (obj[key] === undefined) { 15 | obj[key] = 0; 16 | } 17 | 18 | obj[key] += count; 19 | } 20 | 21 | function objToSortedList(obj) { 22 | var key, count, list = []; 23 | 24 | for (key in obj) { 25 | list.push([key, obj[key]]); 26 | } 27 | 28 | list.sort(function (a, b) { 29 | return b[1] - a[1]; 30 | }); 31 | 32 | return list; 33 | } 34 | 35 | function table(title, items, container) { 36 | var t = document.createElement('table'), 37 | titleNode = document.createElement('h2'), 38 | i, len, item, j, jlen, tr, td; 39 | 40 | for (i = 0, len = items.length; i < len; i += 1) { 41 | item = items[i]; 42 | tr = document.createElement('tr'); 43 | for (j = 0, jlen = item.length; j < jlen; j += 1) { 44 | td = document.createElement('td'); 45 | td.innerHTML = "" + item[j]; 46 | tr.appendChild(td); 47 | } 48 | 49 | t.appendChild(tr); 50 | } 51 | 52 | titleNode.innerHTML = title; 53 | container.appendChild(titleNode); 54 | container.appendChild(t); 55 | } 56 | 57 | function render(data, moduleFilter) { 58 | var graph = Viva.Graph.graph(), 59 | maxCount = 1, 60 | i, len, item, moduleName, refModuleName, refModules, 61 | funCalls, funName, funs, 62 | count, totalModuleCalls = {}, totalFunCalls = {}, 63 | functionClauses = {}, funData, fullName, 64 | totalFunLength = {}, seenModules = [], noCallModules; 65 | 66 | document.getElementById('demo-section').style.display = "none"; 67 | 68 | graph.addNode("ROOT", {name: "ROOT"}); 69 | 70 | for (i = 0, len = data.length; i < len; i += 1) { 71 | item = data[i]; 72 | if (item.status === "ok") { 73 | moduleName = item.module; 74 | seenModules.push(moduleName); 75 | graph.addNode(moduleName, {name: moduleName, info: item}); 76 | graph.addLink(moduleName, "ROOT", {count: 1}); 77 | 78 | refModules = item.data.modules; 79 | for (refModuleName in refModules) { 80 | count = refModules[refModuleName]; 81 | 82 | if (!moduleFilter(refModuleName)) { 83 | graph.addNode(refModuleName, {name: refModuleName}); 84 | graph.addLink(moduleName, refModuleName, {count: count}); 85 | } 86 | 87 | maxCount = Math.max(maxCount, count); 88 | 89 | incr(totalModuleCalls, refModuleName, count); 90 | } 91 | 92 | funs = item.data.functions; 93 | for (funName in funs) { 94 | funData = funs[funName]; 95 | count = funData.length; 96 | fullName = moduleName + ":" + funName; 97 | incr(totalFunLength, fullName, count); 98 | functionClauses[fullName] = funData.inner_clauses; 99 | } 100 | 101 | funCalls = item.data.external_funs; 102 | for (funName in funCalls) { 103 | count = funCalls[funName]; 104 | incr(totalFunCalls, funName, count); 105 | } 106 | 107 | } else { 108 | console.error("Invalid module", item); 109 | } 110 | } 111 | 112 | var graphics = Viva.Graph.View.svgGraphics(); 113 | 114 | graphics.node(function(node) { 115 | var textNode = Viva.Graph.svg('text'), 116 | name = node.data ? node.data.name : node.id; 117 | 118 | // it's one of the analyzed modules 119 | if (node.data.info) { 120 | textNode 121 | .attr('font-weight', 'bold') 122 | .attr('border', '1px solid #333') 123 | .attr('background-color', '#333'); 124 | } 125 | return textNode.text(name) 126 | .attr('text-anchor', 'middle') 127 | .attr('cursor', 'pointer'); 128 | }); 129 | 130 | function countToStroke(count, max) { 131 | return (count / max) * 4; 132 | } 133 | 134 | graphics.link(function(link){ 135 | return Viva.Graph.svg('path') 136 | .attr('stroke', '#333') 137 | .attr('stroke-width', countToStroke(link.data.count, maxCount)); 138 | }).placeLink(function(linkUI, fromPos, toPos) { 139 | // linkUI - is the object returend from link() callback above. 140 | var data = 'M' + fromPos.x + ',' + fromPos.y + 141 | 'L' + toPos.x + ',' + toPos.y; 142 | // 'Path data' (http://www.w3.org/TR/SVG/paths.html#DAttribute ) 143 | // is a common way of rendering paths in SVG: 144 | linkUI.attr("d", data); 145 | }); 146 | 147 | /*graphics.placeNode(function(nodeUI, pos) { 148 | console.log("place", nodeUI, pos); 149 | });*/ 150 | 151 | var container = document.getElementById('graph'), 152 | renderer = Viva.Graph.View.renderer(graph, { 153 | graphics : graphics, 154 | container: container 155 | }); 156 | 157 | container.style.display = "block"; 158 | renderer.run(); 159 | 160 | function isntStdlibModule(row) { 161 | return !MODULES_OBJ[row[0]]; 162 | } 163 | 164 | function isntStdlibCall(row) { 165 | return !MODULES_OBJ[row[0].split(":")[0]]; 166 | } 167 | 168 | noCallModules = seenModules.filter(function (name) { 169 | return totalModuleCalls[name] === undefined; 170 | }); 171 | 172 | noCallModules.sort(); 173 | 174 | noCallModules = noCallModules.map(function (name) { 175 | return [name]; 176 | }); 177 | 178 | if (noCallModules.length > 0) { 179 | table("Modules not Called", noCallModules, document.body); 180 | } 181 | 182 | table("Total Module Calls", objToSortedList(totalModuleCalls).filter(isntStdlibModule), document.body); 183 | table("Total Fun Calls", objToSortedList(totalFunCalls).filter(isntStdlibCall), document.body); 184 | table("Function Lengths", objToSortedList(totalFunLength), document.body); 185 | table("Function Branches", objToSortedList(functionClauses), document.body); 186 | 187 | table("Total Module Calls (with Stdlib)", objToSortedList(totalModuleCalls), document.body); 188 | table("Total Fun Calls (with Stdlib)", objToSortedList(totalFunCalls), document.body); 189 | 190 | var node = JsonHuman.format(EXAMPLE_OUTPUT); 191 | var title = document.createElement('h2'); 192 | title.innerHTML = "Raw Data"; 193 | document.body.appendChild(title); 194 | document.body.appendChild(node); 195 | } 196 | 197 | 198 | function filterStdLibModules(name) { 199 | return !!MODULES_OBJ[name]; 200 | } 201 | 202 | function onFileChange(event) { 203 | var file = event.target.files[0], 204 | reader = new window.FileReader(); 205 | 206 | reader.onload = function(e) { 207 | render(JSON.parse(reader.result), filterStdLibModules); 208 | }; 209 | 210 | reader.readAsText(file); 211 | } 212 | 213 | function main () { 214 | var dataFile = document.getElementById('data-file'); 215 | 216 | dataFile.addEventListener('change', onFileChange); 217 | var node = JsonHuman.format(EXAMPLE_OUTPUT); 218 | document.getElementById('example-output').appendChild(node); 219 | } 220 | 221 | 222 | window.addEventListener('load', main); 223 | }()); 224 | -------------------------------------------------------------------------------- /resources/js/json.human.js: -------------------------------------------------------------------------------- 1 | /*globals define, module, require, document*/ 2 | (function (root, factory) { 3 | "use strict"; 4 | if (typeof define === 'function' && define.amd) { 5 | define([], factory); 6 | } else if (typeof module !== 'undefined' && module.exports) { 7 | module.exports = factory(); 8 | } else { 9 | root.JsonHuman = factory(); 10 | } 11 | }(this, function () { 12 | "use strict"; 13 | 14 | var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 15 | 16 | function makePrefixer(prefix) { 17 | return function (name) { 18 | return prefix + "-" + name; 19 | }; 20 | } 21 | 22 | function isArray(obj) { 23 | return toString.call(obj) === '[object Array]'; 24 | } 25 | 26 | function sn(tagName, className, data) { 27 | var result = document.createElement(tagName); 28 | 29 | result.className = className; 30 | result.appendChild(document.createTextNode("" + data)); 31 | 32 | return result; 33 | } 34 | 35 | function scn(tagName, className, child) { 36 | var result = document.createElement(tagName), 37 | i, len; 38 | 39 | result.className = className; 40 | 41 | if (isArray(child)) { 42 | for (i = 0, len = child.length; i < len; i += 1) { 43 | result.appendChild(child[i]); 44 | } 45 | } else { 46 | result.appendChild(child); 47 | } 48 | 49 | return result; 50 | } 51 | 52 | function linkNode(child, href, target){ 53 | var a = scn("a", HYPERLINK_CLASS_NAME, child); 54 | a.setAttribute('href', href); 55 | a.setAttribute('target', target); 56 | return a; 57 | } 58 | 59 | var toString = Object.prototype.toString, 60 | prefixer = makePrefixer("jh"), 61 | p = prefixer, 62 | ARRAY = 2, 63 | BOOL = 4, 64 | INT = 8, 65 | FLOAT = 16, 66 | STRING = 32, 67 | OBJECT = 64, 68 | SPECIAL_OBJECT = 128, 69 | FUNCTION = 256, 70 | UNK = 1, 71 | 72 | STRING_CLASS_NAME = p("type-string"), 73 | STRING_EMPTY_CLASS_NAME = p("type-string") + " " + p("empty"), 74 | 75 | BOOL_TRUE_CLASS_NAME = p("type-bool-true"), 76 | BOOL_FALSE_CLASS_NAME = p("type-bool-false"), 77 | BOOL_IMAGE = p("type-bool-image"), 78 | INT_CLASS_NAME = p("type-int") + " " + p("type-number"), 79 | FLOAT_CLASS_NAME = p("type-float") + " " + p("type-number"), 80 | 81 | OBJECT_CLASS_NAME = p("type-object"), 82 | OBJ_KEY_CLASS_NAME = p("key") + " " + p("object-key"), 83 | OBJ_VAL_CLASS_NAME = p("value") + " " + p("object-value"), 84 | OBJ_EMPTY_CLASS_NAME = p("type-object") + " " + p("empty"), 85 | 86 | FUNCTION_CLASS_NAME = p("type-function"), 87 | 88 | ARRAY_KEY_CLASS_NAME = p("key") + " " + p("array-key"), 89 | ARRAY_VAL_CLASS_NAME = p("value") + " " + p("array-value"), 90 | ARRAY_CLASS_NAME = p("type-array"), 91 | ARRAY_EMPTY_CLASS_NAME = p("type-array") + " " + p("empty"), 92 | 93 | HYPERLINK_CLASS_NAME = p('a'), 94 | 95 | UNKNOWN_CLASS_NAME = p("type-unk"); 96 | 97 | function getType(obj) { 98 | var type = typeof obj; 99 | 100 | switch (type) { 101 | case "boolean": 102 | return BOOL; 103 | case "string": 104 | return STRING; 105 | case "number": 106 | return (obj % 1 === 0) ? INT : FLOAT; 107 | case "function": 108 | return FUNCTION; 109 | default: 110 | if (isArray(obj)) { 111 | return ARRAY; 112 | } else if (obj === Object(obj)) { 113 | if (obj.constructor === Object) { 114 | return OBJECT; 115 | } 116 | return OBJECT | SPECIAL_OBJECT 117 | } else { 118 | return UNK; 119 | } 120 | } 121 | } 122 | 123 | function _format(data, options, parentKey) { 124 | 125 | var result, container, key, keyNode, valNode, len, childs, tr, value, 126 | isEmpty = true, 127 | isSpecial = false, 128 | accum = [], 129 | type = getType(data); 130 | 131 | // Initialized & used only in case of objects & arrays 132 | var hyperlinksEnabled, aTarget, hyperlinkKeys ; 133 | 134 | if (type === BOOL) { 135 | var boolOpt = options.bool; 136 | container = document.createElement('div'); 137 | 138 | if (boolOpt.showImage) { 139 | var img = document.createElement('img'); 140 | img.setAttribute('class', BOOL_IMAGE); 141 | 142 | img.setAttribute('src', 143 | '' + (data ? boolOpt.img.true : boolOpt.img.false)); 144 | 145 | container.appendChild(img); 146 | } 147 | 148 | if (boolOpt.showText) { 149 | container.appendChild(data ? 150 | sn("span", BOOL_TRUE_CLASS_NAME, boolOpt.text.true) : 151 | sn("span", BOOL_FALSE_CLASS_NAME, boolOpt.text.false)); 152 | } 153 | 154 | result = container; 155 | 156 | } else if (type === STRING) { 157 | if (data === "") { 158 | result = sn("span", STRING_EMPTY_CLASS_NAME, "(Empty Text)"); 159 | } else { 160 | result = sn("span", STRING_CLASS_NAME, data); 161 | } 162 | } else if (type === INT) { 163 | result = sn("span", INT_CLASS_NAME, data); 164 | } else if (type === FLOAT) { 165 | result = sn("span", FLOAT_CLASS_NAME, data); 166 | } else if (type & OBJECT) { 167 | if (type & SPECIAL_OBJECT) { 168 | isSpecial = true; 169 | } 170 | childs = []; 171 | 172 | aTarget = options.hyperlinks.target; 173 | hyperlinkKeys = options.hyperlinks.keys; 174 | 175 | // Is Hyperlink Key 176 | hyperlinksEnabled = 177 | options.hyperlinks.enable && 178 | hyperlinkKeys && 179 | hyperlinkKeys.length > 0; 180 | 181 | for (key in data) { 182 | isEmpty = false; 183 | 184 | value = data[key]; 185 | 186 | valNode = _format(value, options, key); 187 | keyNode = sn("th", OBJ_KEY_CLASS_NAME, key); 188 | 189 | if( hyperlinksEnabled && 190 | typeof(value) === 'string' && 191 | indexOf.call(hyperlinkKeys, key) >= 0){ 192 | 193 | valNode = scn("td", OBJ_VAL_CLASS_NAME, linkNode(valNode, value, aTarget)); 194 | } else { 195 | valNode = scn("td", OBJ_VAL_CLASS_NAME, valNode); 196 | } 197 | 198 | tr = document.createElement("tr"); 199 | tr.appendChild(keyNode); 200 | tr.appendChild(valNode); 201 | 202 | childs.push(tr); 203 | } 204 | 205 | if (isSpecial) { 206 | result = sn('span', STRING_CLASS_NAME, data.toString()) 207 | } else if (isEmpty) { 208 | result = sn("span", OBJ_EMPTY_CLASS_NAME, "(Empty Object)"); 209 | } else { 210 | result = scn("table", OBJECT_CLASS_NAME, scn("tbody", '', childs)); 211 | } 212 | } else if (type === FUNCTION) { 213 | result = sn("span", FUNCTION_CLASS_NAME, data); 214 | } else if (type === ARRAY) { 215 | if (data.length > 0) { 216 | childs = []; 217 | var showArrayIndices = options.showArrayIndex; 218 | 219 | aTarget = options.hyperlinks.target; 220 | hyperlinkKeys = options.hyperlinks.keys; 221 | 222 | // Hyperlink of arrays? 223 | hyperlinksEnabled = parentKey && options.hyperlinks.enable && 224 | hyperlinkKeys && 225 | hyperlinkKeys.length > 0 && 226 | indexOf.call(hyperlinkKeys, parentKey) >= 0; 227 | 228 | for (key = 0, len = data.length; key < len; key += 1) { 229 | 230 | keyNode = sn("th", ARRAY_KEY_CLASS_NAME, key); 231 | value = data[key]; 232 | 233 | if (hyperlinksEnabled && typeof(value) === "string") { 234 | valNode = _format(value, options, key); 235 | valNode = scn("td", ARRAY_VAL_CLASS_NAME, 236 | linkNode(valNode, value, aTarget)); 237 | } else { 238 | valNode = scn("td", ARRAY_VAL_CLASS_NAME, 239 | _format(value, options, key)); 240 | } 241 | 242 | tr = document.createElement("tr"); 243 | 244 | if (showArrayIndices) { 245 | tr.appendChild(keyNode); 246 | } 247 | tr.appendChild(valNode); 248 | 249 | childs.push(tr); 250 | } 251 | 252 | result = scn("table", ARRAY_CLASS_NAME, scn("tbody", '', childs)); 253 | } else { 254 | result = sn("span", ARRAY_EMPTY_CLASS_NAME, "(Empty List)"); 255 | } 256 | } else { 257 | result = sn("span", UNKNOWN_CLASS_NAME, data); 258 | } 259 | 260 | return result; 261 | } 262 | 263 | function format(data, options) { 264 | options = validateOptions(options || {}); 265 | 266 | var result; 267 | 268 | result = _format(data, options); 269 | result.className = result.className + " " + prefixer("root"); 270 | 271 | return result; 272 | } 273 | 274 | function validateOptions(options){ 275 | options = validateArrayIndexOption(options); 276 | options = validateHyperlinkOptions(options); 277 | options = validateBoolOptions(options); 278 | 279 | // Add any more option validators here 280 | 281 | return options; 282 | } 283 | 284 | 285 | function validateArrayIndexOption(options) { 286 | if(options.showArrayIndex === undefined){ 287 | options.showArrayIndex = true; 288 | } else { 289 | // Force to boolean just in case 290 | options.showArrayIndex = options.showArrayIndex ? true: false; 291 | } 292 | 293 | return options; 294 | } 295 | 296 | function validateHyperlinkOptions(options){ 297 | var hyperlinks = { 298 | enable : false, 299 | keys : null, 300 | target : '' 301 | }; 302 | 303 | if(options.hyperlinks && options.hyperlinks.enable) { 304 | hyperlinks.enable = true; 305 | 306 | hyperlinks.keys = isArray(options.hyperlinks.keys) ? options.hyperlinks.keys : []; 307 | 308 | if(options.hyperlinks.target) { 309 | hyperlinks.target = '' + options.hyperlinks.target; 310 | } else { 311 | hyperlinks.target = '_blank'; 312 | } 313 | } 314 | 315 | options.hyperlinks = hyperlinks; 316 | 317 | return options; 318 | } 319 | 320 | function validateBoolOptions(options){ 321 | if(!options.bool){ 322 | options.bool = { 323 | text: { 324 | true : "true", 325 | false : "false" 326 | }, 327 | img : { 328 | true: "", 329 | false: "" 330 | }, 331 | showImage : false, 332 | showText : true 333 | }; 334 | } else { 335 | var boolOptions = options.bool; 336 | 337 | // Show text if no option 338 | if(!boolOptions.showText && !boolOptions.showImage){ 339 | boolOptions.showImage = false; 340 | boolOptions.showText = true; 341 | } 342 | 343 | if(boolOptions.showText){ 344 | if(!boolOptions.text){ 345 | boolOptions.text = { 346 | true : "true", 347 | false : "false" 348 | }; 349 | } else { 350 | var t = boolOptions.text.true, f = boolOptions.text.false; 351 | 352 | if(getType(t) != STRING || t === ''){ 353 | boolOptions.text.true = 'true'; 354 | } 355 | 356 | if(getType(f) != STRING || f === ''){ 357 | boolOptions.text.false = 'false'; 358 | } 359 | } 360 | } 361 | 362 | if(boolOptions.showImage){ 363 | if(!boolOptions.img.true && !boolOptions.img.false){ 364 | boolOptions.showImage = false; 365 | } 366 | } 367 | } 368 | 369 | return options; 370 | } 371 | 372 | return { 373 | format: format 374 | }; 375 | })); 376 | -------------------------------------------------------------------------------- /src/erlplorer.app.src: -------------------------------------------------------------------------------- 1 | {application, erlplorer, 2 | [{description, "Explore Erlang and Efene Source Code"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {applications, 6 | [kernel, 7 | stdlib 8 | ]}, 9 | {env,[]}, 10 | {modules, []}, 11 | 12 | {maintainers, []}, 13 | {licenses, []}, 14 | {links, []} 15 | ]}. 16 | -------------------------------------------------------------------------------- /src/erlplorer.fn: -------------------------------------------------------------------------------- 1 | 2 | fn main @public 3 | case "info" :: Paths: 4 | Result = lists.map(Paths) <<- case Path: 5 | handle_file(Path) 6 | end 7 | 8 | print_and_exit(Result) 9 | case "search" :: Query :: Paths: 10 | match compile_ast_query(Query): 11 | case ok, ModName, BinMod, Warnings: 12 | print_warnings(Warnings) 13 | (module, QMod) = erlang.load_module(ModName, BinMod) 14 | 15 | for Pattern in Paths: 16 | for Path in filelib.wildcard(Pattern): 17 | search_path(Path, QMod) 18 | end 19 | end 20 | 21 | erlang.halt(0) 22 | case error, Errors, Warnings: 23 | print_warnings(Warnings) 24 | print_errors(Errors) 25 | erlang.halt(0) 26 | case Error: 27 | io.format("Error: ~p~n", [Error]) 28 | erlang.halt(0) 29 | end 30 | case "count-atoms" :: Paths: 31 | StateOut = lists.foldl({}, Paths) <<- case Path, StateIn: 32 | count_atoms(Path, StateIn) 33 | end 34 | 35 | format_atom_count(StateOut) 36 | case Args: 37 | io.format("Unknown Arguments: ~p~n", [Args]) 38 | usage() 39 | erlang.halt(0) 40 | end 41 | 42 | fn usage case: 43 | io.format("USAGE: erlplorer info +~n") 44 | io.format(" erlplorer search +~n") 45 | end 46 | 47 | fn print_warnings 48 | case []: 49 | ok 50 | case Warnings: 51 | io.format("~nWarnings: ~n~p~n", [Warnings]) 52 | end 53 | 54 | fn print_errors case Errors: 55 | io.format("~nErrors: ~n~p~n", [Errors]) 56 | end 57 | 58 | fn compile_ast_query case Code: 59 | ModCode = "dummy() -> " ++ Code ++ ".\n" 60 | match erl_scan.string(ModCode): 61 | case ok, Tokens, _EndLocation: 62 | (ok, Ast) = erl_parse.parse_form(Tokens) 63 | (ok, QueryAst) = extract_query_ast(Ast) 64 | AbsQueryAst = abstract_query_ast(QueryAst) 65 | Id = erlang.unique_integer([positive]) 66 | ModName = list_to_atom("erlplorer_query_mod_" ++ integer_to_list(Id)) 67 | QModAst = query_ast_to_mod(AbsQueryAst, ModName) 68 | #_ io.format("ast: ~p~n", [QModAst]) 69 | #_ io.format("code:~n~s~n", [pp_mod_ast(QModAst)]) 70 | compile.forms(QModAst, [return, binary]) 71 | case error, ErrorInfo, _ErrorLocation: 72 | (error, ErrorInfo, []) 73 | end 74 | end 75 | 76 | fn pp_mod_ast case ModAst: 77 | erl_prettypr.format(erl_syntax.form_list(ModAst)) 78 | end 79 | 80 | fn query_match @public case {filename=Filename}, Ast: 81 | Line = element(2, Ast) 82 | io.format("~s:~p ~s~n", [Filename, Line, try_pp_ast(Ast)]) 83 | end 84 | 85 | fn try_pp_ast case Ast: 86 | try 87 | erl_pp.expr(Ast) 88 | catch case _, _: 89 | try 90 | erl_pp.form(Ast) 91 | catch case _, _: 92 | "" 93 | end 94 | end 95 | end 96 | 97 | fn query_ast_to_mod case Ast, ModName: 98 | [(attribute, 1, file, (atom_to_list(ModName), 1)), 99 | (attribute, 1, module, ModName), 100 | (attribute, 2, export, [(walk, 2)]), 101 | 102 | (function, 4, walk, 2, 103 | [(clause, 4, 104 | [(var, 4, `erlplorer_State`), 105 | (`match`, 4, (var, 4, `erlplorer_Node`), Ast)], 106 | [], 107 | [(call, 5, 108 | (remote, 5, (atom, 5, erlplorer), (atom, 5, query_match)), 109 | [(var, 5, `erlplorer_State`), (var, 5, `erlplorer_Node`)]), 110 | 111 | (tuple, 6, [(var, 6, `erlplorer_Node`), (var, 6, `erlplorer_State`)])]), 112 | (clause, 7, 113 | [(var, 7, `erlplorer_State`), (var, 7, `erlplorer_Node`)], 114 | [], 115 | [(tuple, 8, [(var, 8, `erlplorer_Node`), (var, 8, `erlplorer_State`)])])]), 116 | (eof, 9)] 117 | end 118 | 119 | fn extract_query_ast 120 | case (function, _Line, dummy, _Arity=0, 121 | [(clause, _CLine, _Args, _Guards, [Body])]): 122 | (ok, Body) 123 | end 124 | 125 | fn fun_match_any_line_ast case _A, _E: (var, 99, `_`) end 126 | 127 | fn abstract_query_ast_lines 128 | case State, Node=(var, _Line, Name): 129 | match atom_to_list(Name): 130 | case "_@" ++ _: 131 | #_ "if name starts with _@ then we leave it as a var to pattern match" 132 | #_ "create an 'identity' function to avoid abstract from abstracting it" 133 | NewNode = fn case _A, _E: Node end 134 | (NewNode, State) 135 | else: 136 | #_ "replace vars for funs that will be called by abstract" 137 | NewNode = fn case _A, _E: 138 | Line = 0 139 | (tuple, Line, [(atom, Line, var), (var, 99, `_`), (atom, Line, Name)]) 140 | end 141 | (NewNode, State) 142 | end 143 | case State, Node when is_integer(element(2, Node)): 144 | #_ "put a function in the second element of the tuple" 145 | #_ "so abstract calls it to get the right node" 146 | NewNode = setelement(2, Node, fn fun_match_any_line_ast:2) 147 | (NewNode, State) 148 | end 149 | 150 | fn abstract_query_ast case Ast: 151 | State0 = {} 152 | Walker = fn abstract_query_ast_lines:2 153 | (AbsAstLines, _State) = ast_walk.expr(Ast, Walker, State0) 154 | erlplorer_util.abstract(AbsAstLines) 155 | end 156 | 157 | fn search_path case Path, QMod: 158 | match parse_file(Path): 159 | case ok, Ast: 160 | State0 = {} 161 | Walker = fn QMod.walk:2 162 | ast_walk.walk(Ast, Walker, State0) 163 | case error, Reason: 164 | io.format("Error parsing ~s: ~p", [Path, Reason]) 165 | end 166 | end 167 | 168 | fn walk_count_atoms 169 | case State, Ast = (atom, _Anno, Atom): 170 | (Ast, maps.update_with(Atom, fn case V: V + 1 end, 1, State)) 171 | case State, Ast: 172 | (Ast, State) 173 | end 174 | 175 | fn count_atoms case Path, StateIn: 176 | match parse_file(Path): 177 | case ok, Ast: 178 | Walker = fn walk_count_atoms:2 179 | (_, State1) = ast_walk.walk(Ast, Walker, StateIn) 180 | State1 181 | case error, Reason: 182 | io.format("Error parsing ~s: ~p", [Path, Reason]) 183 | StateIn 184 | end 185 | end 186 | 187 | fn format_atom_count case State: 188 | AtomList0 = maps.to_list(State) 189 | AtomListSorted = lists.sort(fn case (_, A), (_, B): A <= B end, AtomList0) 190 | for (Atom, Count) in AtomListSorted: 191 | io.format("~p: ~p~n", [Atom, Count]) 192 | end 193 | end 194 | 195 | fn parse_file case Path: 196 | match lists.reverse(Path): 197 | case "lre." ++ _: 198 | epp.parse_file(Path, [], []) 199 | case "nf." ++ _: 200 | efene.to_mod(Path) 201 | else: 202 | (error, (invalid_file, Path)) 203 | end 204 | end 205 | 206 | fn fun_part_to_name 207 | case (atom, _, Name): atom_to_list(Name) 208 | case (var, _, _Name): "_" 209 | case (call, _, _, _): "_" 210 | case (record_field, _Line, _Var, Name, (atom, _, Field)): 211 | io_lib.format("#~p.~p", [Name, Field]) 212 | end 213 | 214 | fn fun_to_name case Mod, Fun, Args: 215 | ModName = fun_part_to_name(Mod) 216 | FunName = fun_part_to_name(Fun) 217 | Arity = length(Args) 218 | list_to_binary(io_lib.format("~s:~s/~p", [ModName, FunName, Arity])) 219 | end 220 | 221 | fn analyze_path @public case Path: 222 | match parse_file(Path): 223 | case ok, Ast: 224 | Walker = fn 225 | case State, Node=(call, _Line, (remote, _La, Mod, Fun), Args): 226 | MFA = fun_to_name(Mod, Fun, Args) 227 | State1 = inc(State, external_funs, MFA) 228 | match Mod: 229 | case (atom, _, ModName): 230 | (Node, inc(State1, modules, ModName)) 231 | else: 232 | (Node, State1) 233 | end 234 | case State={functions=Funs}, Node=(function, Line, Name, Arity, Clauses): 235 | (clause, _, _, _, Forms) = lists.last(Clauses) 236 | LastForm = lists.last(Forms) 237 | LastLine = element(2, LastForm) 238 | ClausesInfo = analyze_clauses(Node) 239 | Info = maps.merge({line: Line, 240 | name: Name, 241 | length: LastLine - Line + 1, 242 | arity: Arity, 243 | clauses: length(Clauses)}, 244 | ClausesInfo) 245 | FunA = io_lib.format("~s/~p", [Name, Arity]) -> list_to_binary() 246 | Funs1 = maps.put(FunA, Info, Funs) 247 | State1 = maps.put(functions, Funs1, State) 248 | (Node, State1) 249 | case State, Node: 250 | (Node, State) 251 | end 252 | 253 | State0 = {modules: {}, external_funs: {}, functions: {}} 254 | (_, Stats) = ast_walk.walk(Ast, Walker, State0) 255 | 256 | (ok, Stats) 257 | case error, Reason: 258 | (error, {reason: (io_lib.format("~p", [Reason]) -> list_to_binary())}) 259 | end 260 | end 261 | 262 | fn add_item_line case Key, Map, Name, Line: 263 | Items = maps.get(Key, Map, {}) 264 | Lines = maps.get(Name, Items, []) 265 | NewLines = Line :: Lines 266 | NewItems = maps.put(Name, NewLines, Items) 267 | maps.put(Key, NewItems, Map) 268 | end 269 | 270 | fn analyze_clauses case Ast: 271 | Walker = fn 272 | case State, Node=(var, Line, Name): 273 | State1 = add_item_line(vars, State, Name, Line) 274 | (Node, State1) 275 | case State, Node=(call, Line, (remote, _La, Mod, Fun), Args): 276 | Name = fun_to_name(Mod, Fun, Args) 277 | State1 = add_item_line(remote_calls, State, Name, Line) 278 | (Node, State1) 279 | case State, Node=(call, Line, (atom, _La, Fun), Args): 280 | Arity = length(Args) 281 | Name = list_to_binary(io_lib.format("~p/~p", [Fun, Arity])) 282 | State1 = add_item_line(calls, State, Name, Line) 283 | (Node, State1) 284 | 285 | case State, Node=(record_index, Line, Name, _Field): 286 | State1 = add_item_line(records, State, Name, Line) 287 | (Node, State1) 288 | case State, Node=(record_field, Line, _Rec, Name, _Field): 289 | State1 = add_item_line(records, State, Name, Line) 290 | (Node, State1) 291 | case State, Node=(record, Line, Name, _Inits): 292 | State1 = add_item_line(records, State, Name, Line) 293 | (Node, State1) 294 | 295 | case State, Node=(`if`, _Line, Clauses): 296 | (Node, inc_key(State, inner_clauses, length(Clauses))) 297 | case State, Node=(`case`, _Line, _Expr, Clauses): 298 | (Node, inc_key(State, inner_clauses, length(Clauses))) 299 | case State, Node=(`receive`, _Line, Clauses): 300 | (Node, inc_key(State, inner_clauses, length(Clauses))) 301 | case State, Node=(`receive`, _Line, _Cs, _To, Clauses): 302 | (Node, inc_key(State, inner_clauses, length(Clauses))) 303 | case State, Node=(`fun`, _Line, (clauses, Clauses)): 304 | (Node, inc_key(State, inner_clauses, length(Clauses))) 305 | case State, Node=(named_fun, _Line, _Name, Clauses): 306 | (Node, inc_key(State, inner_clauses, length(Clauses))) 307 | case State, Node=(`try`, _Line, _Es, Scs, Ccs, _As): 308 | (Node, inc_key(State, inner_clauses, length(Scs) + length(Ccs))) 309 | 310 | case State, Node: 311 | (Node, State) 312 | end 313 | 314 | State0 = {vars: {}, remote_calls: {}, calls: {}, records: {}, 315 | inner_clauses: 0} 316 | (_, Stats) = ast_walk.walk([Ast], Walker, State0) 317 | 318 | Stats 319 | end 320 | 321 | fn inc_key case Map, Key, Value: 322 | CurrentCount = maps.get(Key, Map, 0) 323 | maps.put(Key, CurrentCount + Value, Map) 324 | end 325 | 326 | fn inc case Map, Key, SubKey: 327 | M1 = maps.get(Key, Map, {}) 328 | CurrentCount = maps.get(SubKey, M1, 0) 329 | M2 = maps.put(SubKey, CurrentCount + 1, M1) 330 | maps.put(Key, M2, Map) 331 | end 332 | 333 | fn print_and_exit case Data: 334 | Out = jsx.encode(Data) 335 | io.format("~s~n", [Out]) 336 | erlang.halt(0) 337 | end 338 | 339 | fn handle_file case Path: 340 | ModuleName = remove_extension(filename.basename(Path)) -> list_to_binary() 341 | match analyze_path(Path): 342 | case ok, Result: 343 | {status: ok, module: ModuleName, data: Result} 344 | case error, Reason: 345 | {status: error, reason: Reason, module: ModuleName} 346 | end 347 | end 348 | 349 | fn remove_extension case Name: 350 | match lists.reverse(Name): 351 | case "lre." ++ Name1: 352 | lists.reverse(Name1) 353 | case "nf." ++ Name1: 354 | lists.reverse(Name1) 355 | else: 356 | Name 357 | end 358 | end 359 | -------------------------------------------------------------------------------- /src/erlplorer_util.erl: -------------------------------------------------------------------------------- 1 | -module(erlplorer_util). 2 | -export([abstract/1]). 3 | 4 | -define(UNICODE(C), 5 | (C < 16#D800 orelse 6 | C > 16#DFFF andalso C < 16#FFFE orelse 7 | C > 16#FFFF andalso C =< 16#10FFFF)). 8 | 9 | enc_func(latin1) -> fun(C) -> C < 256 end; 10 | enc_func(unicode) -> fun(C) -> ?UNICODE(C) end; 11 | enc_func(utf8) -> fun(C) -> ?UNICODE(C) end; 12 | enc_func(none) -> none; 13 | enc_func(Fun) when is_function(Fun, 1) -> Fun; 14 | enc_func(Term) -> erlang:error({badarg, Term}). 15 | 16 | abstract(T) -> 17 | Anno = erl_anno:new(0), 18 | abstract(T, Anno, enc_func(epp:default_encoding())). 19 | 20 | % if it's a function call it so it returns the right node 21 | abstract(T, A, E) when is_function(T) -> T(A, E); 22 | 23 | abstract(T, A, _E) when is_integer(T) -> {integer,A,T}; 24 | abstract(T, A, _E) when is_float(T) -> {float,A,T}; 25 | abstract(T, A, _E) when is_atom(T) -> {atom,A,T}; 26 | abstract([], A, _E) -> {nil,A}; 27 | abstract(B, A, _E) when is_bitstring(B) -> 28 | {bin, A, [abstract_byte(Byte, A) || Byte <- bitstring_to_list(B)]}; 29 | abstract([H|T], A, none=E) -> 30 | {cons,A,abstract(H, A, E),abstract(T, A, E)}; 31 | abstract(List, A, E) when is_list(List) -> 32 | abstract_list(List, [], A, E); 33 | abstract(Tuple, A, E) when is_tuple(Tuple) -> 34 | {tuple,A,abstract_tuple_list(tuple_to_list(Tuple), A, E)}; 35 | abstract(Map, A, E) when is_map(Map) -> 36 | {map,A,abstract_map_fields(maps:to_list(Map),A,E)}. 37 | 38 | abstract_list([H|T], String, A, E) -> 39 | case is_integer(H) andalso H >= 0 andalso E(H) of 40 | true -> 41 | abstract_list(T, [H|String], A, E); 42 | false -> 43 | AbstrList = {cons,A,abstract(H, A, E),abstract(T, A, E)}, 44 | not_string(String, AbstrList, A, E) 45 | end; 46 | abstract_list([], String, A, _E) -> 47 | {string, A, lists:reverse(String)}; 48 | abstract_list(T, String, A, E) -> 49 | not_string(String, abstract(T, A, E), A, E). 50 | 51 | not_string([C|T], Result, A, E) -> 52 | not_string(T, {cons, A, {integer, A, C}, Result}, A, E); 53 | not_string([], Result, _A, _E) -> 54 | Result. 55 | 56 | abstract_tuple_list([H|T], A, E) -> 57 | [abstract(H, A, E)|abstract_tuple_list(T, A, E)]; 58 | abstract_tuple_list([], _A, _E) -> 59 | []. 60 | 61 | abstract_map_fields(Fs,A,E) -> 62 | [{map_field_assoc,A,abstract(K,A,E),abstract(V,A,E)}||{K,V}<-Fs]. 63 | 64 | abstract_byte(Byte, A) when is_integer(Byte) -> 65 | {bin_element, A, {integer, A, Byte}, default, default}; 66 | abstract_byte(Bits, A) -> 67 | Sz = bit_size(Bits), 68 | <> = Bits, 69 | {bin_element, A, {integer, A, Val}, {integer, A, Sz}, default}. 70 | 71 | --------------------------------------------------------------------------------