code_node 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/cog/templates/code_node/generator.rb.erb +42 -3
- data/cog/templates/code_node/graph.dot.erb +0 -2
- data/cog/templates/code_node/node.dot.erb +1 -1
- data/lib/code_node/dsl/graph_definer.rb +60 -13
- data/lib/code_node/ir/graph.rb +164 -97
- data/lib/code_node/ir/node.rb +210 -122
- data/lib/code_node/ir/node_matcher.rb +26 -0
- data/lib/code_node/sexp_walker.rb +6 -2
- data/lib/code_node/version.rb +1 -1
- data/lib/code_node.rb +16 -3
- metadata +4 -3
@@ -2,11 +2,50 @@
|
|
2
2
|
<% else %>require '<%= tool.name %>'
|
3
3
|
<% end %>
|
4
4
|
|
5
|
-
# Makes a graph of the project_source_path
|
5
|
+
# Makes a graph of the project_source_path.
|
6
|
+
# Nodes in the graph are clases and modules from your project.
|
6
7
|
CodeNode.graph '<%= @name %>' do |g|
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
# Nodes
|
10
|
+
# =====
|
11
|
+
|
12
|
+
# Ignore nodes with no relations to other nodes
|
10
13
|
g.ignore &:island?
|
14
|
+
|
15
|
+
# Uncomment and change the value to ignore a specific node
|
16
|
+
# g.ignore 'Foo::Bar::Car'
|
17
|
+
|
18
|
+
# Uncomment to ignore nodes named ClassMethods
|
19
|
+
# g.ignore /::ClassMethods$/
|
20
|
+
|
21
|
+
# Uncomment and change the value to ignore all nodes descending
|
22
|
+
# from a specific node
|
23
|
+
# g.ignore {|node| node.inherits_from? 'Foo::Bar::Car'}
|
24
|
+
|
25
|
+
# Apply styles common to all classes
|
26
|
+
g.style :shape => 'ellipse', :fillcolor => '#cccccc', :fontcolor => :black do |node|
|
27
|
+
node.class?
|
28
|
+
end
|
29
|
+
|
30
|
+
# Apply styles common to all nodes
|
31
|
+
g.style :shape => 'box', :fillcolor => '#333333', :fontcolor => :white do |node|
|
32
|
+
node.module?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Apply more specific style here
|
36
|
+
# ...
|
11
37
|
|
38
|
+
# Edges
|
39
|
+
# =====
|
40
|
+
#
|
41
|
+
# There is currently no way to style edges from the DSL.
|
42
|
+
# For now, there are only four categories of edges:
|
43
|
+
# - containment
|
44
|
+
# - inheritance
|
45
|
+
# - inclusion
|
46
|
+
# - extension
|
47
|
+
#
|
48
|
+
# They are style in the template. You can override the template
|
49
|
+
# and change the color if you like.
|
50
|
+
# $ cog -t code_node template new -f code_node/graph.dot
|
12
51
|
end
|
@@ -4,13 +4,11 @@ graph [rankdir="LR"]
|
|
4
4
|
node [style="filled"]
|
5
5
|
|
6
6
|
/* Module nodes */
|
7
|
-
node [shape="box" fillcolor="#333333" fontcolor="#ffffff"];
|
8
7
|
<% @graph.each_module do |node| %>
|
9
8
|
<%= node.stamp 'code_node/node.dot' %>
|
10
9
|
<% end %>
|
11
10
|
|
12
11
|
/* Class nodes */
|
13
|
-
node [shape="ellipse" fillcolor="#cccccc" fontcolor="#000000"];
|
14
12
|
<% @graph.each_class do |node| %>
|
15
13
|
<%= node.stamp 'code_node/node.dot' %>
|
16
14
|
<% end %>
|
@@ -1 +1 @@
|
|
1
|
-
<%= key %> [label="<%= label %>"];
|
1
|
+
<%= key %> [label="<%= label %>" <%= stamp_styles %>];
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'code_node/ir/node_matcher'
|
2
|
+
|
1
3
|
module CodeNode
|
2
4
|
module DSL
|
3
5
|
|
@@ -10,28 +12,73 @@ module CodeNode
|
|
10
12
|
@graph = graph
|
11
13
|
end
|
12
14
|
|
13
|
-
#
|
14
|
-
# Nodes matching the ignore rule will not be included in the graph.
|
15
|
-
# Ignore rules can be given in one of three ways.
|
15
|
+
# Specify an ignore rule for the graph.
|
16
|
+
# Nodes matching the ignore rule will not be included in the graph. Ignore rules can be given in one of three ways,
|
16
17
|
#
|
17
|
-
# * +String+ provide a fully qualified path
|
18
|
-
# * +Regexp+ provide a pattern that will be tested against the {IR::Node#path}
|
18
|
+
# * +String+ provide a fully qualified path which will have to match {IR::Node::QueryMethods#path} exactly
|
19
|
+
# * +Regexp+ provide a pattern that will be tested against the {IR::Node::QueryMethods#path}
|
19
20
|
# * +Proc+ provide a block. If the block returns +true+ the node will be ignored
|
20
21
|
#
|
21
|
-
# @
|
22
|
-
#
|
22
|
+
# @example
|
23
|
+
# CodeNode.graph 'my_graph' do |g|
|
24
|
+
#
|
25
|
+
# # Using a full path
|
26
|
+
# g.ignore 'Foo::Bar::Car'
|
27
|
+
#
|
28
|
+
# # Using a regular expression
|
29
|
+
# g.ignore /ClassMethods$/
|
30
|
+
#
|
31
|
+
# # Using a block
|
32
|
+
# g.ignore do |node|
|
33
|
+
# node.inherits_from? 'Exception'
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @param name [String, Regexp, nil] fully qualified path or regular expression which will be compared to {IR::Node::QueryMethods#path}
|
38
|
+
# @yieldparam node [IR::Node::QueryMethods] if provided, return +true+ to ignore the node
|
23
39
|
# @return [nil]
|
24
40
|
def ignore(name=nil, &block)
|
25
41
|
if (name.nil? && block.nil?) || (name && block)
|
26
42
|
raise ArgumentError.new('Provide either a name or a block')
|
27
43
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
44
|
+
matcher = IR::NodeMatcher.new name || block
|
45
|
+
@graph.instance_eval {@exclude_matchers << matcher}
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# Specify a rule for styling nodes.
|
50
|
+
# Nodes matching the given rule will have the provided style attributes applied. Rules can be given in one of three ways,
|
51
|
+
#
|
52
|
+
# * +String+ provide a fully qualified path which will have to match {IR::Node::QueryMethods#path} exactly
|
53
|
+
# * +Regexp+ provide a pattern that will be tested against the {IR::Node::QueryMethods#path}
|
54
|
+
# * +Proc+ provide a block
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# CodeNode.graph 'my_graph' do |g|
|
58
|
+
#
|
59
|
+
# # Using a full path
|
60
|
+
# g.style 'Foo::Bar::Car', :shape => 'box'
|
61
|
+
#
|
62
|
+
# # Using a regular expression
|
63
|
+
# g.style /ClassMethods$/, :fillcolor => '#336699'
|
64
|
+
#
|
65
|
+
# # Using a block
|
66
|
+
# g.style :penwidth => 3 do |node|
|
67
|
+
# node.extends? 'ActiveSupport::Concern'
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# @param name [String, Regexp, nil] fully qualified path or regular expression which will be compared to {IR::Node::QueryMethods#path}
|
72
|
+
# @param style [Hash] a set of attributes and values to apply to matching nodes. For a full list of applicable to GraphViz nodes: http://www.graphviz.org/content/attrs
|
73
|
+
# @yieldparam node [IR::Node::QueryMethods] if provided, return +true+ to indicate a match
|
74
|
+
# @return [nil]
|
75
|
+
def style(name=nil, style={}, &block)
|
76
|
+
style, name = name, nil if name.is_a?(Hash)
|
77
|
+
if (name.nil? && block.nil?) || (name && block)
|
78
|
+
raise ArgumentError.new('Provide either a name or a block')
|
34
79
|
end
|
80
|
+
matcher = IR::NodeMatcher.new name || block
|
81
|
+
@graph.instance_eval {@style_matchers << [matcher, style]}
|
35
82
|
nil
|
36
83
|
end
|
37
84
|
end
|
data/lib/code_node/ir/graph.rb
CHANGED
@@ -3,129 +3,196 @@ require 'code_node/ir/node'
|
|
3
3
|
module CodeNode
|
4
4
|
module IR
|
5
5
|
|
6
|
+
# A collection of {Node}s
|
6
7
|
class Graph
|
7
8
|
|
8
|
-
#
|
9
|
-
|
9
|
+
# {Graph} methods which are useful in templates
|
10
|
+
module TemplateMethods
|
11
|
+
|
12
|
+
# Iterate through each {Node} with {Node::QueryMethods#class?} in the graph
|
13
|
+
# @yieldparam node [Node::TemplateMethods] a class node. Does not yield ignored nodes.
|
14
|
+
# @return [nil]
|
15
|
+
def each_class(&block)
|
16
|
+
@nodes.values.select do |node|
|
17
|
+
node.class?
|
18
|
+
end.sort.each &block
|
19
|
+
end
|
20
|
+
|
21
|
+
# Iterate through each {Node} with {Node::QueryMethods#module?} in the graph
|
22
|
+
# @yieldparam node [Node::TemplateMethods] a module node. Does not yield ignored nodes.
|
23
|
+
# @return [nil]
|
24
|
+
def each_module(&block)
|
25
|
+
@nodes.values.select do |node|
|
26
|
+
node.module?
|
27
|
+
end.sort.each &block
|
28
|
+
end
|
10
29
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
30
|
+
# Iterate through each containment relation in the graph
|
31
|
+
# @example
|
32
|
+
# # a -> b (A contains B)
|
33
|
+
# module A
|
34
|
+
# module B
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
# @yieldparam a [Node::TemplateMethods] the container node
|
38
|
+
# @yieldparam b [Node::TemplateMethods] the contained node
|
39
|
+
# @return [nil]
|
40
|
+
def each_containment(&block)
|
41
|
+
@nodes.values.sort.each do |node|
|
42
|
+
if node.parent
|
43
|
+
block.call node.parent, node
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
19
47
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
48
|
+
# Iterate through each inheritance relation in the graph
|
49
|
+
# @example
|
50
|
+
# # a -> b (A inherits from B)
|
51
|
+
# class A < B
|
52
|
+
# end
|
53
|
+
# @yieldparam a [Node::TemplateMethods] the derived class node
|
54
|
+
# @yieldparam b [Node::TemplateMethods] the super class node
|
55
|
+
# @return [nil]
|
56
|
+
def each_inheritance(&block)
|
57
|
+
@nodes.values.sort.each do |node|
|
58
|
+
if node.super_class_node
|
59
|
+
block.call node, node.super_class_node
|
60
|
+
end
|
30
61
|
end
|
31
|
-
x << s[1]
|
32
|
-
x.reverse
|
33
|
-
elsif s[0] == :self
|
34
|
-
@scope.last.mark_as_singleton
|
35
|
-
nil
|
36
62
|
end
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
63
|
+
|
64
|
+
# Iterate through each inclusion relation in the graph
|
65
|
+
# @example
|
66
|
+
# # a -> b (A includes B)
|
67
|
+
# module A
|
68
|
+
# include B
|
69
|
+
# end
|
70
|
+
# @yieldparam a [Node::TemplateMethods] the which includes
|
71
|
+
# @yieldparam b [Node::TemplateMethods] the included node
|
72
|
+
# @return [nil]
|
73
|
+
def each_inclusion(&block)
|
74
|
+
@nodes.values.sort.each do |node|
|
75
|
+
node.inclusions.each do |other|
|
76
|
+
block.call node, other
|
77
|
+
end
|
44
78
|
end
|
45
|
-
else
|
46
|
-
Node.new name, :parent => @scope.last, :node_type => node_type
|
47
79
|
end
|
48
80
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
81
|
+
# Iterate through each extension relation in the graph
|
82
|
+
# @example
|
83
|
+
# # a -> b (A extends B)
|
84
|
+
# module A
|
85
|
+
# extend B
|
86
|
+
# end
|
87
|
+
# @yieldparam a [Node::TemplateMethods] the which extends
|
88
|
+
# @yieldparam b [Node::TemplateMethods] the extended node
|
89
|
+
# @return [nil]
|
90
|
+
def each_extension(&block)
|
91
|
+
@nodes.values.sort.each do |node|
|
92
|
+
node.extensions.each do |other|
|
93
|
+
block.call node, other
|
94
|
+
end
|
95
|
+
end
|
54
96
|
end
|
55
|
-
|
56
|
-
end
|
57
|
-
|
58
|
-
def <<(node)
|
59
|
-
@nodes[node.path] ||= node
|
60
|
-
@nodes[node.path]
|
97
|
+
|
61
98
|
end
|
62
99
|
|
63
|
-
#
|
64
|
-
# @
|
65
|
-
|
66
|
-
def each_class(&block)
|
67
|
-
@nodes.values.select do |node|
|
68
|
-
node.class? && !should_exclude?(node)
|
69
|
-
end.sort.each &block
|
70
|
-
end
|
100
|
+
# {Graph} methods used during the graph building phase
|
101
|
+
# @api developer
|
102
|
+
module BuilderMethods
|
71
103
|
|
72
|
-
|
73
|
-
# @yield [Node] a module node. Does not yield ignored nodes.
|
74
|
-
# @return [nil]
|
75
|
-
def each_module(&block)
|
76
|
-
@nodes.values.select do |node|
|
77
|
-
node.module? && !should_exclude?(node)
|
78
|
-
end.sort.each &block
|
79
|
-
end
|
104
|
+
attr_reader :scope
|
80
105
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
106
|
+
def apply_styles
|
107
|
+
@nodes.each_value do |node|
|
108
|
+
@style_matchers.each do |pair|
|
109
|
+
if pair[0].matches? node
|
110
|
+
node.style.update pair[1]
|
111
|
+
end
|
112
|
+
end
|
86
113
|
end
|
87
114
|
end
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
115
|
+
|
116
|
+
# @return [FixNum] were any more nodes pruned?
|
117
|
+
def prune
|
118
|
+
prunees = []
|
119
|
+
@nodes.each_value do |node|
|
120
|
+
if @exclude_matchers.any? {|m| m.matches? node}
|
121
|
+
prunees << node
|
122
|
+
end
|
123
|
+
end
|
124
|
+
prunees.each do |node|
|
125
|
+
puts " #{node.path}"
|
126
|
+
node.prune
|
127
|
+
@nodes.delete node.path
|
95
128
|
end
|
129
|
+
prunees.length
|
96
130
|
end
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
@
|
102
|
-
|
103
|
-
|
104
|
-
|
131
|
+
|
132
|
+
# Find a node or create it and add it to the graph
|
133
|
+
# @api developer
|
134
|
+
# @param node_type [Symbol] either <tt>:module</tt> or <tt>:class</tt>
|
135
|
+
# @param s [Symbol, Sexp] either flat name, or a Sexp representing a color (<tt>:</tt>) separated path.
|
136
|
+
# @yieldparam node [Node]
|
137
|
+
# @return [Node]
|
138
|
+
def node_for(node_type, s, opt={}, &block)
|
139
|
+
name = if s.is_a? Symbol
|
140
|
+
s
|
141
|
+
elsif s[0] == :const
|
142
|
+
s[1]
|
143
|
+
elsif s[0] == :colon2
|
144
|
+
x = []
|
145
|
+
while s[0] == :colon2
|
146
|
+
x << s[2] ; s = s[1]
|
105
147
|
end
|
148
|
+
x << s[1]
|
149
|
+
x.reverse
|
150
|
+
elsif s[0] == :self
|
151
|
+
@scope.last.mark_as_singleton
|
152
|
+
nil
|
106
153
|
end
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
if !should_exclude?(node) && !should_exclude?(other)
|
115
|
-
block.call node, other
|
154
|
+
return if name.nil?
|
155
|
+
|
156
|
+
node = if opt[:not_sure_if_nested]
|
157
|
+
if @scope.length > 1 && @scope[-2].find(name)
|
158
|
+
@scope[-2].find name
|
159
|
+
else
|
160
|
+
Node.new name, :node_type => node_type
|
116
161
|
end
|
162
|
+
else
|
163
|
+
Node.new name, :parent => @scope.last, :node_type => node_type
|
117
164
|
end
|
165
|
+
|
166
|
+
node = self << node
|
167
|
+
unless block.nil? || node.nil?
|
168
|
+
@scope << node
|
169
|
+
block.call node
|
170
|
+
@scope.pop
|
171
|
+
end
|
172
|
+
node
|
173
|
+
end
|
174
|
+
|
175
|
+
# Add the given node to the graph and return it. If a node with the same path is already in the graph, do not add it again, and return the original node.
|
176
|
+
# @param node [Node] a node to add to the graph
|
177
|
+
# @return [Node] the newly added node, or another node with the same path which was already in the graph
|
178
|
+
def <<(node)
|
179
|
+
@nodes[node.path] ||= node
|
180
|
+
@nodes[node.path]
|
118
181
|
end
|
182
|
+
|
119
183
|
end
|
120
|
-
|
121
|
-
private
|
122
184
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
185
|
+
include BuilderMethods
|
186
|
+
include TemplateMethods
|
187
|
+
|
188
|
+
# @api developer
|
189
|
+
def initialize
|
190
|
+
@exclude_matchers = []
|
191
|
+
@style_matchers = []
|
192
|
+
@nodes = {}
|
193
|
+
@scope = []
|
127
194
|
end
|
195
|
+
|
128
196
|
end
|
129
|
-
|
130
197
|
end
|
131
198
|
end
|
data/lib/code_node/ir/node.rb
CHANGED
@@ -9,44 +9,230 @@ module CodeNode
|
|
9
9
|
|
10
10
|
include Cog::Generator
|
11
11
|
|
12
|
-
#
|
13
|
-
|
14
|
-
|
12
|
+
# Node methods which are useful in templates
|
13
|
+
module TemplateMethods
|
14
|
+
|
15
|
+
# @return [String] the name of the node. Not necessarilly unique.
|
16
|
+
# @see {#path}
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
# @return [Node] node which contains this node
|
20
|
+
# @example
|
21
|
+
# module Foo # Foo is the parent
|
22
|
+
# module Bar # to Bar
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
attr_reader :parent
|
26
|
+
|
27
|
+
# The child nodes of this node
|
28
|
+
# @return [Hash<String,Node>] a mapping from node {#path} names to nodes
|
29
|
+
# @example
|
30
|
+
# module Foo # Foo has children
|
31
|
+
# module Bar # Bar
|
32
|
+
# end # and
|
33
|
+
# class Car # Car
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
attr_reader :children
|
37
|
+
|
38
|
+
# @return [Node,nil] the super class of this class. Will be +nil+ for modules.
|
39
|
+
def super_class_node
|
40
|
+
@inherits_from
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Array<Node>] module nodes for which this node has an +include+ statement
|
44
|
+
def inclusions
|
45
|
+
@includes.values.sort
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Array<Node>] module nodes for which this node has an +extend+ statement
|
49
|
+
def extensions
|
50
|
+
@extends.values.sort
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [String] fully qualified identifier for the node in the form <tt>Foo_Bar_Car</tt>. Ideal for graphviz identifiers.
|
54
|
+
def key
|
55
|
+
@path.join '_'
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [String] how the node will be labelled in the graph. Nodes without parents display their full {#path}, while nodes with parents only display their {#name}.
|
59
|
+
def label
|
60
|
+
@parent.nil? ? path : name
|
61
|
+
end
|
62
|
+
|
63
|
+
# Stamp the accumulated GraphViz styles in a format suitable for inclusion in a <tt>.dot</tt> file
|
64
|
+
# @return [String] style in the form <tt>key1="value1" key2="value2"</tt>...
|
65
|
+
def stamp_styles
|
66
|
+
x = []
|
67
|
+
style.each_pair do |key, value|
|
68
|
+
x << "#{key}=\"#{value}\""
|
69
|
+
end
|
70
|
+
x.join ' '
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
# {Node} methods which are useful for querying in matchers
|
76
|
+
module QueryMethods
|
15
77
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
# end
|
21
|
-
# end
|
22
|
-
attr_reader :parent
|
78
|
+
# @return [Boolean] does this node represent a module?
|
79
|
+
def module?
|
80
|
+
@node_type == :module
|
81
|
+
end
|
23
82
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
83
|
+
# @return [Boolean] does this node represent a class?
|
84
|
+
def class?
|
85
|
+
@node_type == :class
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [String] fully qualified name of the node in the form <tt>'Foo::Bar::Car'</tt>. Not good as a graphviz identifier because of the colon (<tt>:</tt>) characters. Use {TemplateMethods#key} for graphviz identifiers instead.
|
89
|
+
def path
|
90
|
+
@path.join '::'
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param path [String] a class or module path in the form <tt>Foo::Bar::Car</tt>
|
94
|
+
# @return [Boolean] does this node include a {#module?} node with the given {#path}?
|
95
|
+
def includes?(path)
|
96
|
+
@includes.member? path
|
97
|
+
end
|
34
98
|
|
99
|
+
# @param path [String] a class or module path in the form <tt>Foo::Bar::Car</tt>
|
100
|
+
# @return [Boolean] does this node extend a {#module?} node with the given {#path}?
|
101
|
+
def extends?(path)
|
102
|
+
@extends.member? path
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param path [String] a class or module path in the form <tt>Foo::Bar::Car</tt>
|
106
|
+
# @return [Boolean] does this node inherit from (directly or indirectly) a {#class?} node with the given {#path}? Note that a node inherits from itself according to this method. Recursively checks the ancestry of the node.
|
107
|
+
def inherits_from?(path)
|
108
|
+
self.path == path || @inherits_from && (@inherits_from.path == path || @inherits_from.inherits_from?(path))
|
109
|
+
end
|
110
|
+
|
111
|
+
# @return [Boolean] whether or not this node represents a singleton module. A singleton module is one which contains an <tt>extend self</tt> statement.
|
112
|
+
def singleton?
|
113
|
+
@singleton
|
114
|
+
end
|
115
|
+
|
116
|
+
# @return [Boolean] whether or not this node is an island. An island is a node with no connections to other nodes.
|
117
|
+
def island?
|
118
|
+
([@parent, @inherits_from].all?(&:nil?) &&
|
119
|
+
[@children, @inherited_by, @extends, @includes, @extended_by, @included_by].all?(&:empty?))
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
# {Node} methods used during the graph building phase
|
125
|
+
# @api developer
|
126
|
+
module BuilderMethods
|
127
|
+
|
128
|
+
attr_reader :style
|
129
|
+
|
130
|
+
# Find a node contained in this node, or contained in this nodes {#parent}, recursively.
|
131
|
+
# @return [Node, nil]
|
132
|
+
def find(name)
|
133
|
+
path = (@path + [name].flatten).join '::'
|
134
|
+
@children[path] || (@parent && @parent.find(name))
|
135
|
+
end
|
136
|
+
|
137
|
+
# Add other as a child of this node
|
138
|
+
# @param other [Node] another node
|
139
|
+
# @return [nil]
|
140
|
+
def contains(other)
|
141
|
+
this = self
|
142
|
+
@children[other.path] = other
|
143
|
+
other.instance_eval {@parent = this}
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
|
147
|
+
# Add other as the super class of this node
|
148
|
+
# @param other [Node] another node
|
149
|
+
# @return [nil]
|
150
|
+
def inherits_from(other)
|
151
|
+
this = self
|
152
|
+
@inherits_from = other
|
153
|
+
other.instance_eval {@inherited_by[this.path] = this}
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
|
157
|
+
# Add other to this nodes includes set
|
158
|
+
# @param other [Node] another node
|
159
|
+
# @return [nil]
|
160
|
+
def includes(other)
|
161
|
+
this = self
|
162
|
+
@includes[other.path] = other
|
163
|
+
other.instance_eval {@included_by[this.path] = this}
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
167
|
+
# Add other to this nodes extends set
|
168
|
+
# @param other [Node] another node
|
169
|
+
# @return [nil]
|
170
|
+
def extends(other)
|
171
|
+
this = self
|
172
|
+
@extends[other.path] = other
|
173
|
+
other.instance_eval {@extended_by[this.path] = this}
|
174
|
+
nil
|
175
|
+
end
|
176
|
+
|
177
|
+
# Mark this module node as a singleton
|
178
|
+
# @return [nil]
|
179
|
+
def mark_as_singleton
|
180
|
+
throw :NodeNotAModule unless module?
|
181
|
+
@singleton = true
|
182
|
+
end
|
183
|
+
|
184
|
+
# Remove any relations involving this node
|
185
|
+
def prune
|
186
|
+
this = self
|
187
|
+
if @inherits_from
|
188
|
+
@inherits_from.instance_eval {@inherited_by.delete this.path}
|
189
|
+
end
|
190
|
+
@inherited_by.each_value do |other|
|
191
|
+
other.instance_eval {@inherits_from = nil}
|
192
|
+
end
|
193
|
+
if @parent
|
194
|
+
@parent.instance_eval {@children.delete this.path}
|
195
|
+
end
|
196
|
+
@children.each_value do |other|
|
197
|
+
other.instance_eval {@parent = nil}
|
198
|
+
end
|
199
|
+
@includes.each_value do |other|
|
200
|
+
other.instance_eval {@included_by.delete this.path}
|
201
|
+
end
|
202
|
+
@included_by.each_value do |other|
|
203
|
+
other.instance_eval {@includes.delete this.path}
|
204
|
+
end
|
205
|
+
@extends.each_value do |other|
|
206
|
+
other.instance_eval {@extended_by.delete this.path}
|
207
|
+
end
|
208
|
+
@extended_by.each_value do |other|
|
209
|
+
other.instance_eval {@extends.delete this.path}
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
include BuilderMethods
|
215
|
+
include TemplateMethods
|
216
|
+
include QueryMethods
|
217
|
+
|
218
|
+
# Initialize a node
|
219
|
+
# @api developer
|
35
220
|
# @param name [String, Array]
|
36
|
-
# @option opt [Node] :parent (nil)
|
221
|
+
# @option opt [Node] :parent (nil) if provided, the parent's path is prepended to name. The parent child is not made at this time though. See {BuilderMethods#contains} instead.
|
37
222
|
# @option opt [Symbol] :node_type (:module) either <tt>:module</tt> or <tt>:class</tt>
|
38
223
|
def initialize(name, opt={})
|
224
|
+
@style = {}
|
39
225
|
@node_type = opt[:node_type] || :module
|
40
|
-
|
41
|
-
parent_path = @parent ? @parent.instance_eval {@path} : []
|
226
|
+
parent_path = opt[:parent] ? opt[:parent].instance_eval {@path} : []
|
42
227
|
@path = if name.is_a? Array
|
43
228
|
parent_path + name
|
44
229
|
else
|
45
230
|
parent_path + [name]
|
46
231
|
end
|
47
232
|
@name = @path.last
|
48
|
-
@parent
|
233
|
+
@parent = nil
|
49
234
|
@children = {}
|
235
|
+
@inherits_from = nil
|
50
236
|
@inherited_by = {}
|
51
237
|
@includes = {}
|
52
238
|
@included_by = {}
|
@@ -54,109 +240,11 @@ module CodeNode
|
|
54
240
|
@extended_by = {}
|
55
241
|
end
|
56
242
|
|
57
|
-
def find(name)
|
58
|
-
path = (@path + [name].flatten).join '::'
|
59
|
-
@children[path] || (@parent && @parent.find(name))
|
60
|
-
end
|
61
|
-
|
62
|
-
# @return [String] fully qualified identifier for the node in the form <tt>Foo_Bar_Car</tt>. Ideal for graphviz identifiers.
|
63
|
-
def key
|
64
|
-
@path.join '_'
|
65
|
-
end
|
66
|
-
|
67
|
-
# @return [String] fully qualified name of the node in the form <tt>'Foo::Bar::Car'</tt>. Not good as a graphviz identifier because of the colon (+:+) characters. Use {#key} for graphviz identifiers instead.
|
68
|
-
def path
|
69
|
-
@path.join '::'
|
70
|
-
end
|
71
|
-
|
72
|
-
# @return [String] how the node will be labelled in the graph. Nodes without parents display their full {#path}, while nodes with parents only display their {#name}.
|
73
|
-
def label
|
74
|
-
@parent.nil? ? path : name
|
75
|
-
end
|
76
|
-
|
77
243
|
# @return [FixNum] order nodes by {#path}
|
78
244
|
def <=>(other)
|
79
245
|
path <=> other.path
|
80
246
|
end
|
81
247
|
|
82
|
-
# @return [Boolean] whether or not this node represents a module
|
83
|
-
def module?
|
84
|
-
@node_type == :module
|
85
|
-
end
|
86
|
-
|
87
|
-
# @return [Boolean] whether or not this node represents a class
|
88
|
-
def class?
|
89
|
-
@node_type == :class
|
90
|
-
end
|
91
|
-
|
92
|
-
# @param other [Node]
|
93
|
-
def inherits_from(other)
|
94
|
-
this = self
|
95
|
-
@inherits_from = other
|
96
|
-
other.instance_eval {@inherited_by[this.path] = this}
|
97
|
-
end
|
98
|
-
|
99
|
-
# @return [Node,nil] the super class of this class. Will be +nil+ for modules.
|
100
|
-
def super_class_node
|
101
|
-
@inherits_from
|
102
|
-
end
|
103
|
-
|
104
|
-
# @param other [Node]
|
105
|
-
def includes(other)
|
106
|
-
this = self
|
107
|
-
@includes[other.path] = other
|
108
|
-
other.instance_eval {@included_by[this.path] = this}
|
109
|
-
end
|
110
|
-
|
111
|
-
# @return [Array<Node>] module nodes for which this node has an +include+ statement
|
112
|
-
def inclusions
|
113
|
-
@includes.values.sort
|
114
|
-
end
|
115
|
-
|
116
|
-
# @param other [Node]
|
117
|
-
def extends(other)
|
118
|
-
this = self
|
119
|
-
@extends[other.path] = other
|
120
|
-
other.instance_eval {@extended_by[this.path] = this}
|
121
|
-
end
|
122
|
-
|
123
|
-
# @return [Array<Node>] module nodes for which this node has an +extend+ statement
|
124
|
-
def extensions
|
125
|
-
@extends.values.sort
|
126
|
-
end
|
127
|
-
|
128
|
-
# Set the given class node as the super class of this node
|
129
|
-
# @param super_node [Node] a {#class?} node
|
130
|
-
# @return [nil]
|
131
|
-
def inherits_from=(super_node)
|
132
|
-
throw :NodeNotAClass unless class?
|
133
|
-
throw :SuperNodeNotAClass unless super_node.class?
|
134
|
-
@inherits_from = super_node
|
135
|
-
end
|
136
|
-
|
137
|
-
# @return [Boolean] whether or not this node inherits from a given node. Note that a node does inherit from itself, according to this method. Recursively checks the ancestry of the node.
|
138
|
-
def inherits_from?(k)
|
139
|
-
key == k || @inherits_from && (@inherits_from.key == k || @inherits_from.inherits_from?(k))
|
140
|
-
end
|
141
|
-
|
142
|
-
# Mark this module node as a singleton
|
143
|
-
# @return [nil]
|
144
|
-
def mark_as_singleton
|
145
|
-
throw :NodeNotAModule unless module?
|
146
|
-
@singleton = true
|
147
|
-
end
|
148
|
-
|
149
|
-
# @return [Boolean] whether or not this node represents a singleton module
|
150
|
-
def singleton?
|
151
|
-
@singleton
|
152
|
-
end
|
153
|
-
|
154
|
-
# @return [Boolean] whether or not this node is an island. An island is a node with no connections to other nodes.
|
155
|
-
def island?
|
156
|
-
([@parent, @inherits_from].all?(&:nil?) &&
|
157
|
-
[@children, @inherited_by, @extends, @includes, @extended_by, @included_by].all?(&:empty?))
|
158
|
-
end
|
159
|
-
|
160
248
|
end
|
161
249
|
end
|
162
250
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module CodeNode
|
2
|
+
module IR
|
3
|
+
|
4
|
+
# @api developer
|
5
|
+
# Encapsulates a pattern match test for nodes
|
6
|
+
class NodeMatcher
|
7
|
+
|
8
|
+
def initialize(pattern)
|
9
|
+
@pattern = pattern
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param node [Node]
|
13
|
+
# @return [Boolean] does the node match?
|
14
|
+
def matches?(node)
|
15
|
+
if @pattern.is_a? Proc
|
16
|
+
@pattern.call node
|
17
|
+
elsif @pattern.is_a? Regexp
|
18
|
+
@pattern =~ node.path
|
19
|
+
else
|
20
|
+
@pattern.to_s == node.path
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
module CodeNode
|
2
2
|
|
3
|
+
# @api developer
|
3
4
|
# Walks a Sexp representing a ruby file looking for classes and modules.
|
4
5
|
class SexpWalker
|
5
6
|
|
@@ -7,7 +8,7 @@ module CodeNode
|
|
7
8
|
#
|
8
9
|
# All files in a code base should be walked once in <tt>:find_nodes</tt> mode, and then walked again in <tt>:find_relations</tt> mode.
|
9
10
|
#
|
10
|
-
# @param graph [Graph] a graph to which nodes and relations will be added
|
11
|
+
# @param graph [IR::Graph] a graph to which nodes and relations will be added
|
11
12
|
# @param sexp [Sexp] the root sexp of a ruby file
|
12
13
|
# @option opt [Symbol] :mode (:find_nodes) one of <tt>:find_nodes</tt> or <tt>:find_relations</tt>
|
13
14
|
def initialize(graph, sexp, opt={})
|
@@ -22,7 +23,7 @@ module CodeNode
|
|
22
23
|
def walk(s = nil)
|
23
24
|
s ||= @root
|
24
25
|
if [:module, :class].member?(s[0])
|
25
|
-
@graph.node_for(s[0], s[1]) do |node|
|
26
|
+
node = @graph.node_for(s[0], s[1]) do |node|
|
26
27
|
if find_relations? && s[0] == :class && !s[2].nil?
|
27
28
|
super_node = @graph.node_for :class, s[2], :not_sure_if_nested => true
|
28
29
|
node.inherits_from super_node unless super_node.nil?
|
@@ -32,6 +33,9 @@ module CodeNode
|
|
32
33
|
walk(c) if c.class == Sexp
|
33
34
|
end
|
34
35
|
end
|
36
|
+
if find_relations? && !@graph.scope.empty?
|
37
|
+
@graph.scope.last.contains node
|
38
|
+
end
|
35
39
|
elsif find_relations? && s[0] == :call && s.length >= 4 && [:extend, :include].member?(s[2]) && !@graph.scope.empty?
|
36
40
|
node = @graph.node_for :module, s[3], :not_sure_if_nested => true
|
37
41
|
unless node.nil?
|
data/lib/code_node/version.rb
CHANGED
data/lib/code_node.rb
CHANGED
@@ -15,6 +15,7 @@ module CodeNode
|
|
15
15
|
# @option opt [Symbol] :ruby_version (:ruby19) either <tt>:ruby18</tt> or <tt>:ruby19</tt>, indicating which parser to use
|
16
16
|
# @yield [GraphDefinition] define rules for creating the graph
|
17
17
|
def self.graph(graph_name, opt={}, &block)
|
18
|
+
feedback_color = :white
|
18
19
|
root = Cog::Config.instance.project_source_path
|
19
20
|
@graph = IR::Graph.new
|
20
21
|
graph_definer = DSL::GraphDefiner.new @graph
|
@@ -29,13 +30,12 @@ module CodeNode
|
|
29
30
|
|
30
31
|
sexp = []
|
31
32
|
[:find_nodes, :find_relations].each_with_index do |mode, pass|
|
32
|
-
puts "#{(pass+1).ordinalize} pass: #{mode.to_s.gsub('_', ' ')}".color(
|
33
|
-
|
33
|
+
puts "#{(pass+1).ordinalize} pass: #{mode.to_s.gsub('_', ' ')}".color(feedback_color)
|
34
34
|
Dir.glob("#{root}/**/*.rb").each_with_index do |filename, i|
|
35
35
|
sexp[i] ||= begin
|
36
36
|
rp.parse(File.read filename)
|
37
37
|
rescue Racc::ParseError
|
38
|
-
STDERR.write "{filename.relative_to_project_root}, skipped...\n".color(:
|
38
|
+
STDERR.write "#{filename.relative_to_project_root}, skipped...\n".color(:yellow)
|
39
39
|
nil
|
40
40
|
end
|
41
41
|
if sexp[i]
|
@@ -44,6 +44,19 @@ module CodeNode
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
47
|
+
|
48
|
+
# Apply styles before pruning because some relations may be destroyed while pruning
|
49
|
+
puts "Applying styles".color(feedback_color)
|
50
|
+
@graph.apply_styles
|
51
|
+
|
52
|
+
# Prune the graph according to ignore rules.
|
53
|
+
# We keep pruning until there are no more changes because some rules don't apply the first time (for example: &:island?)
|
54
|
+
puts "Pruning nodes".color(feedback_color)
|
55
|
+
i = 1
|
56
|
+
while (x = @graph.prune) > 0
|
57
|
+
puts " #{x} nodes pruned on #{i.ordinalize} pass".color(feedback_color)
|
58
|
+
i += 1
|
59
|
+
end
|
47
60
|
|
48
61
|
# Activate code_node while rendering templates
|
49
62
|
# so that cog will be able to find code_node templates
|
metadata
CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 0.0.2
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Kevin Tonon
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-11-
|
18
|
+
date: 2012-11-20 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: cog
|
@@ -119,6 +119,7 @@ files:
|
|
119
119
|
- lib/code_node/dsl.rb
|
120
120
|
- lib/code_node/ir/graph.rb
|
121
121
|
- lib/code_node/ir/node.rb
|
122
|
+
- lib/code_node/ir/node_matcher.rb
|
122
123
|
- lib/code_node/ir.rb
|
123
124
|
- lib/code_node/sexp_walker.rb
|
124
125
|
- lib/code_node/version.rb
|