use_packwerk 0.50.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f4f9377346178fe79c699c9493993c0fb270170dcb200e1e443df752b6d7d32c
4
+ data.tar.gz: e9bf3060d53092534f2193fe93d68188cf750b2bbb824d60dc2c6ec6205c51f5
5
+ SHA512:
6
+ metadata.gz: 0a4ef1cc30ac3db3613a1cb5d294a9bdd5afa2bdf1c70cbfb47db4b592c98d01130d5a3722ac3ebad206c0d36a906016148304cbe84f6fa193186bd4d734a213
7
+ data.tar.gz: 0430dfe13a6d016ea79b499ad0c28a53fdfa02dbd211c3777a32aae6f8b96b7bc6694c4e03d84add447505080a6d428faac4fffa6d3d861049ba09ed5edc6113
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # UsePackwerk
2
+
3
+ UsePackwerk is a gem that helps in creating and maintaining packs. It exists to help perform some basic operations needed for pack setup and configuration. It provides a basic ruby file packager utility for [`packwerk`](https://github.com/Shopify/packwerk/). It assumes you are using [`stimpack`](https://github.com/rubyatscale/stimpack) to organize your packages.
4
+
5
+ ## Usage
6
+ ### General Help
7
+ `bin/use_packwerk --help`
8
+
9
+ ### Pack Creation
10
+ `bin/create_pack -n packs/your_pack_name_here`
11
+
12
+ ### Moving files to packs
13
+ `bin/move_to_pack -n packs/your_pack_name_here -f path/to/file.rb,path/to/directory`
14
+ This is used for moving files into a pack (the pack must already exist).
15
+ Note this works for moving files to packs from the monolith or from other packs
16
+
17
+ Make sure there are no spaces between the comma-separated list of paths of directories.
18
+
19
+ ### Moving a file to public API
20
+ `bin/make_public -f path/to/file.rb,path/to/directory`
21
+ This moves a file or directory to public API (that is -- the `app/public` folder).
22
+
23
+ Make sure there are no spaces between the comma-separated list of paths of directories.
24
+
25
+ ### Listing top privacy violations
26
+ `bin/list_top_privacy_violations -n packs/my_pack`
27
+ Want to create interfaces? Not sure how your pack's code is being used?
28
+
29
+ You can use this command to list the top privacy violations.
30
+
31
+ If no pack name is passed in, this will list out violations across all packs.
32
+
33
+ ### Listing top dependency violations
34
+ `bin/list_top_dependency_violations -n packs/my_pack`
35
+ Want to see who is depending on you? Not sure how your pack's code is being used in an unstated way
36
+
37
+ You can use this command to list the top dependency violations.
38
+
39
+ If no pack name is passed in, this will list out violations across all packs.
40
+
41
+ ### Adding a dependency
42
+ `bin/add_pack_dependency -n packs/my_pack -d packs/dependency_pack_name`
43
+
44
+ This can be used to quickly modify a `package.yml` file and add a dependency. It also cleans up the list of dependencies to sort the list and remove redundant entries.
45
+
46
+ ## Discussions, Issues, Questions, and More
47
+ To keep things organized, here are some recommended homes:
48
+
49
+ ### Issues:
50
+ https://github.com/Gusto/use_packwerk/issues
51
+
52
+ ### Questions:
53
+ https://github.com/Gusto/use_packwerk/discussions/categories/q-a
54
+
55
+ ### General discussions:
56
+ https://github.com/Gusto/use_packwerk/discussions/categories/general
57
+
58
+ ### Ideas, new features, requests for change:
59
+ https://github.com/Gusto/use_packwerk/discussions/categories/ideas
60
+
61
+ ### Showcasing your work:
62
+ https://github.com/Gusto/use_packwerk/discussions/categories/show-and-tell
data/bin/use_packwerk ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # typed: strict
3
+
4
+ require_relative '../lib/use_packwerk'
5
+ UsePackwerk::CLI.start(ARGV)
@@ -0,0 +1,71 @@
1
+ # typed: strict
2
+
3
+ require 'thor'
4
+
5
+ module UsePackwerk
6
+ class CLI < Thor
7
+ extend T::Sig
8
+
9
+ desc "create packs/your_pack", "Create pack with name packs/your_pack"
10
+ sig { params(pack_name: String).void }
11
+ def create(pack_name)
12
+ UsePackwerk.create_pack!(pack_name: pack_name)
13
+ end
14
+
15
+ desc "add_dependency packs/from_pack packs/to_pack", "Add packs/to_pack to packs/from_pack/package.yml list of dependencies"
16
+ long_desc <<~LONG_DESC
17
+ Use this to add a dependency between packs.
18
+
19
+ When you use bin/use_packwerk add_dependency packs/from_pack packs/to_pack, this command will
20
+ modify packs/from_pack/package.yml's list of dependencies and add packs/to_pack.
21
+
22
+ This command will also sort the list and make it unique.
23
+ LONG_DESC
24
+ sig { params(from_pack: String, to_pack: String).void }
25
+ def add_dependency(from_pack, to_pack)
26
+ UsePackwerk.add_dependency!(
27
+ pack_name: from_pack,
28
+ dependency_name: to_pack
29
+ )
30
+ end
31
+
32
+ desc "list_top_dependency_violations packs/your_pack", "List the top dependency violations of packs/your_pack"
33
+ option :limit, type: :numeric, default: 10, aliases: :l, banner: 'Specify the limit of constants to analyze'
34
+ sig { params(pack_name: String).void }
35
+ def list_top_dependency_violations(pack_name)
36
+ UsePackwerk.list_top_dependency_violations(
37
+ pack_name: pack_name,
38
+ limit: options[:limit]
39
+ )
40
+ end
41
+
42
+ desc "list_top_privacy_violations packs/your_pack", "List the top privacy violations of packs/your_pack"
43
+ option :limit, type: :numeric, default: 10, aliases: :l, banner: 'Specify the limit of constants to analyze'
44
+ sig { params(pack_name: String).void }
45
+ def list_top_privacy_violations(pack_name)
46
+ UsePackwerk.list_top_privacy_violations(
47
+ pack_name: pack_name,
48
+ limit: options[:limit]
49
+ )
50
+ end
51
+
52
+ desc "make_public path/to/file.rb path/to/directory", "Pass in a space-separated list of file or directory paths to make public"
53
+ sig { params(paths: String).void }
54
+ def make_public(*paths)
55
+ UsePackwerk.make_public!(
56
+ paths_relative_to_root: paths,
57
+ per_file_processors: [UsePackwerk::RubocopPostProcessor.new, UsePackwerk::CodeOwnershipPostProcessor.new],
58
+ )
59
+ end
60
+
61
+ desc "move packs/destination_pack path/to/file.rb path/to/directory", "Pass in a destination pack and a space-separated list of file or directory paths to move to the destination pack"
62
+ sig { params(pack_name: String, paths: String).void }
63
+ def move(pack_name, *paths)
64
+ UsePackwerk.move_to_pack!(
65
+ pack_name: pack_name,
66
+ paths_relative_to_root: paths,
67
+ per_file_processors: [UsePackwerk::RubocopPostProcessor.new, UsePackwerk::CodeOwnershipPostProcessor.new],
68
+ )
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,58 @@
1
+ # typed: strict
2
+
3
+ module UsePackwerk
4
+ class CodeOwnershipPostProcessor
5
+ include PerFileProcessorInterface
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def initialize
10
+ @teams = T.let([], T::Array[String])
11
+ @did_move_files = T.let(false, T::Boolean)
12
+ end
13
+
14
+ sig { override.params(file_move_operation: Private::FileMoveOperation).void }
15
+ def before_move_file!(file_move_operation)
16
+ relative_path_to_origin = file_move_operation.origin_pathname
17
+ relative_path_to_destination = file_move_operation.destination_pathname
18
+
19
+ code_owners_allow_list_file = Pathname.new('config/code_ownership.yml')
20
+
21
+ if code_owners_allow_list_file.exist?
22
+ UsePackwerk.replace_in_file(
23
+ file: code_owners_allow_list_file.to_s,
24
+ find: relative_path_to_origin,
25
+ replace_with: relative_path_to_destination,
26
+ )
27
+ end
28
+
29
+ team = CodeOwnership.for_file(relative_path_to_origin.to_s)
30
+
31
+ if team
32
+ @teams << team.name
33
+ else
34
+ @teams << 'Unknown'
35
+ end
36
+
37
+ if !CodeOwnership.for_package(file_move_operation.destination_pack).nil?
38
+ CodeOwnership.remove_file_annotation!(relative_path_to_origin.to_s)
39
+ @did_move_files = true
40
+ end
41
+ end
42
+
43
+ sig { void }
44
+ def print_final_message!
45
+ if @teams.any?
46
+ Logging.section('Code Ownership') do
47
+ Logging.print('This section contains info about the current ownership distribution of the moved files.')
48
+ @teams.group_by { |team| team }.sort_by { |team, instances| -instances.count }.each do |team, instances|
49
+ Logging.print " #{team} - #{instances.count} files"
50
+ end
51
+ if @did_move_files
52
+ Logging.print "Since the destination package has package-based ownership, file-annotations were removed from moved files."
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,49 @@
1
+ # typed: strict
2
+
3
+ module UsePackwerk
4
+ class Configuration
5
+ extend T::Sig
6
+
7
+ sig { params(enforce_dependencies: T::Boolean).void }
8
+ attr_writer :enforce_dependencies
9
+
10
+ sig { params(documentation_link: String).void }
11
+ attr_writer :documentation_link
12
+
13
+ sig { void }
14
+ def initialize
15
+ @enforce_dependencies = T.let(@enforce_dependencies, T.nilable(T::Boolean))
16
+ @documentation_link = T.let(documentation_link, T.nilable(String) )
17
+ end
18
+
19
+ sig { returns(T::Boolean) }
20
+ def enforce_dependencies
21
+ if !@enforce_dependencies.nil?
22
+ @enforce_dependencies
23
+ else
24
+ true
25
+ end
26
+ end
27
+
28
+ # Configure a link to show up for users who are looking for more info
29
+ sig { returns(String) }
30
+ def documentation_link
31
+ "https://go/packwerk"
32
+ end
33
+ end
34
+
35
+ class << self
36
+ extend T::Sig
37
+
38
+ sig { returns(Configuration) }
39
+ def config
40
+ @config = T.let(@config, T.nilable(Configuration))
41
+ @config ||= Configuration.new
42
+ end
43
+
44
+ sig { params(blk: T.proc.params(arg0: Configuration).void).void }
45
+ def configure(&blk) # rubocop:disable Lint/UnusedMethodArgument
46
+ yield(config)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+
3
+ require 'colorized_string'
4
+
5
+ module UsePackwerk
6
+ module Logging
7
+ extend T::Sig
8
+
9
+ sig { params(title: String, block: T.proc.void).void }
10
+ def self.section(title, &block)
11
+ print_divider
12
+ puts ColorizedString.new("#{title}").green.bold
13
+ puts "\n"
14
+ yield
15
+ end
16
+
17
+ sig { params(text: String).void }
18
+ def self.print_bold_green(text)
19
+ puts ColorizedString.new(text).green.bold
20
+ end
21
+
22
+ sig { params(text: String).void }
23
+ def self.print(text)
24
+ puts text
25
+ end
26
+
27
+ sig { void }
28
+ def self.print_divider
29
+ puts '=' * 100
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+
3
+ module UsePackwerk
4
+ module PerFileProcessorInterface
5
+ extend T::Sig
6
+ extend T::Helpers
7
+
8
+ abstract!
9
+
10
+ sig { abstract.params(file_move_operation: Private::FileMoveOperation).void }
11
+ def before_move_file!(file_move_operation)
12
+ end
13
+
14
+ sig { void }
15
+ def print_final_message!
16
+ nil
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,77 @@
1
+ # typed: strict
2
+
3
+ module UsePackwerk
4
+ module Private
5
+ class FileMoveOperation < T::Struct
6
+ extend T::Sig
7
+
8
+ const :origin_pathname, Pathname
9
+ const :destination_pathname, Pathname
10
+ const :destination_pack, ParsePackwerk::Package
11
+
12
+ sig { params(origin_pathname: Pathname, new_package_root: Pathname).returns(Pathname) }
13
+ def self.destination_pathname_for_package_move(origin_pathname, new_package_root)
14
+ parts = origin_pathname.to_s.split('/')
15
+ toplevel_directory = parts[0]
16
+
17
+ case toplevel_directory.to_s
18
+ # This allows us to move files from monolith to packs
19
+ when 'app', 'spec', 'lib'
20
+ new_package_root.join(origin_pathname).cleanpath
21
+ # This allows us to move files from packs to packs
22
+ when *PERMITTED_PACK_LOCATIONS # parts looks like ['packs', 'organisms', 'app', 'services', 'bird_like', 'eagle.rb']
23
+ new_package_root.join(T.must(parts[2..]).join('/')).cleanpath
24
+ else
25
+ raise StandardError.new("Don't know how to find destination path for #{origin_pathname.inspect}")
26
+ end
27
+ end
28
+
29
+ sig { params(origin_pathname: Pathname).returns(Pathname) }
30
+ def self.destination_pathname_for_new_public_api(origin_pathname)
31
+ parts = origin_pathname.to_s.split('/')
32
+ toplevel_directory = Pathname.new(parts[0])
33
+
34
+ case toplevel_directory.to_s
35
+ # This allows us to make API in the monolith public
36
+ when 'app', 'spec'
37
+ toplevel_directory.join('public').join(T.must(parts[2..]).join('/')).cleanpath
38
+ # This allows us to make API in existing packs public
39
+ when *PERMITTED_PACK_LOCATIONS # parts looks like ['packs', 'organisms', 'app', 'services', 'bird_like', 'eagle.rb']
40
+ pack_name = Pathname.new(parts[1])
41
+ toplevel_directory.join(pack_name).join('app/public').join(T.must(parts[4..]).join('/')).cleanpath
42
+ else
43
+ raise StandardError.new("Don't know how to find destination path for #{origin_pathname.inspect}")
44
+ end
45
+ end
46
+
47
+ sig { returns(FileMoveOperation) }
48
+ def spec_file_move_operation
49
+ # This could probably be implemented by some "strategy pattern" where different extension types are handled by different helpers
50
+ # Such a thing could also include, for example, when moving a controller, moving its ERB view too.
51
+ if origin_pathname.extname == '.rake'
52
+ new_origin_pathname = origin_pathname.sub('/lib/', '/spec/lib/').sub(/^lib\//, 'spec/lib/').sub('.rake', '_spec.rb')
53
+ new_destination_pathname = destination_pathname.sub('/lib/', '/spec/lib/').sub(/^lib\//, 'spec/lib/').sub('.rake', '_spec.rb')
54
+ else
55
+ new_origin_pathname = origin_pathname.sub('/app/', '/spec/').sub(/^app\//, 'spec/').sub('.rb', '_spec.rb')
56
+ new_destination_pathname = destination_pathname.sub('/app/', '/spec/').sub(/^app\//, 'spec/').sub('.rb', '_spec.rb')
57
+ end
58
+ FileMoveOperation.new(
59
+ origin_pathname: new_origin_pathname,
60
+ destination_pathname: new_destination_pathname,
61
+ destination_pack: destination_pack,
62
+ )
63
+ end
64
+
65
+ private
66
+
67
+ sig { params(path: Pathname).returns(FileMoveOperation) }
68
+ def relative_to(path)
69
+ FileMoveOperation.new(
70
+ origin_pathname: origin_pathname.relative_path_from(path),
71
+ destination_pathname: destination_pathname.relative_path_from(path),
72
+ destination_pack: destination_pack,
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,219 @@
1
+ # typed: strict
2
+
3
+ module UsePackwerk
4
+ module Private
5
+ module PackRelationshipAnalyzer
6
+ extend T::Sig
7
+
8
+ sig do
9
+ params(
10
+ pack_name: T.nilable(String),
11
+ limit: Integer,
12
+ ).void
13
+ end
14
+ def self.list_top_privacy_violations(pack_name, limit)
15
+ all_packages = ParsePackwerk.all
16
+ if !pack_name.nil?
17
+ pack_name = Private.clean_pack_name(pack_name)
18
+ package = all_packages.find { |package| package.name == pack_name }
19
+ if package.nil?
20
+ raise StandardError.new("Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`")
21
+ end
22
+
23
+ pack_specific_content = <<~PACK_CONTENT
24
+ You are listing top #{limit} privacy violations for #{pack_name}. See #{UsePackwerk.config.documentation_link} for other utilities!
25
+ Pass in a limit to display more or less, e.g. `bin/list_top_privacy_violations -n #{pack_name} -l 1000`
26
+
27
+ This script is intended to help you find which of YOUR pack's private classes, constants, or modules other packs are using the most.
28
+ Anything not in #{pack_name}/app/public is considered private API.
29
+ PACK_CONTENT
30
+
31
+ to_package_names = [pack_name]
32
+ else
33
+ pack_specific_content = <<~PACK_CONTENT
34
+ You are listing top #{limit} privacy violations for all packs. See #{UsePackwerk.config.documentation_link} for other utilities!
35
+ Pass in a limit to display more or less, e.g. `bin/list_top_privacy_violations -n #{pack_name} -l 1000`
36
+
37
+ This script is intended to help you find which of YOUR pack's private classes, constants, or modules other packs are using the most.
38
+ Anything not in pack_name/app/public is considered private API.
39
+ PACK_CONTENT
40
+
41
+ to_package_names = all_packages.map(&:name)
42
+ end
43
+
44
+ violations_by_count = {}
45
+ total_pack_violation_count = 0
46
+
47
+ Logging.section('👋 Hi there') do
48
+ intro = <<~INTRO
49
+ #{pack_specific_content}
50
+
51
+ When using this script, ask yourself some questions like:
52
+ - What do I want to support?
53
+ - What do I *not* want to support?
54
+ - What is considered simply an implementation detail, and what is essential to the behavior of my pack?
55
+ - What is a simple, minimialistic API for clients to engage with the behavior of your pack?
56
+ - How do I ensure my public API is not coupled to specific client's use cases?
57
+
58
+ Looking at privacy violations can help guide the development of your public API, but it is just the beginning!
59
+
60
+ The script will output in the following format:
61
+
62
+ SomeConstant # This is the name of a class, constant, or module defined in your pack, outside of app/public
63
+ - Total Count: 5 # This is the total number of uses of this outside your pack
64
+ - By package: # This is a breakdown of the use of this constant by other packages
65
+ # This is the number of files in this pack that this constant is used.
66
+ # Check `packs/other_pack_a/deprecated_references.yml` under the '#{pack_name}'.'SomeConstant' key to see where this constant is used
67
+ - packs/other_pack_a: 3
68
+ - packs/other_pack_b: 2
69
+ SomeClass # This is the second most violated class, constant, or module defined in your pack
70
+ - Total Count: 2
71
+ - By package:
72
+ - packs/other_pack_a: 1
73
+ - packs/other_pack_b: 1
74
+
75
+ Lastly, remember you can use `bin/make_public -f #{pack_name}/path/to/file.rb` to make your class, constant, or module public API.
76
+ INTRO
77
+ Logging.print_bold_green(intro)
78
+ end
79
+
80
+ # TODO: This is a copy of the implementation below. We may want to refactor out this implementation detail before making changes that apply to both.
81
+ all_packages.each do |client_package|
82
+ PackageProtections::ProtectedPackage.from(client_package).violations.select(&:privacy?).each do |violation|
83
+ next unless to_package_names.include?(violation.to_package_name)
84
+ if pack_name.nil?
85
+ violated_symbol = "#{violation.class_name} (#{violation.to_package_name})"
86
+ else
87
+ violated_symbol = "#{violation.class_name}"
88
+ end
89
+ violations_by_count[violated_symbol] ||= {}
90
+ violations_by_count[violated_symbol][:total_count] ||= 0
91
+ violations_by_count[violated_symbol][:by_package] ||= {}
92
+ violations_by_count[violated_symbol][:by_package][client_package.name] ||= 0
93
+ violations_by_count[violated_symbol][:total_count] += violation.files.count
94
+ violations_by_count[violated_symbol][:by_package][client_package.name] += violation.files.count
95
+ total_pack_violation_count += violation.files.count
96
+ end
97
+ end
98
+
99
+ Logging.print("Total Count: #{total_pack_violation_count}")
100
+
101
+ sorted_violations = violations_by_count.sort_by { |violated_symbol, count_info| [-count_info[:total_count], violated_symbol] }
102
+ sorted_violations.first(limit).each do |violated_symbol, count_info|
103
+ percentage_of_total = (count_info[:total_count] * 100.0 / total_pack_violation_count).round(2)
104
+ Logging.print(violated_symbol)
105
+ Logging.print(" - Total Count: #{count_info[:total_count]} (#{percentage_of_total}% of total)")
106
+
107
+ Logging.print(" - By package:")
108
+ count_info[:by_package].sort_by{ |client_package_name, count| [-count, client_package_name] }.each do |client_package_name, count|
109
+ Logging.print(" - #{client_package_name}: #{count}")
110
+ end
111
+ end
112
+ end
113
+
114
+ sig do
115
+ params(
116
+ pack_name: T.nilable(String),
117
+ limit: Integer
118
+ ).void
119
+ end
120
+ def self.list_top_dependency_violations(pack_name, limit)
121
+ all_packages = ParsePackwerk.all
122
+
123
+ if !pack_name.nil?
124
+ pack_name = Private.clean_pack_name(pack_name)
125
+ package = all_packages.find { |package| package.name == pack_name }
126
+ if package.nil?
127
+ raise StandardError.new("Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`")
128
+ end
129
+
130
+ pack_specific_content = <<~PACK_CONTENT
131
+ You are listing top #{limit} dependency violations for #{pack_name}. See #{UsePackwerk.config.documentation_link} for other utilities!
132
+ Pass in a limit to display more or less, e.g. `bin/list_top_dependency_violations -n #{pack_name} -l 1000`
133
+
134
+ This script is intended to help you find which of YOUR pack's private classes, constants, or modules other packs are using the most.
135
+ Anything not in #{pack_name}/app/public is considered private API.
136
+ PACK_CONTENT
137
+
138
+ to_package_names = [pack_name]
139
+ else
140
+ pack_specific_content = <<~PACK_CONTENT
141
+ You are listing top #{limit} dependency violations for all packs. See #{UsePackwerk.config.documentation_link} for other utilities!
142
+ Pass in a limit to display more or less, e.g. `bin/list_top_dependency_violations -n #{pack_name} -l 1000`
143
+
144
+ This script is intended to help you find which of YOUR pack's private classes, constants, or modules other packs are using the most.
145
+ Anything not in pack_name/app/public is considered private API.
146
+ PACK_CONTENT
147
+
148
+ to_package_names = all_packages.map(&:name)
149
+ end
150
+
151
+ violations_by_count = {}
152
+ total_pack_violation_count = 0
153
+
154
+ Logging.section('👋 Hi there') do
155
+ intro = <<~INTRO
156
+ #{pack_specific_content}
157
+
158
+ When using this script, ask yourself some questions like:
159
+ - What do I want to support?
160
+ - What do I *not* want to support?
161
+ - Which direction should a dependency go?
162
+ - What packs should depend on you, and what packs should not depend on you?
163
+ - Would it be simpler if other packs only depended on interfaces to your pack rather than implementation?
164
+
165
+ Looking at dependency violations can help guide the development of your public API, but it is just the beginning!
166
+
167
+ The script will output in the following format:
168
+
169
+ SomeConstant # This is the name of a class, constant, or module defined in your pack, outside of app/public
170
+ - Total Count: 5 # This is the total number of unstated uses of this outside your pack
171
+ - By package: # This is a breakdown of the use of this constant by other packages
172
+ # This is the number of files in this pack that this constant is used.
173
+ # Check `packs/other_pack_a/deprecated_references.yml` under the '#{pack_name}'.'SomeConstant' key to see where this constant is used
174
+ - packs/other_pack_a: 3
175
+ - packs/other_pack_b: 2
176
+ SomeClass # This is the second most violated class, constant, or module defined in your pack
177
+ - Total Count: 2
178
+ - By package:
179
+ - packs/other_pack_a: 1
180
+ - packs/other_pack_b: 1
181
+ INTRO
182
+ Logging.print_bold_green(intro)
183
+ end
184
+
185
+ # TODO: This is a copy of the implementation above. We may want to refactor out this implementation detail before making changes that apply to both.
186
+ all_packages.each do |client_package|
187
+ PackageProtections::ProtectedPackage.from(client_package).violations.select(&:dependency?).each do |violation|
188
+ next unless to_package_names.include?(violation.to_package_name)
189
+ if pack_name.nil?
190
+ violated_symbol = "#{violation.class_name} (#{violation.to_package_name})"
191
+ else
192
+ violated_symbol = "#{violation.class_name}"
193
+ end
194
+ violations_by_count[violated_symbol] ||= {}
195
+ violations_by_count[violated_symbol][:total_count] ||= 0
196
+ violations_by_count[violated_symbol][:by_package] ||= {}
197
+ violations_by_count[violated_symbol][:by_package][client_package.name] ||= 0
198
+ violations_by_count[violated_symbol][:total_count] += violation.files.count
199
+ violations_by_count[violated_symbol][:by_package][client_package.name] += violation.files.count
200
+ total_pack_violation_count += violation.files.count
201
+ end
202
+ end
203
+
204
+ Logging.print("Total Count: #{total_pack_violation_count}")
205
+
206
+ sorted_violations = violations_by_count.sort_by { |violated_symbol, count_info| [-count_info[:total_count], violated_symbol] }
207
+ sorted_violations.first(limit).each do |violated_symbol, count_info|
208
+ percentage_of_total = (count_info[:total_count] * 100.0 / total_pack_violation_count).round(2)
209
+ Logging.print(violated_symbol)
210
+ Logging.print(" - Total Count: #{count_info[:total_count]} (#{percentage_of_total}% of total)")
211
+ Logging.print(" - By package:")
212
+ count_info[:by_package].sort_by{ |client_package_name, count| [-count, client_package_name] }.each do |client_package_name, count|
213
+ Logging.print(" - #{client_package_name}: #{count}")
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end