class Society::Parser
The Parser
class is responsible for producing an ObjectGraph
from one or more ruby sources.
Constants
- ACTIVERECORD_NODES
- AREdge
ActiveRecord edge, containing the direct reference and any arguments.
- CONSTANT_NAME_NODES
- FORMATTERS
- NAMESPACE_NODES
- NAMESPACE_SEPARATOR
- NSNode
AST
Node
with the current namespace and type (module/class) preserved.
Attributes
Public Class Methods
Public: Generate a list of files from a collection of paths, creating a new Parser
with them. Note: Since the files are not read, a new Parser
MAY be returned such that initiating processing will cause a crash later.
file_paths - Any number of Strings representing paths to files.
Returns a Parser
.
# File lib/society/parser.rb, line 15 def self.for_files(*file_paths) files = file_paths.flatten.flat_map do |path| File.directory?(path) ? Dir.glob(File.join(path, '**', '*.rb')) : path end new(files.lazy.map { |f| File.read(f) }) end
Public: Create a Parser
, staging ruby source files to be analyzed.
source - An Enumerable containing ruby source strings.
# File lib/society/parser.rb, line 34 def initialize(source) @source = source.map { |file| graph_from(file) } end
Public Instance Methods
Public: Return a list of known classes from the object graph.
Returns an Array of Strings.
# File lib/society/parser.rb, line 63 def classes graph.map(&:name) end
Public: Return the ObjectGraph
representing the analyzed source. Calling this method will trigger the analysis of the source if the object was created with lazy enumerables.
Returns an ObjectGraph
.
# File lib/society/parser.rb, line 56 def graph @graph ||= resolve_known_edges(source.reduce(ObjectGraph.new, &:+)) end
Public: Generate a report from the object graph.
format - A symbol representing any known output format. output_path - Path to which output should be written. (default: nil)
Returns nothing.
# File lib/society/parser.rb, line 44 def report(format, output_path=nil) raise ArgumentError, "Unknown format #{format}" unless known_formats.include?(format) options = { json_data: graph.to_json } options[:output_path] = output_path unless output_path.nil? FORMATTERS[format].new(options).write end
Private Instance Methods
Internal: Find all references to edges via ActiveRecord associations (belongs_to, has_one, has_many, has_and_belongs_to_many) in the current scope.
ast - AST to be searched for references to ActiveRecord associations.
This object is globbed, so it may be mutated safely.
Returns an Array of AREdges.
# File lib/society/parser.rb, line 223 def activerecord_edges(*ast) activerecord_nodes(ast).reduce([]) do |edges, node| if node.is_a?(Array) && !NAMESPACE_NODES.include?(node.first) node_type, args = node if ACTIVERECORD_NODES.include?(node_type[1]) edges.push(activerecord_references(args)) end end edges end.compact.map { |edge| AREdge.new(edge[:reference], edge[:args]) } end
Internal: Find and return all instances of ActiveRecord association nodes.
These will match the following pattern:
[:command, [:@ident, "has_many", [2, 10]], [:args_add_block, [[:symbol_literal, [:symbol, [:@ident, "associations", [2, 22]]]], [:bare_assoc_hash, [[:assoc_new, [:@label, "polymorphic:", [2, 33]], [:var_ref, [:@kw, "true", [2, 46]]]]]]], false]]
Note: the bare_assoc_hash node is optional and only appears in cases where additional arguments beyond the association name are passed.
ast - AST to be searched for references to ActiveRecord associations.
Returns an Array of AST nodes.
# File lib/society/parser.rb, line 253 def activerecord_nodes(ast) ast.reduce([]) do |nodes, node| if node.is_a?(Array) && !NAMESPACE_NODES.include?(node.first) if [:command].include?(node.first) if ACTIVERECORD_NODES.include?(node[1][1]) nodes.push(node[1..-1]) end else node.each { |sub| ast.push(sub) } end end nodes end end
Internal: Process argument blocks (args_add_block nodes), returning a Hash representative of the arguments passed to a given ActiveRecord association command.
The block will match the following pattern:
[:args_add_block, [[:symbol_literal, [:symbol, [:@ident, "associations", [2, 22]]]], [:bare_assoc_hash, [[:assoc_new, [:@label, "polymorphic:", [2, 33]], [:var_ref, [:@kw, "true", [2, 46]]]]]]], false]
Note: the bare_assoc_hash node is optional and only appears in cases where additional arguments beyond the association name are passed.
args - AST representing an arguments block to be processed.
Returns a Hash or nil.
# File lib/society/parser.rb, line 285 def activerecord_references(args) return nil unless args.is_a?(Array) && args.first == :args_add_block arg_tree = args[1] arg_tree.reduce({}) do |references, node| if node.is_a?(Array) && !NAMESPACE_NODES.include?(node.first) references.merge(process_reference_ast(node)) else references end end end
Internal: Generate a Node
with metainformation populated from ActiveRecord association information.
node - Node
object to which the arglist should be added as meta
information.
args_hash - Hash containing arguments passed to the ActiveRecord
association if any; nil otherwise.
ref - Reference for the ActiveRecord association.
Returns a Node
.
# File lib/society/parser.rb, line 400 def add_meta_to_node(node, args_hash, ref) refs = ({ reference: ref }).merge(args_hash || {}) node + Node.new(name: node.name, type: node.type, meta: [refs]) end
Internal: Generate a Hash from a block describing a Hash.
The block will match the following pattern:
[[:assoc_new, [:@label, "polymorphic:", [2, 33]], [:var_ref, [:@kw, "true", [2, 46]]]]]
node - AST representing a hash definition block to be processed.
Returns a Hash.
# File lib/society/parser.rb, line 334 def arguments_hash(node) node.select { |node| node.first == :assoc_new }.reduce({}) do |hash, node| key, val = node[1,2].map do |node| if node.is_a?(Array) node.flatten.detect { |element| element.is_a?(String) } end end key && val ? hash.merge({ key.gsub(/:/, '') => val }) : hash end end
Internal: Find all explicit references to edges within the current scope.
parent - String containing the name of the current node. ast - AST to be searched for references to constants. This object is
globbed, so it may be mutated safely.
Returns an Array of Strings.
# File lib/society/parser.rb, line 202 def direct_reference_edges(parent, *ast) ast.reduce([]) do |edges, node| if node.is_a?(Array) && !NAMESPACE_NODES.include?(node.first) if CONSTANT_NAME_NODES.include?(node.first) edges.push(node) else node.each { |sub| ast.push(sub) } end end edges end.map { |node| node_name([], node) }.reject { |node| parent == node } end
Internal: Determine all edges for a given ActiveRecord association based on a search of a global graph for corresponding associations (e.g. as: for polymorphic associations.) Only one type of association will be resolved for any given set of meta-information; the ActiveRecord reference itself is used as a fallback.
graph - ObjectGraph
containing nodes to search for associations. meta - Hash containing meta information to use in resolving the
association.
Returns an Array of Strings.
# File lib/society/parser.rb, line 437 def edge_names_from_meta_node(graph, meta) edge = meta['class_name'] || process_through_meta_node(graph, meta) || process_polymorphic_meta_node(graph, meta) || meta[:reference] [edge].flatten.map(&:pluralize).map(&:classify) end
Internal: Generate a list of nodes representing a change of namespace (classes/modules) from an abstract syntax tree, preserving the namespace associated with them.
namespace - Array containing the current namespace, to be preserved along
with the AST.
ast - AST to be searched for namespace separators. Note that due
to this object being globbed, mutating this object will not mutate the state of the object passed to this method.
Returns an Array of NSNodes.
# File lib/society/parser.rb, line 138 def filter_namespace(namespace, *ast) ast.reduce([]) do |nodes, node| if node.is_a?(Array) if NAMESPACE_NODES.include?(node.first) nodes.push(NSNode.new(namespace, node.first, node[1..-1])) else node.each { |sub| ast.push(sub) } end end nodes end end
Internal: Find all references to edges (defined as references to external constants) within the current scope.
parent - String containing the name of the current node. ast - AST to be searched for references to constants.
Returns an Array of Strings and AREdges.
# File lib/society/parser.rb, line 191 def find_edges(parent, ast) direct_reference_edges(parent, ast) + activerecord_edges(ast) end
Internal: Generate an ObjectGraph
from a string containing ruby source.
source - String containing ruby source.
Returns an ObjectGraph
.
# File lib/society/parser.rb, line 87 def graph_from(source) ast = Ripper.sexp(source) nodes_from(ast).reduce(Society::ObjectGraph.new, &:<<) end
Internal: List known output formatters.
Returns an Array of Symbols.
# File lib/society/parser.rb, line 485 def known_formats FORMATTERS.keys end
Internal: Determine the name of a given node which creates a new namespace (module/class).
References to constants appear in the following two forms, with the indicator that a constant follows (CONSTANT_NAME_NODES
) always in the leftmost branch:
[:const_ref, [:@const, "Klass", [1, 6]]]
and:
[:const_path_ref, [:var_ref, [:@const, "Namespaced", [1, 6]]], [:@const, "Klass", [1, 18]]]
namespace - Array containing the current namespace, used to determine the
full namespace of the node.
ast - AST to be searched for references to constants. This object
is globbed, so it may be mutated safely.
Raises ArgumentError if no name can be found. Returns a String.
# File lib/society/parser.rb, line 170 def node_name(namespace, *ast) ast.reduce([]) do |path, node| if node.is_a?(Array) if CONSTANT_NAME_NODES.include?(node.first) name = path.push(node.flatten.select { |e| e.is_a?(String) }) return((namespace + name).flatten.join(NAMESPACE_SEPARATOR)) end ast.push(node.first) end path end raise(ArgumentError, 'No constant name found in the tree.') end
Internal: Generate a list of Nodes from a string containing ruby source. Note: All edges are considered unresolved at this stage.
ast - Array containing an abstract syntax tree generated by Ripper.
Returns an Array of Nodes.
# File lib/society/parser.rb, line 98 def nodes_from(ast) walk_ast(ast).map do |name, data| init_node = Society::Node.new(name: name, type: data[:type]) find_edges(name, data[:ast]).reduce(init_node) do |node, new_edge| edge = [Society::Edge.new(to: new_edge)] type = data[:type] Society::Node.new(name: name, type: type, unresolved: edge) + node end end end
Internal: Resolve references for polymorphic ActiveRecord associations.
graph - ObjectGraph
containing nodes to search for associations. meta - Hash containing meta information to use in resolving the
association.
Returns an Array of Strings.
# File lib/society/parser.rb, line 470 def process_polymorphic_meta_node(graph, meta) return nil unless meta['polymorphic'] graph.select do |n| n.meta.select { |m| m['as'] == meta[:reference] }.any? end.map(&:name) end
Internal: Process argument blocks (args_add_block nodes), returning a Hash representative of one of the arguments passed to a given ActiveRecord association command.
The block will match the following patterns:
[:symbol_literal, [:symbol, [:@ident, "associations", [2, 22]]]]
or:
[:bare_assoc_hash, [[:assoc_new, [:@label, "polymorphic:", [2, 33]], [:var_ref, [:@kw, "true", [2, 46]]]]]]
Note: the bare_assoc_hash node is optional and only appears in cases where additional arguments beyond the association name are passed.
node - AST representing an argument block to be processed.
Returns a Hash.
# File lib/society/parser.rb, line 314 def process_reference_ast(node) if [:symbol_literal].include?(node.first) { reference: node.flatten.detect { |e| e.is_a?(String) } } elsif [:bare_assoc_hash].include?(node.first) { args: arguments_hash(node[1]) } else { } end end
Internal: Resolve references for 'through' ActiveRecord associations.
graph - ObjectGraph
containing nodes to search for associations. meta - Hash containing meta information to use in resolving the
association.
Returns an Array of Strings.
# File lib/society/parser.rb, line 453 def process_through_meta_node(graph, meta) return nil unless meta['through'] through = meta['through'].pluralize.classify ref = meta['source'] || meta[:reference] graph.select { |n| n.name == through }.flat_map do |n| n.meta.select { |m| [ref, ref.singularize].include?(m[:reference]) } end.map { |meta| edge_names_from_meta_node(graph, meta) } end
Internal: Generate a Node
with edges resolved by searching the graph for nodes with corresponding ActiveRecord associations (e.g. as: relations for polymorphic ActiveRecord associations.)
graph - ObjectGraph
containing nodes to search for associations. node - Node
object for which ActiveRecord associations will be resolved.
Returns a Node
.
# File lib/society/parser.rb, line 413 def resolve_activerecord_associations(graph, node) return node if node.meta.empty? init_data = { name: node.name, type: node.type } node.meta.reduce(Society::Node.new(init_data)) do |node, meta| edges = edge_names_from_meta_node(graph, meta).map do |edge_name| Society::Edge.new(to: edge_name) end node + Society::Node.new(init_data.merge({ edges: edges })) end end
Internal: Attempt to resolve all directly referenced edges for the nodes contained within an ObjectGraph
, discarding all unresolved edges after this step.
graph - ObjectGraph
to process.
Returns an ObjectGraph
.
# File lib/society/parser.rb, line 362 def resolve_direct_edges(graph) known_nodes = graph.map(&:name) new_graph = graph.map do |node| known = node.unresolved.select { |edge| known_nodes.include?(edge.to) } Society::Node.new(name: node.name, type: node.type, edges: known) end Society::ObjectGraph.new(new_graph) end
Internal: Attempt to resolve all ActiveRecord association edges for the nodes contained within an ObjectGraph
, discarding all unresolved edges after this step.
graph - ObjectGraph
to process.
Returns an ObjectGraph
.
# File lib/society/parser.rb, line 378 def resolve_known_activerecord_edges(graph) aredges = graph.map do |node| node.unresolved.select { |edge| edge.to.is_a?(AREdge) } .map(&:to).each_with_object(node).to_a.map(&:reverse) end.flatten(1) argraph = aredges.reduce(graph) do |graph, edge_tuple| node, edge = edge_tuple graph << add_meta_to_node(node, edge[:args], edge[:reference]) end graph + argraph.map { |n| resolve_activerecord_associations(argraph, n) } end
Internal: Attempt to resolve all edges for the nodes contained within an ObjectGraph
.
graph - ObjectGraph
to process.
Returns an ObjectGraph
.
# File lib/society/parser.rb, line 351 def resolve_known_edges(graph) resolve_known_activerecord_edges(graph) + resolve_direct_edges(graph) end
Internal: Isolate individual namespaces, generating a hash containing Namespace => AST pairs.
ast - Array containing an abstract syntax tree generated by Ripper.
Returns a Hash mapping Namespace => AST.
# File lib/society/parser.rb, line 115 def walk_ast(ast) scoped_nodes = filter_namespace([], ast) scoped_nodes.reduce({}) do |nodes, node| namespace = node[:namespace] + [node_name(node[:namespace], node[:ast])] filter_namespace(namespace, node[:ast]).each do |sub| scoped_nodes.push(sub) end nodes.merge({ namespace.last => node }) end end