packwerk-extensions 0.1.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 077e4a805c3061339f9c18e26d1768bbe3c3966e9e4c958b078ba1bcd0292c07
4
- data.tar.gz: c292cb8cb900a9746d20e426a94ca89fc2a55cea542662812cb14f450466ae55
3
+ metadata.gz: 24439a703540da854784770755e1343825565c3803e5f445d2e201a2641b830c
4
+ data.tar.gz: 2abc2d11f203b5c4f779cffcd9b918950f196e8077f38ffd6607c48984e8142f
5
5
  SHA512:
6
- metadata.gz: 601a651716181f5da48a302a7dd3b9a5eb3d46fa81a13ebd7138602aef8c11576344d724a2907b7bb950161ba11f0bb2a68207a9338de4b4e780c569320e8900
7
- data.tar.gz: 288a0af58508deebdc3904a4a716ff933c837f4ca1cf840ece8ef8f4509936dfc6839fa7737e0f6d5805a007a00803dcafde59054a8e4089a11228a1d4b2486e
6
+ metadata.gz: 1174851513f542ffb1cffc27adab3f7cb7a8b0daa018476d57ce06547521727ee88c0c722b504d1ff81e2fe154b5a15073d99a7d05ce99f2057c98788e9484f5
7
+ data.tar.gz: db04d45d6cb81beb4b198fafee35246cb5aa53ef53d74ef4d399860339c0b8a95c8098fa32ef529e7ba5dc872d74715d2da030c45c57e1fbe237b1aae4d8643d
data/README.md CHANGED
@@ -6,7 +6,7 @@ Currently, it ships the following checkers to help improve the boundaries betwee
6
6
  - A `privacy` checker that ensures other packages are using your package's public API
7
7
  - A `visibility` checker that allows packages to be private except to an explicit group of other packages.
8
8
  - A `folder_visibility` checker that allows packages to their sibling packs and parent pack (to be used in an application that uses folder packs)
9
- - An `architecture` checker that allows packages to specify their "layer" and requires that each layer only communicate with layers below it.
9
+ - A `layer` (formerly `architecture`) checker that allows packages to specify their "layer" and requires that each layer only communicate with layers below it.
10
10
 
11
11
  ## Installation
12
12
 
@@ -26,7 +26,7 @@ require:
26
26
  - packwerk/privacy/checker
27
27
  - packwerk/visibility/checker
28
28
  - packwerk/folder_visibility/checker
29
- - packwerk/architecture/checker
29
+ - packwerk/layer/checker
30
30
  ```
31
31
 
32
32
  ## Privacy Checker
@@ -34,14 +34,18 @@ The privacy checker extension was originally extracted from [packwerk](https://g
34
34
 
35
35
  A package's privacy boundary is violated when there is a reference to the package's private constants from a source outside the package.
36
36
 
37
- To enforce privacy for your package, set `enforce_privacy` to `true` on your pack:
37
+ To enforce privacy for your package, set `enforce_privacy` to `true` or `strict` on your pack:
38
38
 
39
39
  ```yaml
40
40
  # components/merchandising/package.yml
41
41
  enforce_privacy: true
42
42
  ```
43
43
 
44
- Setting `enforce_privacy` to true will make all references to private constants in your package a violation.
44
+ Setting `enforce_privacy` to `true` will make all references to private constants in your package a violation.
45
+
46
+ Setting `enforce_privacy` to `strict` will forbid all references to private constants in your package. **This includes violations that have been added to other packages' `package_todo.yml` files.**
47
+
48
+ Note: You will need to remove all existing privacy violations before setting `enforce_privacy` to `strict`.
45
49
 
46
50
  ### Using public folders
47
51
  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.
@@ -56,9 +60,86 @@ Example:
56
60
  public_path: my/custom/path/
57
61
  ```
58
62
 
