packwerk 1.0.0 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/pull_request_template.md +8 -7
  3. data/.github/workflows/ci.yml +1 -1
  4. data/.gitignore +1 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +5 -2
  7. data/README.md +5 -3
  8. data/TROUBLESHOOT.md +1 -1
  9. data/USAGE.md +56 -19
  10. data/exe/packwerk +1 -1
  11. data/lib/packwerk.rb +3 -3
  12. data/lib/packwerk/application_load_paths.rb +68 -0
  13. data/lib/packwerk/application_validator.rb +96 -70
  14. data/lib/packwerk/association_inspector.rb +50 -20
  15. data/lib/packwerk/cache_deprecated_references.rb +55 -0
  16. data/lib/packwerk/checker.rb +23 -0
  17. data/lib/packwerk/checking_deprecated_references.rb +5 -2
  18. data/lib/packwerk/cli.rb +65 -56
  19. data/lib/packwerk/commands/detect_stale_violations_command.rb +60 -0
  20. data/lib/packwerk/commands/offense_progress_marker.rb +24 -0
  21. data/lib/packwerk/commands/result.rb +13 -0
  22. data/lib/packwerk/commands/update_deprecations_command.rb +81 -0
  23. data/lib/packwerk/configuration.rb +6 -34
  24. data/lib/packwerk/const_node_inspector.rb +28 -17
  25. data/lib/packwerk/dependency_checker.rb +16 -5
  26. data/lib/packwerk/deprecated_references.rb +24 -1
  27. data/lib/packwerk/detect_stale_deprecated_references.rb +14 -0
  28. data/lib/packwerk/file_processor.rb +4 -4
  29. data/lib/packwerk/formatters/offenses_formatter.rb +48 -0
  30. data/lib/packwerk/formatters/progress_formatter.rb +6 -2
  31. data/lib/packwerk/generators/application_validation.rb +2 -2
  32. data/lib/packwerk/generators/templates/package.yml +4 -0
  33. data/lib/packwerk/generators/templates/packwerk +2 -2
  34. data/lib/packwerk/generators/templates/packwerk.yml.erb +1 -1
  35. data/lib/packwerk/inflector.rb +17 -8
  36. data/lib/packwerk/node.rb +78 -39
  37. data/lib/packwerk/node_processor.rb +14 -3
  38. data/lib/packwerk/node_processor_factory.rb +39 -0
  39. data/lib/packwerk/offense.rb +4 -6
  40. data/lib/packwerk/output_style.rb +20 -0
  41. data/lib/packwerk/output_styles/coloured.rb +29 -0
  42. data/lib/packwerk/output_styles/plain.rb +26 -0
  43. data/lib/packwerk/package.rb +8 -1
  44. data/lib/packwerk/package_set.rb +13 -5
  45. data/lib/packwerk/parsed_constant_definitions.rb +4 -4
  46. data/lib/packwerk/parsers/erb.rb +4 -0
  47. data/lib/packwerk/parsers/factory.rb +10 -1
  48. data/lib/packwerk/privacy_checker.rb +26 -5
  49. data/lib/packwerk/run_context.rb +70 -46
  50. data/lib/packwerk/sanity_checker.rb +1 -1
  51. data/lib/packwerk/spring_command.rb +1 -1
  52. data/lib/packwerk/updating_deprecated_references.rb +2 -39
  53. data/lib/packwerk/version.rb +1 -1
  54. data/packwerk.gemspec +2 -2
  55. metadata +15 -8
  56. data/lib/packwerk/output_styles.rb +0 -41
  57. data/static/packwerk-check-demo.png +0 -0
  58. data/static/packwerk_check.gif +0 -0
  59. data/static/packwerk_check_violation.gif +0 -0
  60. data/static/packwerk_update.gif +0 -0
  61. data/static/packwerk_validate.gif +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdc39eaf91ef0f8f247cc31b54a3bb1680fcc0ddee4e9552f60a8c9bbdcf3410
4
- data.tar.gz: d449d1c6484b490d79a6dd3d98b0cdc84bf2ffdae1aa0564544ee3aa1e592f12
3
+ metadata.gz: cd008f86d84fd0224aac9d8f92bc104123f23869e9381c947b3e51864523c947
4
+ data.tar.gz: debf7f89ce8f8419a02f2af84c3f1900c7bc2f2ac79cce535284e3e697133d74
5
5
  SHA512:
