package_protections 1.0.0 → 1.2.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: a1189b0779bbda54403506984a5719d37ac3444776e34150defe591ecb96fc81
4
- data.tar.gz: 1006d4722077a1581cf3509358eb4f5b58d5e443bd6c9d203309472bb26f980d
3
+ metadata.gz: 41803352508cd5d4ede0fe0caf1121516474fe3e95a030e4db7e0f05badfa9a2
4
+ data.tar.gz: bc594511c3b3da35dcd9efcfba0f989928e074e0da429a8f0f538e0537eff5be
5
5
  SHA512:
6
- metadata.gz: 383d9bac29ac9e7be26d41de6d7af08eeb85d635e471f91a1a1ad08fd565aaaa25f42763d9e0bfc75cded4beb6d911bff267744d2a6408651132b93c4a30d70b
7
- data.tar.gz: 1fd35343e9db0689656378111537df0369910874f0a59f38dcb2fa1bc712c6233e45e945cccec591c3f6a71da89e64b62aeffaa2cd565146a217aa43d09900a1
6
+ metadata.gz: 352aa3ea22bf0d7cf765023c35ed88c6026fda2b52036aa73fbcf35e4aca1df52df24486ce028a238dcf50db89f2864f4080e77d6da52d9f7be7fa984cd70dc7
7
+ data.tar.gz: 6bb8ab188d27a3656cd6979d00fcaf725165176b17a42704be114ef0bbfbe7150facf21ae44ebc3c1633ce5b21876fd9b8088179c6e702f8addd49f890d7e1e3
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
 
