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.
@@ -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: