code-explorer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +42 -0
- data/VERSION +1 -0
- data/bin/call-graph +12 -0
- data/bin/class-dependencies +177 -0
- data/bin/code-explorer +39 -0
- data/bin/required-files +11 -0
- data/lib/code_explorer/call_graph.rb +91 -0
- data/lib/code_explorer/dot.rb +20 -0
- data/lib/code_explorer/numbered_lines.rb +18 -0
- data/lib/code_explorer/version.rb +3 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fb4f5f3aec4c52c667605028d891dccf6bab2b6d
|
4
|
+
data.tar.gz: c3590e42ee2bf91facc0553fee487c19873b884c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7757f66ac7088828f58beb5ddcfb84f84dd415e5680c5493a1f3bcc23e22c6e602b3f59a2aaf38eafe1eb641f289d1a10ae8dc8170d8078a55049119bc745436
|
7
|
+
data.tar.gz: d0e8a1630fdf7a037ffbced5fa1bf7daad69990d33377cc4daa8c41965e9b0632804784924e559a475a3154b670a0b031b0824701e312a4fc89594272762aa92
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Call Graph
|
2
|
+
|
3
|
+
This makes a call graph among methods of a single Ruby file.
|
4
|
+
|
5
|
+
I made it to help me orient myself in unfamiliar legacy code and to help
|
6
|
+
identify cohesive parts that could be split out.
|
7
|
+
|
8
|
+
Yes, it is quick and dirty.
|
9
|
+
|
10
|
+
## Requirements
|
11
|
+
|
12
|
+
- [parser gem](https://github.com/whitequark/parser)
|
13
|
+
- [Graphviz](http://www.graphviz.org/)
|
14
|
+
|
15
|
+
## License
|
16
|
+
|
17
|
+
MIT
|
18
|
+
|
19
|
+
## Running from Source
|
20
|
+
|
21
|
+
```sh
|
22
|
+
bundle install --path vendor/bundle
|
23
|
+
bundle exec bin/code-explorer # otherwise Sinatra will not start
|
24
|
+
```
|
25
|
+
|
26
|
+
## Example
|
27
|
+
|
28
|
+
[One file in YaST][p-rb] has around 2700 lines and 73 methods. The call graph
|
29
|
+
below was made with
|
30
|
+
```console
|
31
|
+
$ bin/call-graph ../yast/packager/src/modules/Packages.rb
|
32
|
+
$ dot -Tpng -oPackages.png ../yast/packager/src/modules/Packages.dot
|
33
|
+
```
|
34
|
+
|
35
|
+
If the resulting size is too big, use ImageMagick:
|
36
|
+
```console
|
37
|
+
$ convert Packages.png -resize 1200 Packages-small.png
|
38
|
+
```
|
39
|
+
|
40
|
+
[p-rb]: https://github.com/yast/yast-packager/blob/a0b38c046e6e4086a986047d0d7cd5d155af5024/src/modules/Packages.rb
|
41
|
+
|
42
|
+

|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/call-graph
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# method dependency graph
|
4
|
+
|
5
|
+
$: << File.expand_path("../lib", __FILE__)
|
6
|
+
|
7
|
+
require "code_explorer/call_graph"
|
8
|
+
|
9
|
+
filename_rb = ARGV[0]
|
10
|
+
dot = call_graph(filename_rb)
|
11
|
+
filename_dot = filename_rb.sub(/\.rb$/, "") + ".dot"
|
12
|
+
File.write(filename_dot, dot)
|
@@ -0,0 +1,177 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# class dependency graph
|
4
|
+
|
5
|
+
require "parser/current"
|
6
|
+
require "pp"
|
7
|
+
|
8
|
+
require "code_explorer/dot"
|
9
|
+
|
10
|
+
def main
|
11
|
+
asts = ARGV.map {|fn| ast_for_filename(fn)}
|
12
|
+
cs = Consts.new
|
13
|
+
cs.report_modules(asts)
|
14
|
+
graph = cs.superclasses
|
15
|
+
puts dot_from_hash(graph)
|
16
|
+
end
|
17
|
+
|
18
|
+
def ast_for_filename(fn)
|
19
|
+
ruby = File.read(fn)
|
20
|
+
Parser::CurrentRuby.parse(ruby)
|
21
|
+
end
|
22
|
+
|
23
|
+
# ruby [String] a ruby program
|
24
|
+
# @return a dot graph string
|
25
|
+
def dep_graph(ast)
|
26
|
+
Consts.new.report_modules(ast)
|
27
|
+
dot_from_hash({})
|
28
|
+
end
|
29
|
+
|
30
|
+
# tracks what constants are resolvable
|
31
|
+
class ConstBinding
|
32
|
+
def initialize(fqname, parent = nil)
|
33
|
+
@fqname = fqname
|
34
|
+
@parent = parent
|
35
|
+
@known = {}
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [ConstBinding] the new scope
|
39
|
+
def open_namespace(fqname)
|
40
|
+
ns = @known[fqname]
|
41
|
+
if ns.is_a? ConstBinding
|
42
|
+
# puts "(reopening #{fqname})"
|
43
|
+
else
|
44
|
+
ns = self.class.new(fqname, self)
|
45
|
+
@known[fqname] = ns
|
46
|
+
end
|
47
|
+
ns
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [ConstBinding] the parent scope
|
51
|
+
def close_namespace
|
52
|
+
@parent
|
53
|
+
end
|
54
|
+
|
55
|
+
def declare_const(fqname)
|
56
|
+
if @known[fqname]
|
57
|
+
# puts "warning: #{fqname} already declared"
|
58
|
+
end
|
59
|
+
@known[fqname] = :const
|
60
|
+
end
|
61
|
+
|
62
|
+
def resolve_declared_const(name)
|
63
|
+
if @fqname.empty?
|
64
|
+
name
|
65
|
+
else
|
66
|
+
"#{@fqname}::#{name}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def resolve_used_const(name)
|
71
|
+
# puts "resolving #{name} in #{@fqname}, known #{@known.inspect}"
|
72
|
+
candidate = resolve_declared_const(name)
|
73
|
+
if @known.include?(candidate)
|
74
|
+
candidate
|
75
|
+
elsif @parent
|
76
|
+
@parent.resolve_used_const(name)
|
77
|
+
else
|
78
|
+
name
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class Consts < Parser::AST::Processor
|
84
|
+
include AST::Sexp
|
85
|
+
|
86
|
+
# @return [ConstBinding]
|
87
|
+
attr_reader :cb
|
88
|
+
# @return [Hash{String => Array<String>}]
|
89
|
+
attr_reader :superclasses
|
90
|
+
|
91
|
+
def initialize
|
92
|
+
@cb = ConstBinding.new("")
|
93
|
+
@superclasses = {}
|
94
|
+
end
|
95
|
+
|
96
|
+
def report_modules(asts)
|
97
|
+
Array(asts).each do |ast|
|
98
|
+
process(ast)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def const_name_from_sexp(node)
|
103
|
+
case node.type
|
104
|
+
when :self
|
105
|
+
"self"
|
106
|
+
when :cbase
|
107
|
+
""
|
108
|
+
when :const, :casgn
|
109
|
+
parent, name, _maybe_value = *node
|
110
|
+
if parent
|
111
|
+
const_name_from_sexp(parent) + "::#{name}"
|
112
|
+
else
|
113
|
+
name.to_s
|
114
|
+
end
|
115
|
+
else
|
116
|
+
raise "Unexpected #{node.type}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def new_scope(name, &block)
|
121
|
+
@cb = cb.open_namespace(name)
|
122
|
+
block.call
|
123
|
+
@cb = cb.close_namespace
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_module(node)
|
127
|
+
name, _body = *node
|
128
|
+
name = cb.resolve_declared_const(const_name_from_sexp(name))
|
129
|
+
# puts "module #{name}"
|
130
|
+
|
131
|
+
new_scope(name) do
|
132
|
+
super
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def on_class(node)
|
137
|
+
name, parent, _body = *node
|
138
|
+
parent ||= s(:const, s(:cbase), :Object)
|
139
|
+
|
140
|
+
name = cb.resolve_declared_const(const_name_from_sexp(name))
|
141
|
+
parent = cb.resolve_used_const(const_name_from_sexp(parent))
|
142
|
+
# puts "class #{name} < #{parent}"
|
143
|
+
|
144
|
+
@superclasses[name] = [parent]
|
145
|
+
|
146
|
+
new_scope(name) do
|
147
|
+
super
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def on_sclass(node)
|
152
|
+
parent, _body = *node
|
153
|
+
|
154
|
+
parent = const_name_from_sexp(parent)
|
155
|
+
name = "<< #{parent}" # cheating
|
156
|
+
# puts "class #{name}"
|
157
|
+
|
158
|
+
new_scope(name) do
|
159
|
+
super
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def on_casgn(node)
|
164
|
+
name = cb.resolve_declared_const(const_name_from_sexp(node))
|
165
|
+
cb.declare_const(name)
|
166
|
+
# puts "casgn #{name}"
|
167
|
+
end
|
168
|
+
|
169
|
+
def on_const(node)
|
170
|
+
name = const_name_from_sexp(node)
|
171
|
+
fqname = cb.resolve_used_const(name)
|
172
|
+
# puts "CONST #{fqname}"
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
main
|
data/bin/code-explorer
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require "sinatra"
|
4
|
+
require "cheetah"
|
5
|
+
|
6
|
+
require "code_explorer/call_graph"
|
7
|
+
require "code_explorer/numbered_lines"
|
8
|
+
|
9
|
+
def path_link(prefix, path, text)
|
10
|
+
"<a href='#{prefix}/#{path}'>#{text}</a>"
|
11
|
+
end
|
12
|
+
|
13
|
+
configure do
|
14
|
+
mime_type :png, "image/png"
|
15
|
+
mime_type :svg, "image/svg+xml"
|
16
|
+
end
|
17
|
+
|
18
|
+
get "/" do
|
19
|
+
files = Dir.glob("**/*.rb").sort
|
20
|
+
files.map do |f|
|
21
|
+
path_link("/files", f, f) + " " +
|
22
|
+
path_link("/call-graph", f, "call graph")
|
23
|
+
end.join("<br>\n")
|
24
|
+
end
|
25
|
+
|
26
|
+
get "/files/*" do |path|
|
27
|
+
numbered_lines(File.read(path))
|
28
|
+
# send_file path, :type => :text
|
29
|
+
end
|
30
|
+
|
31
|
+
get "/call-graph/*" do |path|
|
32
|
+
dot = call_graph(path)
|
33
|
+
|
34
|
+
type = :svg
|
35
|
+
graph = Cheetah.run(["dot", "-T#{type}"], stdin: dot, stdout: :capture)
|
36
|
+
|
37
|
+
content_type type
|
38
|
+
graph
|
39
|
+
end
|
data/bin/required-files
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# required-files ~/mygem/lib mygem
|
3
|
+
# lists all files from mygem loaded when we "require mygem",
|
4
|
+
# in the correct order
|
5
|
+
INCLUDE=$1
|
6
|
+
REQUIRE=$2
|
7
|
+
strace -eopen ruby -I $INCLUDE -r $REQUIRE -e 1 |& \
|
8
|
+
grep -v ENOENT | \
|
9
|
+
grep $INCLUDE | \
|
10
|
+
uniq | \
|
11
|
+
sed -e 's/[^"]*"\([^"]*\)".*/\1/'
|
@@ -0,0 +1,91 @@
|
|
1
|
+
|
2
|
+
require "parser/current"
|
3
|
+
require "pp"
|
4
|
+
|
5
|
+
require_relative "dot"
|
6
|
+
|
7
|
+
# filename_rb [String] file with a ruby program
|
8
|
+
# @return a dot graph string
|
9
|
+
def call_graph(filename_rb)
|
10
|
+
ruby = File.read(filename_rb)
|
11
|
+
ast = Parser::CurrentRuby.parse(ruby, filename_rb)
|
12
|
+
defs = defs_from_ast(ast)
|
13
|
+
def_names = defs.map {|d| def_name(d) }
|
14
|
+
defs_to_hrefs = defs.map {|d| [def_name(d), "/files/" + def_location(d)] }.to_h
|
15
|
+
|
16
|
+
defs_to_calls = {}
|
17
|
+
defs.each do |d|
|
18
|
+
calls = calls_from_def(d)
|
19
|
+
call_names = calls.map {|c| send_name(c)}
|
20
|
+
call_names = call_names.find_all{ |cn| def_names.include?(cn) }
|
21
|
+
defs_to_calls[def_name(d)] = call_names
|
22
|
+
end
|
23
|
+
|
24
|
+
dot_from_hash(defs_to_calls, defs_to_hrefs)
|
25
|
+
end
|
26
|
+
|
27
|
+
def def_name(node)
|
28
|
+
case node.type
|
29
|
+
when :def
|
30
|
+
name, _args, _body = *node
|
31
|
+
when :defs
|
32
|
+
_obj, name, _args, _body = *node
|
33
|
+
else
|
34
|
+
raise
|
35
|
+
end
|
36
|
+
name
|
37
|
+
end
|
38
|
+
|
39
|
+
def def_location(node)
|
40
|
+
range = node.loc.expression
|
41
|
+
file = range.source_buffer.name
|
42
|
+
line = range.line
|
43
|
+
"#{file}#line=#{line}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def send_name(node)
|
47
|
+
_receiver, name, *_args = *node
|
48
|
+
name
|
49
|
+
end
|
50
|
+
|
51
|
+
class Defs < Parser::AST::Processor
|
52
|
+
def initialize
|
53
|
+
@defs = []
|
54
|
+
@sends = []
|
55
|
+
end
|
56
|
+
|
57
|
+
def defs_from_ast(ast)
|
58
|
+
@defs = []
|
59
|
+
process(ast)
|
60
|
+
@defs
|
61
|
+
end
|
62
|
+
|
63
|
+
def sends_from_ast(ast)
|
64
|
+
@sends = []
|
65
|
+
process(ast)
|
66
|
+
@sends
|
67
|
+
end
|
68
|
+
|
69
|
+
def on_def(node)
|
70
|
+
@defs << node
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
74
|
+
def on_defs(node)
|
75
|
+
@defs << node
|
76
|
+
super
|
77
|
+
end
|
78
|
+
|
79
|
+
def on_send(node)
|
80
|
+
@sends << node
|
81
|
+
super
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def defs_from_ast(ast)
|
86
|
+
Defs.new.defs_from_ast(ast)
|
87
|
+
end
|
88
|
+
|
89
|
+
def calls_from_def(ast)
|
90
|
+
Defs.new.sends_from_ast(ast)
|
91
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
# @param graph [Hash{String => Array<String>}] vertex -> reachable vertices
|
3
|
+
# @param hrefs [Hash{String => String}] vertex -> href
|
4
|
+
def dot_from_hash(graph, hrefs = {})
|
5
|
+
dot = ""
|
6
|
+
dot << "digraph g {\n"
|
7
|
+
dot << "rankdir=LR;\n"
|
8
|
+
graph.keys.sort.each do |vertex|
|
9
|
+
href = hrefs[vertex]
|
10
|
+
href = "href=\"#{href}\" " if href
|
11
|
+
|
12
|
+
dot << "\"#{vertex}\"[#{href}];\n"
|
13
|
+
destinations = graph[vertex].sort
|
14
|
+
destinations.each do |d|
|
15
|
+
dot << "\"#{vertex}\" -> \"#{d}\";\n"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
dot << "}\n"
|
19
|
+
dot
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
# relies on escape_html
|
3
|
+
|
4
|
+
# convert plain text to HTML where lines are hyperlinkable (emulate RFC 5147)
|
5
|
+
def numbered_lines(text)
|
6
|
+
lines = text.lines
|
7
|
+
count_width = lines.count.to_s.size
|
8
|
+
lines.each_with_index.map do |line, i|
|
9
|
+
i += 1 # lines are counted from 1
|
10
|
+
|
11
|
+
show_line_num = i.to_s.rjust(count_width).gsub(" ", " ")
|
12
|
+
escaped_line = escape_html(line.chomp).gsub(" ", " ")
|
13
|
+
id = "line=#{i}" # RFC 5147 fragment identifier
|
14
|
+
|
15
|
+
"<tt><a id='#{id}' href='##{id}'>#{show_line_num}</a></tt> " \
|
16
|
+
"<code>#{escaped_line}</code><br>\n"
|
17
|
+
end.join("")
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: code-explorer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Martin Vidner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-06-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: parser
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sinatra
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: cheetah
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Find your way around source code written in Ruby
|
56
|
+
email: martin@vidner.net
|
57
|
+
executables:
|
58
|
+
- call-graph
|
59
|
+
- class-dependencies
|
60
|
+
- code-explorer
|
61
|
+
- required-files
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- README.md
|
66
|
+
- VERSION
|
67
|
+
- bin/call-graph
|
68
|
+
- bin/class-dependencies
|
69
|
+
- bin/code-explorer
|
70
|
+
- bin/required-files
|
71
|
+
- lib/code_explorer/call_graph.rb
|
72
|
+
- lib/code_explorer/dot.rb
|
73
|
+
- lib/code_explorer/numbered_lines.rb
|
74
|
+
- lib/code_explorer/version.rb
|
75
|
+
homepage: https://github.com/mvidner/code-explorer
|
76
|
+
licenses:
|
77
|
+
- MIT
|
78
|
+
metadata: {}
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 2.2.2
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: Explore Ruby code
|
99
|
+
test_files: []
|
100
|
+
has_rdoc:
|