63
+ ### Defining public constants through sigil
64
+
65
+ > [!WARNING]
66
+ > This way of of defining the public API of a package should be considered WIP. It is not supported by all tooling in the RubyAtScale ecosystem, as @alexevanczuk pointed out in a [comment on the PR](https://github.com/rubyatscale/packwerk-extensions/pull/35#discussion_r1334331797):
67
+ >
68
+ > There are a couple of other places that will require changes related to this sigil. Namely, everything that is coupled to the public folder implementation of privacy.
69
+ >
70
+ > In the rubyatscale org:
71
+ >
72
+ > * pack_stats, example https://github.com/rubyatscale/pack_stats/blob/main/lib/pack_stats/private/metrics/public_usage.rb. (IMO though we can just remove this metric – it has never been useful)
73
+ > * Other places that mention public_path or app/public.
74
+ > * Org wide search for app/public link
75
+ > * Org wide search for public_path link
76
+ > * packs (the Rust port of packwerk – I could take this one over unless someone is interested in implementing whatever we come up with there
77
+
78
+
79
+
80
+ You may make individual files public within a private package by usage of a comment within the first 5 lines of the `.rb` file containing `pack_public: true`.
81
+
82
+ Example:
83
+
84
+ ```ruby
85
+ # pack_public: true
86
+ module Foo
87
+ class Update
88
+ end
89
+ end
90
+ ```
91
+ Now `Foo::Update` is considered public even though the `foo` package might be set to `enforce_privacy: (true || strict)`.
92
+
93
+ It's important to note that when combining `public_api: true` with the declaration of `private_constants`,
94
+ `packwerk validate` will raise an exception if both are used for the same constant. This must be resolved by removing
95
+ the sigil from the `.rb` file or removing the constant from the list of `private_constants`.
96
+
97
+ If you are using rubocop, it may be configured in such a way that there must be an empty line after the magic keywords at the top of the file. Currently, this extension is not modifying rubocop in any way so it does not recognize `pack_public: true` as a valid magic keyword option. That means placing it at the end of the magic keywords will throw a rubocop exception. However, you can place it first in the list to avoid an exception in rubocop.
98
+ ```
99
+ -----
100
+ # typed: ignore
101
+ # frozen_string_literal: true
102
+ # pack_public: true
103
+
104
+ class Foo
105
+ ...
106
+ end => Layout/EmptyLineAfterMagicComment: Add an empty line after magic comments.
107
+
108
+ ------
109
+ # typed: ignore
110
+ # frozen_string_literal: true
111
+
112
+ # pack_public: true
113
+
114
+ class Foo
115
+ ...
116
+ end => Less than ideal. This won't raise an issue in rubocop, however, only the first 5 lines are scanned for the magic comment of pack_public so there is risk at it being missed. It also is requiring extra empty lines in the group of magic comments.
117
+
118
+ -----
119
+ # pack_public: true
120
+ # typed: ignore
121
+ # frozen_string_literal: true
122
+
123
+ class Foo
124
+ ...
125
+ end => Ideal solution. No exceptions from rubocop and very low risk of the magic comment being out of range since
126
+ ```
127
+
59
128
  ### Using specific private constants
60
129
  Sometimes it is desirable to only enforce privacy on a subset of constants in a package. You can do so by defining a `private_constants` list in your package.yml. Note that `enforce_privacy` must be set to `true` or `'strict'` for this to work.
61
130
 
131
+ ### Ignore strict mode for violation coming from specific path patterns
132
+ If you want to activate `'strict'` mode on your package but have a few privacy violations you know you will deal with later,
133
+ you can set a list of patterns to exclude.
134
+
135
+ ```yaml
136
+ enforce_privacy: strict
137
+ strict_privacy_ignored_patterns:
138
+ - engines/another_engine/test/**/*
139
+ ```
140
+
141
+ In this example, violations on constants of your engine referenced in those files `engines/another_engine/test/**/*` will not fail Packwerk checks.
142
+
62
143
  ### Package Privacy violation
63
144
  Packwerk thinks something is a privacy violation if you're referencing a constant, class, or module defined in the private implementation (i.e. not the public folder) of another package. We care about these because we want to make sure we only use parts of a package that have been exposed as public API.
64
145
 
@@ -78,7 +159,7 @@ You may be accessing the implementation of a piece of functionality that is supp
78
159
  The functionality you’re looking for may not be intended to be reused across packages at all. If there is no public interface for it but you have a good reason to use it from outside of its package, find the people responsible for the package and discuss a solution with them.
79
160
 
80
161
  ## Visibility Checker
81
- The visibility checker can be used to allow a package to be private implementation detail of other packages.
162
+ The visibility checker can be used to allow a package to be a private implementation detail of other packages.
82
163
 
83
164
  To enforce visibility for your package, set `enforce_visibility` to `true` on your pack and specify `visible_to` for other packages that can use your package.
84
165
 
@@ -113,12 +194,12 @@ packs/b/packs/h OK (sibling)
113
194
  packs/c VIOLATION
114
195
  ```
115
196
 
116
- ## Architecture Checker
117
- The architecture checker can be used to enforce constraints on what can depend on what.
197
+ ## Layer Checker
198
+ The layer checker can be used to enforce constraints on what can depend on what.
118
199
 
119
- To enforce architecture for your package, first define the `architecture_layers` in `packwerk.yml`, for example:
200
+ To enforce layers for your package, first define the `layers` in `packwerk.yml`, for example:
120
201
  ```
121
- architecture_layers:
202
+ layers:
122
203
  - package
123
204
  - utility
124
205
  ```
@@ -126,12 +207,33 @@ architecture_layers:
126
207
  Then, turn on the checker in your package:
127
208
  ```yaml
128
209
  # components/merchandising/package.yml
129
- enforce_architecture: true
210
+ enforce_layers: true
130
211
  layer: utility
131
212
  ```
132
213
 
133
214
  Now this pack can only depend on other utility packages.
134
215
 
216
+ ### Deprecated Architecture Checker
217
+ The "Layer Checker" was formerly named "Architecture Checker". The associated keys were:
218
+ - packwerk.yml `architecture_layers`, which is now `layers`
219
+ - package.yml `enforce_architecture`, which is now `enforce_layers`
220
+ - package.yml `layer` is still a valid key
221
+ - package_todo.yml - `architecture`, which is now `layer`
222
+
223
+ ```bash
224
+ # script to migrate code from deprecated "architecture" violations to "layer" violations
225
+ # sed and ripgrep required
226
+
227
+ # replace 'architecture_layers' with 'layers' in packwerk.yml
228
+ sed -i '' 's/architecture_layers/layers/g' ./packwerk.yml
229
+
230
+ # replace 'enforce_architecture' with 'enforce_layers' in package.yml files
231
+ `rg -l 'enforce_architecture' -g 'package.yml' | xargs sed -i '' 's,enforce_architecture,enforce_layers,g'`
232
+
233
+ # replace '- architecture' with '- layer' in package_todo.yml files
234
+ `rg -l 'architecture' -g 'package_todo.yml' | xargs sed -i '' 's/- architecture/- layer/g'`
235
+ ```
236
+
135
237
 
136
238
  ## Contributing
137
239
 
@@ -1,16 +1,17 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'packwerk/architecture/layers'
5
- require 'packwerk/architecture/package'
6
- require 'packwerk/architecture/validator'
4
+ require 'packwerk/layer/config'
5
+ require 'packwerk/layer/layers'
6
+ require 'packwerk/layer/package'
7
+ require 'packwerk/layer/validator'
7
8
 
8
9
  module Packwerk
9
- module Architecture
10
+ module Layer
10
11
  # This enforces "layered architecture," which allows each class to be designated as one of N layers
11
12
  # configured by the client in `packwerk.yml`, for example:
12
13
  #
13
- # architecture_layers:
14
+ # layers:
14
15
  # - orchestrator
15
16
  # - business_domain
16
17
  # - platform
@@ -18,7 +19,7 @@ module Packwerk
18
19
  # - specification
19
20
  #
20
21
  # Then a package can configure:
21
- # enforce_architecture: true | false | strict
22
+ # enforce_layers: true | false | strict
22
23
  # layer: utility
23
24
  #
24
25
  # This is intended to provide:
@@ -30,11 +31,14 @@ module Packwerk
30
31
  extend T::Sig
31
32
  include Packwerk::Checker
32
33
 
33
- VIOLATION_TYPE = T.let('architecture', String)
34
+ sig { void }
35
+ def initialize
36
+ @violation_type = T.let(@violation_type, T.nilable(String))
37
+ end
34
38
 
35
39
  sig { override.returns(String) }
36
40
  def violation_type
37
- VIOLATION_TYPE
41
+ @violation_type ||= layer_config.violation_key
38
42
  end
39
43
 
40
44
  sig do
@@ -55,7 +59,7 @@ module Packwerk
55
59
  end
56
60
  def strict_mode_violation?(listed_offense)
57
61
  constant_package = listed_offense.reference.package
58
- constant_package.config['enforce_architecture'] == 'strict'
62
+ constant_package.config[layer_config.enforce_key] == 'strict'
59
63
  end
60
64
 
61
65
  sig do
@@ -68,9 +72,10 @@ module Packwerk
68
72
  referencing_package = Package.from(reference.package, layers)
69
73
 
70
74
  message = <<~MESSAGE
71
- Architecture layer violation: '#{reference.constant.name}' belongs to '#{reference.constant.package}', whose architecture layer type is "#{constant_package.layer}."
72
- This constant cannot be referenced by '#{reference.package}', whose architecture layer type is "#{referencing_package.layer}."
73
- Can we organize our code logic to respect the layers of these packs? See all layers in packwerk.yml.
75
+ Layer violation: '#{reference.constant.name}' belongs to '#{reference.constant.package}', whose layer type is "#{constant_package.layer}".
76
+ This constant cannot be referenced by '#{reference.package}', whose layer type is "#{referencing_package.layer}".
77
+ Packs in a lower layer may not access packs in a higher layer. See the `layers` in packwerk.yml. Current hierarchy:
78
+ - #{layers.names_list.join("\n- ")}
74
79
 
75
80
  #{standard_help_message(reference)}
76
81
  MESSAGE
@@ -91,7 +96,12 @@ module Packwerk
91
96
 
92
97
  sig { returns(Layers) }
93
98
  def layers
94
- @layers ||= T.let(Layers.new, T.nilable(Packwerk::Architecture::Layers))
99
+ @layers ||= T.let(Layers.new, T.nilable(Packwerk::Layer::Layers))
100
+ end
101
+
102
+ sig { returns(Config) }
103
+ def layer_config
104
+ @layer_config ||= T.let(Config.new, T.nilable(Config))
95
105
  end
96
106
  end
97
107
  end
@@ -0,0 +1,46 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Layer
6
+ class Config
7
+ extend T::Sig
8
+
9
+ ARCHITECTURE_VIOLATION_TYPE = T.let('architecture', String)
10
+ ARCHITECTURE_ENFORCE = T.let('enforce_architecture', String)
11
+ LAYER_VIOLATION_TYPE = T.let('layer', String)
12
+ LAYER_ENFORCE = T.let('enforce_layers', String)
13
+
14
+ sig { void }
15
+ def initialize
16
+ @layers_key_configured = T.let(@layers_key_configured, T.nilable(T::Boolean))
17
+ @layers_list = T.let(@layers_list, T.nilable(T::Array[String]))
18
+ end
19
+
20
+ sig { returns(T::Array[String]) }
21
+ def layers_list
22
+ @layers_list ||= YAML.load_file('packwerk.yml')[layers_key] || []
23
+ end
24
+
25
+ sig { returns(T::Boolean) }
26
+ def layers_key_configured?
27
+ @layers_key_configured ||= YAML.load_file('packwerk.yml')['architecture_layers'].nil?
28
+ end
29
+
30
+ sig { returns(String) }
31
+ def layers_key
32
+ layers_key_configured? ? 'layers' : 'architecture_layers'
33
+ end
34
+
35
+ sig { returns(String) }
36
+ def violation_key
37
+ layers_key_configured? ? LAYER_VIOLATION_TYPE : ARCHITECTURE_VIOLATION_TYPE
38
+ end
39
+
40
+ sig { returns(String) }
41
+ def enforce_key
42
+ layers_key_configured? ? LAYER_ENFORCE : ARCHITECTURE_ENFORCE
43
+ end
44
+ end
45
+ end
46
+ end
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
- module Architecture
5
+ module Layer
6
6
  class Layers
7
7
  extend T::Sig
8
8
 
@@ -27,11 +27,9 @@ module Packwerk
27
27
  @names ||= Set.new(names_list)
28
28
  end
29
29
 
30
- private
31
-
32
30
  sig { returns(T::Array[String]) }
33
31
  def names_list
34
- @names_list ||= YAML.load_file('packwerk.yml')['architecture_layers'] || []
32
+ @names_list ||= Config.new.layers_list
35
33
  end
36
34
  end
37
35
  end
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
- module Architecture
5
+ module Layer
6
6
  class Package < T::Struct
7
7
  extend T::Sig
8
8
 
@@ -46,7 +46,7 @@ module Packwerk
46
46
 
47
47
  Package.new(
48
48
  layer: layer,
49
- enforcement_setting: config['enforce_architecture'],
49
+ enforcement_setting: config[Config.new.enforce_key],
50
50
  config: config
51
51
  )
52
52
  end
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Packwerk
5
- module Architecture
5
+ module Layer
6
6
  class Validator
7
7
  extend T::Sig
8
8
  include Packwerk::Validator
@@ -16,19 +16,21 @@ module Packwerk
16
16
  package_set.each do |package|
17
17
  config = package.config
18
18
  f = Pathname.new(package.name).join('package.yml').to_s
19
+ package = Package.from(package, layers)
20
+
19
21
  next if !config
20
22
 
21
- result = check_enforce_architecture_setting(f, config['enforce_architecture'])
23
+ result = check_enforce_key(package, f, config)
22
24
  results << result
23
25
  next if !result.ok?
24
26
 
25
- package = Package.from(package, layers)
27
+ result = check_enforce_layers_setting(f, config[layer_config.enforce_key])
28
+ results << result
29
+ next if !result.ok?
26
30
 
27
31
  result = check_layer_setting(package, f)
28
32
  results << result
29
33
  next if !result.ok?
30
-
31
- results += check_dependencies_setting(package_set, package, f)
32
34
  end
33
35
 
34
36
  merge_results(results, separator: "\n---\n")
@@ -36,33 +38,39 @@ module Packwerk
36
38
 
37
39
  sig { returns(Layers) }
38
40
  def layers
39
- @layers ||= T.let(Layers.new, T.nilable(Packwerk::Architecture::Layers))
41
+ @layers ||= T.let(Layers.new, T.nilable(Packwerk::Layer::Layers))
42
+ end
43
+
44
+ sig { returns(Config) }
45
+ def layer_config
46
+ @layer_config ||= T.let(Config.new, T.nilable(Config))
40
47
  end
41
48
 
42
49
  sig { override.returns(T::Array[String]) }
43
50
  def permitted_keys
44
- %w[enforce_architecture layer]
51
+ [layer_config.enforce_key, 'layer']
45
52
  end
46
53
 
47
54
  sig do
48
- params(package_set: PackageSet, package: Package, config_file_path: String).returns(T::Array[Result])
55
+ params(package: Package, config_file_path: String, config: T::Hash[T.untyped, T.untyped]).returns(Result)
49
56
  end
50
- def check_dependencies_setting(package_set, package, config_file_path)
51
- results = T.let([], T::Array[Result])
52
- package.config.fetch('dependencies', []).each do |dependency|
53
- other_packwerk_package = package_set.fetch(dependency)
54
- next if other_packwerk_package.nil?
57
+ def check_enforce_key(package, config_file_path, config)
58
+ enforce_layer_present = !config[Config::LAYER_ENFORCE].nil?
59
+ enforce_architecture_present = !config[Config::ARCHITECTURE_ENFORCE].nil?
55
60
 
56
- other_package = Package.from(other_packwerk_package, layers)
57
- next if package.can_depend_on?(other_package, layers: layers)
58
-
59
- results << Result.new(
61
+ if layer_config.enforce_key == Config::LAYER_ENFORCE && enforce_architecture_present
62
+ Result.new(
63
+ ok: false,
64
+ error_value: "Unexpected `enforce_architecture` option in #{config_file_path.inspect}. Did you mean `enforce_layers`?"
65
+ )
66
+ elsif layer_config.enforce_key == Config::ARCHITECTURE_ENFORCE && enforce_layer_present
67
+ Result.new(
60
68
  ok: false,
61
- error_value: "Invalid 'dependencies' in #{config_file_path.inspect}. '#{config_file_path}' has a layer type of '#{package.layer},' which cannot rely on '#{other_packwerk_package.name},' which has a layer type of '#{other_package.layer}.' `architecture_layers` can be found in packwerk.yml."
69
+ error_value: "Unexpected `enforce_layers` option in #{config_file_path.inspect}. Did you mean `enforce_architecture`?"
62
70
  )
71
+ else
72
+ Result.new(ok: true)
63
73
  end
64
-
65
- results
66
74
  end
67
75
 
68
76
  sig do
@@ -75,14 +83,14 @@ module Packwerk
75
83
  if layer.nil? && package.enforces?
76
84
  Result.new(
77
85
  ok: false,
78
- error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{package.layer.inspect}. `layer` must be set if `enforce_architecture` is on."
86
+ error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{package.layer.inspect}. `layer` must be set if `#{layer_config.enforce_key}` is on."
79
87
  )
80
88
  elsif valid_layer
81
89
  Result.new(ok: true)
82
90
  else
83
91
  Result.new(
84
92
  ok: false,
85
- error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{layer.inspect}. Must be one of #{layers.names.to_a.inspect}"
93
+ error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{layer.inspect}. Must be one of #{layers.names_list.inspect}"
86
94
  )
87
95
  end
88
96
  end
@@ -90,19 +98,19 @@ module Packwerk
90
98
  sig do
91
99
  params(config_file_path: String, setting: T.untyped).returns(Result)
92
100
  end
93
- def check_enforce_architecture_setting(config_file_path, setting)
101
+ def check_enforce_layers_setting(config_file_path, setting)
94
102
  activated_value = [true, 'strict'].include?(setting)
95
103
  valid_value = [true, nil, false, 'strict'].include?(setting)
96
104
  layers_set = layers.names.any?
97
105
  if !valid_value
98
106
  Result.new(
99
107
  ok: false,
100
- error_value: "Invalid 'enforce_architecture' option in #{config_file_path.inspect}: #{setting.inspect}"
108
+ error_value: "Invalid '#{layer_config.enforce_key}' option in #{config_file_path.inspect}: #{setting.inspect}"
101
109
  )
102
110
  elsif activated_value && !layers_set
103
111
  Result.new(
104
112
  ok: false,
105
- error_value: "Cannot set 'enforce_architecture' option in #{config_file_path.inspect} until `architectural_layers` have been specified in `packwerk.yml`"
113
+ error_value: "Cannot set '#{layer_config.enforce_key}' option in #{config_file_path.inspect} until `layers` have been specified in `packwerk.yml`"
106
114
  )
107
115
  else
108
116
  Result.new(ok: true)
@@ -12,6 +12,37 @@ module Packwerk
12
12
  include Packwerk::Checker
13
13
 
14
14
  VIOLATION_TYPE = T.let('privacy', String)
15
+ PUBLICIZED_SIGIL = T.let('pack_public: true', String)
16
+ PUBLICIZED_SIGIL_REGEX = T.let(/#.*pack_public:\s*true/, Regexp)
17
+ @publicized_locations = T.let({}, T::Hash[String, T::Boolean])
18
+
19
+ class << self
20
+ extend T::Sig
21
+
22
+ sig { returns(T::Hash[String, T::Boolean]) }
23
+ def publicized_locations
24
+ @publicized_locations
25
+ end
26
+
27
+ sig { params(location: String).returns(T::Boolean) }
28
+ def publicized_location?(location)
29
+ unless publicized_locations.key?(location)
30
+ publicized_locations[location] = check_for_publicized_sigil(location)
31
+ end
32
+
33
+ T.must(publicized_locations[location])
34
+ end
35
+
36
+ sig { params(location: String).returns(T::Boolean) }
37
+ def check_for_publicized_sigil(location)
38
+ content_contains_sigil?(File.readlines(location))
39
+ end
40
+
41
+ sig { params(lines: T::Array[String]).returns(T::Boolean) }
42
+ def content_contains_sigil?(lines)
43
+ T.must(lines[0..4]).any? { |l| l =~ PUBLICIZED_SIGIL_REGEX }
44
+ end
45
+ end
15
46
 
16
47
  sig { override.returns(String) }
17
48
  def violation_type
@@ -28,6 +59,7 @@ module Packwerk
28
59
  privacy_package = Package.from(constant_package)
29
60
 
30
61
  return false if privacy_package.public_path?(reference.constant.location)
62
+ return false if self.class.publicized_location?(reference.constant.location)
31
63
 
32
64
  privacy_option = privacy_package.enforce_privacy
33
65
  return false if enforcement_disabled?(privacy_option)
@@ -44,7 +76,14 @@ module Packwerk
44
76
  end
45
77
  def strict_mode_violation?(listed_offense)
46
78
  publishing_package = listed_offense.reference.constant.package
47
- publishing_package.config['enforce_privacy'] == 'strict'
79
+
80
+ return false unless publishing_package.config['enforce_privacy'] == 'strict'
81
+ return false if exclude_from_strict?(
82
+ publishing_package.config['strict_privacy_ignored_patterns'] || [],
83
+ Pathname.new(listed_offense.reference.relative_path).cleanpath
84
+ )
85
+
86
+ true
48
87
  end
49
88
 
50
89
  sig do
@@ -98,6 +137,13 @@ module Packwerk
98
137
 
99
138
  standard_message.chomp
100
139
  end
140
+
141
+ sig { params(globs: T::Array[String], path: Pathname).returns(T::Boolean) }
142
+ def exclude_from_strict?(globs, path)
143
+ globs.any? do |glob|
144
+ path.fnmatch(glob, File::FNM_EXTGLOB)
145
+ end
146
+ end
101
147
  end
102
148
  end
103
149
  end
@@ -11,6 +11,7 @@ module Packwerk
11
11
  const :enforce_privacy, T.nilable(T.any(T::Boolean, String))
12
12
  const :private_constants, T::Array[String]
13
13
  const :ignored_private_constants, T::Array[String]
14
+ const :strict_privacy_ignored_patterns, T::Array[String]
14
15
 
15
16
  sig { params(path: String).returns(T::Boolean) }
16
17
  def public_path?(path)
@@ -27,7 +28,8 @@ module Packwerk
27
28
  user_defined_public_path: user_defined_public_path(package),
28
29
  enforce_privacy: package.config['enforce_privacy'],
29
30
  private_constants: package.config['private_constants'] || [],
30
- ignored_private_constants: package.config['ignored_private_constants'] || []
31
+ ignored_private_constants: package.config['ignored_private_constants'] || [],
32
+ strict_privacy_ignored_patterns: package.config['strict_privacy_ignored_patterns'] || []
31
33
  )
32
34
  end
33
35
 
@@ -31,7 +31,7 @@ module Packwerk
31
31
 
32
32
  sig { override.returns(T::Array[String]) }
33
33
  def permitted_keys
34
- %w[public_path enforce_privacy private_constants ignored_private_constants]
34
+ %w[public_path enforce_privacy private_constants ignored_private_constants strict_privacy_ignored_patterns]
35
35
  end
36
36
 
37
37
  private
@@ -110,9 +110,8 @@ module Packwerk
110
110
  def check_private_constant_location(configuration, package_set, name, location, config_file_path)
111
111
  declared_package = package_set.package_from_path(relative_path(configuration, config_file_path))
112
112
  constant_package = package_set.package_from_path(location)
113
-
114
113
  if constant_package == declared_package
115
- Result.new(ok: true)
114
+ check_for_publicized_constant(location, constant_package, name)
116
115
  else
117
116
  Result.new(
118
117
  ok: false,
@@ -122,6 +121,23 @@ module Packwerk
122
121
  end
123
122
  end
124
123
 
124
+ sig { params(location: String, constant_package: Packwerk::Package, name: T.untyped).returns(Result) }
125
+ def check_for_publicized_constant(location, constant_package, name)
126
+ if Packwerk::Privacy::Checker.publicized_location?(location)
127
+ sigil = Packwerk::Privacy::Checker::PUBLICIZED_SIGIL
128
+ Result.new(
129
+ ok: false,
130
+ error_value: "'#{name}' is an explicitly publicized constant declared in #{location} through usage of " \
131
+ "'#{sigil}'. However, the package '#{constant_package}' is also declaring it as a private " \
132
+ "constant. This conflict must be resolved. Either remove '#{sigil}' from #{location} or " \
133
+ 'remove this constant from the list of private constants in the config for ' \
134
+ "'#{constant_package}'."
135
+ )
136
+ else
137
+ Result.new(ok: true)
138
+ end
139
+ end
140
+
125
141
  sig { params(constants: T.untyped, config_file_path: String).returns(T::Array[Result]) }
126
142
  def assert_constants_can_be_loaded(constants, config_file_path)
127
143
  constants.map do |constant|
@@ -14,7 +14,7 @@ module Packwerk
14
14
  visible_settings = package_manifests_settings_for(configuration, 'visible_to')
15
15
  results = T.let([], T::Array[Result])
16
16
 
17
- all_package_names = package_set.map(&:name).to_set
17
+ all_package_names = package_set.to_set(&:name)
18
18
 
19
19
  package_manifests_settings_for(configuration, 'enforce_visibility').each do |config, setting|
20
20
  next if setting.nil?
@@ -7,7 +7,7 @@ require 'packwerk'
7
7
  require 'packwerk/privacy/checker'
8
8
  require 'packwerk/visibility/checker'
9
9
  require 'packwerk/folder_visibility/checker'
10
- require 'packwerk/architecture/checker'
10
+ require 'packwerk/layer/checker'
11
11
 
12
12
  module Packwerk
13
13
  module Extensions
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: packwerk-extensions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineers
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-23 00:00:00.000000000 Z
11
+ date: 2024-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: packwerk
@@ -187,13 +187,14 @@ extra_rdoc_files: []
187
187
  files:
188
188
  - README.md
189
189
  - lib/packwerk-extensions.rb
190
- - lib/packwerk/architecture/checker.rb
191
- - lib/packwerk/architecture/layers.rb
192
- - lib/packwerk/architecture/package.rb
193
- - lib/packwerk/architecture/validator.rb
194
190
  - lib/packwerk/folder_visibility/checker.rb
195
191
  - lib/packwerk/folder_visibility/package.rb
196
192
  - lib/packwerk/folder_visibility/validator.rb
193
+ - lib/packwerk/layer/checker.rb
194
+ - lib/packwerk/layer/config.rb
195
+ - lib/packwerk/layer/layers.rb
196
+ - lib/packwerk/layer/package.rb
197
+ - lib/packwerk/layer/validator.rb
197
198
  - lib/packwerk/privacy/checker.rb
198
199
  - lib/packwerk/privacy/package.rb
199
200
  - lib/packwerk/privacy/validator.rb
@@ -208,7 +209,7 @@ metadata:
208
209
  source_code_uri: https://github.com/rubyatscale/packwerk-extensions
209
210
  changelog_uri: https://github.com/rubyatscale/packwerk-extensions/releases
210
211
  allowed_push_host: https://rubygems.org
211
- post_install_message:
212
+ post_install_message:
212
213
  rdoc_options: []
213
214
  require_paths:
214
215
  - lib
@@ -216,15 +217,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
216
217
  requirements:
217
218
  - - ">="
218
219
  - !ruby/object:Gem::Version
219
- version: 2.6.0
220
+ version: '2.7'
220
221
  required_rubygems_version: !ruby/object:Gem::Requirement
221
222
  requirements:
222
223
  - - ">="
223
224
  - !ruby/object:Gem::Version
224
225
  version: '0'
225
226
  requirements: []
226
- rubygems_version: 3.1.6
227
- signing_key:
227
+ rubygems_version: 3.5.9
228
+ signing_key:
228
229
  specification_version: 4
229
230
  summary: A collection of extensions for packwerk packages.
230
231
  test_files: []