visualize_packwerk 0.0.1
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/README.md +32 -0
- data/lib/visualize_packwerk/graph_interface.rb +17 -0
- data/lib/visualize_packwerk/node_interface.rb +29 -0
- data/lib/visualize_packwerk/package_graph.rb +48 -0
- data/lib/visualize_packwerk/package_node.rb +28 -0
- data/lib/visualize_packwerk/package_relationships.rb +158 -0
- data/lib/visualize_packwerk/railtie.rb +15 -0
- data/lib/visualize_packwerk/tasks/visualize_packwerk.rake +73 -0
- data/lib/visualize_packwerk/team_graph.rb +52 -0
- data/lib/visualize_packwerk/team_node.rb +27 -0
- data/lib/visualize_packwerk.rb +17 -0
- data/sorbet/config +2 -0
- data/sorbet/rbi/gems/activesupport@7.0.3.1.rbi +76 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +522 -0
- data/sorbet/rbi/gems/code_ownership@1.28.0.rbi +411 -0
- data/sorbet/rbi/gems/code_teams@1.0.0.rbi +138 -0
- data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
- data/sorbet/rbi/gems/concurrent-ruby@1.1.10.rbi +8 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +866 -0
- data/sorbet/rbi/gems/i18n@1.12.0.rbi +8 -0
- data/sorbet/rbi/gems/json@2.6.2.rbi +1423 -0
- data/sorbet/rbi/gems/method_source@1.0.0.rbi +8 -0
- data/sorbet/rbi/gems/minitest@5.16.2.rbi +9 -0
- data/sorbet/rbi/gems/package_protections@1.3.0.rbi +654 -0
- data/sorbet/rbi/gems/parallel@1.22.1.rbi +163 -0
- data/sorbet/rbi/gems/parse_packwerk@0.11.0.rbi +148 -0
- data/sorbet/rbi/gems/parser@3.1.2.0.rbi +4261 -0
- data/sorbet/rbi/gems/pry@0.14.1.rbi +8 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +8 -0
- data/sorbet/rbi/gems/rake@13.0.6.rbi +1854 -0
- data/sorbet/rbi/gems/rbi@0.0.15.rbi +2340 -0
- data/sorbet/rbi/gems/regexp_parser@2.5.0.rbi +8 -0
- data/sorbet/rbi/gems/rexml@3.2.5.rbi +8 -0
- data/sorbet/rbi/gems/rspec-core@3.11.0.rbi +7698 -0
- data/sorbet/rbi/gems/rspec-expectations@3.11.0.rbi +6201 -0
- data/sorbet/rbi/gems/rspec-mocks@3.11.1.rbi +3625 -0
- data/sorbet/rbi/gems/rspec-support@3.11.0.rbi +1176 -0
- data/sorbet/rbi/gems/rspec@3.11.0.rbi +40 -0
- data/sorbet/rbi/gems/rubocop-ast@1.19.1.rbi +8 -0
- data/sorbet/rbi/gems/rubocop-sorbet@0.6.11.rbi +8 -0
- data/sorbet/rbi/gems/rubocop@1.33.0.rbi +8 -0
- data/sorbet/rbi/gems/ruby-graphviz@1.2.5.rbi +840 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +8 -0
- data/sorbet/rbi/gems/spoom@1.1.11.rbi +1600 -0
- data/sorbet/rbi/gems/tapioca@0.8.3.rbi +1978 -0
- data/sorbet/rbi/gems/thor@1.2.1.rbi +2921 -0
- data/sorbet/rbi/gems/tzinfo@2.0.5.rbi +8 -0
- data/sorbet/rbi/gems/unicode-display_width@2.2.0.rbi +8 -0
- data/sorbet/rbi/gems/unparser@0.6.5.rbi +8 -0
- data/sorbet/rbi/gems/webrick@1.7.0.rbi +1802 -0
- data/sorbet/rbi/gems/yard-sorbet@0.6.1.rbi +288 -0
- data/sorbet/rbi/gems/yard@0.9.28.rbi +12863 -0
- data/sorbet/tapioca/config.yml +13 -0
- data/sorbet/tapioca/require.rb +7 -0
- metadata +241 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e70be5b42437ab3ffacbbff1a3ec57c1116b8bc5fd1e4fc91163631414a35dae
|
4
|
+
data.tar.gz: 7cc7f753055f8f655d0787f3452f3b600eef10182689d637ac2a49eaf96706ff
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fe981d2228c1b9039020e5b715cb578b3c5a5a8f6bbade61513151d1a770d06eb327463563b96468e00e0359e7aa6fcadf991d10f03ddec1e923396266c008c0
|
7
|
+
data.tar.gz: 8de59866b806300164ccd476d9e481b7aba1ff328d746c246aec49fe3d65c353511dc2db8713807787b1e991c6fcf8040281fee7ff03766c821111950373b1c2
|
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# visualize_packwerk
|
2
|
+
|
3
|
+
This pack contains rake tasks to help visualize relationships between packwerk packs.
|
4
|
+
|
5
|
+
# Usage
|
6
|
+
## Building a package graph for a selection of packages (owned by 5 teams max)
|
7
|
+
```
|
8
|
+
bin/rails visualize_packwerk:package_relationships['packs/pack1','packs/pack2']
|
9
|
+
```
|
10
|
+
|
11
|
+
# Building a package graph for specific teams (5 teams max)
|
12
|
+
```
|
13
|
+
bin/rails visualize_packwerk:package_relationships_for_teams['Team1','Team2']
|
14
|
+
```
|
15
|
+
|
16
|
+
# Building a package graph for all packages (this is slow and produces a huge file)
|
17
|
+
```
|
18
|
+
bin/rails visualize_packwerk:package_relationships
|
19
|
+
```
|
20
|
+
|
21
|
+
# Building a TEAM graph for specific teams
|
22
|
+
```
|
23
|
+
bin/rails visualize_packwerk:team_relationships['Team1','Team2']
|
24
|
+
```
|
25
|
+
|
26
|
+
# Building a TEAM graph for all teams (this is slow and produces a huge file)
|
27
|
+
```
|
28
|
+
bin/rails visualize_packwerk:team_relationships
|
29
|
+
```
|
30
|
+
|
31
|
+
# Want to change something or add a feature?
|
32
|
+
Submit a PR or post an issue!
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module VisualizePackwerk
|
4
|
+
# This stores graphviz-independent views of our package graph.
|
5
|
+
# It should be optimized for fast lookup (leveraging internal indexes, which are stable due to the immutability of the package nodes)
|
6
|
+
# A `TeamGraph` should be able to consume this and basically just create a reduced version
|
7
|
+
# Lastly, each one should implement a common interface, and graphviz should use that interface and take in either types of graph via the interface
|
8
|
+
module GraphInterface
|
9
|
+
extend T::Sig
|
10
|
+
extend T::Helpers
|
11
|
+
interface!
|
12
|
+
|
13
|
+
sig { abstract.returns(T::Set[NodeInterface]) }
|
14
|
+
def nodes
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module VisualizePackwerk
|
4
|
+
module NodeInterface
|
5
|
+
extend T::Sig
|
6
|
+
extend T::Helpers
|
7
|
+
interface!
|
8
|
+
|
9
|
+
sig { abstract.returns(String) }
|
10
|
+
def name
|
11
|
+
end
|
12
|
+
|
13
|
+
sig { abstract.returns(String) }
|
14
|
+
def group_name
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { abstract.returns(T::Hash[String, Integer]) }
|
18
|
+
def violations_by_node_name
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { abstract.returns(T::Array[String]) }
|
22
|
+
def dependencies
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { abstract.params(node_name: String).returns(T::Boolean) }
|
26
|
+
def depends_on?(node_name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module VisualizePackwerk
|
4
|
+
class PackageGraph
|
5
|
+
extend T::Sig
|
6
|
+
include GraphInterface
|
7
|
+
|
8
|
+
sig { returns(T::Set[PackageNode]) }
|
9
|
+
attr_reader :package_nodes
|
10
|
+
|
11
|
+
sig { override.returns(T::Set[NodeInterface]) }
|
12
|
+
def nodes
|
13
|
+
package_nodes
|
14
|
+
end
|
15
|
+
|
16
|
+
sig { params(package_nodes: T::Set[PackageNode]).void }
|
17
|
+
def initialize(package_nodes:)
|
18
|
+
@package_nodes = package_nodes
|
19
|
+
@index_by_name = T.let({}, T::Hash[String, PackageNode])
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { returns(PackageGraph) }
|
23
|
+
def self.construct
|
24
|
+
package_nodes = Set.new
|
25
|
+
ParsePackwerk.all.each do |p|
|
26
|
+
# We could consider ignoring the root!
|
27
|
+
# We would also need to ignore it when parsing PackageNodes.
|
28
|
+
# next if p.name == ParsePackwerk::ROOT_PACKAGE_NAME
|
29
|
+
owner = CodeOwnership.for_package(p)
|
30
|
+
violations_by_package = PackageProtections::ProtectedPackage.from(p).violations.group_by(&:to_package_name).transform_values(&:count)
|
31
|
+
|
32
|
+
package_nodes << PackageNode.new(
|
33
|
+
name: p.name,
|
34
|
+
team_name: owner&.name || 'Unknown',
|
35
|
+
violations_by_package: violations_by_package,
|
36
|
+
dependencies: Set.new(p.dependencies)
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
PackageGraph.new(package_nodes: package_nodes)
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { params(name: String).returns(PackageNode) }
|
44
|
+
def package_by_name(name)
|
45
|
+
@index_by_name[name] ||= T.must(package_nodes.find { |node| node.name == name })
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module VisualizePackwerk
|
4
|
+
class PackageNode < T::Struct
|
5
|
+
extend T::Sig
|
6
|
+
include NodeInterface
|
7
|
+
|
8
|
+
const :name, String
|
9
|
+
const :team_name, String
|
10
|
+
const :violations_by_package, T::Hash[String, Integer]
|
11
|
+
const :dependencies, T::Set[String]
|
12
|
+
|
13
|
+
sig { override.returns(T::Hash[String, Integer]) }
|
14
|
+
def violations_by_node_name
|
15
|
+
violations_by_package
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { override.returns(String) }
|
19
|
+
def group_name
|
20
|
+
team_name
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { override.params(node_name: String).returns(T::Boolean) }
|
24
|
+
def depends_on?(node_name)
|
25
|
+
dependencies.include?(node_name) || (violations_by_package[node_name] || 0) > 0
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module VisualizePackwerk
|
4
|
+
class PackageRelationships
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
OUTPUT_FILENAME = T.let('packwerk.png'.freeze, String)
|
8
|
+
|
9
|
+
sig { void }
|
10
|
+
def initialize
|
11
|
+
@colors_by_team = T.let({}, T::Hash[String, String])
|
12
|
+
@remaining_colors = T.let(
|
13
|
+
[
|
14
|
+
# Found using https://htmlcolorcodes.com/color-picker/
|
15
|
+
'#77EE77', # green
|
16
|
+
'#DFEE77', # yellow
|
17
|
+
'#77EEE6', # teal
|
18
|
+
'#EEC077', # orange
|
19
|
+
'#EE77BF', # pink
|
20
|
+
'#EE6F6F', # red
|
21
|
+
'#ED6EDE', # magenta
|
22
|
+
'#8E8CFE', # blue
|
23
|
+
'#EEA877', # red-orange
|
24
|
+
], T::Array[String]
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { params(teams: T::Array[CodeTeams::Team]).void }
|
29
|
+
def create_package_graph_for_teams!(teams)
|
30
|
+
packages = ParsePackwerk.all.select do |package|
|
31
|
+
teams.map(&:name).include?(CodeOwnership.for_package(package)&.name)
|
32
|
+
end
|
33
|
+
|
34
|
+
create_package_graph!(packages)
|
35
|
+
end
|
36
|
+
|
37
|
+
sig { params(teams: T::Array[CodeTeams::Team], show_all_teams: T::Boolean).void }
|
38
|
+
def create_team_graph!(teams, show_all_teams: false)
|
39
|
+
package_graph = PackageGraph.construct
|
40
|
+
team_graph = TeamGraph.from_package_graph(package_graph)
|
41
|
+
highlighted_node_names = teams.map(&:name)
|
42
|
+
|
43
|
+
draw_graph!(team_graph, highlighted_node_names, show_all_nodes: show_all_teams)
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { params(packages: T::Array[ParsePackwerk::Package], show_all_packs: T::Boolean).void }
|
47
|
+
def create_package_graph!(packages, show_all_packs: false)
|
48
|
+
graph = PackageGraph.construct
|
49
|
+
highlighted_node_names = packages.map(&:name)
|
50
|
+
draw_graph!(graph, highlighted_node_names, show_all_nodes: show_all_packs)
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { params(packages: T::Array[ParsePackwerk::Package], show_all_nodes: T::Boolean).void }
|
54
|
+
def create_graph!(packages, show_all_nodes: false)
|
55
|
+
graph = PackageGraph.construct
|
56
|
+
highlighted_node_names = packages.map(&:name)
|
57
|
+
draw_graph!(graph, highlighted_node_names, show_all_nodes: show_all_nodes)
|
58
|
+
end
|
59
|
+
|
60
|
+
sig { params(graph: GraphInterface, highlighted_node_names: T::Array[String], show_all_nodes: T::Boolean).void }
|
61
|
+
def draw_graph!(graph, highlighted_node_names, show_all_nodes: false)
|
62
|
+
# SFDP looks better than dot in some cases, but less good in other cases.
|
63
|
+
# If your visualization looks bad, change the layout to other_layout!
|
64
|
+
# https://graphviz.org/docs/layouts/
|
65
|
+
default_layout = :dot
|
66
|
+
# other_layout = :sfdp
|
67
|
+
graphviz_graph = GraphViz.new(:G, type: :digraph, dpi: 100, layout: default_layout)
|
68
|
+
|
69
|
+
# Create graph nodes
|
70
|
+
graphviz_nodes = T.let({}, T::Hash[String, GraphViz::Node])
|
71
|
+
|
72
|
+
nodes_to_draw = graph.nodes
|
73
|
+
|
74
|
+
nodes_to_draw.each do |node|
|
75
|
+
next unless highlighted_node_names.any? { |highlighted_node_name| node.depends_on?(highlighted_node_name) } || highlighted_node_names.include?(node.name)
|
76
|
+
|
77
|
+
highlight_node = highlighted_node_names.include?(node.name) && !show_all_nodes
|
78
|
+
graphviz_nodes[node.name] = add_node(node, graphviz_graph, highlight_node)
|
79
|
+
end
|
80
|
+
|
81
|
+
max_edge_width = 10
|
82
|
+
|
83
|
+
# Draw all edges
|
84
|
+
nodes_to_draw.each do |node|
|
85
|
+
node.dependencies.each do |to_node|
|
86
|
+
next unless highlighted_node_names.include?(to_node)
|
87
|
+
|
88
|
+
graphviz_graph.add_edges(
|
89
|
+
graphviz_nodes[node.name],
|
90
|
+
graphviz_nodes[to_node],
|
91
|
+
{ color: 'darkgreen' }
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
node.violations_by_node_name.each do |to_node_name, violation_count|
|
96
|
+
next unless highlighted_node_names.include?(to_node_name)
|
97
|
+
|
98
|
+
edge_width = [
|
99
|
+
[(violation_count / 5).to_i, 1].max, # rubocop:disable Lint/NumberConversion
|
100
|
+
max_edge_width,
|
101
|
+
].min
|
102
|
+
|
103
|
+
graphviz_graph.add_edges(
|
104
|
+
graphviz_nodes[node.name],
|
105
|
+
graphviz_nodes[to_node_name],
|
106
|
+
{ color: 'red', penwidth: edge_width }
|
107
|
+
)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Save graph to filesystem
|
112
|
+
puts "Outputting to: #{OUTPUT_FILENAME}"
|
113
|
+
graphviz_graph.output(png: OUTPUT_FILENAME)
|
114
|
+
puts 'Finished!'
|
115
|
+
end
|
116
|
+
|
117
|
+
sig { params(node: NodeInterface, graph: GraphViz, highlight_node: T::Boolean).returns(GraphViz::Node) }
|
118
|
+
def add_node(node, graph, highlight_node)
|
119
|
+
default_node_options = {
|
120
|
+
fontsize: 26.0,
|
121
|
+
fontcolor: 'white',
|
122
|
+
fillcolor: 'black',
|
123
|
+
color: 'black',
|
124
|
+
height: 1.0,
|
125
|
+
style: 'filled, rounded',
|
126
|
+
shape: 'box',
|
127
|
+
}
|
128
|
+
|
129
|
+
node_options = if highlight_node
|
130
|
+
default_node_options.merge(
|
131
|
+
fillcolor: highlight_by_group(node),
|
132
|
+
color: highlight_by_group(node),
|
133
|
+
fontcolor: 'black'
|
134
|
+
)
|
135
|
+
else
|
136
|
+
default_node_options
|
137
|
+
end
|
138
|
+
|
139
|
+
graph.add_nodes(node.name, **node_options)
|
140
|
+
end
|
141
|
+
|
142
|
+
sig { params(node: NodeInterface).returns(String) }
|
143
|
+
def highlight_by_group(node)
|
144
|
+
highlighted_package_color = @colors_by_team[node.group_name]
|
145
|
+
if !highlighted_package_color
|
146
|
+
highlighted_package_color = @remaining_colors.first
|
147
|
+
raise 'Can only color nodes a max of 5 unique colors for now' if highlighted_package_color.nil?
|
148
|
+
|
149
|
+
@remaining_colors.delete(highlighted_package_color)
|
150
|
+
@colors_by_team[node.group_name] = highlighted_package_color
|
151
|
+
end
|
152
|
+
|
153
|
+
highlighted_package_color
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private_constant :PackageRelationships
|
158
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# typed: ignore
|
2
|
+
|
3
|
+
require 'visualize_packwerk'
|
4
|
+
require 'rails'
|
5
|
+
|
6
|
+
module VisualizePackwerk
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
railtie_name :visualize_packwerk
|
9
|
+
|
10
|
+
rake_tasks do
|
11
|
+
path = File.expand_path(__dir__)
|
12
|
+
Dir.glob("#{path}/tasks/visualize_packwerk.rake").each { |f| load f }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module VisualizePackwerk
|
4
|
+
class TaskLoader
|
5
|
+
include Rake::DSL
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { void }
|
9
|
+
def create_tasks!
|
10
|
+
namespace(:visualize_packwerk) do
|
11
|
+
# This creates the array of symbols that are needed to declare an argument to a rake task
|
12
|
+
package_args = (1..100).map { |i| "package#{i}".to_sym }
|
13
|
+
|
14
|
+
desc('Graph packages')
|
15
|
+
task(:package_relationships, package_args => :environment) do |_task, args|
|
16
|
+
show_all_packs = args.to_hash.values.none?
|
17
|
+
packages = if show_all_packs
|
18
|
+
ParsePackwerk.all
|
19
|
+
else
|
20
|
+
args.to_hash.values.map do |pack_name|
|
21
|
+
found_package = ParsePackwerk.all.find { |p| p.name == pack_name }
|
22
|
+
if found_package.nil?
|
23
|
+
abort "Could not find pack with name: #{pack_name}"
|
24
|
+
end
|
25
|
+
|
26
|
+
found_package
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
PackageRelationships.new.create_package_graph!(packages, show_all_packs: show_all_packs)
|
31
|
+
end
|
32
|
+
|
33
|
+
# This creates the array of symbols that are needed to declare an argument to a rake task
|
34
|
+
team_args = (1..5).map { |i| "team#{i}".to_sym }
|
35
|
+
|
36
|
+
desc('Graph packages for teams')
|
37
|
+
task(:package_relationships_for_teams, team_args => :environment) do |_task, args|
|
38
|
+
teams = args.to_hash.values.map do |team_name|
|
39
|
+
team = CodeTeams.find(team_name)
|
40
|
+
if team.nil?
|
41
|
+
abort("Could not find team with name: #{team_name}. Check your config/teams/subdirectory/team.yml for correct team spelling, e.g. `Product Infrastructure`")
|
42
|
+
end
|
43
|
+
|
44
|
+
team
|
45
|
+
end
|
46
|
+
|
47
|
+
PackageRelationships.new.create_package_graph_for_teams!(teams)
|
48
|
+
end
|
49
|
+
|
50
|
+
desc('Graph team relationships')
|
51
|
+
task(:team_relationships, team_args => :environment) do |_task, args|
|
52
|
+
show_all_teams = args.to_hash.values.none?
|
53
|
+
teams = if show_all_teams
|
54
|
+
CodeTeams.all
|
55
|
+
else
|
56
|
+
args.to_hash.values.map do |team_name|
|
57
|
+
team = CodeTeams.find(team_name)
|
58
|
+
if team.nil?
|
59
|
+
abort("Could not find team with name: #{team_name}. Check your config/teams/subdirectory/team.yml for correct team spelling, e.g. `Product Infrastructure`")
|
60
|
+
end
|
61
|
+
|
62
|
+
team
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
PackageRelationships.new.create_team_graph!(teams, show_all_teams: show_all_teams)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
VisualizePackwerk::TaskLoader.new.create_tasks!
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module VisualizePackwerk
|
4
|
+
#
|
5
|
+
# A team graph reduces a PackageGraph by aggregating over packages owned by teams
|
6
|
+
#
|
7
|
+
class TeamGraph
|
8
|
+
extend T::Sig
|
9
|
+
include GraphInterface
|
10
|
+
|
11
|
+
sig { override.returns(T::Set[NodeInterface]) }
|
12
|
+
def nodes
|
13
|
+
@team_nodes
|
14
|
+
end
|
15
|
+
|
16
|
+
sig { params(team_nodes: T::Set[TeamNode]).void }
|
17
|
+
def initialize(team_nodes:)
|
18
|
+
@team_nodes = team_nodes
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { params(package_graph: PackageGraph).returns(TeamGraph) }
|
22
|
+
def self.from_package_graph(package_graph)
|
23
|
+
team_nodes = T.let(Set.new, T::Set[TeamNode])
|
24
|
+
package_graph.package_nodes.group_by(&:team_name).each do |team, package_nodes_for_team|
|
25
|
+
violations_by_team = {}
|
26
|
+
package_nodes_for_team.map(&:violations_by_package).each do |new_violations_by_package|
|
27
|
+
new_violations_by_package.each do |pack_name, count|
|
28
|
+
# We first get the pack owner of the violated package
|
29
|
+
other_team = package_graph.package_by_name(pack_name).team_name
|
30
|
+
violations_by_team[other_team] ||= 0
|
31
|
+
# Then we add the violations on that team together
|
32
|
+
# TODO: We may want to ignore this if team == other_team to avoid arrows pointing to self, but maybe not!
|
33
|
+
violations_by_team[other_team] += count
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
dependencies = Set.new
|
38
|
+
package_nodes_for_team.map(&:dependencies).reduce(Set.new, :+).each do |dependency|
|
39
|
+
dependencies << package_graph.package_by_name(dependency).team_name
|
40
|
+
end
|
41
|
+
|
42
|
+
team_nodes << TeamNode.new(
|
43
|
+
name: team,
|
44
|
+
violations_by_team: violations_by_team,
|
45
|
+
dependencies: dependencies
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
TeamGraph.new(team_nodes: team_nodes)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module VisualizePackwerk
|
4
|
+
class TeamNode < T::Struct
|
5
|
+
extend T::Sig
|
6
|
+
include NodeInterface
|
7
|
+
|
8
|
+
const :name, String
|
9
|
+
const :violations_by_team, T::Hash[String, Integer]
|
10
|
+
const :dependencies, T::Set[String]
|
11
|
+
|
12
|
+
sig { override.returns(T::Hash[String, Integer]) }
|
13
|
+
def violations_by_node_name
|
14
|
+
violations_by_team
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { override.returns(String) }
|
18
|
+
def group_name
|
19
|
+
name
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { override.params(node_name: String).returns(T::Boolean) }
|
23
|
+
def depends_on?(node_name)
|
24
|
+
dependencies.include?(node_name) || (violations_by_node_name[node_name] || 0) > 0
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module VisualizePackwerk
|
4
|
+
require 'visualize_packwerk/railtie' if defined?(Rails)
|
5
|
+
require 'parse_packwerk'
|
6
|
+
require 'code_ownership'
|
7
|
+
require 'package_protections'
|
8
|
+
require 'graphviz'
|
9
|
+
|
10
|
+
require 'visualize_packwerk/node_interface'
|
11
|
+
require 'visualize_packwerk/graph_interface'
|
12
|
+
require 'visualize_packwerk/team_node'
|
13
|
+
require 'visualize_packwerk/package_node'
|
14
|
+
require 'visualize_packwerk/team_graph'
|
15
|
+
require 'visualize_packwerk/package_graph'
|
16
|
+
require 'visualize_packwerk/package_relationships'
|
17
|
+
end
|
data/sorbet/config
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# typed: false
|
2
|
+
|
3
|
+
# DO NOT EDIT MANUALLY
|
4
|
+
# This is an autogenerated file for types exported from the `activesupport` gem.
|
5
|
+
# Please instead update this file by running `bin/tapioca gem activesupport`.
|
6
|
+
|
7
|
+
class FalseClass
|
8
|
+
include ::JSON::Ext::Generator::GeneratorMethods::FalseClass
|
9
|
+
end
|
10
|
+
|
11
|
+
class Float < ::Numeric
|
12
|
+
include ::JSON::Ext::Generator::GeneratorMethods::Float
|
13
|
+
end
|
14
|
+
|
15
|
+
class IO
|
16
|
+
include ::Enumerable
|
17
|
+
include ::File::Constants
|
18
|
+
end
|
19
|
+
|
20
|
+
class IO::ConsoleMode
|
21
|
+
def echo=(_arg0); end
|
22
|
+
def raw(*_arg0); end
|
23
|
+
def raw!(*_arg0); end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def initialize_copy(_arg0); end
|
28
|
+
end
|
29
|
+
|
30
|
+
class IO::EAGAINWaitReadable < ::Errno::EAGAIN
|
31
|
+
include ::IO::WaitReadable
|
32
|
+
end
|
33
|
+
|
34
|
+
class IO::EAGAINWaitWritable < ::Errno::EAGAIN
|
35
|
+
include ::IO::WaitWritable
|
36
|
+
end
|
37
|
+
|
38
|
+
class IO::EINPROGRESSWaitReadable < ::Errno::EINPROGRESS
|
39
|
+
include ::IO::WaitReadable
|
40
|
+
end
|
41
|
+
|
42
|
+
class IO::EINPROGRESSWaitWritable < ::Errno::EINPROGRESS
|
43
|
+
include ::IO::WaitWritable
|
44
|
+
end
|
45
|
+
|
46
|
+
IO::EWOULDBLOCKWaitReadable = IO::EAGAINWaitReadable
|
47
|
+
IO::EWOULDBLOCKWaitWritable = IO::EAGAINWaitWritable
|
48
|
+
|
49
|
+
class Integer < ::Numeric
|
50
|
+
include ::JSON::Ext::Generator::GeneratorMethods::Integer
|
51
|
+
end
|
52
|
+
|
53
|
+
class NameError < ::StandardError
|
54
|
+
include ::DidYouMean::Correctable
|
55
|
+
end
|
56
|
+
|
57
|
+
class NilClass
|
58
|
+
include ::JSON::Ext::Generator::GeneratorMethods::NilClass
|
59
|
+
end
|
60
|
+
|
61
|
+
class Numeric
|
62
|
+
include ::Comparable
|
63
|
+
end
|
64
|
+
|
65
|
+
class Symbol
|
66
|
+
include ::Comparable
|
67
|
+
end
|
68
|
+
|
69
|
+
class TrueClass
|
70
|
+
include ::JSON::Ext::Generator::GeneratorMethods::TrueClass
|
71
|
+
end
|
72
|
+
|
73
|
+
class URI::Generic
|
74
|
+
include ::URI::RFC2396_REGEXP
|
75
|
+
include ::URI
|
76
|
+
end
|