use_packwerk 0.50.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 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