Skip to content

Commit

Permalink
Support only_module=1 on backend
Browse files Browse the repository at this point in the history
  • Loading branch information
alpaca-tc committed Apr 10, 2024
1 parent d56241c commit 229ff2b
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 34 deletions.
3 changes: 2 additions & 1 deletion lib/diver_down/web.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ def call(env)
bit_id = Regexp.last_match[:bit_id].to_i
compound = request.params['compound'] == '1'
concentrate = request.params['concentrate'] == '1'
action.combine_definitions(bit_id, compound, concentrate)
only_module = request.params['only_module'] == '1'
action.combine_definitions(bit_id, compound, concentrate, only_module)
in ['GET', %r{\A/api/sources/(?<source>[^/]+)\.json\z}]
source = Regexp.last_match[:source]
action.source(source)
Expand Down
5 changes: 3 additions & 2 deletions lib/diver_down/web/action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ def pid
# @param bit_id [Integer]
# @param compound [Boolean]
# @param concentrate [Boolean]
def combine_definitions(bit_id, compound, concentrate)
# @param only_module [Boolean]
def combine_definitions(bit_id, compound, concentrate, only_module)
ids = DiverDown::Web::BitId.bit_id_to_ids(bit_id)

valid_ids = ids.select do
Expand All @@ -175,7 +176,7 @@ def combine_definitions(bit_id, compound, concentrate)
end

if definition
definition_to_dot = DiverDown::Web::DefinitionToDot.new(definition, @module_store, compound:, concentrate:)
definition_to_dot = DiverDown::Web::DefinitionToDot.new(definition, @module_store, compound:, concentrate:, only_module:)

json(
titles:,
Expand Down
157 changes: 127 additions & 30 deletions lib/diver_down/web/definition_to_dot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ module DiverDown
class Web
class DefinitionToDot
ATTRIBUTE_DELIMITER = ' '
MODULE_DELIMITER = '::'

# Between modules is prominently distanced
MODULE_MINLEN = 3

class MetadataStore
Metadata = Data.define(:id, :type, :data, :module_store) do
Expand Down Expand Up @@ -138,14 +142,15 @@ def length
# @param module_store [DiverDown::ModuleStore]
# @param compound [Boolean]
# @param concentrate [Boolean] https://graphviz.org/docs/attrs/concentrate/
def initialize(definition, module_store, compound: false, concentrate: false)
def initialize(definition, module_store, compound: false, concentrate: false, only_module: false)
@definition = definition
@module_store = module_store
@io = DiverDown::IndentedStringIo.new
@indent = 0
@compound = compound
@compound = compound || only_module # When only-module is enabled, dependencies between modules are displayed as compound.
@compound_map = Hash.new { |h, k| h[k] = {} } # Hash{ ltail => Hash{ lhead => issued id } }
@concentrate = concentrate
@only_module = only_module
@metadata_store = MetadataStore.new(module_store)
end

Expand All @@ -156,14 +161,17 @@ def metadata

# @return [String]
def to_s
sources = definition.sources.sort_by(&:source_name)

io.puts %(strict digraph "#{definition.title}" {)
io.indented do
io.puts('compound=true') if @compound
io.puts('concentrate=true') if @concentrate
sources.each do
insert_source(_1)

if @only_module
render_only_modules
else
definition.sources.sort_by(&:source_name).each do
insert_source(_1)
end
end
end
io.puts '}'
Expand All @@ -174,6 +182,92 @@ def to_s

attr_reader :definition, :module_store, :io

def render_only_modules
# Hash{ from_module => { to_module => Array<DiverDown::Definition::Dependency> } }
dependency_map = Hash.new { |h, k| h[k] = Hash.new { |hi, ki| hi[ki] = [] } }

definition.sources.sort_by(&:source_name).each do |source|
source_modules = module_store.get(source.source_name)
next if source_modules.empty?

source.dependencies.each do |dependency|
dependency_modules = module_store.get(dependency.source_name)
next if dependency_modules.empty?

dependency_map[source_modules][dependency_modules].push(dependency)
end
end

# Remove duplicated prefix modules
# from [["A"], ["A", "B"]] to [["A", "B"]]
uniq_modules = [*dependency_map.keys, *dependency_map.values.map(&:keys).flatten(1)].uniq
uniq_modules.reject! do |modules|
modules.empty? ||
uniq_modules.any? { _1[0..modules.size - 1] == modules && _1.length > modules.size }
end

uniq_modules.each do |specific_module_names|
buf = swap_io do
indexes = (0..(specific_module_names.length - 1)).to_a

chain_yield(indexes) do |index, next_proc|
module_names = specific_module_names[0..index]
module_name = specific_module_names[index]

io.puts %(subgraph "#{module_label(module_names)}" {)
io.indented do
io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
io.puts %(label="#{module_name}")
io.puts %("#{module_name}" #{build_attributes(label: module_name, id: @metadata_store.issue_modules_id(module_names))})

next_proc&.call
end
io.puts '}'
end
end

io.write buf.string
end

dependency_map.each do |from_modules, h|
h.each do |to_modules, all_dependencies|
# Do not render standalone source
# Do not render self-dependency
next if from_modules.empty? || to_modules.empty? || from_modules == to_modules

dependencies = DiverDown::Definition::Dependency.combine(*all_dependencies)

dependencies.each do
attributes = {}
ltail = module_label(*from_modules)
lhead = module_label(*to_modules)

# Already rendered dependencies between modules
# Add the dependency to the edge of the compound
if @compound_map[ltail].include?(lhead)
compound_id = @compound_map[ltail][lhead]
@metadata_store.append_dependency(compound_id, _1)
next
end

compound_id = @metadata_store.issue_dependency_id(_1)
@compound_map[ltail][lhead] = compound_id

attributes.merge!(
id: compound_id,
ltail:,
lhead:,
minlen: MODULE_MINLEN
)

io.write(%("#{from_modules[-1]}" -> "#{to_modules[-1]}"))
io.write(%( #{build_attributes(**attributes)}), indent: false) unless attributes.empty?
io.write("\n")
end
end
end
end

def insert_source(source)
if module_store.get(source.source_name).empty?
io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_source_id(source))})
Expand Down Expand Up @@ -205,7 +299,7 @@ def insert_source(source)
id: compound_id,
ltail:,
lhead:,
minlen: (between_modules ? 3 : nil) # Between modules is prominently distanced
minlen: MODULE_MINLEN
)
else
attributes.merge!(
Expand All @@ -222,42 +316,45 @@ def insert_source(source)
def insert_modules(source)
buf = swap_io do
all_module_names = module_store.get(source.source_name)
*head_module_indexes, _tail_module_index = (0..(all_module_names.length - 1)).to_a
indexes = (0..(all_module_names.length - 1)).to_a

# last subgraph
last_module_writer = proc do
module_names = all_module_names
chain_yield(indexes) do |index, next_proc|
module_names = all_module_names[0..index]
module_name = module_names[-1]

io.puts %(subgraph "#{module_label(module_name)}" {)
io.puts %(subgraph "#{module_label(module_names)}" {)
io.indented do
io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
io.puts %(label="#{module_name}")
io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_source_id(source))})

if next_proc
next_proc.call
else
# last. equals indexes[-1] == index
io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_source_id(source))})
end
end
io.puts '}'
end
end

# wrapper subgraph
modules_writer = head_module_indexes.inject(last_module_writer) do |next_writer, module_index|
proc do
module_names = all_module_names[0..module_index]
module_name = module_names[-1]
io.write buf.string
end

io.puts %(subgraph "#{module_label(module_name)}" {)
io.indented do
io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
io.puts %(label="#{module_name}")
next_writer.call
end
io.puts '}'
end
end
def chain_yield(values, &block)
*head, tail = values

modules_writer.call
last_proc = proc do
block.call(tail, nil)
end

io.write buf.string
chain_proc = head.inject(last_proc) do |next_proc, value|
proc do
block.call(value, next_proc)
end
end

chain_proc.call
end

# rubocop:disable Lint/UnderscorePrefixedVariableName
Expand Down Expand Up @@ -295,7 +392,7 @@ def swap_io
def module_label(*modules)
return if modules.empty?

"cluster_#{modules[0]}"
"cluster_#{modules.join(MODULE_DELIMITER)}"
end
end
end
Expand Down
105 changes: 104 additions & 1 deletion spec/diver_down/web/definition_to_dot_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def build_definition(title: 'title', sources: [])
subgraph "cluster_A" {
id="graph_1"
label="A"
subgraph "cluster_B" {
subgraph "cluster_A::B" {
id="graph_2"
label="B"
"a.rb" [label="a.rb" id="graph_3"]
Expand Down Expand Up @@ -404,6 +404,109 @@ def build_definition(title: 'title', sources: [])
)
end

it 'returns compound module digraph with multiple method_ids if only_module = true' do
definition = build_definition(
sources: [
{
source_name: 'a.rb',
dependencies: [
{
source_name: 'b.rb',
method_ids: [
{
name: 'call_b',
context: 'class',
paths: [],
},
],
}, {
source_name: 'c.rb',
method_ids: [
{
name: 'call_c',
context: 'class',
paths: [],
},
],
}, {
source_name: 'd.rb',
method_ids: [
{
name: 'call_d',
context: 'class',
paths: [],
},
],
},
],
}, {
source_name: 'b.rb',
}, {
source_name: 'c.rb',
}, {
source_name: 'd.rb',
},
]
)

module_store.set('a.rb', ['A'])
module_store.set('b.rb', ['B'])
module_store.set('c.rb', ['B'])
module_store.set('d.rb', ['B', 'C'])
module_store.set('unknown.rb', ['Unknown'])

instance = described_class.new(definition, module_store, only_module: true)
expect(instance.to_s).to eq(<<~DOT)
strict digraph "title" {
compound=true
subgraph "cluster_A" {
id="graph_1"
label="A"
"A" [label="A" id="graph_1"]
}
subgraph "cluster_B" {
id="graph_2"
label="B"
"B" [label="B" id="graph_2"]
subgraph "cluster_B::C" {
id="graph_3"
label="C"
"C" [label="C" id="graph_3"]
}
}
"A" -> "B" [id="graph_4" ltail="cluster_A" lhead="cluster_B" minlen="3"]
"A" -> "C" [id="graph_5" ltail="cluster_A" lhead="cluster_B::C" minlen="3"]
}
DOT

expect(instance.metadata).to eq(
[
{ id: 'graph_1', type: 'module', modules: [{ module_name: 'A' }] },
{ id: 'graph_2', type: 'module', modules: [{ module_name: 'B' }] },
{ id: 'graph_3', type: 'module', modules: [{ module_name: 'B' }, { module_name: 'C' }] },
{
id: 'graph_4',
type: 'dependency',
dependencies: [
{
source_name: 'b.rb',
method_ids: [
{ name: 'call_b', context: 'class' },
],
}, {
source_name: 'c.rb',
method_ids: [
{ name: 'call_c', context: 'class' },
],

},
],
},
{ id: 'graph_5', type: 'dependency', dependencies: [{ source_name: 'd.rb', method_ids: [{ name: 'call_d', context: 'class' }] }] },
]
)
end

it 'returns concentrate digraph if concentrate = true' do
definition = build_definition(
sources: [
Expand Down

0 comments on commit 229ff2b

Please sign in to comment.