package_protections 1.0.0 → 1.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: a1189b0779bbda54403506984a5719d37ac3444776e34150defe591ecb96fc81
4
- data.tar.gz: 1006d4722077a1581cf3509358eb4f5b58d5e443bd6c9d203309472bb26f980d
3
+ metadata.gz: 64a576d5910c24c2f966b5f8db6c025af93a31b866c3956cd98b54adbc7d52da
4
+ data.tar.gz: e8a696185cdffdb44eb1a136ad4dba73498d249faacadcb731d1559eb7e94847
5
5
  SHA512:
6
- metadata.gz: 383d9bac29ac9e7be26d41de6d7af08eeb85d635e471f91a1a1ad08fd565aaaa25f42763d9e0bfc75cded4beb6d911bff267744d2a6408651132b93c4a30d70b
7
- data.tar.gz: 1fd35343e9db0689656378111537df0369910874f0a59f38dcb2fa1bc712c6233e45e945cccec591c3f6a71da89e64b62aeffaa2cd565146a217aa43d09900a1
6
+ metadata.gz: 63194b36d9bd8edf9faab0136a58994b9a93dcfb49001ab75d057dbb7bf2522b1b235ffae8352ffbe6b3d3f13c8f5f171a3e7498ade041787ff72cd466762958
7
+ data.tar.gz: 67417571f0fe028ef5960985518f753de09a7b253ff56dd878f4dec89e3ca0e796a9f928da78db68da1cd29e47bd6bbd56ebff4a4882941bf6635033eb2bf76b
data/README.md CHANGED
@@ -141,7 +141,7 @@ end
141
141
  In this example, `MyCustomProtection` needs to implement the `PackageProtections::ProtectionInterface` (for protections powered by `packwerk` that look at new and existing violations) OR `PackageProtections::RubocopProtectionInterface` (for protections powered by `rubocop` that look at the AST). It's recommended to take a look at the existing protections as examples. If you're having any trouble with this, please file an issue and we'll be glad to help.
142
142
 
143
143
  ## Incorporating into your CI Pipeline
144
- Your CI pipeline can execute the public API ta and fail if there are any offenses.
144
+ Your CI pipeline can execute the public API and fail if there are any offenses.
145
145
 
146
146
  ## Discussions, Issues, Questions, and More
147
147
  To keep things organized, here are some recommended homes:
@@ -28,8 +28,8 @@ module PackageProtections
28
28
  [
29
29
  Private::OutgoingDependencyProtection.new,
30
30
  Private::IncomingPrivacyProtection.new,
31
- Private::TypedApiProtection.new,
32
- Private::MultipleNamespacesProtection.new,
31
+ RuboCop::Cop::PackageProtections::TypedPublicApi.new,
32
+ RuboCop::Cop::PackageProtections::NamespacedUnderPackageName.new,
33
33
  Private::VisibilityProtection.new
34
34
  ]
35
35
  end
@@ -3,11 +3,9 @@
3
3
 
4
4
  require 'package_protections/private/colorized_string'
5
5
  require 'package_protections/private/output'
6
- require 'package_protections/private/typed_api_protection'
7
6
  require 'package_protections/private/incoming_privacy_protection'
8
7
  require 'package_protections/private/outgoing_dependency_protection'
9
8
  require 'package_protections/private/metadata_modifiers'
10
- require 'package_protections/private/multiple_namespaces_protection'
11
9
  require 'package_protections/private/visibility_protection'
12
10
  require 'package_protections/private/configuration'
13
11
 
@@ -3,26 +3,6 @@
3
3
  # typed: strict
4
4
  module PackageProtections
5
5
  module RubocopProtectionInterface
