packwerk-extensions 0.1.7 → 0.1.9

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: 15205fb6e29f594b5bfea27a6c2ef0488fa1ec2b2d6ae5265bfa906f7bac49a9
4
- data.tar.gz: 6e795da44e384491a129e496a8541284317d0c523db59d431731119529535bbb
3
+ metadata.gz: 39d34d165ec6218799d3416a91249917b0d5b64272af7a8ba60c66e3473876c5
4
+ data.tar.gz: 040f5a83a6fef23e26efef5149fce578329c5db46b9d8f3ebe0d380e56250683
5
5
  SHA512:
6
- metadata.gz: da2182b6b67511413dc606f943303130a955ba87f92e6798543a995eb94dc5577b1f1951decbb23751ce4c08535b361fc91778ebc6cea357d62f3860d4081e94
7
- data.tar.gz: ee4701f2cc1ecf037823ffa7abe0cce8fa8d5bb2c61dcf587b85ab4331611968f7dfad84439a141479341c38f5a8a326e4b76c1b3040fa3672429c483a39a52c
6
+ metadata.gz: e6f3ac7caadd78432c6b5a45f593c63445e23bbc30784528b13c505175e4358f0efb0de65eeca41b6a049431d24855834d191b2a1b10892dda56016eb777e937
7
+ data.tar.gz: 7d4fbd68b612ce1cb185e574fca289bf8266133cda8f47a47d95a14adf098c5fa2805d6cdf15ba4c47287b5afcc4674328b7308e255c713cffc24b43c457cd8b
data/README.md CHANGED
@@ -5,7 +5,8 @@
5
5
  Currently, it ships the following checkers to help improve the boundaries between packages. These checkers are:
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
- - An experimental `architecture` checker that allows packages to specify their "layer" and requires that each layer only communicate with layers below it.
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
10
 
10
11
  ## Installation
11
12
 
@@ -24,6 +25,7 @@ Alternatively, you can require individual checkers:
24
25
  require:
25
26
  - packwerk/privacy/checker
26
27
  - packwerk/visibility/checker
28
+ - packwerk/folder_visibility/checker
27
29
  - packwerk/architecture/checker
28
30
  ```
29
31
 
@@ -54,9 +56,86 @@ Example:
54
56
  public_path: my/custom/path/
55
57
  ```
56
58
 
