code-explorer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
+ ![Packages.png, an example output](example.png)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -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
@@ -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
@@ -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(" ", "&nbsp;")
12
+ escaped_line = escape_html(line.chomp).gsub(" ", "&nbsp;")
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
@@ -0,0 +1,3 @@
1
+ module CodeExplorer
2
+ VERSION = File.read(File.dirname(__FILE__) + "/../../VERSION").strip
3
+ 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: