From 106ac802a79633f3f12d797f8f5517ca781d24dd Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Wed, 10 Apr 2024 17:01:49 +0900 Subject: [PATCH 1/3] Render unique module id --- lib/diver_down/web/definition_to_dot.rb | 8 ++++++- spec/diver_down/web/definition_to_dot_spec.rb | 24 ++++--------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/lib/diver_down/web/definition_to_dot.rb b/lib/diver_down/web/definition_to_dot.rb index da79f59..1907efa 100644 --- a/lib/diver_down/web/definition_to_dot.rb +++ b/lib/diver_down/web/definition_to_dot.rb @@ -96,7 +96,13 @@ def issue_dependency_id(dependency) # @param module_names [Array] # @return [String] def issue_modules_id(module_names) - build_metadata_and_return_id(:module, module_names) + issued_metadata = @to_h.values.find { _1.type == :module && _1.data == module_names } + + if issued_metadata + issued_metadata.id + else + build_metadata_and_return_id(:module, module_names) + end end # @param id [String] diff --git a/spec/diver_down/web/definition_to_dot_spec.rb b/spec/diver_down/web/definition_to_dot_spec.rb index 27134e3..c61b80c 100644 --- a/spec/diver_down/web/definition_to_dot_spec.rb +++ b/spec/diver_down/web/definition_to_dot_spec.rb @@ -214,9 +214,9 @@ def build_definition(title: 'title', sources: []) "b.rb" [label="b.rb" id="graph_5"] } subgraph "cluster_B" { - id="graph_6" + id="graph_4" label="B" - "c.rb" [label="c.rb" id="graph_7"] + "c.rb" [label="c.rb" id="graph_6"] } } DOT @@ -267,14 +267,6 @@ def build_definition(title: 'title', sources: []) ], }, { id: 'graph_6', - type: 'module', - modules: [ - { - module_name: 'B', - }, - ], - }, { - id: 'graph_7', type: 'source', source_name: 'c.rb', modules: [ @@ -339,9 +331,9 @@ def build_definition(title: 'title', sources: []) "b.rb" [label="b.rb" id="graph_5"] } subgraph "cluster_B" { - id="graph_6" + id="graph_4" label="B" - "c.rb" [label="c.rb" id="graph_7"] + "c.rb" [label="c.rb" id="graph_6"] } } DOT @@ -402,14 +394,6 @@ def build_definition(title: 'title', sources: []) ], }, { id: 'graph_6', - type: 'module', - modules: [ - { - module_name: 'B', - }, - ], - }, { - id: 'graph_7', type: 'source', source_name: 'c.rb', modules: [ From d56241cafb2eecbe7f3c532cafc5f6d7a9b9179a Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Wed, 10 Apr 2024 18:42:34 +0900 Subject: [PATCH 2/3] Support only_module=1 on frontend --- frontend/pages/Home/Show.tsx | 3 ++- .../ConfigureGraphOptionsDialog.tsx | 15 +++++++++++++++ .../repositories/combinedDefinitionRepository.ts | 3 ++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/frontend/pages/Home/Show.tsx b/frontend/pages/Home/Show.tsx index 4d82276..8786437 100644 --- a/frontend/pages/Home/Show.tsx +++ b/frontend/pages/Home/Show.tsx @@ -22,12 +22,13 @@ export const Show: React.FC = () => { const [graphOptions, setGraphOptions] = useLocalStorage('HomeShow-GraphOptions', { compound: false, concentrate: false, + onlyModule: false, }) const { data: combinedDefinition, isLoading, mutate: mutateCombinedDefinition, - } = useCombinedDefinition(selectedDefinitionIds, graphOptions.compound, graphOptions.concentrate) + } = useCombinedDefinition(selectedDefinitionIds, graphOptions.compound, graphOptions.concentrate, graphOptions.onlyModule) const onCloseDialog = useCallback(() => { setVisibleDialog(null) diff --git a/frontend/pages/Home/components/ConfigureGraphOptionsDialog/ConfigureGraphOptionsDialog.tsx b/frontend/pages/Home/components/ConfigureGraphOptionsDialog/ConfigureGraphOptionsDialog.tsx index 790bd9c..0085975 100644 --- a/frontend/pages/Home/components/ConfigureGraphOptionsDialog/ConfigureGraphOptionsDialog.tsx +++ b/frontend/pages/Home/components/ConfigureGraphOptionsDialog/ConfigureGraphOptionsDialog.tsx @@ -7,6 +7,7 @@ import { spacing } from '@/constants/theme' export type GraphOptions = { compound: boolean concentrate: boolean + onlyModule: boolean } type Props = { @@ -45,6 +46,13 @@ export const ConfigureGraphOptionsDialog: React.FC = ({ isOpen, onClickCl [setTemporaryViewOptions], ) + const onChangeOnlyModule = useCallback( + (event: React.ChangeEvent) => { + setTemporaryViewOptions((prev) => ({ ...prev, onlyModule: event.target.checked })) + }, + [setTemporaryViewOptions], + ) + return ( = ({ isOpen, onClickCl > + + + + diff --git a/frontend/repositories/combinedDefinitionRepository.ts b/frontend/repositories/combinedDefinitionRepository.ts index b101b96..6bcb130 100644 --- a/frontend/repositories/combinedDefinitionRepository.ts +++ b/frontend/repositories/combinedDefinitionRepository.ts @@ -108,10 +108,11 @@ const fetchDefinitionShow = async (requestPath: string): Promise (value ? '1' : null) -export const useCombinedDefinition = (ids: number[], compound: boolean, concentrate: boolean) => { +export const useCombinedDefinition = (ids: number[], compound: boolean, concentrate: boolean, onlyModule: boolean) => { const params = { compound: toBooleanFlag(compound), concentrate: toBooleanFlag(concentrate), + only_module: toBooleanFlag(onlyModule), } const requestPath = `${path.api.definitions.show(ids)}?${stringify(params)}` const shouldFetch = ids.length > 0 From 229ff2b58972b2bce9c250cee02a878672e14450 Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Wed, 10 Apr 2024 18:43:11 +0900 Subject: [PATCH 3/3] Support only_module=1 on backend --- lib/diver_down/web.rb | 3 +- lib/diver_down/web/action.rb | 5 +- lib/diver_down/web/definition_to_dot.rb | 157 ++++++++++++++---- spec/diver_down/web/definition_to_dot_spec.rb | 105 +++++++++++- 4 files changed, 236 insertions(+), 34 deletions(-) diff --git a/lib/diver_down/web.rb b/lib/diver_down/web.rb index e82d160..9f0c148 100644 --- a/lib/diver_down/web.rb +++ b/lib/diver_down/web.rb @@ -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/(?[^/]+)\.json\z}] source = Regexp.last_match[:source] action.source(source) diff --git a/lib/diver_down/web/action.rb b/lib/diver_down/web/action.rb index a5865d6..8005e5d 100644 --- a/lib/diver_down/web/action.rb +++ b/lib/diver_down/web/action.rb @@ -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 @@ -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:, diff --git a/lib/diver_down/web/definition_to_dot.rb b/lib/diver_down/web/definition_to_dot.rb index 1907efa..74598b7 100644 --- a/lib/diver_down/web/definition_to_dot.rb +++ b/lib/diver_down/web/definition_to_dot.rb @@ -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 @@ -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 @@ -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 '}' @@ -174,6 +182,92 @@ def to_s attr_reader :definition, :module_store, :io + def render_only_modules + # Hash{ from_module => { to_module => Array } } + 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))}) @@ -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!( @@ -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 @@ -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 diff --git a/spec/diver_down/web/definition_to_dot_spec.rb b/spec/diver_down/web/definition_to_dot_spec.rb index c61b80c..e6a3668 100644 --- a/spec/diver_down/web/definition_to_dot_spec.rb +++ b/spec/diver_down/web/definition_to_dot_spec.rb @@ -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"] @@ -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: [