59
+ ### Defining public constants through sigil
60
+
61
+ > [!WARNING]
62
+ > 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):
63
+ >
64
+ > 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.
65
+ >
66
+ > In the rubyatscale org:
67
+ >
68
+ > * 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)
69
+ > * Other places that mention public_path or app/public.
70
+ > * Org wide search for app/public link
71
+ > * Org wide search for public_path link
72
+ > * packs (the Rust port of packwerk – I could take this one over unless someone is interested in implementing whatever we come up with there
73
+
74
+
75
+
76
+ You may make individual files public withhin a private package by usage of a comment within the first 5 lines of the `.rb` file containing `pack_public: true`.
77
+
78
+ Example:
79
+
80
+ ```ruby
81
+ # pack_public: true
82
+ module Foo
83
+ class Update
84
+ end
85
+ end
86
+ ```
87
+ Now `Foo::Update` is considered public even though the `foo` package might be set to `enforce_private: (true || :strict)`.
88
+
89
+ It's important to note that when combining `public_api: true` with the declaration of `private_constants`,
90
+ `packwerk validate` will raise an exception if both are used for the same constant. This must be resolved by removing
91
+ the sigil from the `.rb` file or removing the constant from the list of `private_constants`.
92
+
93
+ 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 anyway so it does not recognize `public_pack: 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.
94
+ ```
95
+ -----
96
+ # typed: ignore
97
+ # frozen_string_literal: true
98
+ # pack_public: true
99
+
100
+ class Foo
101
+ ...
102
+ end => Layout/EmptyLineAfterMagicComment: Add an empty line after magic comments.
103
+
104
+ ------
105
+ # typed: ignore
106
+ # frozen_string_literal: true
107
+
108
+ # pack_public: true
109
+
110
+ class Foo
111
+ ...
112
+ 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 public_pack so there is risk at it being missed. It also is requiring extra empty lines in the group of magic comments.
113
+
114
+ -----
115
+ # pack_public: true
116
+ # typed: ignore
117
+ # frozen_string_literal: true
118
+
119
+ class Foo
120
+ ...
121
+ end => Ideal solution. No exceptions from rubocop and very low risk of the magic comment being out of range since
122
+ ```
123
+
57
124
  ### Using specific private constants
58
125
  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.
59
126
 
127
+ ### Ignore strict mode for violation coming from specific path patterns
128
+ If you want to activate `'strict'` mode on you package but have a few privacy violations you know you will deal with later,
129
+ you can set a list of patterns to exclude.
130
+
131
+ ```yaml
132
+ enforce_privacy: strict
133
+ strict_privacy_ignored_patterns:
134
+ - engines/another_engine/test/**/*
135
+ ```
136
+
137
+ In this example, violations on constants of your engine referenced in those files `engines/another_engine/test/**/*` will not fail Packwerk checks.
138
+
60
139
  ### Package Privacy violation
61
140
  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.
62
141
 
@@ -87,6 +166,30 @@ visible_to:
87
166
  - components/other_package
88
167
  ```
89
168
 
169
+ ## Folder-Visibility Checker
170
+ The folder visibility checker can be used to allow a package to be private to their sibling packs and parent packs and will create todos if used by any other package.
171
+
172
+ To enforce visibility for your package, set `enforce_folder_visibility` to `true` on your pack.
173
+
174
+ ```yaml
175
+ # components/merchandising/package.yml
176
+ enforce_folder_visibility: true
177
+ ```
178
+
179
+ Here is an example of paths and whether their use of `packs/b/packs/e` is OK or not, assuming that protects itself via `enforce_folder_visibility`
180
+
181
+ ```
182
+ . OK (parent of parent)
183
+ packs/a VIOLATION
184
+ packs/b OK (parent)
185
+ packs/b/packs/d OK (sibling)
186
+ packs/b/packs/e ENFORCE_NESTED_VISIBILITY: TRUE
187
+ packs/b/packs/e/packs/f VIOLATION
188
+ packs/b/packs/e/packs/g VIOLATION
189
+ packs/b/packs/h OK (sibling)
190
+ packs/c VIOLATION
191
+ ```
192
+
90
193
  ## Architecture Checker
91
194
  The architecture checker can be used to enforce constraints on what can depend on what.
92
195
 
@@ -105,3 +208,19 @@ layer: utility
105
208
  ```
106
209
 
107
210
  Now this pack can only depend on other utility packages.
211
+
212
+
213
+ ## Contributing
214
+
215
+ Got another checker you would like to add? Add it to this repo!
216
+
217
+ Please ensure these commands pass for you locally:
218
+
219
+ ```
220
+ bundle
221
+ srb tc
222
+ bin/rubocop
223
+ bin/rake test
224
+ ```
225
+
226
+ Then, submit a PR!
@@ -0,0 +1,93 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'packwerk/folder_visibility/package'
5
+ require 'packwerk/folder_visibility/validator'
6
+
7
+ module Packwerk
8
+ module FolderVisibility
9
+ class Checker
10
+ extend T::Sig
11
+ include Packwerk::Checker
12
+
13
+ VIOLATION_TYPE = T.let('folder_visibility', String)
14
+
15
+ sig { override.returns(String) }
16
+ def violation_type
17
+ VIOLATION_TYPE
18
+ end
19
+
20
+ sig do
21
+ override
22
+ .params(reference: Packwerk::Reference)
23
+ .returns(T::Boolean)
24
+ end
25
+ def invalid_reference?(reference)
26
+ referencing_package = reference.package
27
+ referenced_package = reference.constant.package
28
+
29
+ return false if enforcement_disabled?(Package.from(referenced_package).enforce_folder_visibility)
30
+
31
+ # the root pack is parent folder of all packs, so we short-circuit this here
32
+ referencing_package_is_root_pack = referencing_package.name == '.'
33
+ return false if referencing_package_is_root_pack
34
+
35
+ packages_are_sibling_folders = Pathname.new(referenced_package.name).dirname == Pathname.new(referencing_package.name).dirname
36
+ return false if packages_are_sibling_folders
37
+
38
+ referencing_package_is_parent_folder = Pathname.new(referenced_package.name).to_s.start_with?(referencing_package.name)
39
+ return false if referencing_package_is_parent_folder
40
+
41
+ true
42
+ end
43
+
44
+ sig do
45
+ override
46
+ .params(listed_offense: Packwerk::ReferenceOffense)
47
+ .returns(T::Boolean)
48
+ end
49
+ def strict_mode_violation?(listed_offense)
50
+ publishing_package = listed_offense.reference.constant.package
51
+ publishing_package.config['enforce_folder_visibility'] == 'strict'
52
+ end
53
+
54
+ sig do
55
+ override
56
+ .params(reference: Packwerk::Reference)
57
+ .returns(String)
58
+ end
59
+ def message(reference)
60
+ source_desc = "'#{reference.package}'"
61
+
62
+ message = <<~MESSAGE
63
+ Folder Visibility violation: '#{reference.constant.name}' belongs to '#{reference.constant.package}', which is not visible to #{source_desc} as it is not a sibling pack or parent pack.
64
+ Is there a different package to use instead, or should '#{reference.constant.package}' also be visible to #{source_desc}?
65
+
66
+ #{standard_help_message(reference)}
67
+ MESSAGE
68
+
69
+ message.chomp
70
+ end
71
+
72
+ private
73
+
74
+ sig do
75
+ params(visibility_option: T.nilable(T.any(T::Boolean, String)))
76
+ .returns(T::Boolean)
77
+ end
78
+ def enforcement_disabled?(visibility_option)
79
+ [false, nil].include?(visibility_option)
80
+ end
81
+
82
+ sig { params(reference: Reference).returns(String) }
83
+ def standard_help_message(reference)
84
+ standard_message = <<~MESSAGE.chomp
85
+ Inference details: this is a reference to #{reference.constant.name} which seems to be defined in #{reference.constant.location}.
86
+ To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations
87
+ MESSAGE
88
+
89
+ standard_message.chomp
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,23 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module FolderVisibility
6
+ class Package < T::Struct
7
+ extend T::Sig
8
+
9
+ const :enforce_folder_visibility, T.nilable(T.any(T::Boolean, String))
10
+
11
+ class << self
12
+ extend T::Sig
13
+
14
+ sig { params(package: ::Packwerk::Package).returns(Package) }
15
+ def from(package)
16
+ Package.new(
17
+ enforce_folder_visibility: package.config['enforce_folder_visibility']
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module FolderVisibility
6
+ class Validator
7
+ extend T::Sig
8
+ include Packwerk::Validator
9
+
10
+ Result = Packwerk::Validator::Result
11
+
12
+ sig { override.params(package_set: PackageSet, configuration: Configuration).returns(Result) }
13
+ def call(package_set, configuration)
14
+ results = T.let([], T::Array[Result])
15
+
16
+ package_manifests_settings_for(configuration, 'enforce_folder_visibility').each do |config, setting|
17
+ next if setting.nil?
18
+
19
+ next if [TrueClass, FalseClass].include?(setting.class) || setting == 'strict'
20
+
21
+ results << Result.new(
22
+ ok: false,
23
+ error_value: "\tInvalid 'enforce_folder_visibility' option: #{setting.inspect} in #{config.inspect}"
24
+ )
25
+ end
26
+
27
+ merge_results(results, separator: "\n---\n")
28
+ end
29
+
30
+ sig { override.returns(T::Array[String]) }
31
+ def permitted_keys
32
+ %w[enforce_folder_visibility]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -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|
@@ -6,6 +6,7 @@ require 'packwerk'
6
6
 
7
7
  require 'packwerk/privacy/checker'
8
8
  require 'packwerk/visibility/checker'
9
+ require 'packwerk/folder_visibility/checker'
9
10
  require 'packwerk/architecture/checker'
10
11
 
11
12
  module Packwerk
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.7
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-09 00:00:00.000000000 Z
11
+ date: 2023-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: packwerk
@@ -191,6 +191,9 @@ files:
191
191
  - lib/packwerk/architecture/layers.rb
192
192
  - lib/packwerk/architecture/package.rb
193
193
  - lib/packwerk/architecture/validator.rb
194
+ - lib/packwerk/folder_visibility/checker.rb
195
+ - lib/packwerk/folder_visibility/package.rb
196
+ - lib/packwerk/folder_visibility/validator.rb
194
197
  - lib/packwerk/privacy/checker.rb
195
198
  - lib/packwerk/privacy/package.rb
196
199
  - lib/packwerk/privacy/validator.rb