packwerk-extensions 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 661762b44e28e84ff3b70007bc039b6f767ddf638524f5a069a832e1f0d2fb34
4
- data.tar.gz: a229b9432f95c10b211a411fee200e48721ff8d3c6b3fd1b745f8211cf58038a
3
+ metadata.gz: 1fb13125668a6ba704b97b2a91ffd7af5daf052b32732c7cea8a0f5c7da6bc75
4
+ data.tar.gz: a82b52e56c9a7b03bcc7b1963105feb3c99225f85a3159e8793dc9d0cf19285a
5
5
  SHA512:
6
- metadata.gz: 32e5084af7d8e7cba4b743d0be13d29c1ea05b0c6e00ab7d0b9a38a229545093cf21f9e18da883987e487588f5bb7a47cc8728912332c9f295a42b03432bafc3
7
- data.tar.gz: 01ff189cc3e645da3eeab3176351ebc0d23d274a9d20269f32733cdb2771169f365857bc946a66e5982c447887f81817fee047b82709e34115f18abaa53a0479
6
+ metadata.gz: 6a7bfa59d26c83525e1b87f0ba0d8270ff1043c42f85189668d8ec5047c534a2e249c082c6fb600ef3d3b1c8fba3351db59425ab07bd8ab372b4956aef6750f9
7
+ data.tar.gz: 9d2e225bf812bb133726667754d969e4bb2ff5c4a4ede918faf2cb9178349946f7041bc9a60b163c8a100e02d5cdacf31289ada63475af040a1432e2c3d9bcdf
data/README.md CHANGED
@@ -1,12 +1,34 @@
1
1
  # packwerk-extensions
2
2
 