@@ -0,0 +1,54 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+
6
+ module ApplicationFixtureHelper
7
+ def write_file(path, content = '')
8
+ pathname = Pathname.new(path)
9
+ FileUtils.mkdir_p(pathname.dirname)
10
+ pathname.write(content)
11
+ path
12
+ end
13
+
14
+ def write_package_yml(
15
+ pack_name,
16
+ dependencies: [],
17
+ enforce_dependencies: true,
18
+ enforce_privacy: true,
19
+ protections: {},
20
+ global_namespaces: [],
21
+ visible_to: []
22
+ )
23
+ defaults = {
24
+ 'prevent_this_package_from_violating_its_stated_dependencies' => 'fail_on_new',
25
+ 'prevent_other_packages_from_using_this_packages_internals' => 'fail_on_new',
26
+ 'prevent_this_package_from_exposing_an_untyped_api' => 'fail_on_new',
27
+ 'prevent_this_package_from_creating_other_namespaces' => 'fail_on_new',
28
+ 'prevent_other_packages_from_using_this_package_without_explicit_visibility' => 'fail_never'
29
+ }
30
+ protections_with_defaults = defaults.merge(protections)
31
+ metadata = { 'protections' => protections_with_defaults }
32
+ if visible_to.any?
33
+ metadata.merge!('visible_to' => visible_to)
34
+ end
35
+
36
+ if global_namespaces.any?
37
+ metadata.merge!('global_namespaces' => global_namespaces)
38
+ end
39
+
40
+ package = ParsePackwerk::Package.new(
41
+ name: pack_name,
42
+ dependencies: dependencies,
43
+ enforce_dependencies: enforce_dependencies,
44
+ enforce_privacy: enforce_privacy,
45
+ metadata: metadata
46
+ )
47
+
48
+ ParsePackwerk.write_package_yml!(package)
49
+ end
50
+
51
+ def delete_app_file(path)
52
+ File.delete(path)
53
+ end
54
+ end
@@ -0,0 +1,99 @@
1
+ def offense(
2
+ package_name, message, file, violation_type
3
+ )
4
+
5
+ package = ParsePackwerk.all.find { |p| p.name == package_name }
6
+ PackageProtections::Offense.new(
7
+ package: package,
8
+ message: message,
9
+ file: file,
10
+ violation_type: violation_type
11
+ )
12
+ end
13
+
14
+ def serialize_offenses_diff(actual_offenses, expected_offense)
15
+ color_by_match = ->(actual, expected) { actual == expected ? Rainbow(actual).green : "#{Rainbow(actual).red} (expected: #{expected})" }
16
+
17
+ actual_offenses.map do |offense|
18
+ # We color each field red or green depending on if the attributes match our expected
19
+ <<~SERIALIZED_OFFENSE
20
+ File: #{color_by_match.call(offense.file, expected_offense.file)}
21
+ Message: #{color_by_match.call(offense.message, expected_offense.message)}
22
+ Violation Type: #{color_by_match.call(offense.violation_type, expected_offense.violation_type)}
23
+ Package: #{color_by_match.call(offense.package.name, expected_offense.package.name)}
24
+ SERIALIZED_OFFENSE
25
+ end
26
+ end
27
+
28
+ def serialize_offenses(actual_offenses)
29
+ actual_offenses.map do |offense|
30
+ <<~SERIALIZED_OFFENSE
31
+ File: #{offense.file}
32
+ Message: #{offense.message}
33
+ Violation Type: #{offense.violation_type}
34
+ Package: #{offense.package.name}
35
+ SERIALIZED_OFFENSE
36
+ end
37
+ end
38
+
39
+ RSpec::Matchers.define(:include_offense) do |expected_offense|
40
+ match do |actual_offenses|
41
+ @actual_offenses = actual_offenses
42
+ @expected_offense = expected_offense
43
+ if ENV['DEBUG']
44
+ PackageProtections.print_offenses(actual_offenses)
45
+ end
46
+ @matching_offense = actual_offenses.find do |actual_offense|
47
+ actual_offense.file == expected_offense.file &&
48
+ actual_offense.message == expected_offense.message &&
49
+ actual_offense.violation_type == expected_offense.violation_type &&
50
+ actual_offense.package.name == expected_offense.package.name
51
+ end
52
+ !@matching_offense.nil?
53
+ end
54
+
55
+ description do
56
+ "to have an offense with type `#{expected_offense.type}` tied to package `#{expected_offense.package_name}` with message `#{expected_offense.message}` and instances `#{expected_offense.submessages.join(', ')}`"
57
+ end
58
+
59
+ failure_message do
60
+ <<~MSG
61
+ Could not find offense! Here are the found offenses:
62
+ #{serialize_offenses_diff(@actual_offenses, expected_offense).join("\n\n")}
63
+ MSG
64
+ end
65
+ end
66
+
67
+ RSpec::Matchers.define(:contain_exactly) do |number_of_offenses|
68
+ match do |actual_offenses|
69
+ @actual_offenses = actual_offenses || []
70
+ @offenses = []
71
+ @actual_offenses.each do |offense|
72
+ @offenses << offense
73
+ end
74
+ @offenses.size == number_of_offenses
75
+ end
76
+
77
+ chain :offense, :number_of_offenses
78
+ chain :offenses, :number_of_offenses
79
+
80
+ description do
81
+ 'to contain offenses'
82
+ end
83
+
84
+ failure_message_when_negated do
85
+ "Found the following offenses:\n#{@offenses.map { |r| "#{r.package_name}: #{r.message}" }}"
86
+ end
87
+
88
+ failure_message do
89
+ if @offenses.empty?
90
+ "Found #{@offenses.size} instead."
91
+ else
92
+ <<~MSG
93
+ Found #{@offenses.size} instead.
94
+
95
+ #{serialize_offenses(@offenses).join("\n")}
96
+ MSG
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Require this file to load code that supports testing using RSpec.
4
+
5
+ require_relative 'application_fixture_helper'
6
+ require_relative 'matchers'
7
+
8
+ def get_resulting_rubocop
9
+ write_file('config/default.yml', <<~YML.strip)
10
+ <%= PackageProtections.rubocop_yml %>
11
+ YML
12
+ YAML.safe_load(ERB.new(File.read('config/default.yml')).result(binding))
13
+ end
14
+
15
+ RSpec.configure do |config|
16
+ config.include ApplicationFixtureHelper
17
+ end
@@ -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(included_globs_for_pack).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.2.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-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -181,13 +181,14 @@ 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
189
+ - lib/package_protections/rspec/application_fixture_helper.rb
190
+ - lib/package_protections/rspec/matchers.rb
191
+ - lib/package_protections/rspec/support.rb
191
192
  - lib/package_protections/rubocop_protection_interface.rb
192
193
  - lib/package_protections/violation_behavior.rb
193
194
  - lib/rubocop/cop/package_protections/namespaced_under_package_name.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