6
- include ProtectionInterface
7
- extend T::Sig
8
- extend T::Helpers
9
-
10
- abstract!
11
-
12
- sig do
13
- abstract
14
- .params(packages: T::Array[ProtectedPackage])
15
- .returns(T::Array[CopConfig])
16
- end
17
- def cop_configs(packages); end
18
-
19
- sig do
20
- params(package: ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
21
- end
22
- def custom_cop_config(package)
23
- {}
24
- end
25
-
26
6
  class CopConfig < T::Struct
27
7
  extend T::Sig
28
8
  const :name, String
@@ -46,6 +26,40 @@ module PackageProtections
46
26
  end
47
27
  end
48
28
 
29
+ include ProtectionInterface
30
+ extend T::Sig
31
+ extend T::Helpers
32
+
33
+ abstract!
34
+
35
+ ###########################################################################
36
+ # Abstract Methods: These are methods that the client needs to implement
37
+ ############################################################################
38
+ sig { abstract.returns(String) }
39
+ def cop_name; end
40
+
41
+ sig do
42
+ abstract.params(file: String).returns(String)
43
+ end
44
+ def message_for_fail_on_any(file); end
45
+
46
+ sig { abstract.returns(T::Array[String]) }
47
+ def included_globs_for_pack; end
48
+
49
+ ###########################################################################
50
+ # Overriddable Methods: These are methods that the client can override,
51
+ # but a default is provided.
52
+ ############################################################################
53
+ sig do
54
+ params(package: ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
55
+ end
56
+ def custom_cop_config(package)
57
+ {}
58
+ end
59
+
60
+ sig { override.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
61
+ def unmet_preconditions_for_behavior(behavior, package); end
62
+
49
63
  sig do
50
64
  override.params(
51
65
  new_violations: T::Array[PerFileViolation]
@@ -73,6 +87,66 @@ module PackageProtections
73
87
  end
74
88
  end
75
89
 
90
+ sig do
91
+ override.params(
92
+ protected_packages: T::Array[ProtectedPackage]
93
+ ).returns(T::Array[Offense])
94
+ end
95
+ def get_offenses_for_existing_violations(protected_packages)
96
+ exclude_list = exclude_for_rule(cop_name)
97
+ offenses = []
98
+
99
+ protected_packages.each do |package|
100
+ violation_behavior = package.violation_behavior_for(identifier)
101
+
102
+ case violation_behavior
103
+ when ViolationBehavior::FailNever, ViolationBehavior::FailOnNew
104
+ next
105
+ when ViolationBehavior::FailOnAny
106
+ # Continue
107
+ else
108
+ T.absurd(violation_behavior)
109
+ end
110
+
111
+ package.original_package.directory.glob('**/**/*.*').each do |relative_path_to_file|
112
+ next unless exclude_list.include?(relative_path_to_file.to_s)
113
+
114
+ file = relative_path_to_file.to_s
115
+ offenses << Offense.new(
116
+ file: file,
117
+ message: message_for_fail_on_any(file),
118
+ violation_type: identifier,
119
+ package: package.original_package
120
+ )
121
+ end
122
+ end
123
+
124
+ offenses
125
+ end
126
+
127
+ sig do
128
+ params(packages: T::Array[ProtectedPackage])
129
+ .returns(T::Array[CopConfig])
130
+ end
131
+ def cop_configs(packages)
132
+ include_paths = T.let([], T::Array[String])
133
+ packages.each do |p|
134
+ next unless p.violation_behavior_for(identifier).enabled?
135
+
136
+ included_globs_for_pack.each do |glob|
137
+ include_paths << p.original_package.directory.join(glob).to_s
138
+ end
139
+ end
140
+
141
+ [
142
+ CopConfig.new(
143
+ name: cop_name,
144
+ enabled: include_paths.any?,
145
+ include_paths: include_paths
146
+ )
147
+ ]
148
+ end
149
+
76
150
  private
77
151
 
78
152
  sig { params(rule: String).returns(T::Set[String]) }
@@ -1,4 +1,4 @@
1
- # typed: ignore
1
+ # typed: true
2
2
 
3
3
  # For String#camelize
4
4
  require 'active_support/core_ext/string/inflections'
@@ -7,7 +7,10 @@ module RuboCop
7
7
  module Cop
8
8
  module PackageProtections
9
9
  class NamespacedUnderPackageName < Base
10
+ extend T::Sig
11
+
10
12
  include RangeHelp
13
+ include ::PackageProtections::RubocopProtectionInterface
11
14
 
12
15
  def on_new_investigation
13
16
  absolute_filepath = Pathname.new(processed_source.file_path)
@@ -39,7 +42,7 @@ module RuboCop
39
42
  #
40
43
  # Therefore, for our implementation, we substitute out the non-namespace producing portions of the filename to count the number of namespaces.
41
44
  # Note this will *not work* properly in applications that have different assumptions about autoloading.
42
- package_last_name = package_name.split('/').last
45
+ package_last_name = T.must(package_name.split('/').last)
43
46
  path_without_package_base = relative_filename.gsub(%r{#{package_name}/app/}, '')
44
47
  if path_without_package_base.include?('concerns')
45
48
  autoload_folder_name = path_without_package_base.split('/').first(2).join('/')
@@ -85,6 +88,77 @@ module RuboCop
85
88
  end
86
89
  end
87
90
 
91
+ IDENTIFIER = 'prevent_this_package_from_creating_other_namespaces'.freeze
92
+
93
+ sig { override.returns(String) }
94
+ def identifier
95
+ IDENTIFIER
96
+ end
97
+
98
+ sig { override.params(behavior: ::PackageProtections::ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
99
+ def unmet_preconditions_for_behavior(behavior, package)
100
+ if !behavior.enabled? && !package.metadata['global_namespaces'].nil?
101
+ "Invalid configuration for package `#{package.name}`. `#{identifier}` must be turned on to use `global_namespaces` configuration."
102
+ else
103
+ # We don't need to validate if the behavior is currentely fail_never
104
+ return if behavior.fail_never?
105
+
106
+ # The reason for this is precondition is the `MultipleNamespacesProtection` assumes this to work properly.
107
+ # To remove this precondition, we need to modify `MultipleNamespacesProtection` to be more generalized!
108
+ if ::PackageProtections::EXPECTED_PACK_DIRECTORIES.include?(Pathname.new(package.name).dirname.to_s) || package.name == ParsePackwerk::ROOT_PACKAGE_NAME
109
+ nil
110
+ else
111
+ "Package #{package.name} must be located in one of #{::PackageProtections::EXPECTED_PACK_DIRECTORIES.join(', ')} (or be the root) to use this protection"
112
+ end
113
+ end
114
+ end
115
+
116
+ sig { override.returns(T::Array[String]) }
117
+ def included_globs_for_pack
118
+ [
119
+ 'app/**/*',
120
+ 'lib/**/*'
121
+ ]
122
+ end
123
+
124
+ sig do
125
+ params(package: ::PackageProtections::ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
126
+ end
127
+ def custom_cop_config(package)
128
+ {
129
+ 'GlobalNamespaces' => package.metadata['global_namespaces']
130
+ }
131
+ end
132
+
133
+ sig do
134
+ override.params(file: String).returns(String)
135
+ end
136
+ def message_for_fail_on_any(file)
137
+ "`#{file}` should be namespaced under the package namespace"
138
+ end
139
+
140
+ sig { override.returns(String) }
141
+ def cop_name
142
+ 'PackageProtections/NamespacedUnderPackageName'
143
+ end
144
+
145
+ sig { override.returns(String) }
146
+ def humanized_protection_name
147
+ 'Multiple Namespaces Violations'
148
+ end
149
+
150
+ sig { override.returns(String) }
151
+ def humanized_protection_description
152
+ <<~MESSAGE
153
+ These files cannot have ANY modules/classes that are not submodules of the package's allowed namespaces.
154
+ This is failing because these files are in `.rubocop_todo.yml` under `#{cop_name}`.
155
+ If you want to be able to ignore these files, you'll need to open the file's package's `package.yml` file and
156
+ change `#{IDENTIFIER}` to `#{::PackageProtections::ViolationBehavior::FailOnNew.serialize}`
157
+
158
+ See https://go/packwerk_cheatsheet_namespaces for more info.
159
+ MESSAGE
160
+ end
161
+
88
162
  private
89
163
 
90
164
  def get_allowed_namespaces(package_name)
@@ -1,4 +1,4 @@
1
- # typed: ignore
1
+ # typed: strict
2
2
 
3
3
  module RuboCop
4
4
  module Cop
@@ -16,6 +16,61 @@ module RuboCop
16
16
  # We can apply this same pattern if we want to use other cops in the context of package protections and prevent clashing.
17
17
  #
18
18
  class TypedPublicApi < Sorbet::StrictSigil
19
+ extend T::Sig
20
+
21
+ include ::PackageProtections::ProtectionInterface
22
+ include ::PackageProtections::RubocopProtectionInterface
23
+
24
+ IDENTIFIER = T.let('prevent_this_package_from_exposing_an_untyped_api'.freeze, String)
25
+
26
+ sig { override.returns(String) }
27
+ def identifier
28
+ IDENTIFIER
29
+ end
30
+
31
+ sig { override.params(behavior: ::PackageProtections::ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
32
+ def unmet_preconditions_for_behavior(behavior, package)
33
+ # We might decide that we should check that `package.enforces_privacy?` is true here too, since that signifies the app has decided they want
34
+ # a public api in `app/public`. For now, we say there are no preconditions, because the user can still make `app/public` even if they are not yet
35
+ # ready to enforce privacy, and they might want to enforce a typed API.
36
+ nil
37
+ end
38
+
39
+ sig { override.returns(String) }
40
+ def cop_name
41
+ 'PackageProtections/TypedPublicApi'
42
+ end
43
+
44
+ sig { override.returns(T::Array[String]) }
45
+ def included_globs_for_pack
46
+ [
47
+ 'app/public/**/*'
48
+ ]
49
+ end
50
+
51
+ sig do
52
+ override.params(file: String).returns(String)
53
+ end
54
+ def message_for_fail_on_any(file)
55
+ "#{file} should be `typed: strict`"
56
+ end
57
+
58
+ sig { override.returns(String) }
59
+ def humanized_protection_name
60
+ 'Typed API Violations'
61
+ end
62
+
63
+ sig { override.returns(String) }
64
+ def humanized_protection_description
65
+ <<~MESSAGE
66
+ These files cannot have ANY Ruby files in the public API that are not typed strict or higher.
67
+ This is failing because these files are in `.rubocop_todo.yml` under `#{cop_name}`.
68
+ If you want to be able to ignore these files, you'll need to open the file's package's `package.yml` file and
69
+ change `#{IDENTIFIER}` to `#{::PackageProtections::ViolationBehavior::FailOnNew.serialize}`
70
+
71
+ See https://go/packwerk_cheatsheet_typed_api for more info.
72
+ MESSAGE
73
+ end
19
74
  end
20
75
  end
21
76
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: package_protections
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.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-06-14 00:00:00.000000000 Z
11
+ date: 2022-06-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -181,10 +181,8 @@ files:
181
181
  - lib/package_protections/private/configuration.rb
182
182
  - lib/package_protections/private/incoming_privacy_protection.rb
183
183
  - lib/package_protections/private/metadata_modifiers.rb
184
- - lib/package_protections/private/multiple_namespaces_protection.rb
185
184
  - lib/package_protections/private/outgoing_dependency_protection.rb
186
185
  - lib/package_protections/private/output.rb
187
- - lib/package_protections/private/typed_api_protection.rb
188
186
  - lib/package_protections/private/visibility_protection.rb
189
187
  - lib/package_protections/protected_package.rb
190
188
  - lib/package_protections/protection_interface.rb
@@ -1,128 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # typed: strict
4
-
5
- module PackageProtections
6
- module Private
7
- class MultipleNamespacesProtection
8
- extend T::Sig
9
-
10
- include ProtectionInterface
11
- include RubocopProtectionInterface
12
-
13
- IDENTIFIER = 'prevent_this_package_from_creating_other_namespaces'
14
- COP_NAME = 'PackageProtections/NamespacedUnderPackageName'
15
-
16
- sig { override.returns(String) }
17
- def identifier
18
- IDENTIFIER
19
- end
20
-
21
- sig { override.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
22
- def unmet_preconditions_for_behavior(behavior, package)
23
- if !behavior.enabled? && !package.metadata['global_namespaces'].nil?
24
- "Invalid configuration for package `#{package.name}`. `#{identifier}` must be turned on to use `global_namespaces` configuration."
25
- else
26
- # We don't need to validate if the behavior is currentely fail_never
27
- return if behavior.fail_never?
28
-
29
- # The reason for this is precondition is the `MultipleNamespacesProtection` assumes this to work properly.
30
- # To remove this precondition, we need to modify `MultipleNamespacesProtection` to be more generalized!
31
- if EXPECTED_PACK_DIRECTORIES.include?(Pathname.new(package.name).dirname.to_s) || package.name == ParsePackwerk::ROOT_PACKAGE_NAME
32
- nil
33
- else
34
- "Package #{package.name} must be located in one of #{EXPECTED_PACK_DIRECTORIES.join(', ')} (or be the root) to use this protection"
35
- end
36
- end
37
- end
38
-
39
- sig do
40
- override
41
- .params(packages: T::Array[ProtectedPackage])
42
- .returns(T::Array[CopConfig])
43
- end
44
- def cop_configs(packages)
45
- include_paths = T.let([], T::Array[String])
46
- packages.each do |p|
47
- next if p.name == ParsePackwerk::ROOT_PACKAGE_NAME
48
-
49
- if p.violation_behavior_for(identifier).enabled?
50
- include_paths << p.original_package.directory.join('app', '**', '*').to_s
51
- include_paths << p.original_package.directory.join('lib', '**', '*').to_s
52
- end
53
- end
54
-
55
- [
56
- CopConfig.new(
57
- name: COP_NAME,
58
- enabled: include_paths.any?,
59
- include_paths: include_paths
60
- )
61
- ]
62
- end
63
-
64
- sig do
65
- params(package: ProtectedPackage).returns(T::Hash[T.untyped, T.untyped])
66
- end
67
- def custom_cop_config(package)
68
- {
69
- 'GlobalNamespaces' => package.metadata['global_namespaces']
70
- }
71
- end
72
-
73
- sig do
74
- override.params(
75
- protected_packages: T::Array[ProtectedPackage]
76
- ).returns(T::Array[Offense])
77
- end
78
- def get_offenses_for_existing_violations(protected_packages)
79
- exclude_list = exclude_for_rule(COP_NAME)
80
- offenses = []
81
-
82
- protected_packages.each do |package|
83
- violation_behavior = package.violation_behavior_for(identifier)
84
-
85
- case violation_behavior
86
- when ViolationBehavior::FailNever, ViolationBehavior::FailOnNew
87
- next
88
- when ViolationBehavior::FailOnAny
89
- # Continue
90
- else
91
- T.absurd(violation_behavior)
92
- end
93
-
94
- package.original_package.directory.glob('**/**/*.*').each do |relative_path_to_file|
95
- next unless exclude_list.include?(relative_path_to_file.to_s)
96
-
97
- file = relative_path_to_file.to_s
98
- offenses << Offense.new(
99
- file: file,
100
- message: "`#{file}` should be namespaced under the package namespace",
101
- violation_type: identifier,
102
- package: package.original_package
103
- )
104
- end
105
- end
106
-
107
- offenses
108
- end
109
-
110
- sig { override.returns(String) }
111
- def humanized_protection_name
112
- 'Multiple Namespaces Violations'
113
- end
114
-
115
- sig { override.returns(String) }
116
- def humanized_protection_description
117
- <<~MESSAGE
118
- These files cannot have ANY modules/classes that are not submodules of the package's allowed namespaces.
119
- This is failing because these files are in `.rubocop_todo.yml` under `#{COP_NAME}`.
120
- If you want to be able to ignore these files, you'll need to open the file's package's `package.yml` file and
121
- change `#{IDENTIFIER}` to `#{ViolationBehavior::FailOnNew.serialize}`
122
-
123
- See https://go/packwerk_cheatsheet_namespaces for more info.
124
- MESSAGE
125
- end
126
- end
127
- end
128
- end
@@ -1,106 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module PackageProtections
5
- module Private
6
- class TypedApiProtection
7
- extend T::Sig
8
-
9
- include ProtectionInterface
10
- include RubocopProtectionInterface
11
-
12
- IDENTIFIER = 'prevent_this_package_from_exposing_an_untyped_api'
13
- COP_NAME = 'PackageProtections/TypedPublicApi'
14
-
15
- sig { override.returns(String) }
16
- def identifier
17
- IDENTIFIER
18
- end
19
-
20
- sig { override.params(behavior: ViolationBehavior, package: ParsePackwerk::Package).returns(T.nilable(String)) }
21
- def unmet_preconditions_for_behavior(behavior, package)
22
- # We might decide that we should check that `package.enforces_privacy?` is true here too, since that signifies the app has decided they want
23
- # a public api in `app/public`. For now, we say there are no preconditions, because the user can still make `app/public` even if they are not yet
24
- # ready to enforce privacy, and they might want to enforce a typed API.
25
- nil
26
- end
27
-
28
- sig do
29
- override
30
- .params(packages: T::Array[ProtectedPackage])
31
- .returns(T::Array[CopConfig])
32
- end
33
- def cop_configs(packages)
34
- include_paths = T.let([], T::Array[String])
35
- packages.each do |p|
36
- if p.violation_behavior_for(identifier).enabled?
37
- directory = p.original_package.directory
38
- include_paths << directory.join('app', 'public', '**', '*').to_s
39
- end
40
- end
41
-
42
- [
43
- CopConfig.new(
44
- name: COP_NAME,
45
- enabled: include_paths.any?,
46
- include_paths: include_paths
47
- )
48
- ]
49
- end
50
-
51
- sig do
52
- override.params(
53
- protected_packages: T::Array[ProtectedPackage]
54
- ).returns(T::Array[Offense])
55
- end
56
- def get_offenses_for_existing_violations(protected_packages)
57
- exclude_list = exclude_for_rule(COP_NAME)
58
-
59
- offenses = T.let([], T::Array[Offense])
60
- protected_packages.flat_map do |protected_package|
61
- violation_behavior = protected_package.violation_behavior_for(identifier)
62
-
63
- case violation_behavior
64
- when ViolationBehavior::FailNever, ViolationBehavior::FailOnNew
65
- next
66
- when ViolationBehavior::FailOnAny
67
- # Continue
68
- else
69
- T.absurd(violation_behavior)
70
- end
71
-
72
- protected_package.original_package.directory.glob('app/public/**/**/*.*').each do |relative_path_to_file|
73
- next unless exclude_list.include?(relative_path_to_file.to_s)
74
-
75
- file = relative_path_to_file.to_s
76
- offenses << Offense.new(
77
- file: file,
78
- message: "#{file} should be `typed: strict`",
79
- violation_type: identifier,
80
- package: protected_package.original_package
81
- )
82
- end
83
- end
84
-
85
- offenses
86
- end
87
-
88
- sig { override.returns(String) }
89
- def humanized_protection_name
90
- 'Typed API Violations'
91
- end
92
-
93
- sig { override.returns(String) }
94
- def humanized_protection_description
95
- <<~MESSAGE
96
- These files cannot have ANY Ruby files in the public API that are not typed strict or higher.
97
- This is failing because these files are in `.rubocop_todo.yml` under `#{COP_NAME}`.
98
- If you want to be able to ignore these files, you'll need to open the file's package's `package.yml` file and
99
- change `#{IDENTIFIER}` to `#{ViolationBehavior::FailOnNew.serialize}`
100
-
101
- See https://go/packwerk_cheatsheet_typed_api for more info.
102
- MESSAGE
103
- end
104
- end
105
- end
106
- end