6
- metadata.gz: c1f2d61a18cb126e85e824e0bc6d2a5854e2dd7114717165fb71ae9a5765f719464c7b66177078c01a5601aca9e8500112f68d695594a650f03da62a0d3054a2
7
- data.tar.gz: a674e776393ab5cc078ab21c9faefa554d54a85010bde8c40e47cb5bf6eed1b3e8e523cecce8f8673a0dfb606f455aac1ae3bb71b12acd7c5dfccffa2b645eda
6
+ metadata.gz: 2bf12776a5e5cd4f3f9d06f3bbabb3d9b0c6d873325dba9a7bc43dc49ffedbc119678b77a2087cface6428bf564e037b75386abb8fca2d06a313e617461db53a
7
+ data.tar.gz: d6ca7a42dea1c5338c40e89b6fe6b14baa43372cf44e22af3f19763ee1048c03629e34f4466b632e7f5b6d82932b8e05b2d02d5e26ebf91852334b9f2ca6d5b2
@@ -7,21 +7,22 @@
7
7
  ## What should reviewers focus on?
8
8
 
9
9
 
10
-
11
10
  ## Type of Change
12
11
 
13
- - [ ] Bug fix (non-breaking change which fixes an issue)
14
- - [ ] New feature (non-breaking change which adds functionality)
15
- - [ ] Code refactor (non-breaking change that doesn't add functionality)
16
- - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
17
- - [ ] This change requires a documentation update
12
+ - [ ] Bugfix
13
+ - [ ] New feature
14
+ - [ ] Non-breaking change (a change that doesn't alter functionality - i.e., code refactor, configs, etc.)
18
15
 
19
16
  ### Additional Release Notes
20
17
 
18
+ - [ ] Breaking change (fix or feature that would cause existing functionality to change)
19
+
21
20
  Include any notes here to include in the release description. For example, if you selected "breaking change" above, leave notes on how users can transition to this version.
22
21
 
23
22
  If no additional notes are necessary, delete this section or leave it unchanged.
24
23
 
25
24
  ## Checklist
26
25
 
27
- - [ ] It is safe to simply rollback this change.
26
+ - [ ] I have updated the documentation accordingly.
27
+ - [ ] I have added tests to cover my changes.
28
+ - [ ] It is safe to rollback this change.
@@ -1,6 +1,6 @@
1
1
  name: CI
2
2
 
3
- on: [push]
3
+ on: [push, pull_request]
4
4
 
5
5
  jobs:
6
6
  build:
data/.gitignore CHANGED
@@ -9,4 +9,5 @@
9
9
  /.bundle/
10
10
  /tmp/
11
11
  .rubocop-*
12
+ .byebug_history
12
13
  sorbet/rbi/hidden-definitions/errors.txt
data/Gemfile CHANGED
@@ -19,4 +19,5 @@ gem("tapioca", require: false)
19
19
 
20
20
  group :development do
21
21
  gem("byebug", require: false)
22
+ gem("minitest-focus", require: false)
22
23
  end
@@ -85,7 +85,7 @@ GIT
85
85
  PATH
86
86
  remote: .
87
87
  specs:
88
- packwerk (1.0.0)
88
+ packwerk (1.1.2)
89
89
  activesupport (>= 5.2)
90
90
  ast
91
91
  better_html
@@ -137,6 +137,8 @@ GEM
137
137
  mini_mime (1.0.2)
138
138
  mini_portile2 (2.4.0)
139
139
  minitest (5.14.0)
140
+ minitest-focus (1.2.1)
141
+ minitest (>= 4, < 6)
140
142
  mocha (1.11.2)
141
143
  nio4r (2.5.2)
142
144
  nokogiri (1.10.9)
@@ -180,7 +182,7 @@ GEM
180
182
  smart_properties (1.15.0)
181
183
  sorbet (0.5.5898)
182
184
  sorbet-static (= 0.5.5898)
183
- sorbet-runtime (0.5.5898)
185
+ sorbet-runtime (0.5.6049)
184
186
  sorbet-static (0.5.5898-universal-darwin-19)
185
187
  spoom (1.0.4)
186
188
  colorize
@@ -220,6 +222,7 @@ DEPENDENCIES
220
222
  byebug
221
223
  constant_resolver
222
224
  m
225
+ minitest-focus
223
226
  mocha
224
227
  packwerk!
225
228
  rails!
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Packwerk is a Ruby gem used to enforce boundaries and modularize Rails applications.
4
4
 
5
5
  Packwerk can be used to:
6
- * Combine group of files into packages
6
+ * Combine groups of files into packages
7
7
  * Define package-level constant visibility (i.e. have publicly accessible constants)
8
8
  * Enforce privacy (inbound) and dependency (outbound) boundaries between packages
9
9
  * Help existing codebases to become more modular without obstructing development
@@ -16,9 +16,9 @@ Packwerk supports MRI versions 2.6 and above.
16
16
 
17
17
  ## Demo
18
18
 
19
- Watch a [1-minute video demo](https://drive.google.com/file/d/1D-t1nYduwgpHAP4DHY-EwVwVNFHlwRWj/view?usp=sharing) on how Packwerk works.
19
+ Watch a [1-minute video demo](https://www.youtube.com/watch?v=NwqlyBAxVpQ&feature=youtu.be) on how Packwerk works.
20
20
 
21
- [![](./static/packwerk-check-demo.png)](https://drive.google.com/file/d/1D-t1nYduwgpHAP4DHY-EwVwVNFHlwRWj/view?usp=sharing)
21
+ [![](./static/packwerk-check-demo.png)](https://www.youtube.com/watch?v=NwqlyBAxVpQ&feature=youtu.be)
22
22
 
23
23
  ## Installation
24
24
 
@@ -28,6 +28,8 @@ Watch a [1-minute video demo](https://drive.google.com/file/d/1D-t1nYduwgpHAP4DH
28
28
  gem 'packwerk'
29
29
  ```
30
30
 
31
+ _Note: Packwerk has to be grouped in production environment within the Gemfile if your Rails app has custom inflections._
32
+
31
33
  2. Install the gem
32
34
 
33
35
  Execute:
@@ -16,7 +16,7 @@ You can specify folders or packages in Packwerk commands for a shorter run time:
16
16
 
17
17
  bundle exec packwerk check components/your_package
18
18
 
19
- bundle exec packwerk update components/your_package
19
+ bundle exec packwerk update-deprecations components/your_package
20
20
 
21
21
  _Note: You cannot specify folders or packages for `packwerk validate` because the command runs for the entire application._
22
22
 
data/USAGE.md CHANGED
@@ -47,11 +47,11 @@ Here is a list of files generated:
47
47
 
48
48
  | File | Location | Description |
49
49
  |-----------------------------|--------------|------------|
50
- | Packwerk configuration | packwerk.yml | See [Setting up configuration file](#Setting-up-configuration-file) |
50
+ | Packwerk configuration | packwerk.yml | See [Setting up the configuration file](#Setting-up-the-configuration-file) |
51
51
  | Root package | package.yml | A package for the root folder |
52
52
  | Bin script | bin/packwerk | For Rails applications to run Packwerk validation on CI, see [Validating the package system](#Validating-the-package-system) |
53
53
  | Validation test | test/packwerk_validator_test.rb | For Ruby projects to run Packwerk validation using tests, see [Validating the package system](#Validating-the-package-system) |
54
- | Custom inflections | configs/inflections.yml | A custom inflections file is only required if you have custom inflections in `inflections.rb`, see [Inflections](#Inflections) |
54
+ | Custom inflections | config/inflections.yml | A custom inflections file is only required if you have custom inflections in `inflections.rb`, see [Inflections](#Inflections) |
55
55
 
56
56
  After that, you may begin creating packages for your application. See [Defining packages](#Defining-packages)
57
57
 
@@ -59,33 +59,58 @@ After that, you may begin creating packages for your application. See [Defining
59
59
 
60
60
  Packwerk reads from the `packwerk.yml` configuration file in the root directory. Packwerk will run with the default configuration if any of these settings are not specified.
61
61
 
62
- | Key | Default value | Description |
63
- |----------------------|------------------------------------|--------------|
64
- | include | **/*.{rb,rake,erb} | list of patterns for folder paths to include |
65
- | exclude | {bin,node_modules,script,tmp}/**/* | list of patterns for folder paths to exclude |
66
- | package_paths | **/ | patterns to find package configuration files, see: Defining packages |
67
- | load_paths | All application autoload paths | list of load paths |
68
- | custom_associations | N/A | list of custom associations, if any |
62
+ | Key | Default value | Description |
63
+ |----------------------|-------------------------------------------|--------------|
64
+ | include | **/*.{rb,rake,erb} | list of patterns for folder paths to include |
65
+ | exclude | {bin,node_modules,script,tmp,vendor}/**/* | list of patterns for folder paths to exclude |
66
+ | package_paths | **/ | a single pattern or a list of patterns to find package configuration files, see: [Defining packages](#Defining-packages) |
67
+ | load_paths | All application autoload paths | list of load paths |
68
+ | custom_associations | N/A | list of custom associations, if any |
69
69
 
70
+ ### Using a custom ERB parser
71
+
72
+ You can specify a custom ERB parser if needed. For example, if you're using `<%graphql>` tags from https://github.com/github/graphql-client in your ERBs, you can use a custom parser subclass to comment them out so that Packwerk can parse the rest of the file:
73
+
74
+ ```ruby
75
+ class CustomParser < Packwerk::Parsers::Erb
76
+ def parse_buffer(buffer, file_path:)
77
+ preprocessed_source = buffer.source
78
+
79
+ # Comment out <%graphql ... %> tags. They won't contain any object
80
+ # references anyways.
81
+ preprocessed_source = preprocessed_source.gsub(/<%graphql/, "<%#")
82
+
83
+ preprocessed_buffer = Parser::Source::Buffer.new(file_path)
84
+ preprocessed_buffer.source = preprocessed_source
85
+ super(preprocessed_buffer, file_path: file_path)
86
+ end
87
+ end
88
+
89
+ Packwerk::Parsers::Factory.instance.erb_parser_class = CustomParser
90
+ ```
70
91
 
71
92
  ### Inflections
72
93
 
73
- Packwerk requires custom inflections to be defined in `inflections.yml` instead of the traditional `inflections.rb`. This is because Packwerk accounts for custom inflections, such as acronyms, when resolving constants. Additionally, Packwerk interprets Active Record associations as references to constants. For example, `has_many :birds` is reference to the `Birds` constant.
94
+ Packwerk requires custom inflections to be defined in `inflections.yml` instead of the traditional `inflections.rb`. This is because Packwerk accounts for custom inflections, such as acronyms, when resolving constants. Additionally, Packwerk interprets Active Record associations as references to constants. For example, `has_many :birds` is a reference to the `Bird` constant.
74
95
 
75
- In order to make your custom inflections compatible with Active Support and Packwerk, you must create an `inflections.yml` file and point `ActiveSupport::Inflector` to that file.
96
+ In order to make your custom inflections compatible with Active Support and Packwerk, you must create a `config/inflections.yml` file and point `ActiveSupport::Inflector` to that file.
76
97
 
77
98
  In `inflections.rb`, add:
78
99
 
79
100
  ```rb
101
+ require "packwerk/inflections/custom"
102
+
80
103
  ActiveSupport::Inflector.inflections do |inflect|
81
104
  # please add all custom inflections in the file below.
82
105
  Packwerk::Inflections::Custom.new(
83
- Rails.root.join("inflections.yml")
106
+ Rails.root.join("config", "inflections.yml")
84
107
  ).apply_to(inflect)
85
108
  end
86
109
  ```
87
110
 
88
- Next, move your existing custom inflections into `inflections.yml`:
111
+ _Note: Packwerk has to be grouped in production environment within the Gemfile if you have custom inflections._
112
+
113
+ Next, move your existing custom inflections into `config/inflections.yml`:
89
114
 
90
115
  ```yaml
91
116
  acronym:
@@ -97,9 +122,11 @@ irregular:
97
122
  - ['reserve', 'reserves']
98
123
  uncountable:
99
124
  - 'payment_details'
125
+ singular:
126
+ - [!ruby/regexp /status$/, 'status']
100
127
  ```
101
128
 
102
- Any new inflectors should be added to `inflections.yml`.
129
+ Any new inflectors should be added to `config/inflections.yml`.
103
130
 
104
131
  ## Validating the package system
105
132
 
@@ -168,7 +195,17 @@ enforce_privacy:
168
195
  It will be a privacy violation when a file outside of the `components/merchandising` package tries to reference `Merchandising::Product`.
169
196
 
170
197
  ##### Using public folders
171
- You may enforce privacy either way mentioned above and still expose a public API for your package by placing constants in the `app/public` folder. The constants in the public folder will be made available for use by the rest of the application.
198
+ You may enforce privacy either way mentioned above and still expose a public API for your package by placing constants in the public folder, which by default is `app/public`. The constants in the public folder will be made available for use by the rest of the application.
199
+
200
+ ##### Defining your own public folder
201
+
202
+ You may prefer to override the default public folder, you can do so on a per-package basis by defining a `public_path`.
203
+
204
+ Example:
205
+
206
+ ```yaml
207
+ public_path: my/custom/path/
208
+ ```
172
209
 
173
210
  #### Enforcing dependency boundary
174
211
  A package's dependency boundary is violated whenever it references a constant in some package that has not been declared as a dependency.
@@ -210,17 +247,17 @@ For existing codebases, packages are likely to have existing boundary violations
210
247
 
211
248
  If so, you will want to stop the bleeding and prevent more violations from occuring. The existing violations in the codebase can be recorded in a [deprecated references list](#Understanding_the_list_of_deprecated_references) by executing:
212
249
 
213
- bundle exec packwerk update
250
+ bundle exec packwerk update-deprecations
214
251
 
215
- Similar to `packwerk check`, you may also run `packwerk update` on folders or packages:
252
+ Similar to `packwerk check`, you may also run `packwerk update-deprecations` on folders or packages:
216
253
 
217
- bundle exec packwerk update components/your_package
254
+ bundle exec packwerk update-deprecations components/your_package
218
255
 
219
256
  ![](static/packwerk_update.gif)
220
257
 
221
258
  _Note: Changing dependencies or enabling dependencies will not require a full update of the codebase, only the package that changed. On the other hand, changing or enabling privacy will require a full update of the codebase._
222
259
 
223
- `packwerk update` should only be run to record existing violations and to remove deprecated references that have been worked off. Running `packwerk update` to resolve a violation should be the very last resort.
260
+ `packwerk update-deprecations` should only be run to record existing violations and to remove deprecated references that have been worked off. Running `packwerk update-deprecations` to resolve a violation should be the very last resort.
224
261
 
225
262
  See: [TROUBLESHOOT.md - Troubleshooting violations](TROUBLESHOOT.md#Troubleshooting_violations)
226
263
 
@@ -3,4 +3,4 @@
3
3
 
4
4
  require "packwerk"
5
5
 
6
- Packwerk::Cli.new(style: Packwerk::OutputStyles::Coloured).run(ARGV.dup)
6
+ Packwerk::Cli.new(style: Packwerk::OutputStyles::Coloured.new).run(ARGV.dup)
@@ -14,7 +14,6 @@ require "packwerk/cli"
14
14
  require "packwerk/configuration"
15
15
  require "packwerk/const_node_inspector"
16
16
  require "packwerk/constant_discovery"
17
- require "packwerk/constant_name_inspector"
18
17
  require "packwerk/dependency_checker"
19
18
  require "packwerk/deprecated_references"
20
19
  require "packwerk/files_for_processing"
@@ -28,13 +27,14 @@ require "packwerk/graph"
28
27
  require "packwerk/inflector"
29
28
  require "packwerk/node_processor"
30
29
  require "packwerk/node_visitor"
31
- require "packwerk/output_styles"
30
+ require "packwerk/output_style"
31
+ require "packwerk/output_styles/plain"
32
+ require "packwerk/output_styles/coloured"
32
33
  require "packwerk/package"
33
34
  require "packwerk/package_set"
34
35
  require "packwerk/parsers"
35
36
  require "packwerk/privacy_checker"
36
37
  require "packwerk/reference_extractor"
37
- require "packwerk/reference_lister"
38
38
  require "packwerk/run_context"
39
39
  require "packwerk/updating_deprecated_references"
40
40
  require "packwerk/version"
@@ -0,0 +1,68 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler"
5
+
6
+ module Packwerk
7
+ module ApplicationLoadPaths
8
+ class << self
9
+ extend T::Sig
10
+
11
+ sig { returns(T::Array[String]) }
12
+ def extract_relevant_paths
13
+ assert_application_booted
14
+ all_paths = extract_application_autoload_paths
15
+ relevant_paths = filter_relevant_paths(all_paths)
16
+ assert_load_paths_present(relevant_paths)
17
+ relative_path_strings(relevant_paths)
18
+ end
19
+
20
+ sig { void }
21
+ def assert_application_booted
22
+ raise "The application needs to be booted to extract load paths" unless defined?(::Rails)
23
+ end
24
+
25
+ sig { returns(T::Array[String]) }
26
+ def extract_application_autoload_paths
27
+ Rails.application.railties
28
+ .select { |railtie| railtie.is_a?(Rails::Engine) }
29
+ .push(Rails.application)
30
+ .flat_map do |engine|
31
+ paths = (engine.config.autoload_paths + engine.config.eager_load_paths + engine.config.autoload_once_paths)
32
+ paths.map(&:to_s).uniq
33
+ end
34
+ end
35
+
36
+ sig do
37
+ params(all_paths: T::Array[String], bundle_path: Pathname, rails_root: Pathname)
38
+ .returns(T::Array[Pathname])
39
+ end
40
+ def filter_relevant_paths(all_paths, bundle_path: Bundler.bundle_path, rails_root: Rails.root)
41
+ bundle_path_match = bundle_path.join("**")
42
+ rails_root_match = rails_root.join("**")
43
+
44
+ all_paths
45
+ .map { |path| Pathname.new(path).expand_path }
46
+ .select { |path| path.fnmatch(rails_root_match.to_s) } # path needs to be in application directory
47
+ .reject { |path| path.fnmatch(bundle_path_match.to_s) } # reject paths from vendored gems
48
+ end
49
+
50
+ sig { params(paths: T::Array[Pathname], rails_root: Pathname).returns(T::Array[String]) }
51
+ def relative_path_strings(paths, rails_root: Rails.root)
52
+ paths
53
+ .map { |path| path.relative_path_from(rails_root).to_s }
54
+ .uniq
55
+ end
56
+
57
+ sig { params(paths: T::Array[T.untyped]).void }
58
+ def assert_load_paths_present(paths)
59
+ if paths.empty?
60
+ raise <<~EOS
61
+ We could not extract autoload paths from your Rails app. This is likely a configuration error.
62
+ Packwerk will not work correctly without any autoload paths.
63
+ EOS
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -9,15 +9,15 @@ require "yaml"
9
9
  require "packwerk/package_set"
10
10
  require "packwerk/graph"
11
11
  require "packwerk/inflector"
12
+ require "packwerk/application_load_paths"
12
13
 
13
14
  module Packwerk
14
15
  class ApplicationValidator
15
- def initialize(config_file_path:, application_load_paths:, configuration:)
16
+ def initialize(config_file_path:, configuration:)
16
17
  @config_file_path = config_file_path
17
18
  @configuration = configuration
18
19
 
19
- # Load paths should be from the application
20
- @application_load_paths = application_load_paths.sort.uniq
20
+ @application_load_paths = ApplicationLoadPaths.extract_relevant_paths
21
21
  end
22
22
 
23
23
  Result = Struct.new(:ok?, :error_value)
@@ -32,19 +32,10 @@ module Packwerk
32
32
  check_acyclic_graph,
33
33
  check_package_manifest_paths,
34
34
  check_valid_package_dependencies,
35
- check_root_package_exist,
35
+ check_root_package_exists,
36
36
  ]
37
37
 
38
- results.reject!(&:ok?)
39
-
40
- if results.empty?
41
- Result.new(true)
42
- else
43
- Result.new(
44
- false,
45
- results.map(&:error_value).join("\n===\n")
46
- )
47
- end
38
+ merge_results(results)
48
39
  end
49
40
 
50
41
  def check_autoload_path_cache
@@ -65,55 +56,41 @@ module Packwerk
65
56
  def check_package_manifests_for_privacy
66
57
  privacy_settings = package_manifests_settings_for("enforce_privacy")
67
58
 
68
- autoload_paths = @configuration.load_paths
69
-
70
59
  resolver = ConstantResolver.new(
71
60
  root_path: @configuration.root_path,
72
- load_paths: autoload_paths
61
+ load_paths: @configuration.load_paths
73
62
  )
74
63
 
75
- errors = []
64
+ results = []
76
65
 
77
- privacy_settings.each do |filepath, setting|
66
+ privacy_settings.each do |config_file_path, setting|
78
67
  next unless setting.is_a?(Array)
68
+ constants = setting
79
69
 
80
- setting.each do |constant|
81
- # make sure the constant can be loaded
82
- constant.constantize # rubocop:disable Sorbet/ConstantsFromStrings
83
- context = resolver.resolve(constant)
84
-
85
- unless context
86
- errors << "#{constant}, listed in #{filepath.inspect}, could not be resolved"
87
- next
88
- end
89
-
90
- expected_filename = constant.underscore + ".rb"
70
+ assert_constants_can_be_loaded(constants)
91
71
 
92
- # We don't support all custom inflections yet, so we may accidentally resolve constants to the
93
- # file that defines their parent namespace. This restriction makes sure that we don't.
94
- next if context.location.end_with?(expected_filename)
72
+ constant_locations = constants.map { |c| [c, resolver.resolve(c)&.location] }
95
73
 
96
- errors << "Explicitly private constants need to have their own files.\n"\
97
- "#{constant}, listed in #{filepath.inspect}, was resolved to #{context.location.inspect}.\n"\
98
- "It should be in something like #{expected_filename.inspect}"
74
+ constant_locations.each do |name, location|
75
+ results << if location
76
+ check_private_constant_location(name, location, config_file_path)
77
+ else
78
+ private_constant_unresolvable(name, config_file_path)
79
+ end
99
80
  end
100
81
  end
101
82
 
102
- if errors.empty?
103
- Result.new(true)
104
- else
105
- Result.new(false, errors.join("\n---\n"))
106
- end
83
+ merge_results(results, separator: "\n---\n")
107
84
  end
108
85
 
109
86
  def check_package_manifest_syntax
110
87
  errors = []
111
88
 
112
- package_manifests(package_glob).each do |f|
89
+ package_manifests.each do |f|
113
90
  hash = YAML.load_file(f)
114
91
  next unless hash
115
92
 
116
- known_keys = %w(enforce_privacy enforce_dependencies dependencies metadata)
93
+ known_keys = %w(enforce_privacy enforce_dependencies public_path dependencies metadata)
117
94
  unknown_keys = hash.keys - known_keys
118
95
 
119
96
  unless unknown_keys.empty?
@@ -134,6 +111,12 @@ module Packwerk
134
111
  end
135
112
  end
136
113
 
114
+ if hash.key?("public_path")
115
+ unless hash["public_path"].is_a?(String)
116
+ errors << "'public_path' option must be a string in #{f.inspect}: #{hash['public_path'].inspect}"
117
+ end
118
+ end
119
+
137
120
  next unless hash.key?("dependencies")
138
121
  next if hash["dependencies"].is_a?(Array)
139
122
 
@@ -164,14 +147,12 @@ module Packwerk
164
147
  def check_inflection_file
165
148
  inflections_file = @configuration.inflections_file
166
149
 
167
- test_inflections = ActiveSupport::Inflector::Inflections.new
168
-
169
- Packwerk::Inflections::Default.apply_to(test_inflections)
170
- Packwerk::Inflections::Custom.new(inflections_file).apply_to(test_inflections)
150
+ application_inflections = ActiveSupport::Inflector.inflections
151
+ packwerk_inflections = Packwerk::Inflector.from_file(inflections_file).inflections
171
152
 
172
153
  results = %i(plurals singulars uncountables humans acronyms).map do |type|
173
- expected = ActiveSupport::Inflector.inflections.public_send(type).to_a
174
- actual = test_inflections.public_send(type).to_a
154
+ expected = application_inflections.public_send(type).to_set
155
+ actual = packwerk_inflections.public_send(type).to_set
175
156
 
176
157
  if expected == actual
177
158
  Result.new(true)
@@ -189,24 +170,16 @@ module Packwerk
189
170
  end
190
171
  end
191
172
 
192
- errors = results.reject(&:ok?)
193
-
194
- if errors.empty?
195
- Result.new(true)
196
- else
197
- Result.new(
198
- false,
199
- "Inflections specified in #{inflections_file} don't line up with application!\n" +
200
- errors.map(&:error_value).join("\n")
201
- )
202
- end
173
+ merge_results(
174
+ results,
175
+ separator: "\n",
176
+ errors_headline: "Inflections specified in #{inflections_file} don't line up with application!\n"
177
+ )
203
178
  end
204
179
 
205
180
  def check_acyclic_graph
206
- packages = Packwerk::PackageSet.load_all_from(".")
207
-
208
- edges = packages.flat_map do |package|
209
- package.dependencies.map { |dependency| [package, packages.fetch(dependency)] }
181
+ edges = package_set.flat_map do |package|
182
+ package.dependencies.map { |dependency| [package, package_set.fetch(dependency)] }
210
183
  end
211
184
  dependency_graph = Packwerk::Graph.new(*edges)
212
185
 
@@ -293,7 +266,7 @@ module Packwerk
293
266
  end
294
267
  end
295
268
 
296
- def check_root_package_exist
269
+ def check_root_package_exists
297
270
  root_package_path = File.join(@configuration.root_path, "package.yml")
298
271
  all_packages_manifests = package_manifests(package_glob)
299
272
 
@@ -312,8 +285,7 @@ module Packwerk
312
285
  private
313
286
 
314
287
  def package_manifests_settings_for(setting)
315
- package_manifests(package_glob)
316
- .map { |f| [f, (YAML.load_file(File.join(f)) || {})[setting]] }
288
+ package_manifests.map { |f| [f, (YAML.load_file(File.join(f)) || {})[setting]] }
317
289
  end
318
290
 
319
291
  def format_yaml_strings(list)
@@ -324,12 +296,17 @@ module Packwerk
324
296
  @configuration.package_paths || "**"
325
297
  end
326
298
 
327
- def package_manifests(glob_pattern)
328
- Dir.glob(File.join(glob_pattern, Packwerk::PackageSet::PACKAGE_CONFIG_FILENAME)).map { |f| File.realpath(f) }
299
+ def package_manifests(glob_pattern = package_glob)
300
+ PackageSet.package_paths(@configuration.root_path, glob_pattern)
301
+ .map { |f| File.realpath(f) }
329
302
  end
330
303
 
331
304
  def relative_paths(paths)
332
- paths.map { |path| Pathname.new(path).relative_path_from(@configuration.root_path) }
305
+ paths.map { |path| relative_path(path) }
306
+ end
307
+
308
+ def relative_path(path)
309
+ Pathname.new(path).relative_path_from(@configuration.root_path)
333
310
  end
334
311
 
335
312
  def invalid_package_path?(path)
@@ -339,5 +316,54 @@ module Packwerk
339
316
  package_path = File.join(@configuration.root_path, path, Packwerk::PackageSet::PACKAGE_CONFIG_FILENAME)
340
317
  !File.file?(package_path)
341
318
  end
319
+
320
+ def assert_constants_can_be_loaded(constants)
321
+ constants.each(&:constantize)
322
+ nil
323
+ end
324
+
325
+ def private_constant_unresolvable(name, config_file_path)
326
+ explicit_filepath = (name.start_with?("::") ? name[2..-1] : name).underscore + ".rb"
327
+
328
+ Result.new(
329
+ false,
330
+ "'#{name}', listed in #{config_file_path}, could not be resolved.\n"\
331
+ "This is probably because it is an autovivified namespace - a namespace module that doesn't have a\n"\
332
+ "file explicitly defining it. Packwerk currently doesn't support declaring autovivified namespaces as\n"\
333
+ "private. Add a #{explicit_filepath} file to explicitly define the constant."
334
+ )
335
+ end
336
+
337
+ def check_private_constant_location(name, location, config_file_path)
338
+ declared_package = package_set.package_from_path(relative_path(config_file_path))
339
+ constant_package = package_set.package_from_path(location)
340
+
341
+ if constant_package == declared_package
342
+ Result.new(true)
343
+ else
344
+ Result.new(
345
+ false,
346
+ "'#{name}' is declared as private in the '#{declared_package}' package but appears to be "\
347
+ "defined\nin the '#{constant_package}' package. Packwerk resolved it to #{location}."
348
+ )
349
+ end
350
+ end
351
+
352
+ def package_set
353
+ @package_set ||= Packwerk::PackageSet.load_all_from(@configuration.root_path, package_pathspec: package_glob)
354
+ end
355
+
356
+ def merge_results(results, separator: "\n===\n", errors_headline: "")
357
+ results.reject!(&:ok?)
358
+
359
+ if results.empty?
360
+ Result.new(true)
361
+ else
362
+ Result.new(
363
+ false,
364
+ errors_headline + results.map(&:error_value).join(separator)
365
+ )
366
+ end
367
+ end
342
368
  end
343
369
  end