3
- `packwerk-extensions` is a home for checker extensions for packwerk.
3
+ `packwerk-extensions` is a home for checker extensions for [packwerk](https://github.com/Shopify/packwerk) 3.
4
+
5
+ Note that packwerk has not yet released packwerk 3. If you'd like to use `packwerk-extensions`, you'll need to point your `Gemfile` at the `packwerk` `main` branch:
6
+ ```ruby
7
+ gem 'packwerk', github: 'Shopify/packwerk', branch: 'main'
8
+ ```
4
9
 
5
10
  Currently, it ships the following checkers to help improve the boundaries between packages. These checkers are:
6
11
  - A `privacy` checker that ensures other packages are using your package's public API
7
12
  - A `visibility` checker that allows packages to be private except to an explicit group of other packages.
8
13
  - An experimental `architecture` checker that allows packages to specify their "layer" and requires that each layer only communicate with layers below it.
9
14
 
15
+ ## Installation
16
+
17
+ To register all checkers included in this gem, add the following to your `packwerk.yml`:
18
+
19
+ ```yaml
20
+ require: packwerk-extensions
21
+ ```
22
+
23
+ Alternatively, you can require individual checkers:
24
+
25
+ ```yaml
26
+ require:
27
+ - packwerk/privacy/checker
28
+ - packwerk/visibility/checker
29
+ - packwerk/architecture/checker
30
+ ```
31
+
10
32
  ## Privacy Checker
11
33
  The privacy checker extension was originally extracted from [packwerk](https://github.com/Shopify/packwerk).
12
34
 
@@ -34,6 +56,9 @@ Example:
34
56
  public_path: my/custom/path/
35
57
  ```
36
58
 
59
+ ### Using specific private constants
60
+ 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
+
37
62
  ### Package Privacy violation
38
63
  A constant that is private to its package has been referenced from outside of the package. Constants are declared private in their package’s `package.yml`.
39
64
 
@@ -2,6 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'packwerk/architecture/layers'
5
+ require 'packwerk/architecture/package'
6
+ require 'packwerk/architecture/validator'
5
7
 
6
8
  module Packwerk
7
9
  module Architecture
@@ -91,20 +91,21 @@ module Packwerk
91
91
  params(config_file_path: String, setting: T.untyped).returns(Result)
92
92
  end
93
93
  def check_enforce_architecture_setting(config_file_path, setting)
94
+ activated_value = [true, 'strict'].include?(setting)
94
95
  valid_value = [true, nil, false, 'strict'].include?(setting)
95
96
  layers_set = layers.names.any?
96
- if valid_value && layers_set
97
- Result.new(ok: true)
98
- elsif valid_value
97
+ if !valid_value
99
98
  Result.new(
100
99
  ok: false,
101
- error_value: "Cannot set 'enforce_architecture' option in #{config_file_path.inspect} until `architectural_layers` have been specified in `packwerk.yml`"
100
+ error_value: "Invalid 'enforce_architecture' option in #{config_file_path.inspect}: #{setting.inspect}"
102
101
  )
103
- else
102
+ elsif activated_value && !layers_set
104
103
  Result.new(
105
104
  ok: false,
106
- error_value: "Invalid 'enforce_architecture' option in #{config_file_path.inspect}: #{setting.inspect}"
105
+ error_value: "Cannot set 'enforce_architecture' option in #{config_file_path.inspect} until `architectural_layers` have been specified in `packwerk.yml`"
107
106
  )
107
+ else
108
+ Result.new(ok: true)
108
109
  end
109
110
  end
110
111
  end
@@ -1,6 +1,9 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'packwerk/privacy/package'
5
+ require 'packwerk/privacy/validator'
6
+
4
7
  module Packwerk
5
8
  module Privacy
6
9
  # Checks whether a given reference references a private constant of another package.
@@ -27,7 +30,9 @@ module Packwerk
27
30
  return false if privacy_package.public_path?(reference.constant.location)
28
31
 
29
32
  privacy_option = privacy_package.enforce_privacy
30
- !enforcement_disabled?(privacy_option)
33
+ return false if enforcement_disabled?(privacy_option)
34
+
35
+ explicitly_private_constant?(reference.constant, explicitly_private_constants: privacy_package.private_constants)
31
36
  end
32
37
 
33
38
  sig do
@@ -60,6 +65,20 @@ module Packwerk
60
65
 
61
66
  private
62
67
 
68
+ sig do
69
+ params(
70
+ constant: ConstantContext,
71
+ explicitly_private_constants: T::Array[String]
72
+ ).returns(T::Boolean)
73
+ end
74
+ def explicitly_private_constant?(constant, explicitly_private_constants:)
75
+ return true if explicitly_private_constants.empty?
76
+
77
+ explicitly_private_constants.include?(constant.name) ||
78
+ # nested constants
79
+ explicitly_private_constants.any? { |epc| constant.name.start_with?("#{epc}::") }
80
+ end
81
+
63
82
  sig do
64
83
  params(privacy_option: T.nilable(T.any(T::Boolean, String, T::Array[String])))
65
84
  .returns(T::Boolean)
@@ -8,7 +8,8 @@ module Packwerk
8
8
 
9
9
  const :public_path, String
10
10
  const :user_defined_public_path, T.nilable(String)
11
- const :enforce_privacy, T.nilable(T.any(T::Boolean, String, T::Array[String]))
11
+ const :enforce_privacy, T.nilable(T.any(T::Boolean, String))
12
+ const :private_constants, T::Array[String]
12
13
 
13
14
  sig { params(path: String).returns(T::Boolean) }
14
15
  def public_path?(path)
@@ -23,7 +24,8 @@ module Packwerk
23
24
  Package.new(
24
25
  public_path: public_path_for(package),
25
26
  user_defined_public_path: user_defined_public_path(package),
26
- enforce_privacy: package.config['enforce_privacy']
27
+ enforce_privacy: package.config['enforce_privacy'],
28
+ private_constants: package.config['private_constants'] || []
27
29
  )
28
30
  end
29
31
 
@@ -19,6 +19,8 @@ module Packwerk
19
19
  results << check_enforce_privacy_setting(config_file_path, setting)
20
20
  end
21
21
 
22
+ results += verify_private_constants_setting(package_set, configuration)
23
+
22
24
  public_path_settings = package_manifests_settings_for(configuration, 'public_path')
23
25
  public_path_settings.each do |config_file_path, setting|
24
26
  results << check_public_path(config_file_path, setting)
@@ -29,11 +31,49 @@ module Packwerk
29
31
 
30
32
  sig { override.returns(T::Array[String]) }
31
33
  def permitted_keys
32
- %w[public_path enforce_privacy]
34
+ %w[public_path enforce_privacy private_constants]
33
35
  end
34
36
 
35
37
  private
36
38
 
39
+ sig { params(package_set: PackageSet, configuration: Configuration).returns(T::Array[Result]) }
40
+ def verify_private_constants_setting(package_set, configuration)
41
+ private_constants_setting = package_manifests_settings_for(configuration, 'private_constants')
42
+ results = T.let([], T::Array[Result])
43
+ resolver = ConstantResolver.new(
44
+ root_path: configuration.root_path,
45
+ load_paths: configuration.load_paths
46
+ )
47
+
48
+ private_constants_setting.each do |config_file_path, setting|
49
+ next if setting.nil?
50
+
51
+ unless setting.is_a?(Array)
52
+ results << Result.new(
53
+ ok: false,
54
+ error_value: "Invalid 'private_constants' setting: #{setting.inspect}"
55
+ )
56
+ next
57
+ end
58
+
59
+ constants = setting
60
+
61
+ results += assert_constants_can_be_loaded(constants, config_file_path)
62
+
63
+ constant_locations = constants.map { |c| [c, resolver.resolve(c)&.location] }
64
+
65
+ constant_locations.each do |name, location|
66
+ results << if location
67
+ check_private_constant_location(configuration, package_set, name, location, config_file_path)
68
+ else
69
+ private_constant_unresolvable(name, config_file_path)
70
+ end
71
+ end
72
+ end
73
+
74
+ results
75
+ end
76
+
37
77
  sig do
38
78
  params(config_file_path: String, setting: T.untyped).returns(Result)
39
79
  end
@@ -52,7 +92,7 @@ module Packwerk
52
92
  params(config_file_path: String, setting: T.untyped).returns(Result)
53
93
  end
54
94
  def check_enforce_privacy_setting(config_file_path, setting)
55
- if [TrueClass, FalseClass, Array, NilClass].include?(setting.class) || setting == 'strict'
95
+ if [TrueClass, FalseClass, NilClass].include?(setting.class) || setting == 'strict'
56
96
  Result.new(ok: true)
57
97
  else
58
98
  Result.new(
@@ -61,6 +101,55 @@ module Packwerk
61
101
  )
62
102
  end
63
103
  end
104
+
105
+ sig do
106
+ params(configuration: Configuration, package_set: PackageSet, name: T.untyped, location: T.untyped,
107
+ config_file_path: T.untyped).returns(Result)
108
+ end
109
+ def check_private_constant_location(configuration, package_set, name, location, config_file_path)
110
+ declared_package = package_set.package_from_path(relative_path(configuration, config_file_path))
111
+ constant_package = package_set.package_from_path(location)
112
+
113
+ if constant_package == declared_package
114
+ Result.new(ok: true)
115
+ else
116
+ Result.new(
117
+ ok: false,
118
+ error_value: "'#{name}' is declared as private in the '#{declared_package}' package but appears to be " \
119
+ "defined\nin the '#{constant_package}' package. Packwerk resolved it to #{location}."
120
+ )
121
+ end
122
+ end
123
+
124
+ sig { params(constants: T.untyped, config_file_path: String).returns(T::Array[Result]) }
125
+ def assert_constants_can_be_loaded(constants, config_file_path)
126
+ constants.map do |constant|
127
+ if constant.start_with?('::')
128
+ constant.try(&:constantize) && Result.new(ok: true)
129
+ else
130
+ error_value = "'#{constant}', listed in the 'private_constants' option " \
131
+ "in #{config_file_path}, is invalid.\nPrivate constants need to be " \
132
+ 'prefixed with the top-level namespace operator `::`.'
133
+ Result.new(
134
+ ok: false,
135
+ error_value: error_value
136
+ )
137
+ end
138
+ end
139
+ end
140
+
141
+ sig { params(name: T.untyped, config_file_path: T.untyped).returns(Result) }
142
+ def private_constant_unresolvable(name, config_file_path)
143
+ explicit_filepath = "#{(name.start_with?('::') ? name[2..] : name).underscore}.rb"
144
+
145
+ Result.new(
146
+ ok: false,
147
+ error_value: "'#{name}', listed in #{config_file_path}, could not be resolved.\n" \
148
+ "This is probably because it is an autovivified namespace - a namespace module that doesn't have a\n" \
149
+ "file explicitly defining it. Packwerk currently doesn't support declaring autovivified namespaces as\n" \
150
+ "private. Add a #{explicit_filepath} file to explicitly define the constant."
151
+ )
152
+ end
64
153
  end
65
154
  end
66
155
  end
@@ -1,6 +1,9 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'packwerk/visibility/package'
5
+ require 'packwerk/visibility/validator'
6
+
4
7
  module Packwerk
5
8
  module Visibility
6
9
  # Checks whether a given reference references a constant from a package that does not permit visibility
@@ -5,16 +5,8 @@ require 'sorbet-runtime'
5
5
  require 'packwerk'
6
6
 
7
7
  require 'packwerk/privacy/checker'
8
- require 'packwerk/privacy/package'
9
- require 'packwerk/privacy/validator'
10
-
11
8
  require 'packwerk/visibility/checker'
12
- require 'packwerk/visibility/package'
13
- require 'packwerk/visibility/validator'
14
-
15
9
  require 'packwerk/architecture/checker'
16
- require 'packwerk/architecture/package'
17
- require 'packwerk/architecture/validator'
18
10
 
19
11
  module Packwerk
20
12
  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.0.8
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-29 00:00:00.000000000 Z
11
+ date: 2023-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: packwerk
@@ -25,19 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: 2.2.1
27
27
  - !ruby/object:Gem::Dependency
28
- name: rails
28
+ name: railties
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: 6.0.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: 6.0.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: sorbet-runtime
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: rubocop
113
127
  requirement: !ruby/object:Gem::Requirement