ancestors_visualization 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +152 -0
- data/Rakefile +6 -0
- data/ancestors_visualization.gemspec +32 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/ancestors_visualization +28 -0
- data/lib/ancestors_visualization/cli.rb +64 -0
- data/lib/ancestors_visualization/diagram_creater.rb +135 -0
- data/lib/ancestors_visualization/graph_viz.rb +116 -0
- data/lib/ancestors_visualization/target_object_fetcher.rb +80 -0
- data/lib/ancestors_visualization/version.rb +3 -0
- data/lib/ancestors_visualization.rb +6 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4dd82babc7919289ae36a8288889f9ac81a131ef653a5a97757d6d65104c25f1
|
4
|
+
data.tar.gz: 5660e8b95fabc7855a8511b9b4dd3a7445c54b9802c2fffc41a063717fe049ef
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c248628a044688bf30ae8a901ec73724f10c46ba547f3474ccb4d7acafabd3ca4e26d0ebc44ed12c99741e56c21eb85ac5badbb7084b136b0a0976a8e59c0384
|
7
|
+
data.tar.gz: 83b2ba0e8c23cbafd16fccb7e7f6880cb0cbabbf94f99c5121029a9becbd62fc19ab5882da8e4d2209274eef5d3968c5d533dc169bd93dae620949a4c320f0a8
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at lonnlilonn@googlemail.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [https://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: https://contributor-covenant.org
|
74
|
+
[version]: https://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 tommy-012
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# AncestorsVisualization
|
2
|
+
## 概要
|
3
|
+
Gem のクラス継承・モジュール利用を可視化する Gem です
|
4
|
+
|
5
|
+
作ったきっかけとしては、Gem の実装を読む時に全体感を把握したいなと思うことがあったので
|
6
|
+
|
7
|
+
例えば、以下のクラス・モジュール(Gem 名は GemName を想定)があったとして
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# sample_gem_dir/lib/gem_name/c1.rb
|
11
|
+
module GemName
|
12
|
+
class C1 < C2
|
13
|
+
include Modules::M1_1
|
14
|
+
extend Modules::M1_2
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# sample_gem_dir/lib/gem_name/modules/m1_1.rb
|
19
|
+
module GemName
|
20
|
+
module Modules
|
21
|
+
module M1_1
|
22
|
+
include M1_1_1
|
23
|
+
extend M1_1_2
|
24
|
+
|
25
|
+
def m1_1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# sample_gem_dir/lib/gem_name/modules/m1_2.rb
|
32
|
+
module GemName
|
33
|
+
module Modules
|
34
|
+
module M1_2
|
35
|
+
def self.m1_2
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# sample_gem_dir/lib/gem_name/modules/m1_1_1.rb
|
42
|
+
module GemName
|
43
|
+
module Modules
|
44
|
+
module M1_1_1
|
45
|
+
def m1_1_1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# sample_gem_dir/lib/gem_name/modules/m1_1_2.rb
|
52
|
+
module GemName
|
53
|
+
module Modules
|
54
|
+
module M1_1_2
|
55
|
+
def self.m1_1_2
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# sample_gem_dir/lib/gem_name/c2.rb
|
62
|
+
module GemName
|
63
|
+
class C2
|
64
|
+
include Modules::M2_1
|
65
|
+
extend Modules::M2_2
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# sample_gem_dir/lib/gem_name/modules/m2_1.rb
|
70
|
+
module GemName
|
71
|
+
module Modules
|
72
|
+
module M2_1
|
73
|
+
def m2_1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# sample_gem_dir/lib/gem_name/modules/m2_2.rb
|
80
|
+
module GemName
|
81
|
+
module Modules
|
82
|
+
module M2_2
|
83
|
+
def self.m2_2
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
本 Gem を使うと、↓ の画像を生成します
|
91
|
+
|
92
|
+

|
93
|
+
|
94
|
+
### ⚠️ 注意点
|
95
|
+
|
96
|
+
- 描画内容は正確でないことがあります
|
97
|
+
- まず前提として、描画対象となるクラス・モジュールは、該当 Gem 名から判定した名前空間配下のみにしてます
|
98
|
+
- 特に制限せずに描画した時に、ノードが多過ぎて見にくかったので
|
99
|
+
- なので、[標準クラス](https://docs.ruby-lang.org/ja/latest/library/_builtin.html)は描画されないです
|
100
|
+
- 実装としては存在するのに、描画されてないクラスがあるかもしれないです
|
101
|
+
- 実装を見てもらうとわかるのですが、描画対象を解析する際に、対象の Gem の作りにいくつか前提を置いています
|
102
|
+
- なので、それが守られていないと、描画に失敗します([例](https://github.com/tommy-012/ancestors_visualization#%E5%AE%9F%E8%A1%8C%E4%BE%8B-2))
|
103
|
+
- あくまでコードリーディングする際の参考として使ってください
|
104
|
+
|
105
|
+
## インストール方法
|
106
|
+
|
107
|
+
以下を Gemfile に追記して `$ bundle install`
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
gem 'ancestors_visualization'
|
111
|
+
```
|
112
|
+
|
113
|
+
Budler を使わない場合は
|
114
|
+
|
115
|
+
```sh
|
116
|
+
$ gem install ancestors_visualization
|
117
|
+
```
|
118
|
+
|
119
|
+
## 使い方
|
120
|
+
|
121
|
+
実行フォーマットは以下
|
122
|
+
|
123
|
+
```sh
|
124
|
+
$ bundle exec ancestors_visualization --gem [Gem 名] --output_path [描画ファイルの出力先]
|
125
|
+
```
|
126
|
+
|
127
|
+
- gem は必須
|
128
|
+
- output_path は必要であれば
|
129
|
+
- デフォルトは、`[Gem 名]_ancestors_[年月日時分秒].png`
|
130
|
+
- 例えば、twitter_ancestors_20220504141646.png
|
131
|
+
|
132
|
+
### 実行例 1
|
133
|
+
例えば、[Twitter Gem](https://github.com/sferik/twitter) に対して実行する場合
|
134
|
+
|
135
|
+
```sh
|
136
|
+
$ bundle exec ancestors_visualization --gem twitter
|
137
|
+
```
|
138
|
+
|
139
|
+

|
140
|
+
|
141
|
+
### 実行例 2
|
142
|
+
例えば、[Rspec](https://github.com/rspec/rspec-core) に対して実行する場合
|
143
|
+
|
144
|
+
該当 Gem の lib 配下のクラス・モジュールを描画対象にしているので、依存先の Gem が本体みたいなケースは、意図しない描画結果になります
|
145
|
+
|
146
|
+
↓ で描画結果を表示しているのですが、何も表示されていないのはそういうことです
|
147
|
+
|
148
|
+

|
149
|
+
|
150
|
+
## ライセンス
|
151
|
+
|
152
|
+
[MIT License](https://opensource.org/licenses/MIT)
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'lib/ancestors_visualization/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "ancestors_visualization"
|
5
|
+
spec.version = AncestorsVisualization::VERSION
|
6
|
+
spec.authors = ["tommy-012"]
|
7
|
+
spec.email = ["lonnlilonn@googlemail.com"]
|
8
|
+
|
9
|
+
spec.homepage = 'https://github.com/tommy-012/ancestors_visualization'
|
10
|
+
spec.summary = 'ancestors-relationship diagram for the gem.'
|
11
|
+
spec.description = 'Automatically generate an ancestors-relationship diagram for the gem.'
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
14
|
+
|
15
|
+
# Specify which files should be added to the gem when it is released.
|
16
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
17
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
18
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_development_dependency 'rake'
|
25
|
+
spec.add_development_dependency 'rspec'
|
26
|
+
spec.add_development_dependency 'pry-byebug'
|
27
|
+
spec.add_development_dependency 'timecop'
|
28
|
+
|
29
|
+
spec.add_runtime_dependency 'choice' # NOTE https://github.com/defunkt/choice
|
30
|
+
spec.add_runtime_dependency 'activesupport'
|
31
|
+
spec.add_runtime_dependency 'ruby-graphviz'
|
32
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "ancestors_visualization"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'choice'
|
4
|
+
require "ancestors_visualization/cli"
|
5
|
+
|
6
|
+
Choice.options do
|
7
|
+
header ''
|
8
|
+
header 'Specific options:'
|
9
|
+
|
10
|
+
option :gem_name do
|
11
|
+
short '-g'
|
12
|
+
long '--gem=GEM'
|
13
|
+
desc '描画対象の Gem を指定する'
|
14
|
+
end
|
15
|
+
|
16
|
+
option :output_path do
|
17
|
+
short '-o'
|
18
|
+
long '--output=OUTPUT'
|
19
|
+
desc '出力先パスを指定する'
|
20
|
+
end
|
21
|
+
|
22
|
+
option :help do
|
23
|
+
long '--help'
|
24
|
+
desc 'Show this message'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
AncestorsVisualization::CLI.new(gem_name: Choice[:gem_name], output_path: Choice[:output_path]).execute
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ancestors_visualization/target_object_fetcher'
|
4
|
+
require 'ancestors_visualization/diagram_creater'
|
5
|
+
|
6
|
+
require 'active_support/all'
|
7
|
+
|
8
|
+
module AncestorsVisualization
|
9
|
+
class CLI
|
10
|
+
def initialize(gem_name:, output_path: nil)
|
11
|
+
@gem_name = gem_name
|
12
|
+
@output_path = output_path || default_output_path
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute
|
16
|
+
create_diagram
|
17
|
+
|
18
|
+
print_result
|
19
|
+
rescue Exception => e
|
20
|
+
$stderr.puts("描画に失敗しました。\nエラー内容: #{e.message}")
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :gem_name, :output_path
|
26
|
+
|
27
|
+
def create_diagram
|
28
|
+
DiagramCreater.new(
|
29
|
+
target_objects: fetch_target_objects,
|
30
|
+
output_path: output_path
|
31
|
+
).create
|
32
|
+
end
|
33
|
+
|
34
|
+
def fetch_target_objects
|
35
|
+
target_object_fetcher.fetch
|
36
|
+
end
|
37
|
+
|
38
|
+
def target_object_fetcher
|
39
|
+
@target_object_fetcher ||= TargetObjectFetcher.new(gem_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def default_output_path
|
43
|
+
"#{Dir.pwd}/#{gem_name}_ancestors_#{Time.current.strftime("%Y%m%d%H%M%S")}.png"
|
44
|
+
end
|
45
|
+
|
46
|
+
def print_result
|
47
|
+
puts '描画結果を以下に出力しました。'
|
48
|
+
puts '```'
|
49
|
+
puts output_path
|
50
|
+
puts '```'
|
51
|
+
|
52
|
+
return if target_object_fetcher.require_failed_files.blank?
|
53
|
+
|
54
|
+
puts "以下のファイルは読み込めなかったため、描画結果に反映されていません。"
|
55
|
+
puts '```'
|
56
|
+
target_object_fetcher.require_failed_files.each do |f|
|
57
|
+
puts "- #{f}"
|
58
|
+
end
|
59
|
+
puts '```'
|
60
|
+
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ancestors_visualization/graph_viz'
|
4
|
+
|
5
|
+
module AncestorsVisualization
|
6
|
+
class DiagramCreater
|
7
|
+
def initialize(target_objects:, output_path:)
|
8
|
+
raise ArgumentError, "'#{File.dirname(output_path)}' does not exist." unless Dir.exist?(File.dirname(output_path))
|
9
|
+
|
10
|
+
@target_objects = target_objects
|
11
|
+
@output_path = output_path
|
12
|
+
end
|
13
|
+
|
14
|
+
def create
|
15
|
+
# NOTE 名前空間の描画を減らすため、起点オブジェクトはクラスに限定する
|
16
|
+
target_classes.each do |klass|
|
17
|
+
draw_diagram(klass)
|
18
|
+
end
|
19
|
+
|
20
|
+
output_diagram
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :target_objects, :output_path
|
26
|
+
|
27
|
+
def target_classes
|
28
|
+
@target_classes ||= target_objects.select {|o| o.instance_of?(Class) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def draw_diagram(object)
|
32
|
+
# NOTE 対象クラスのinclude先のモジュールは描画したい
|
33
|
+
object_with_ancestor_modules(object).each do |object|
|
34
|
+
relation = ObjectRelation.new(object, target_objects)
|
35
|
+
|
36
|
+
return if relation.source.blank?
|
37
|
+
|
38
|
+
src = graph_viz.find_or_create_node(relation.source.to_s)
|
39
|
+
|
40
|
+
relation.destinations.each do |dst|
|
41
|
+
graph_viz.link(
|
42
|
+
source: src,
|
43
|
+
destination: graph_viz.find_or_create_node(dst.to_s),
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
relation.extend_destinations.each do |dst|
|
48
|
+
graph_viz.link(
|
49
|
+
source: src,
|
50
|
+
destination: graph_viz.find_or_create_node(dst.to_s),
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def object_with_ancestor_modules(object)
|
57
|
+
[object].concat(object.ancestors.select {|o| o.instance_of?(Module) })
|
58
|
+
end
|
59
|
+
|
60
|
+
def output_diagram
|
61
|
+
file_type = File.extname(output_path).delete('.').to_sym
|
62
|
+
|
63
|
+
graph_viz.output(file_type: file_type, file_path: output_path)
|
64
|
+
end
|
65
|
+
|
66
|
+
def graph_viz
|
67
|
+
@graph_viz ||= AncestorsVisualization::GraphViz.new
|
68
|
+
end
|
69
|
+
|
70
|
+
class ObjectRelation
|
71
|
+
def initialize(object, target_objects)
|
72
|
+
@object = object
|
73
|
+
@target_objects = target_objects
|
74
|
+
end
|
75
|
+
|
76
|
+
def source
|
77
|
+
return nil unless under_target_namespaces?(object)
|
78
|
+
|
79
|
+
@source ||= object
|
80
|
+
end
|
81
|
+
|
82
|
+
def destinations
|
83
|
+
@destinations ||= begin
|
84
|
+
dsts = ancestors_without_self(object)
|
85
|
+
|
86
|
+
ancestors_without_self(object).each do |klass|
|
87
|
+
dsts -= ancestors_without_self(klass)
|
88
|
+
end
|
89
|
+
|
90
|
+
dsts.select {|o| under_target_namespaces?(o) }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def extend_destinations
|
95
|
+
@extend_destinations ||= begin
|
96
|
+
dsts = extended_modules(object)
|
97
|
+
|
98
|
+
ancestors_without_self(object).each do |klass|
|
99
|
+
dsts -= extended_modules(klass)
|
100
|
+
end
|
101
|
+
|
102
|
+
dsts.select {|o| under_target_namespaces?(o) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
attr_reader :object, :target_objects
|
109
|
+
|
110
|
+
def under_target_namespaces?(object)
|
111
|
+
target_namespaces.include?(object.to_s.split('::').first)
|
112
|
+
end
|
113
|
+
|
114
|
+
def target_namespaces
|
115
|
+
@target_namespaces ||= target_objects.map{|o| o.to_s.split('::').first }.compact_blank.uniq
|
116
|
+
end
|
117
|
+
|
118
|
+
def ancestors_without_self(klass)
|
119
|
+
klass.ancestors - [klass]
|
120
|
+
end
|
121
|
+
|
122
|
+
def extended_modules(object)
|
123
|
+
target_modules.select {|m| extend?(object, m) }
|
124
|
+
end
|
125
|
+
|
126
|
+
def target_modules
|
127
|
+
@target_modules ||= target_objects.select {|o| o.instance_of?(Module) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def extend?(src, dst)
|
131
|
+
src.singleton_class.include?(dst)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ruby-graphviz'
|
4
|
+
|
5
|
+
module AncestorsVisualization
|
6
|
+
class GraphViz
|
7
|
+
GRAPHVIZ_SETTING = {
|
8
|
+
use: :dot,
|
9
|
+
type: :digraph,
|
10
|
+
rankdir: :LR,
|
11
|
+
ranksep: 0.5,
|
12
|
+
nodesep: 0.5,
|
13
|
+
pad: "0.4,0.4",
|
14
|
+
margin: "0,0",
|
15
|
+
concentrate: true,
|
16
|
+
labelloc: :t,
|
17
|
+
fontsize: 13,
|
18
|
+
splines: 'spline', # NOTE https://graphviz.org/docs/attrs/splines/
|
19
|
+
}
|
20
|
+
|
21
|
+
GRAPH_SETTING = {
|
22
|
+
labelloc: "t",
|
23
|
+
labeljust: "l",
|
24
|
+
fillcolor: "#888888"
|
25
|
+
}
|
26
|
+
|
27
|
+
EDGE_SETTING = {
|
28
|
+
color: '#444444'
|
29
|
+
}
|
30
|
+
|
31
|
+
NODE_SETTING = {
|
32
|
+
style: "filled",
|
33
|
+
fontname: "Helvetica Neue"
|
34
|
+
}
|
35
|
+
|
36
|
+
OTHER_NODE_COLOR = '#f2f2f2'
|
37
|
+
CLASS_NODE_COLOR = '#c4ddec'
|
38
|
+
MODULE_NODE_COLOR = '#ecd3c4'
|
39
|
+
|
40
|
+
EXPECTED_FILE_TYPE = [
|
41
|
+
:pdf,
|
42
|
+
:png,
|
43
|
+
:jpg,
|
44
|
+
:svg
|
45
|
+
]
|
46
|
+
|
47
|
+
NAME_SPACE_DELIMITER = '::'
|
48
|
+
|
49
|
+
def find_or_create_node(klass_or_module_path_name)
|
50
|
+
name_space = klass_or_module_path_name.deconstantize
|
51
|
+
|
52
|
+
add_graph(name_space)
|
53
|
+
|
54
|
+
target_graph = name_space.split(NAME_SPACE_DELIMITER).inject(graph_viz) {|graph, name| graph.get_graph(cluster_name(name)) }
|
55
|
+
|
56
|
+
if (existing_node = target_graph.find_node(klass_or_module_path_name)).present?
|
57
|
+
existing_node
|
58
|
+
else
|
59
|
+
klass_or_module_name = klass_or_module_path_name.split(NAME_SPACE_DELIMITER).last
|
60
|
+
|
61
|
+
target_graph.add_nodes(klass_or_module_path_name, label: klass_or_module_name, fillcolor: node_color(klass_or_module_path_name), **NODE_SETTING)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def link(source:, destination:)
|
66
|
+
return if source.neighbors.present? && source.neighbors.map(&:id).include?(destination.id)
|
67
|
+
|
68
|
+
graph_viz.add_edges(source, destination, EDGE_SETTING)
|
69
|
+
end
|
70
|
+
|
71
|
+
def output(file_type:, file_path:)
|
72
|
+
raise ArgumentError, "file_type is #{file_type}. file_type must be #{EXPECTED_FILE_TYPE.join(',')}." if EXPECTED_FILE_TYPE.exclude?(file_type)
|
73
|
+
|
74
|
+
graph_viz.output(file_type => file_path)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def add_graph(name_space)
|
80
|
+
return if name_space.blank?
|
81
|
+
|
82
|
+
parent_graph = graph_viz
|
83
|
+
|
84
|
+
name_space.split(NAME_SPACE_DELIMITER).each do |part_name_space|
|
85
|
+
if existing_graph = parent_graph.get_graph(cluster_name(part_name_space))
|
86
|
+
parent_graph = existing_graph
|
87
|
+
|
88
|
+
next
|
89
|
+
end
|
90
|
+
|
91
|
+
parent_graph = parent_graph.add_graph(cluster_name(part_name_space), label: part_name_space, **GRAPH_SETTING)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def cluster_name(name)
|
96
|
+
"cluster_#{name}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def node_color(object)
|
100
|
+
case object.constantize.class.to_s
|
101
|
+
when 'Class'
|
102
|
+
CLASS_NODE_COLOR
|
103
|
+
when 'Module'
|
104
|
+
MODULE_NODE_COLOR
|
105
|
+
else
|
106
|
+
OTHER_NODE_COLOR
|
107
|
+
end
|
108
|
+
rescue
|
109
|
+
OTHER_NODE_COLOR
|
110
|
+
end
|
111
|
+
|
112
|
+
def graph_viz
|
113
|
+
@graph_viz ||= ::GraphViz.new(:G, GRAPHVIZ_SETTING)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AncestorsVisualization
|
4
|
+
class TargetObjectFetcher
|
5
|
+
attr_accessor :require_failed_files
|
6
|
+
|
7
|
+
def initialize(gem_name)
|
8
|
+
raise ArgumentError, "#{gem_name} is not found." unless exists_gem?(gem_name)
|
9
|
+
|
10
|
+
@gem_name = gem_name
|
11
|
+
@require_failed_files = Set.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch
|
15
|
+
require_gem
|
16
|
+
|
17
|
+
fetch_gem_object
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :gem_name
|
23
|
+
|
24
|
+
def exists_gem?(gem_name)
|
25
|
+
result = exec_command('bundle list --name-only')
|
26
|
+
|
27
|
+
result.split("\n").include?(gem_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def exec_command(command)
|
31
|
+
result = `#{command}`
|
32
|
+
|
33
|
+
raise RuntimeError, "`#{command}` is failed." unless $?.success?
|
34
|
+
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
def require_gem
|
39
|
+
Dir.glob("#{gem_load_path}/**/*.rb").each do |file|
|
40
|
+
begin
|
41
|
+
require file
|
42
|
+
rescue Exception
|
43
|
+
# NOTE オプション扱いで別 Gem に依存しているケース等、読み込めないことがある
|
44
|
+
require_failed_files << file
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def gem_load_path
|
50
|
+
@gem_load_path = begin
|
51
|
+
result = exec_command("bundle info #{gem_name} --path")
|
52
|
+
|
53
|
+
# TODO 対象 Gem の設定を参照できればそう修正する
|
54
|
+
load_dir = 'lib'
|
55
|
+
|
56
|
+
"#{result.chomp}/#{load_dir}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def fetch_gem_object
|
61
|
+
ObjectSpace.each_object(Module).select do |object|
|
62
|
+
fetch_target?(object)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def fetch_target?(object)
|
67
|
+
top_namespace_name = object.to_s.split('::').first.underscore
|
68
|
+
|
69
|
+
# NOTE lib 直下のファイル・ディレクトリ名から名前空間を推定しているため、名前空間が正しい保証はない
|
70
|
+
file_and_dir_names.any? {|n| n == top_namespace_name }
|
71
|
+
rescue
|
72
|
+
# NOTE object.to_s が未定義でエラーになることがある
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
def file_and_dir_names
|
77
|
+
@file_and_dir_names ||= Dir.glob("#{gem_load_path}/*").map {|path| File.basename(path, '.*') }.uniq
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
metadata
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ancestors_visualization
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- tommy-012
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-05-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry-byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: timecop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: choice
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activesupport
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: ruby-graphviz
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Automatically generate an ancestors-relationship diagram for the gem.
|
112
|
+
email:
|
113
|
+
- lonnlilonn@googlemail.com
|
114
|
+
executables:
|
115
|
+
- ancestors_visualization
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- ".gitignore"
|
120
|
+
- ".rspec"
|
121
|
+
- ".travis.yml"
|
122
|
+
- CODE_OF_CONDUCT.md
|
123
|
+
- Gemfile
|
124
|
+
- LICENSE.txt
|
125
|
+
- README.md
|
126
|
+
- Rakefile
|
127
|
+
- ancestors_visualization.gemspec
|
128
|
+
- bin/console
|
129
|
+
- bin/setup
|
130
|
+
- exe/ancestors_visualization
|
131
|
+
- lib/ancestors_visualization.rb
|
132
|
+
- lib/ancestors_visualization/cli.rb
|
133
|
+
- lib/ancestors_visualization/diagram_creater.rb
|
134
|
+
- lib/ancestors_visualization/graph_viz.rb
|
135
|
+
- lib/ancestors_visualization/target_object_fetcher.rb
|
136
|
+
- lib/ancestors_visualization/version.rb
|
137
|
+
homepage: https://github.com/tommy-012/ancestors_visualization
|
138
|
+
licenses:
|
139
|
+
- MIT
|
140
|
+
metadata: {}
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: 2.3.0
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubygems_version: 3.1.6
|
157
|
+
signing_key:
|
158
|
+
specification_version: 4
|
159
|
+
summary: ancestors-relationship diagram for the gem.
|
160
|
+
test_files: []
|