packwerk 1.0.1 → 1.1.3

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.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/.github/pull_request_template.md +8 -7
  3. data/.github/workflows/ci.yml +14 -5
  4. data/.ruby-version +1 -1
  5. data/Gemfile +2 -1
  6. data/Gemfile.lock +130 -110
  7. data/README.md +8 -1
  8. data/USAGE.md +23 -2
  9. data/dev.yml +1 -1
  10. data/exe/packwerk +1 -1
  11. data/gemfiles/Gemfile-rails-6-0 +22 -0
  12. data/lib/packwerk.rb +4 -2
  13. data/lib/packwerk/application_load_paths.rb +68 -0
  14. data/lib/packwerk/application_validator.rb +94 -74
  15. data/lib/packwerk/association_inspector.rb +24 -9
  16. data/lib/packwerk/cache_deprecated_references.rb +55 -0
  17. data/lib/packwerk/checker.rb +3 -0
  18. data/lib/packwerk/checking_deprecated_references.rb +5 -2
  19. data/lib/packwerk/cli.rb +56 -55
  20. data/lib/packwerk/commands/detect_stale_violations_command.rb +60 -0
  21. data/lib/packwerk/commands/offense_progress_marker.rb +24 -0
  22. data/lib/packwerk/commands/result.rb +13 -0
  23. data/lib/packwerk/commands/update_deprecations_command.rb +81 -0
  24. data/lib/packwerk/configuration.rb +5 -37
  25. data/lib/packwerk/const_node_inspector.rb +28 -17
  26. data/lib/packwerk/dependency_checker.rb +13 -5
  27. data/lib/packwerk/deprecated_references.rb +23 -0
  28. data/lib/packwerk/detect_stale_deprecated_references.rb +14 -0
  29. data/lib/packwerk/file_processor.rb +4 -4
  30. data/lib/packwerk/formatters/offenses_formatter.rb +48 -0
  31. data/lib/packwerk/formatters/progress_formatter.rb +6 -2
  32. data/lib/packwerk/inflector.rb +17 -8
  33. data/lib/packwerk/node.rb +61 -38
  34. data/lib/packwerk/node_processor.rb +4 -4
  35. data/lib/packwerk/node_processor_factory.rb +39 -0
  36. data/lib/packwerk/node_visitor.rb +1 -1
  37. data/lib/packwerk/offense.rb +4 -6
  38. data/lib/packwerk/output_style.rb +20 -0
  39. data/lib/packwerk/output_styles/coloured.rb +29 -0
  40. data/lib/packwerk/output_styles/plain.rb +26 -0
  41. data/lib/packwerk/package_set.rb +9 -3
  42. data/lib/packwerk/parsed_constant_definitions.rb +4 -4
  43. data/lib/packwerk/parsers/erb.rb +4 -0
  44. data/lib/packwerk/parsers/factory.rb +10 -1
  45. data/lib/packwerk/privacy_checker.rb +23 -5
  46. data/lib/packwerk/run_context.rb +69 -46
  47. data/lib/packwerk/sanity_checker.rb +1 -1
  48. data/lib/packwerk/spring_command.rb +1 -1
  49. data/lib/packwerk/updating_deprecated_references.rb +2 -39
  50. data/lib/packwerk/version.rb +1 -1
  51. data/library.yml +1 -1
  52. data/packwerk.gemspec +1 -1
  53. data/service.yml +2 -2
  54. data/shipit.rubygems.yml +5 -1
  55. data/sorbet/rbi/gems/{actioncable@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actioncable@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +56 -36
  56. data/sorbet/rbi/gems/{actionmailbox@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionmailbox@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +25 -28
  57. data/sorbet/rbi/gems/{actionmailer@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionmailer@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +43 -24
  58. data/sorbet/rbi/gems/{actionpack@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionpack@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +382 -284
  59. data/sorbet/rbi/gems/{actiontext@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actiontext@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +76 -40
  60. data/sorbet/rbi/gems/{actionview@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → actionview@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +206 -195
  61. data/sorbet/rbi/gems/{activejob@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activejob@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +64 -75
  62. data/sorbet/rbi/gems/{activemodel@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activemodel@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +103 -56
  63. data/sorbet/rbi/gems/{activerecord@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activerecord@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +1250 -898
  64. data/sorbet/rbi/gems/{activestorage@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activestorage@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +92 -120
  65. data/sorbet/rbi/gems/{activesupport@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → activesupport@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +292 -193
  66. data/sorbet/rbi/gems/{ast@2.4.1.rbi → ast@2.4.2.rbi} +2 -1
  67. data/sorbet/rbi/gems/{better_html@1.0.15.rbi → better_html@1.0.16.rbi} +2 -2
  68. data/sorbet/rbi/gems/{concurrent-ruby@1.1.6.rbi → concurrent-ruby@1.1.8.rbi} +12 -9
  69. data/sorbet/rbi/gems/{erubi@1.9.0.rbi → erubi@1.10.0.rbi} +3 -1
  70. data/sorbet/rbi/gems/{i18n@1.8.2.rbi → i18n@1.8.10.rbi} +19 -52
  71. data/sorbet/rbi/gems/{loofah@2.5.0.rbi → loofah@2.9.0.rbi} +3 -1
  72. data/sorbet/rbi/gems/marcel@1.0.0.rbi +70 -0
  73. data/sorbet/rbi/gems/{mini_mime@1.0.2.rbi → mini_mime@1.0.3.rbi} +6 -6
  74. data/sorbet/rbi/gems/{mini_portile2@2.4.0.rbi → minitest-focus@1.2.1.rbi} +2 -2
  75. data/sorbet/rbi/gems/{minitest@5.14.0.rbi → minitest@5.14.4.rbi} +31 -29
  76. data/sorbet/rbi/gems/{mocha@1.11.2.rbi → mocha@1.12.0.rbi} +25 -36
  77. data/sorbet/rbi/gems/{nio4r@2.5.2.rbi → nio4r@2.5.7.rbi} +21 -20
  78. data/sorbet/rbi/gems/{nokogiri@1.10.9.rbi → nokogiri@1.11.2.rbi} +193 -154
  79. data/sorbet/rbi/gems/{parallel@1.19.1.rbi → parallel@1.20.1.rbi} +1 -1
  80. data/sorbet/rbi/gems/parlour@6.0.0.rbi +1272 -0
  81. data/sorbet/rbi/gems/{parser@2.7.1.4.rbi → parser@3.0.0.0.rbi} +287 -174
  82. data/sorbet/rbi/gems/{pry@0.13.1.rbi → pry@0.14.0.rbi} +1 -1
  83. data/sorbet/rbi/gems/racc@1.5.2.rbi +57 -0
  84. data/sorbet/rbi/gems/{rack@2.2.2.rbi → rack@2.2.3.rbi} +23 -35
  85. data/sorbet/rbi/gems/{rails@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → rails@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +1 -1
  86. data/sorbet/rbi/gems/{railties@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi → railties@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi} +132 -121
  87. data/sorbet/rbi/gems/{rake@13.0.1.rbi → rake@13.0.3.rbi} +16 -20
  88. data/sorbet/rbi/gems/regexp_parser@2.1.1.rbi +8 -0
  89. data/sorbet/rbi/gems/rubocop-ast@1.4.1.rbi +8 -0
  90. data/sorbet/rbi/gems/{rubocop-performance@1.5.2.rbi → rubocop-performance@1.10.2.rbi} +1 -1
  91. data/sorbet/rbi/gems/{rubocop-shopify@1.0.2.rbi → rubocop-shopify@2.0.1.rbi} +1 -1
  92. data/sorbet/rbi/gems/{rubocop-sorbet@0.3.7.rbi → rubocop-sorbet@0.6.1.rbi} +1 -1
  93. data/sorbet/rbi/gems/{rubocop@0.82.0.rbi → rubocop@1.12.0.rbi} +1 -1
  94. data/sorbet/rbi/gems/{ruby-progressbar@1.10.1.rbi → ruby-progressbar@1.11.0.rbi} +1 -1
  95. data/sorbet/rbi/gems/spoom@1.1.0.rbi +1061 -0
  96. data/sorbet/rbi/gems/{spring@2.1.0.rbi → spring@2.1.1.rbi} +7 -7
  97. data/sorbet/rbi/gems/{sprockets-rails@3.2.1.rbi → sprockets-rails@3.2.2.rbi} +88 -68
  98. data/sorbet/rbi/gems/{sprockets@4.0.0.rbi → sprockets@4.0.2.rbi} +8 -7
  99. data/sorbet/rbi/gems/{tapioca@0.4.5.rbi → tapioca@0.4.19.rbi} +109 -24
  100. data/sorbet/rbi/gems/{thor@1.0.1.rbi → thor@1.1.0.rbi} +16 -15
  101. data/sorbet/rbi/gems/{tzinfo@2.0.2.rbi → tzinfo@2.0.4.rbi} +21 -2
  102. data/sorbet/rbi/gems/{unicode-display_width@1.7.0.rbi → unicode-display_width@2.0.0.rbi} +1 -1
  103. data/sorbet/rbi/gems/{websocket-driver@0.7.1.rbi → websocket-driver@0.7.3.rbi} +29 -29
  104. data/sorbet/rbi/gems/{websocket-extensions@0.1.4.rbi → websocket-extensions@0.1.5.rbi} +2 -2
  105. data/sorbet/rbi/gems/zeitwerk@2.4.2.rbi +177 -0
  106. metadata +66 -58
  107. data/lib/packwerk/output_styles.rb +0 -41
  108. data/sorbet/rbi/gems/jaro_winkler@1.5.4.rbi +0 -8
  109. data/sorbet/rbi/gems/marcel@0.3.3.rbi +0 -30
  110. data/sorbet/rbi/gems/mimemagic@0.3.5.rbi +0 -47
  111. data/sorbet/rbi/gems/parlour@4.0.1.rbi +0 -561
  112. data/sorbet/rbi/gems/spoom@1.0.4.rbi +0 -418
  113. data/sorbet/rbi/gems/zeitwerk@2.3.0.rbi +0 -8
  114. data/static/packwerk-check-demo.png +0 -0
  115. data/static/packwerk_check.gif +0 -0
  116. data/static/packwerk_check_violation.gif +0 -0
  117. data/static/packwerk_update.gif +0 -0
  118. data/static/packwerk_validate.gif +0 -0
data/README.md CHANGED
@@ -1,9 +1,16 @@
1
1
  # Packwerk [![Build Status](https://github.com/Shopify/packwerk/workflows/CI/badge.svg)](https://github.com/Shopify/packwerk/actions?query=workflow%3ACI)
2
2
 
3
+ ## NOTE: Packwerk is considered to be feature-complete for Shopify's uses. We are currently accepting bug fixes only, and it is not being actively developed. Please fork this project if you are interested in adding new features.
4
+
5
+ > "I know who you are and because of that I know what you do."
6
+ > This knowledge is a dependency that raises the cost of change.
7
+
8
+ -- _Sandi Metz, Practical Object-Oriented Design in Ruby_
9
+
3
10
  Packwerk is a Ruby gem used to enforce boundaries and modularize Rails applications.
4
11
 
5
12
  Packwerk can be used to:
6
- * Combine group of files into packages
13
+ * Combine groups of files into packages
7
14
  * Define package-level constant visibility (i.e. have publicly accessible constants)
8
15
  * Enforce privacy (inbound) and dependency (outbound) boundaries between packages
9
16
  * Help existing codebases to become more modular without obstructing development
data/USAGE.md CHANGED
@@ -63,14 +63,35 @@ Packwerk reads from the `packwerk.yml` configuration file in the root directory.
63
63
  |----------------------|-------------------------------------------|--------------|
64
64
  | include | **/*.{rb,rake,erb} | list of patterns for folder paths to include |
65
65
  | exclude | {bin,node_modules,script,tmp,vendor}/**/* | list of patterns for folder paths to exclude |
66
- | package_paths | **/ | patterns to find package configuration files, see: Defining packages |
66
+ | package_paths | **/ | a single pattern or a list of patterns to find package configuration files, see: [Defining packages](#Defining-packages) |
67
67
  | load_paths | All application autoload paths | list of load paths |
68
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
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
 
data/dev.yml CHANGED
@@ -3,7 +3,7 @@ name: packwerk
3
3
  type: ruby
4
4
 
5
5
  up:
6
- - ruby: 2.6.6
6
+ - ruby: 3.0.0
7
7
  - bundler
8
8
 
9
9
  commands:
data/exe/packwerk CHANGED
@@ -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)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ source("https://rubygems.org")
4
+
5
+ gemspec path: ".."
6
+
7
+ # Specify the same dependency sources as the application Gemfile
8
+
9
+ gem("spring")
10
+ gem("rails", '~> 6.0.0')
11
+ gem("constant_resolver", require: false)
12
+ gem("sorbet-runtime", require: false)
13
+ gem("rubocop-performance", require: false)
14
+ gem("rubocop-sorbet", require: false)
15
+ gem("mocha", require: false)
16
+ gem("rubocop-shopify", require: false)
17
+ gem("tapioca", require: false)
18
+
19
+ group :development do
20
+ gem("byebug", require: false)
21
+ gem("minitest-focus", require: false)
22
+ end
data/lib/packwerk.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  require "sorbet-runtime"
5
5
  require "active_support"
6
6
  require "constant_resolver"
7
+ require "fileutils"
7
8
 
8
9
  require "packwerk/offense"
9
10
 
@@ -14,7 +15,6 @@ require "packwerk/cli"
14
15
  require "packwerk/configuration"
15
16
  require "packwerk/const_node_inspector"
16
17
  require "packwerk/constant_discovery"
17
- require "packwerk/constant_name_inspector"
18
18
  require "packwerk/dependency_checker"
19
19
  require "packwerk/deprecated_references"
20
20
  require "packwerk/files_for_processing"
@@ -28,7 +28,9 @@ require "packwerk/graph"
28
28
  require "packwerk/inflector"
29
29
  require "packwerk/node_processor"
30
30
  require "packwerk/node_visitor"
31
- require "packwerk/output_styles"
31
+ require "packwerk/output_style"
32
+ require "packwerk/output_styles/plain"
33
+ require "packwerk/output_styles/coloured"
32
34
  require "packwerk/package"
33
35
  require "packwerk/package_set"
34
36
  require "packwerk/parsers"
@@ -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,51 +56,37 @@ 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)
70
+ assert_constants_can_be_loaded(constants)
84
71
 
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"
72
+ constant_locations = constants.map { |c| [c, resolver.resolve(c)&.location] }
91
73
 
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)
95
-
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
 
@@ -124,26 +101,26 @@ module Packwerk
124
101
 
125
102
  if hash.key?("enforce_privacy")
126
103
  unless [TrueClass, FalseClass, Array].include?(hash["enforce_privacy"].class)
127
- errors << "Invalid 'enforce_privacy' option in #{f.inspect}: #{hash['enforce_privacy'].inspect}"
104
+ errors << "Invalid 'enforce_privacy' option in #{f.inspect}: #{hash["enforce_privacy"].inspect}"
128
105
  end
129
106
  end
130
107
 
131
108
  if hash.key?("enforce_dependencies")
132
109
  unless [TrueClass, FalseClass].include?(hash["enforce_dependencies"].class)
133
- errors << "Invalid 'enforce_dependencies' option in #{f.inspect}: #{hash['enforce_dependencies'].inspect}"
110
+ errors << "Invalid 'enforce_dependencies' option in #{f.inspect}: #{hash["enforce_dependencies"].inspect}"
134
111
  end
135
112
  end
136
113
 
137
114
  if hash.key?("public_path")
138
115
  unless hash["public_path"].is_a?(String)
139
- errors << "'public_path' option must be a string in #{f.inspect}: #{hash['public_path'].inspect}"
116
+ errors << "'public_path' option must be a string in #{f.inspect}: #{hash["public_path"].inspect}"
140
117
  end
141
118
  end
142
119
 
143
120
  next unless hash.key?("dependencies")
144
121
  next if hash["dependencies"].is_a?(Array)
145
122
 
146
- errors << "Invalid 'dependencies' option in #{f.inspect}: #{hash['dependencies'].inspect}"
123
+ errors << "Invalid 'dependencies' option in #{f.inspect}: #{hash["dependencies"].inspect}"
147
124
  end
148
125
 
149
126
  if errors.empty?
@@ -170,14 +147,12 @@ module Packwerk
170
147
  def check_inflection_file
171
148
  inflections_file = @configuration.inflections_file
172
149
 
173
- test_inflections = ActiveSupport::Inflector::Inflections.new
174
-
175
- Packwerk::Inflections::Default.apply_to(test_inflections)
176
- 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
177
152
 
178
153
  results = %i(plurals singulars uncountables humans acronyms).map do |type|
179
- expected = ActiveSupport::Inflector.inflections.public_send(type).to_set
180
- actual = test_inflections.public_send(type).to_set
154
+ expected = application_inflections.public_send(type).to_set
155
+ actual = packwerk_inflections.public_send(type).to_set
181
156
 
182
157
  if expected == actual
183
158
  Result.new(true)
@@ -195,24 +170,16 @@ module Packwerk
195
170
  end
196
171
  end
197
172
 
198
- errors = results.reject(&:ok?)
199
-
200
- if errors.empty?
201
- Result.new(true)
202
- else
203
- Result.new(
204
- false,
205
- "Inflections specified in #{inflections_file} don't line up with application!\n" +
206
- errors.map(&:error_value).join("\n")
207
- )
208
- 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
+ )
209
178
  end
210
179
 
211
180
  def check_acyclic_graph
212
- packages = Packwerk::PackageSet.load_all_from(".")
213
-
214
- edges = packages.flat_map do |package|
215
- 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)] }
216
183
  end
217
184
  dependency_graph = Packwerk::Graph.new(*edges)
218
185
 
@@ -227,7 +194,7 @@ module Packwerk
227
194
  cycle_strings = dependency_graph.cycles.map do |cycle|
228
195
  cycle_strings = cycle.map(&:to_s)
229
196
  cycle_strings << cycle.first.to_s
230
- "\t- #{cycle_strings.join('')}"
197
+ "\t- #{cycle_strings.join("")}"
231
198
  end
232
199
 
233
200
  if dependency_graph.acyclic?
@@ -299,7 +266,7 @@ module Packwerk
299
266
  end
300
267
  end
301
268
 
302
- def check_root_package_exist
269
+ def check_root_package_exists
303
270
  root_package_path = File.join(@configuration.root_path, "package.yml")
304
271
  all_packages_manifests = package_manifests(package_glob)
305
272
 
@@ -318,8 +285,7 @@ module Packwerk
318
285
  private
319
286
 
320
287
  def package_manifests_settings_for(setting)
321
- package_manifests(package_glob)
322
- .map { |f| [f, (YAML.load_file(File.join(f)) || {})[setting]] }
288
+ package_manifests.map { |f| [f, (YAML.load_file(File.join(f)) || {})[setting]] }
323
289
  end
324
290
 
325
291
  def format_yaml_strings(list)
@@ -330,12 +296,17 @@ module Packwerk
330
296
  @configuration.package_paths || "**"
331
297
  end
332
298
 
333
- def package_manifests(glob_pattern)
334
- PackageSet.package_paths(@configuration.root_path, glob_pattern).map(&:to_s)
299
+ def package_manifests(glob_pattern = package_glob)
300
+ PackageSet.package_paths(@configuration.root_path, glob_pattern)
301
+ .map { |f| File.realpath(f) }
335
302
  end
336
303
 
337
304
  def relative_paths(paths)
338
- 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)
339
310
  end
340
311
 
341
312
  def invalid_package_path?(path)
@@ -345,5 +316,54 @@ module Packwerk
345
316
  package_path = File.join(@configuration.root_path, path, Packwerk::PackageSet::PACKAGE_CONFIG_FILENAME)
346
317
  !File.file?(package_path)
347
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
348
368
  end
349
369
  end