packwerk-extensions 0.0.8 → 0.1.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: 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