society 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CODE_OF_CONDUCT.md +5 -5
- data/Gemfile +1 -1
- data/LICENSE.txt +19 -12
- data/lib/society.rb +5 -6
- data/lib/society/edge.rb +32 -6
- data/lib/society/formatter/report/html.rb +1 -0
- data/lib/society/formatter/report/templates/components/society-assets/society.js +16 -13
- data/lib/society/node.rb +86 -0
- data/lib/society/object_graph.rb +57 -5
- data/lib/society/parser.rb +454 -31
- data/lib/society/version.rb +1 -1
- data/society.gemspec +4 -5
- data/spec/cli_spec.rb +1 -1
- data/spec/object_graph_spec.rb +211 -17
- data/spec/parser_spec.rb +203 -12
- metadata +5 -37
- data/lib/society/association_processor.rb +0 -206
- data/lib/society/clusterer.rb +0 -188
- data/lib/society/formatter/graph/json.rb +0 -54
- data/lib/society/reference_processor.rb +0 -60
- data/society_graph.json +0 -1
- data/spec/association_processor_spec.rb +0 -174
- data/spec/clusterer_spec.rb +0 -37
- data/spec/fixtures/clustering/clusterer_fixtures.rb +0 -144
- data/spec/fixtures/clustering/edges_1.txt +0 -322
- data/spec/formatter/graph/json_spec.rb +0 -52
- data/spec/reference_processor_spec.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec8cb5a95664084f02de65ed939e89ec04417295
|
4
|
+
data.tar.gz: 462b53c5009c262f69fab7ddb6c3ba60ce38f227
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a84b67d0617f3074d37efe668bec9cb4184615dfa08954134adcfe32f433f546b3cdbe66c857b7a7bfc146a07fb30174f1bfba9216a94172f1e37b0455746c4a
|
7
|
+
data.tar.gz: 8d7d5eb2196abd41d6eeff66453131b1636cb906d34d2dc24469cae44eaadf0a2c5a4b03c7349eab64a5bb7e9292f3552b8f79ab2a61cdaf6bff548745e433c3
|
data/CODE_OF_CONDUCT.md
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
This project has adopted version 0.4 of the [Contributor Covenant](https://github.com/bantik/contributor_covenant/).
|
1
|
+
# Contributor Code of Conduct
|
4
2
|
|
5
3
|
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
6
4
|
|
7
|
-
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
8
|
|
9
9
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
10
|
|
11
11
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
12
|
|
13
|
-
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
@@ -1,15 +1,22 @@
|
|
1
|
-
Copyright (c) 2014 Coraline Ada Ehmke
|
1
|
+
Copyright (c) 2014 Coraline Ada Ehmke
|
2
2
|
|
3
|
-
|
4
|
-
it under the terms of the GNU General Public License as published by
|
5
|
-
the Free Software Foundation, either version 3 of the License, or
|
6
|
-
(at your option) any later version.
|
3
|
+
MIT License
|
7
4
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/society.rb
CHANGED
@@ -1,15 +1,14 @@
|
|
1
|
-
require "
|
1
|
+
require "haml"
|
2
|
+
require "json"
|
3
|
+
require "ripper"
|
2
4
|
require "fileutils"
|
3
5
|
require "active_support/core_ext/string/inflections"
|
4
6
|
|
5
|
-
require_relative "society/association_processor"
|
6
|
-
require_relative "society/reference_processor"
|
7
7
|
require_relative "society/edge"
|
8
|
-
require_relative "society/
|
9
|
-
require_relative "society/
|
8
|
+
require_relative "society/node"
|
9
|
+
require_relative "society/object_graph"
|
10
10
|
require_relative "society/formatter/report/html"
|
11
11
|
require_relative "society/formatter/report/json"
|
12
|
-
require_relative "society/object_graph"
|
13
12
|
require_relative "society/parser"
|
14
13
|
require_relative "society/version"
|
15
14
|
|
data/lib/society/edge.rb
CHANGED
@@ -1,14 +1,40 @@
|
|
1
1
|
module Society
|
2
|
+
|
3
|
+
# The Edge class represents an edge between two nodes in a graph. An edge is
|
4
|
+
# assumed to represent a direct relationship between two Classes or Modules.
|
2
5
|
class Edge
|
3
6
|
|
4
|
-
attr_reader :
|
5
|
-
|
7
|
+
attr_reader :to, :weight
|
8
|
+
|
9
|
+
# Public: Create a new Edge.
|
10
|
+
#
|
11
|
+
# to - Node to target.
|
12
|
+
# weight - Weight of the edge, representing the number of references to the
|
13
|
+
# node referenced. (Default: 1)
|
14
|
+
def initialize(to:, weight: 1)
|
15
|
+
@to = to
|
16
|
+
@weight = weight
|
17
|
+
end
|
18
|
+
|
19
|
+
# Public: Add two Edges' weights, returning a new Edge.
|
20
|
+
#
|
21
|
+
# edge - An Edge.
|
22
|
+
#
|
23
|
+
# Returns a new Edge if both edges target the same node.
|
24
|
+
# Returns nil otherwise.
|
25
|
+
def +(edge)
|
26
|
+
return nil unless edge.to == to
|
27
|
+
|
28
|
+
Edge.new(to: to, weight: weight + edge.weight)
|
29
|
+
end
|
6
30
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
31
|
+
# Public: Return the name of the node to which the edge points.
|
32
|
+
#
|
33
|
+
# Returns a string.
|
34
|
+
def to_s
|
35
|
+
to.to_s
|
11
36
|
end
|
37
|
+
alias_method :inspect, :to_s
|
12
38
|
|
13
39
|
end
|
14
40
|
|
@@ -24,14 +24,14 @@
|
|
24
24
|
};
|
25
25
|
|
26
26
|
NetworkGraph.prototype.transformData = function(data) {
|
27
|
-
|
28
|
-
return {
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
27
|
+
return(Object.keys(data).map(function(node) {
|
28
|
+
return {
|
29
|
+
name: node,
|
30
|
+
relations: Object.keys(data[node]).filter(function(edge) {
|
31
|
+
return(Object.keys(data).indexOf(edge) != -1);
|
32
|
+
})
|
33
|
+
};
|
34
|
+
}));
|
35
35
|
};
|
36
36
|
|
37
37
|
NetworkGraph.prototype.init = function() {
|
@@ -249,12 +249,15 @@
|
|
249
249
|
var findCluster = data.clusters ? _findCluster : function() { return 0; };
|
250
250
|
|
251
251
|
return {
|
252
|
-
nodes: data.
|
253
|
-
return { name: node
|
252
|
+
nodes: Object.keys(data).map(function(node, index) {
|
253
|
+
return { name: node, group: findCluster(index) };
|
254
254
|
}),
|
255
|
-
links: data.
|
256
|
-
|
257
|
-
|
255
|
+
links: Object.keys(data).reduce(function(edges, node, source) {
|
256
|
+
var new_edges = Object.keys(data[node]).map(function(edge, target) {
|
257
|
+
return { source: source, target: target, value: data[node][edge] };
|
258
|
+
});
|
259
|
+
return(edges.concat(new_edges));
|
260
|
+
}, [])
|
258
261
|
};
|
259
262
|
};
|
260
263
|
|
data/lib/society/node.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Society
|
2
|
+
|
3
|
+
# The Node class represents a single node in a graph. In this case, nodes
|
4
|
+
# are assumed to be either Classes or Modules.
|
5
|
+
class Node
|
6
|
+
|
7
|
+
attr_reader :name, :type, :edges, :unresolved, :meta
|
8
|
+
|
9
|
+
# Public: Creates a new node.
|
10
|
+
#
|
11
|
+
# name - Name to be assigned to the node. Assumed to be a Class or
|
12
|
+
# Module name.
|
13
|
+
# type - Type of node. Assumed to be :class or :module.
|
14
|
+
# edges - Edges which point to other nodes.
|
15
|
+
# unresolved - References to nodes which have not yet been resolved.
|
16
|
+
# (default: [])
|
17
|
+
# meta - Information to be tracked about the node itself.
|
18
|
+
# (default: [])
|
19
|
+
def initialize(name:, type:, edges: [], unresolved: [], meta: [])
|
20
|
+
@name = name
|
21
|
+
@type = type
|
22
|
+
@edges = edges
|
23
|
+
@unresolved = unresolved
|
24
|
+
@meta = meta
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Reports whether another node intersects.
|
28
|
+
# Nodes are considered to be intersecting if their name and type are equal.
|
29
|
+
#
|
30
|
+
# node - Another Node.
|
31
|
+
#
|
32
|
+
# Returns true or false.
|
33
|
+
def intersects?(node)
|
34
|
+
node.name == name && node.type == type
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Create a node representing the sum of the current node and
|
38
|
+
# another, intersecting node.
|
39
|
+
#
|
40
|
+
# node - Another node object.
|
41
|
+
#
|
42
|
+
# Returns self if self is passed.
|
43
|
+
# Returns nil if the nodes do not intersect.
|
44
|
+
# Returns a new node containing the sum of both nodes' edges otherwise.
|
45
|
+
def +(node)
|
46
|
+
return self if self == node
|
47
|
+
return nil unless self.intersects?(node)
|
48
|
+
|
49
|
+
new_edges = accumulate_edges(edges, node.edges)
|
50
|
+
new_unresolved = accumulate_edges(unresolved, node.unresolved)
|
51
|
+
new_meta = meta + node.meta
|
52
|
+
|
53
|
+
return Node.new(name: name, type: type, edges: new_edges,
|
54
|
+
unresolved: new_unresolved, meta: new_meta)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public: Return the name of the node.
|
58
|
+
#
|
59
|
+
# Returns a string.
|
60
|
+
def to_s
|
61
|
+
name.to_s
|
62
|
+
end
|
63
|
+
alias_method :inspect, :to_s
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Internal: Reduce two lists of edges to a single list with equivalent
|
68
|
+
# edges summed.
|
69
|
+
#
|
70
|
+
# edges_a - Array of Edges.
|
71
|
+
# edges_b - Array of Edges.
|
72
|
+
#
|
73
|
+
# Returns an array of Edges.
|
74
|
+
def accumulate_edges(edges_a, edges_b)
|
75
|
+
new_edges = (edges_a + edges_b).flatten.reduce([]) do |edges, edge|
|
76
|
+
if edges.detect { |e| e.to == edge.to }
|
77
|
+
edges.map { |e| e + edge || e }
|
78
|
+
else
|
79
|
+
edges + [edge]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
data/lib/society/object_graph.rb
CHANGED
@@ -1,12 +1,64 @@
|
|
1
1
|
module Society
|
2
2
|
|
3
|
-
class
|
3
|
+
# The ObjectGraph class represents a graph of interrelated nodes as an Array
|
4
|
+
# of nodes which can be iterated over.
|
5
|
+
class ObjectGraph < Array
|
4
6
|
|
5
|
-
|
7
|
+
# Public: Override Array#initialize, accepting any number of nodes and
|
8
|
+
# lists of nodes including other ObjectGraphs to create a single graph from
|
9
|
+
# them.
|
10
|
+
#
|
11
|
+
# nodes - Any number of nodes or lists of nodes.
|
12
|
+
def initialize(*nodes)
|
13
|
+
super(nodes.flatten)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Add two graphs together, returning a new ObjectGraph containing
|
17
|
+
# the sum of all nodes contained in both.
|
18
|
+
#
|
19
|
+
# other - Another graph.
|
20
|
+
#
|
21
|
+
# Returns an ObjectGraph.
|
22
|
+
def +(other)
|
23
|
+
other.reduce(self) do |graph, node|
|
24
|
+
if graph.select { |n| n.intersects?(node) }.any?
|
25
|
+
Society::ObjectGraph.new(graph.map { |n| n + node || n })
|
26
|
+
else
|
27
|
+
Society::ObjectGraph.new(graph, node)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Public: Create a new graph, adding another node.
|
33
|
+
#
|
34
|
+
# node - Node to be added to the new graph.
|
35
|
+
#
|
36
|
+
# Returns an ObjectGraph.
|
37
|
+
def <<(node)
|
38
|
+
self + Society::ObjectGraph.new(node)
|
39
|
+
end
|
40
|
+
alias_method :push, :<<
|
41
|
+
|
42
|
+
# Public: Return the graph represented as a Hash.
|
43
|
+
#
|
44
|
+
# Returns a hash.
|
45
|
+
def to_h
|
46
|
+
self.reduce({}) do |hash, node|
|
47
|
+
hash.merge({ node.name => node.edges })
|
48
|
+
end
|
49
|
+
end
|
6
50
|
|
7
|
-
|
8
|
-
|
9
|
-
|
51
|
+
# Public: Return the graph as a JSON string.
|
52
|
+
#
|
53
|
+
# Returns a string.
|
54
|
+
def to_json
|
55
|
+
to_h.reduce({}) do |hash, node|
|
56
|
+
name, edges_raw = node
|
57
|
+
edges = edges_raw.reduce({}) do |edges, edge|
|
58
|
+
edges.merge({ edge.to => edge.weight })
|
59
|
+
end
|
60
|
+
hash.merge({ name => edges })
|
61
|
+
end.to_json
|
10
62
|
end
|
11
63
|
|
12
64
|
end
|
data/lib/society/parser.rb
CHANGED
@@ -1,66 +1,489 @@
|
|
1
1
|
module Society
|
2
2
|
|
3
|
+
# The Parser class is responsible for producing an ObjectGraph from one or
|
4
|
+
# more ruby sources.
|
3
5
|
class Parser
|
4
6
|
|
7
|
+
# Public: Generate a list of files from a collection of paths, creating a
|
8
|
+
# new Parser with them.
|
9
|
+
# Note: Since the files are not read, a new Parser MAY be returned such
|
10
|
+
# that initiating processing will cause a crash later.
|
11
|
+
#
|
12
|
+
# file_paths - Any number of Strings representing paths to files.
|
13
|
+
#
|
14
|
+
# Returns a Parser.
|
5
15
|
def self.for_files(*file_paths)
|
6
|
-
|
16
|
+
files = file_paths.flatten.flat_map do |path|
|
17
|
+
File.directory?(path) ? Dir.glob(File.join(path, '**', '*.rb')) : path
|
18
|
+
end
|
19
|
+
new(files.lazy.map { |f| File.read(f) })
|
7
20
|
end
|
8
21
|
|
9
|
-
|
10
|
-
|
22
|
+
# Public: Create a Parser with a collection of ruby sources to be analyzed.
|
23
|
+
#
|
24
|
+
# source - Any number of Strings containing ruby source.
|
25
|
+
#
|
26
|
+
# Returns a Parser.
|
27
|
+
def self.for_source(*source)
|
28
|
+
new(source.lazy)
|
11
29
|
end
|
12
30
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
31
|
+
# Public: Create a Parser, staging ruby source files to be analyzed.
|
32
|
+
#
|
33
|
+
# source - An Enumerable containing ruby source strings.
|
34
|
+
def initialize(source)
|
35
|
+
@source = source.map { |file| graph_from(file) }
|
17
36
|
end
|
18
37
|
|
38
|
+
# Public: Generate a report from the object graph.
|
39
|
+
#
|
40
|
+
# format - A symbol representing any known output format.
|
41
|
+
# output_path - Path to which output should be written. (default: nil)
|
42
|
+
#
|
43
|
+
# Returns nothing.
|
19
44
|
def report(format, output_path=nil)
|
20
45
|
raise ArgumentError, "Unknown format #{format}" unless known_formats.include?(format)
|
21
|
-
options = { json_data:
|
46
|
+
options = { json_data: graph.to_json }
|
22
47
|
options[:output_path] = output_path unless output_path.nil?
|
23
48
|
FORMATTERS[format].new(options).write
|
24
49
|
end
|
25
50
|
|
51
|
+
# Public: Return the ObjectGraph representing the analyzed source. Calling
|
52
|
+
# this method will trigger the analysis of the source if the object was
|
53
|
+
# created with lazy enumerables.
|
54
|
+
#
|
55
|
+
# Returns an ObjectGraph.
|
56
|
+
def graph
|
57
|
+
@graph ||= resolve_known_edges(source.reduce(ObjectGraph.new, &:+))
|
58
|
+
end
|
59
|
+
|
60
|
+
# Public: Return a list of known classes from the object graph.
|
61
|
+
#
|
62
|
+
# Returns an Array of Strings.
|
63
|
+
def classes
|
64
|
+
graph.map(&:name)
|
65
|
+
end
|
66
|
+
|
26
67
|
private
|
27
68
|
|
28
|
-
|
29
|
-
html: Society::Formatter::Report::HTML,
|
30
|
-
json: Society::Formatter::Report::Json
|
31
|
-
}
|
69
|
+
attr_reader :source
|
32
70
|
|
33
|
-
|
34
|
-
|
71
|
+
# AST Node with the current namespace and type (module/class) preserved.
|
72
|
+
NSNode = Struct.new(:namespace, :type, :ast)
|
73
|
+
# ActiveRecord edge, containing the direct reference and any arguments.
|
74
|
+
AREdge = Struct.new(:reference, :args)
|
75
|
+
|
76
|
+
NAMESPACE_NODES = [:class, :module]
|
77
|
+
NAMESPACE_SEPARATOR = '::'
|
78
|
+
CONSTANT_NAME_NODES = [:const_ref, :const_path_ref, :@const]
|
79
|
+
ACTIVERECORD_NODES = %w(belongs_to has_one has_many
|
80
|
+
has_and_belongs_to_many)
|
81
|
+
|
82
|
+
# Internal: Generate an ObjectGraph from a string containing ruby source.
|
83
|
+
#
|
84
|
+
# source - String containing ruby source.
|
85
|
+
#
|
86
|
+
# Returns an ObjectGraph.
|
87
|
+
def graph_from(source)
|
88
|
+
ast = Ripper.sexp(source)
|
89
|
+
nodes_from(ast).reduce(Society::ObjectGraph.new, &:<<)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Internal: Generate a list of Nodes from a string containing ruby source.
|
93
|
+
# Note: All edges are considered unresolved at this stage.
|
94
|
+
#
|
95
|
+
# ast - Array containing an abstract syntax tree generated by Ripper.
|
96
|
+
#
|
97
|
+
# Returns an Array of Nodes.
|
98
|
+
def nodes_from(ast)
|
99
|
+
walk_ast(ast).map do |name, data|
|
100
|
+
init_node = Society::Node.new(name: name, type: data[:type])
|
101
|
+
find_edges(name, data[:ast]).reduce(init_node) do |node, new_edge|
|
102
|
+
edge = [Society::Edge.new(to: new_edge)]
|
103
|
+
type = data[:type]
|
104
|
+
Society::Node.new(name: name, type: type, unresolved: edge) + node
|
105
|
+
end
|
106
|
+
end
|
35
107
|
end
|
36
108
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
109
|
+
# Internal: Isolate individual namespaces, generating a hash containing
|
110
|
+
# Namespace => AST pairs.
|
111
|
+
#
|
112
|
+
# ast - Array containing an abstract syntax tree generated by Ripper.
|
113
|
+
#
|
114
|
+
# Returns a Hash mapping Namespace => AST.
|
115
|
+
def walk_ast(ast)
|
116
|
+
scoped_nodes = filter_namespace([], ast)
|
43
117
|
|
44
|
-
|
118
|
+
scoped_nodes.reduce({}) do |nodes, node|
|
119
|
+
namespace = node[:namespace] + [node_name(node[:namespace], node[:ast])]
|
120
|
+
filter_namespace(namespace, node[:ast]).each do |sub|
|
121
|
+
scoped_nodes.push(sub)
|
122
|
+
end
|
123
|
+
nodes.merge({ namespace.last => node })
|
45
124
|
end
|
46
125
|
end
|
47
126
|
|
48
|
-
|
49
|
-
|
127
|
+
# Internal: Generate a list of nodes representing a change of namespace
|
128
|
+
# (classes/modules) from an abstract syntax tree, preserving the namespace
|
129
|
+
# associated with them.
|
130
|
+
#
|
131
|
+
# namespace - Array containing the current namespace, to be preserved along
|
132
|
+
# with the AST.
|
133
|
+
# ast - AST to be searched for namespace separators. Note that due
|
134
|
+
# to this object being globbed, mutating this object will not
|
135
|
+
# mutate the state of the object passed to this method.
|
136
|
+
#
|
137
|
+
# Returns an Array of NSNodes.
|
138
|
+
def filter_namespace(namespace, *ast)
|
139
|
+
ast.reduce([]) do |nodes, node|
|
140
|
+
if node.is_a?(Array)
|
141
|
+
if NAMESPACE_NODES.include?(node.first)
|
142
|
+
nodes.push(NSNode.new(namespace, node.first, node[1..-1]))
|
143
|
+
else
|
144
|
+
node.each { |sub| ast.push(sub) }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
nodes
|
148
|
+
end
|
50
149
|
end
|
51
150
|
|
52
|
-
|
53
|
-
|
151
|
+
# Internal: Determine the name of a given node which creates a new
|
152
|
+
# namespace (module/class).
|
153
|
+
#
|
154
|
+
# References to constants appear in the following two forms, with the
|
155
|
+
# indicator that a constant follows (CONSTANT_NAME_NODES) always in the
|
156
|
+
# leftmost branch:
|
157
|
+
# [:const_ref, [:@const, "Klass", [1, 6]]]
|
158
|
+
# and:
|
159
|
+
# [:const_path_ref,
|
160
|
+
# [:var_ref, [:@const, "Namespaced", [1, 6]]],
|
161
|
+
# [:@const, "Klass", [1, 18]]]
|
162
|
+
#
|
163
|
+
# namespace - Array containing the current namespace, used to determine the
|
164
|
+
# full namespace of the node.
|
165
|
+
# ast - AST to be searched for references to constants. This object
|
166
|
+
# is globbed, so it may be mutated safely.
|
167
|
+
#
|
168
|
+
# Raises ArgumentError if no name can be found.
|
169
|
+
# Returns a String.
|
170
|
+
def node_name(namespace, *ast)
|
171
|
+
ast.reduce([]) do |path, node|
|
172
|
+
if node.is_a?(Array)
|
173
|
+
if CONSTANT_NAME_NODES.include?(node.first)
|
174
|
+
name = path.push(node.flatten.select { |e| e.is_a?(String) })
|
175
|
+
return((namespace + name).flatten.join(NAMESPACE_SEPARATOR))
|
176
|
+
end
|
177
|
+
ast.push(node.first)
|
178
|
+
end
|
179
|
+
path
|
180
|
+
end
|
181
|
+
raise(ArgumentError, 'No constant name found in the tree.')
|
182
|
+
end
|
183
|
+
|
184
|
+
# Internal: Find all references to edges (defined as references to external
|
185
|
+
# constants) within the current scope.
|
186
|
+
#
|
187
|
+
# parent - String containing the name of the current node.
|
188
|
+
# ast - AST to be searched for references to constants.
|
189
|
+
#
|
190
|
+
# Returns an Array of Strings and AREdges.
|
191
|
+
def find_edges(parent, ast)
|
192
|
+
direct_reference_edges(parent, ast) + activerecord_edges(ast)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Internal: Find all explicit references to edges within the current scope.
|
196
|
+
#
|
197
|
+
# parent - String containing the name of the current node.
|
198
|
+
# ast - AST to be searched for references to constants. This object is
|
199
|
+
# globbed, so it may be mutated safely.
|
200
|
+
#
|
201
|
+
# Returns an Array of Strings.
|
202
|
+
def direct_reference_edges(parent, *ast)
|
203
|
+
ast.reduce([]) do |edges, node|
|
204
|
+
if node.is_a?(Array) && !NAMESPACE_NODES.include?(node.first)
|
205
|
+
if CONSTANT_NAME_NODES.include?(node.first)
|
206
|
+
edges.push(node)
|
207
|
+
else
|
208
|
+
node.each { |sub| ast.push(sub) }
|
209
|
+
end
|
210
|
+
end
|
211
|
+
edges
|
212
|
+
end.map { |node| node_name([], node) }.reject { |node| parent == node }
|
213
|
+
end
|
214
|
+
|
215
|
+
# Internal: Find all references to edges via ActiveRecord associations
|
216
|
+
# (belongs_to, has_one, has_many, has_and_belongs_to_many) in the current
|
217
|
+
# scope.
|
218
|
+
#
|
219
|
+
# ast - AST to be searched for references to ActiveRecord associations.
|
220
|
+
# This object is globbed, so it may be mutated safely.
|
221
|
+
#
|
222
|
+
# Returns an Array of AREdges.
|
223
|
+
def activerecord_edges(*ast)
|
224
|
+
activerecord_nodes(ast).reduce([]) do |edges, node|
|
225
|
+
if node.is_a?(Array) && !NAMESPACE_NODES.include?(node.first)
|
226
|
+
node_type, args = node
|
227
|
+
if ACTIVERECORD_NODES.include?(node_type[1])
|
228
|
+
edges.push(activerecord_references(args))
|
229
|
+
end
|
230
|
+
end
|
231
|
+
edges
|
232
|
+
end.compact.map { |edge| AREdge.new(edge[:reference], edge[:args]) }
|
233
|
+
end
|
234
|
+
|
235
|
+
# Internal: Find and return all instances of ActiveRecord association
|
236
|
+
# nodes.
|
237
|
+
#
|
238
|
+
# These will match the following pattern:
|
239
|
+
# [:command,
|
240
|
+
# [:@ident, "has_many", [2, 10]],
|
241
|
+
# [:args_add_block,
|
242
|
+
# [[:symbol_literal, [:symbol, [:@ident, "associations", [2, 22]]]],
|
243
|
+
# [:bare_assoc_hash,
|
244
|
+
# [[:assoc_new,
|
245
|
+
# [:@label, "polymorphic:", [2, 33]],
|
246
|
+
# [:var_ref, [:@kw, "true", [2, 46]]]]]]], false]]
|
247
|
+
# Note: the bare_assoc_hash node is optional and only appears in cases
|
248
|
+
# where additional arguments beyond the association name are passed.
|
249
|
+
#
|
250
|
+
# ast - AST to be searched for references to ActiveRecord associations.
|
251
|
+
#
|
252
|
+
# Returns an Array of AST nodes.
|
253
|
+
def activerecord_nodes(ast)
|
254
|
+
ast.reduce([]) do |nodes, node|
|
255
|
+
if node.is_a?(Array) && !NAMESPACE_NODES.include?(node.first)
|
256
|
+
if [:command].include?(node.first)
|
257
|
+
if ACTIVERECORD_NODES.include?(node[1][1])
|
258
|
+
nodes.push(node[1..-1])
|
259
|
+
end
|
260
|
+
else
|
261
|
+
node.each { |sub| ast.push(sub) }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
nodes
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Internal: Process argument blocks (args_add_block nodes), returning a
|
269
|
+
# Hash representative of the arguments passed to a given ActiveRecord
|
270
|
+
# association command.
|
271
|
+
#
|
272
|
+
# The block will match the following pattern:
|
273
|
+
# [:args_add_block,
|
274
|
+
# [[:symbol_literal, [:symbol, [:@ident, "associations", [2, 22]]]],
|
275
|
+
# [:bare_assoc_hash,
|
276
|
+
# [[:assoc_new,
|
277
|
+
# [:@label, "polymorphic:", [2, 33]],
|
278
|
+
# [:var_ref, [:@kw, "true", [2, 46]]]]]]], false]
|
279
|
+
# Note: the bare_assoc_hash node is optional and only appears in cases
|
280
|
+
# where additional arguments beyond the association name are passed.
|
281
|
+
#
|
282
|
+
# args - AST representing an arguments block to be processed.
|
283
|
+
#
|
284
|
+
# Returns a Hash or nil.
|
285
|
+
def activerecord_references(args)
|
286
|
+
return nil unless args.is_a?(Array) && args.first == :args_add_block
|
287
|
+
arg_tree = args[1]
|
288
|
+
arg_tree.reduce({}) do |references, node|
|
289
|
+
if node.is_a?(Array) && !NAMESPACE_NODES.include?(node.first)
|
290
|
+
references.merge(process_reference_ast(node))
|
291
|
+
else
|
292
|
+
references
|
293
|
+
end
|
294
|
+
end
|
54
295
|
end
|
55
296
|
|
56
|
-
|
57
|
-
|
58
|
-
|
297
|
+
# Internal: Process argument blocks (args_add_block nodes), returning a
|
298
|
+
# Hash representative of one of the arguments passed to a given
|
299
|
+
# ActiveRecord association command.
|
300
|
+
#
|
301
|
+
# The block will match the following patterns:
|
302
|
+
# [:symbol_literal, [:symbol, [:@ident, "associations", [2, 22]]]]
|
303
|
+
# or:
|
304
|
+
# [:bare_assoc_hash,
|
305
|
+
# [[:assoc_new,
|
306
|
+
# [:@label, "polymorphic:", [2, 33]],
|
307
|
+
# [:var_ref, [:@kw, "true", [2, 46]]]]]]
|
308
|
+
# Note: the bare_assoc_hash node is optional and only appears in cases
|
309
|
+
# where additional arguments beyond the association name are passed.
|
310
|
+
#
|
311
|
+
# node - AST representing an argument block to be processed.
|
312
|
+
#
|
313
|
+
# Returns a Hash.
|
314
|
+
def process_reference_ast(node)
|
315
|
+
if [:symbol_literal].include?(node.first)
|
316
|
+
{ reference: node.flatten.detect { |e| e.is_a?(String) } }
|
317
|
+
elsif [:bare_assoc_hash].include?(node.first)
|
318
|
+
{ args: arguments_hash(node[1]) }
|
319
|
+
else
|
320
|
+
{ }
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Internal: Generate a Hash from a block describing a Hash.
|
325
|
+
#
|
326
|
+
# The block will match the following pattern:
|
327
|
+
# [[:assoc_new,
|
328
|
+
# [:@label, "polymorphic:", [2, 33]],
|
329
|
+
# [:var_ref, [:@kw, "true", [2, 46]]]]]
|
330
|
+
#
|
331
|
+
# node - AST representing a hash definition block to be processed.
|
332
|
+
#
|
333
|
+
# Returns a Hash.
|
334
|
+
def arguments_hash(node)
|
335
|
+
node.select { |node| node.first == :assoc_new }.reduce({}) do |hash, node|
|
336
|
+
key, val = node[1,2].map do |node|
|
337
|
+
if node.is_a?(Array)
|
338
|
+
node.flatten.detect { |element| element.is_a?(String) }
|
339
|
+
end
|
340
|
+
end
|
341
|
+
key && val ? hash.merge({ key.gsub(/:/, '') => val }) : hash
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Internal: Attempt to resolve all edges for the nodes contained within an
|
346
|
+
# ObjectGraph.
|
347
|
+
#
|
348
|
+
# graph - ObjectGraph to process.
|
349
|
+
#
|
350
|
+
# Returns an ObjectGraph.
|
351
|
+
def resolve_known_edges(graph)
|
352
|
+
resolve_known_activerecord_edges(graph) + resolve_direct_edges(graph)
|
353
|
+
end
|
354
|
+
|
355
|
+
# Internal: Attempt to resolve all directly referenced edges for the nodes
|
356
|
+
# contained within an ObjectGraph, discarding all unresolved edges after
|
357
|
+
# this step.
|
358
|
+
#
|
359
|
+
# graph - ObjectGraph to process.
|
360
|
+
#
|
361
|
+
# Returns an ObjectGraph.
|
362
|
+
def resolve_direct_edges(graph)
|
363
|
+
known_nodes = graph.map(&:name)
|
364
|
+
new_graph = graph.map do |node|
|
365
|
+
known = node.unresolved.select { |edge| known_nodes.include?(edge.to) }
|
366
|
+
Society::Node.new(name: node.name, type: node.type, edges: known)
|
367
|
+
end
|
368
|
+
Society::ObjectGraph.new(new_graph)
|
59
369
|
end
|
60
370
|
|
61
|
-
|
62
|
-
|
63
|
-
|
371
|
+
# Internal: Attempt to resolve all ActiveRecord association edges for the
|
372
|
+
# nodes contained within an ObjectGraph, discarding all unresolved edges
|
373
|
+
# after this step.
|
374
|
+
#
|
375
|
+
# graph - ObjectGraph to process.
|
376
|
+
#
|
377
|
+
# Returns an ObjectGraph.
|
378
|
+
def resolve_known_activerecord_edges(graph)
|
379
|
+
aredges = graph.map do |node|
|
380
|
+
node.unresolved.select { |edge| edge.to.is_a?(AREdge) }
|
381
|
+
.map(&:to).each_with_object(node).to_a.map(&:reverse)
|
382
|
+
end.flatten(1)
|
383
|
+
argraph = aredges.reduce(graph) do |graph, edge_tuple|
|
384
|
+
node, edge = edge_tuple
|
385
|
+
graph << add_meta_to_node(node, edge[:args], edge[:reference])
|
386
|
+
end
|
387
|
+
graph + argraph.map { |n| resolve_activerecord_associations(argraph, n) }
|
388
|
+
end
|
389
|
+
|
390
|
+
# Internal: Generate a Node with metainformation populated from
|
391
|
+
# ActiveRecord association information.
|
392
|
+
#
|
393
|
+
# node - Node object to which the arglist should be added as meta
|
394
|
+
# information.
|
395
|
+
# args_hash - Hash containing arguments passed to the ActiveRecord
|
396
|
+
# association if any; nil otherwise.
|
397
|
+
# ref - Reference for the ActiveRecord association.
|
398
|
+
#
|
399
|
+
# Returns a Node.
|
400
|
+
def add_meta_to_node(node, args_hash, ref)
|
401
|
+
refs = ({ reference: ref }).merge(args_hash || {})
|
402
|
+
node + Node.new(name: node.name, type: node.type, meta: [refs])
|
403
|
+
end
|
404
|
+
|
405
|
+
# Internal: Generate a Node with edges resolved by searching the graph for
|
406
|
+
# nodes with corresponding ActiveRecord associations (e.g. as: relations
|
407
|
+
# for polymorphic ActiveRecord associations.)
|
408
|
+
#
|
409
|
+
# graph - ObjectGraph containing nodes to search for associations.
|
410
|
+
# node - Node object for which ActiveRecord associations will be resolved.
|
411
|
+
#
|
412
|
+
# Returns a Node.
|
413
|
+
def resolve_activerecord_associations(graph, node)
|
414
|
+
return node if node.meta.empty?
|
415
|
+
init_data = { name: node.name, type: node.type }
|
416
|
+
|
417
|
+
node.meta.reduce(Society::Node.new(init_data)) do |node, meta|
|
418
|
+
edges = edge_names_from_meta_node(graph, meta).map do |edge_name|
|
419
|
+
Society::Edge.new(to: edge_name)
|
420
|
+
end
|
421
|
+
node + Society::Node.new(init_data.merge({ edges: edges }))
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# Internal: Determine all edges for a given ActiveRecord association based
|
426
|
+
# on a search of a global graph for corresponding associations (e.g. as:
|
427
|
+
# for polymorphic associations.)
|
428
|
+
# Only one type of association will be resolved for any given set of
|
429
|
+
# meta-information; the ActiveRecord reference itself is used as a
|
430
|
+
# fallback.
|
431
|
+
#
|
432
|
+
# graph - ObjectGraph containing nodes to search for associations.
|
433
|
+
# meta - Hash containing meta information to use in resolving the
|
434
|
+
# association.
|
435
|
+
#
|
436
|
+
# Returns an Array of Strings.
|
437
|
+
def edge_names_from_meta_node(graph, meta)
|
438
|
+
edge = meta['class_name'] ||
|
439
|
+
process_through_meta_node(graph, meta) ||
|
440
|
+
process_polymorphic_meta_node(graph, meta) ||
|
441
|
+
meta[:reference]
|
442
|
+
|
443
|
+
[edge].flatten.map(&:pluralize).map(&:classify)
|
444
|
+
end
|
445
|
+
|
446
|
+
# Internal: Resolve references for 'through' ActiveRecord associations.
|
447
|
+
#
|
448
|
+
# graph - ObjectGraph containing nodes to search for associations.
|
449
|
+
# meta - Hash containing meta information to use in resolving the
|
450
|
+
# association.
|
451
|
+
#
|
452
|
+
# Returns an Array of Strings.
|
453
|
+
def process_through_meta_node(graph, meta)
|
454
|
+
return nil unless meta['through']
|
455
|
+
|
456
|
+
through = meta['through'].pluralize.classify
|
457
|
+
ref = meta['source'] || meta[:reference]
|
458
|
+
graph.select { |n| n.name == through }.flat_map do |n|
|
459
|
+
n.meta.select { |m| [ref, ref.singularize].include?(m[:reference]) }
|
460
|
+
end.map { |meta| edge_names_from_meta_node(graph, meta) }
|
461
|
+
end
|
462
|
+
|
463
|
+
# Internal: Resolve references for polymorphic ActiveRecord associations.
|
464
|
+
#
|
465
|
+
# graph - ObjectGraph containing nodes to search for associations.
|
466
|
+
# meta - Hash containing meta information to use in resolving the
|
467
|
+
# association.
|
468
|
+
#
|
469
|
+
# Returns an Array of Strings.
|
470
|
+
def process_polymorphic_meta_node(graph, meta)
|
471
|
+
return nil unless meta['polymorphic']
|
472
|
+
graph.select do |n|
|
473
|
+
n.meta.select { |m| m['as'] == meta[:reference] }.any?
|
474
|
+
end.map(&:name)
|
475
|
+
end
|
476
|
+
|
477
|
+
FORMATTERS = {
|
478
|
+
html: Society::Formatter::Report::HTML,
|
479
|
+
json: Society::Formatter::Report::Json
|
480
|
+
}
|
481
|
+
|
482
|
+
# Internal: List known output formatters.
|
483
|
+
#
|
484
|
+
# Returns an Array of Symbols.
|
485
|
+
def known_formats
|
486
|
+
FORMATTERS.keys
|
64
487
|
end
|
65
488
|
|
66
489
|
end
|