package_protections 2.2.1 → 2.3.1

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: 6797406abd58b1f207223519e6351b0c26ee54c7c79d7400f74400b71a16e556
4
- data.tar.gz: e7a583cebe8634d730f665c31aaee955d6d1d2b83e5322a07c0d1c1f917abda1
3
+ metadata.gz: 918d945d5ceb1f2df5f544a50c74ebc8977d36819c4d5204866e90e8518115fc
4
+ data.tar.gz: e80077db49323a725e67d41c861e78da371f7e5de986c9240e9de1a4308f6686
5
5
  SHA512:
6
- metadata.gz: f40cb6189fc8d3b4364fce64e2933f929af1ad35a2a956cc9cea4ff46042cf2719379e0e8f8e6e82e1df68dcf1cf07a3326733c68e55bfe50a2d1914a9a3b34b
7
- data.tar.gz: ff10108a8577060d176820a74a939da8897bc566117a9f68e5c6f7c832cbd7047448a216913972bb7b47801c524dddd088c10a6f55a750b60ffd2b915a6d0764
6
+ metadata.gz: 81569a675bd20e6993b018d71f29cffb45d90621b9e6f036a746d89c8674b1d43e9a6d7bed6df7e4689ce3e008dc0a89b8506da9f959daa0611cf43d9adb2dc2
7
+ data.tar.gz: 3c290042abe91b8bbd285dae9091db68729fb99dd8f0b861c99cbac131f9705caf581d6c117e582e34b73ec38ae3b9f5ac70b885cab41974cd89f6295f03662c
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # typed: strict
4
+
4
5
  module PackageProtections
5
6
  # Perhaps this should be in ParsePackwerk. For now, this is here to help us break down violations per file.
6
7
  # This is analogous to `Packwerk::ReferenceOffense`
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # typed: strict
4
+
4
5
  module PackageProtections
5
6
  module Private
6
7
  class MetadataModifiers
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # typed: strict
4
+
4
5
  module PackageProtections
5
6
  module Private
6
7
  class Output
@@ -128,7 +128,7 @@ module PackageProtections
128
128
  @private_cop_config ||= begin
129
129
  protected_packages = all_protected_packages
130
130
  protection = T.cast(PackageProtections.with_identifier(identifier), PackageProtections::RubocopProtectionInterface)
131
- protected_packages.map { |p| [p.name, protection.custom_cop_config(p)] }.to_h
131
+ protected_packages.to_h { |p| [p.name, protection.custom_cop_config(p)] }
132
132
  end
133
133
  end
134
134
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # typed: strict
4
+
4
5
  module PackageProtections
5
6
  class ProtectedPackage < T::Struct
6
7
  extend T::Sig
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # typed: strict
4
+
4
5
  module PackageProtections
5
6
  module ProtectionInterface
6
7
  extend T::Sig
@@ -17,7 +17,6 @@ module ApplicationFixtureHelper
17
17
  enforce_dependencies: true,
18
18
  enforce_privacy: true,
19
19
  protections: {},
20
- global_namespaces: [],
21
20
  visible_to: []
22
21
  )
23
22
  defaults = {
@@ -33,10 +32,6 @@ module ApplicationFixtureHelper
33
32
  metadata.merge!('visible_to' => visible_to)
34
33
  end
35
34
 
36
- if global_namespaces.any?
37
- metadata.merge!('global_namespaces' => global_namespaces)
38
- end
39
-
40
35
  package = ParsePackwerk::Package.new(
41
36
  name: pack_name,
42
37
  dependencies: dependencies,
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # typed: strict
4
+
4
5
  module PackageProtections
5
6
  module RubocopProtectionInterface
6
7
  class CopConfig < T::Struct
@@ -9,6 +10,7 @@ module PackageProtections
9
10
  const :enabled, T::Boolean, default: true
10
11
  const :include_paths, T::Array[String], default: []
11
12
  const :exclude_paths, T::Array[String], default: []
13
+ const :metadata, T.untyped, default: {}
12
14
 
13
15
  sig { returns(String) }
14
16
  def to_rubocop_yml_compatible_format
@@ -22,6 +24,10 @@ module PackageProtections
22
24
  cop_config['Exclude'] = exclude_paths
23
25
  end
24
26
 
27
+ if metadata.any?
28
+ cop_config.merge!(metadata)
29
+ end
30
+
25
31
  { name => cop_config }.to_yaml.gsub("---\n", '')
26
32
  end
27
33
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # typed: strict
4
+
4
5
  module PackageProtections
5
6
  class IncorrectPublicApiUsageError < StandardError; end
6
7
 
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # typed: strict
4
+
4
5
  require 'sorbet-runtime'
5
6
  require 'open3'
6
7
  require 'set'
7
8
  require 'parse_packwerk'
8
9
  require 'rubocop'
9
10
  require 'rubocop-sorbet'
11
+ require 'rubocop-modularization'
10
12
 
11
13
  #
12
14
  # Welcome to PackageProtections!
@@ -66,7 +68,7 @@ module PackageProtections
66
68
  sig { params(identifier: Identifier).returns(ProtectionInterface) }
67
69
  def self.with_identifier(identifier)
68
70
  @map ||= T.let(@map, T.nilable(T::Hash[Identifier, ProtectionInterface]))
69
- @map ||= all.map { |protection| [protection.identifier, protection] }.to_h
71
+ @map ||= all.to_h { |protection| [protection.identifier, protection] }
70
72
  @map.fetch(identifier)
71
73
  end
72
74
 
@@ -2,81 +2,14 @@
2
2
 
3
3
  # For String#camelize
4
4
  require 'active_support/core_ext/string/inflections'
5
- require 'rubocop/cop/package_protections/namespaced_under_package_name/desired_zeitwerk_api'
6
5
 
7
6
  module RuboCop
8
7
  module Cop
9
8
  module PackageProtections
10
- #
11
- # TODO:
12
- # This class is in serious need of being split up into helpful abstractions.
13
- # A really helpful abstraction would be one that takes a file path and can spit out information about
14
- # namespacing, such as the exposed namespace, the file path for a different namespace, and more.
15
- #
16
- class NamespacedUnderPackageName < Base
9
+ class NamespacedUnderPackageName < Modularization::NamespacedUnderPackageName
17
10
  extend T::Sig
18
-
19
- include RangeHelp
20
11
  include ::PackageProtections::RubocopProtectionInterface
21
12
 
22
- sig { void }
23
- def on_new_investigation
24
- absolute_filepath = Pathname.new(processed_source.file_path)
25
- relative_filepath = absolute_filepath.relative_path_from(Pathname.pwd)
26
- relative_filename = relative_filepath.to_s
27
-
28
- # This cop only works for files ruby files in `app`
29
- return if !relative_filename.include?('app/') || relative_filepath.extname != '.rb'
30
-
31
- relative_filename = relative_filepath.to_s
32
- package_for_path = ParsePackwerk.package_from_path(relative_filename)
33
- return if package_for_path.nil?
34
-
35
- namespace_context = self.class.desired_zeitwerk_api.for_file(relative_filename, package_for_path)
36
- return if namespace_context.nil?
37
-
38
- allowed_global_namespaces = Set.new([
39
- namespace_context.expected_namespace,
40
- *::PackageProtections.config.globally_permitted_namespaces
41
- ])
42
-
43
- package_name = package_for_path.name
44
- actual_namespace = namespace_context.current_namespace
45
-
46
- if allowed_global_namespaces.include?(actual_namespace)
47
- # No problem!
48
- else
49
- package_enforces_namespaces = !::PackageProtections::ProtectedPackage.from(package_for_path).violation_behavior_for(NamespacedUnderPackageName::IDENTIFIER).fail_never?
50
- expected_namespace = namespace_context.expected_namespace
51
- relative_desired_path = namespace_context.expected_filepath
52
- pack_owning_this_namespace = self.class.namespaces_to_packs[actual_namespace]
53
-
54
- if package_enforces_namespaces
55
- add_offense(
56
- source_range(processed_source.buffer, 1, 0),
57
- message: format(
58
- '`%<package_name>s` prevents modules/classes that are not submodules of the package namespace. Should be namespaced under `%<expected_namespace>s` with path `%<expected_path>s`. See https://go/packwerk_cheatsheet_namespaces for more info.',
59
- package_name: package_name,
60
- expected_namespace: expected_namespace,
61
- expected_path: relative_desired_path
62
- )
63
- )
64
- elsif pack_owning_this_namespace
65
- add_offense(
66
- source_range(processed_source.buffer, 1, 0),
67
- message: format(
68
- '`%<pack_owning_this_namespace>s` prevents other packs from sitting in the `%<actual_namespace>s` namespace. This should be namespaced under `%<expected_namespace>s` with path `%<expected_path>s`. See https://go/packwerk_cheatsheet_namespaces for more info.',
69
- package_name: package_name,
70
- pack_owning_this_namespace: pack_owning_this_namespace,
71
- expected_namespace: expected_namespace,
72
- actual_namespace: actual_namespace,
73
- expected_path: relative_desired_path
74
- )
75
- )
76
- end
77
- end
78
- end
79
-
80
13
  # We override `cop_configs` for this protection.
81
14
  # The default behavior disables cops when a package has turned off a protection.
82
15
  # However: namespace violations can occur even when one package has TURNED OFF their namespace protection
@@ -87,22 +20,34 @@ module RuboCop
87
20
  .returns(T::Array[::PackageProtections::RubocopProtectionInterface::CopConfig])
88
21
  end
89
22
  def cop_configs(packages)
90
- include_paths = T.let([], T::Array[String])
23
+ include_packs = T.let([], T::Array[String])
91
24
  packages.each do |p|
92
- included_globs_for_pack.each do |glob|
93
- include_paths << p.original_package.directory.join(glob).to_s
25
+ enabled_for_pack = !p.violation_behavior_for(NamespacedUnderPackageName::IDENTIFIER).fail_never?
26
+ if enabled_for_pack
27
+ include_packs << p.name
94
28
  end
95
29
  end
96
30
 
97
31
  [
98
32
  ::PackageProtections::RubocopProtectionInterface::CopConfig.new(
99
33
  name: cop_name,
100
- enabled: include_paths.any?,
101
- include_paths: include_paths
34
+ enabled: include_packs.any?,
35
+ metadata: {
36
+ 'IncludePacks' => include_packs,
37
+ 'GloballyPermittedNamespaces' => ::PackageProtections.config.globally_permitted_namespaces
38
+ }
102
39
  )
103
40
  ]
104
41
  end
105
42
 
43
+ sig { override.returns(T::Array[String]) }
44
+ def included_globs_for_pack
45
+ [
46
+ 'app/**/*',
47
+ 'lib/**/*'
48
+ ]
49
+ end
50
+
106
51
  IDENTIFIER = T.let('prevent_this_package_from_creating_other_namespaces'.freeze, String)
107
52
 
108
53
  sig { override.returns(String) }
@@ -132,14 +77,6 @@ module RuboCop
132
77
  end
133
78
  end
134
79
 
135
- sig { override.returns(T::Array[String]) }
136
- def included_globs_for_pack
137
- [
138
- 'app/**/*',
139
- 'lib/**/*'
140
- ]
141
- end
142
-
143
80
  sig do
144
81
  override.params(file: String).returns(String)
145
82
  end
@@ -168,31 +105,6 @@ module RuboCop
168
105
  See https://go/packwerk_cheatsheet_namespaces for more info.
169
106
  MESSAGE
170
107
  end
171
-
172
- sig { returns(DesiredZeitwerkApi) }
173
- def self.desired_zeitwerk_api
174
- # This is cached at the class level so we will cache more expensive operations
175
- # across rubocop requests.
176
- @desired_zeitwerk_api ||= T.let(nil, T.nilable(DesiredZeitwerkApi))
177
- @desired_zeitwerk_api ||= DesiredZeitwerkApi.new
178
- end
179
-
180
- sig { returns(T::Hash[String, String]) }
181
- def self.namespaces_to_packs
182
- @namespaces_to_packs = T.let(nil, T.nilable(T::Hash[String, String]))
183
- @namespaces_to_packs ||= begin
184
- all_packs_enforcing_namespaces = ParsePackwerk.all.reject do |p|
185
- ::PackageProtections::ProtectedPackage.from(p).violation_behavior_for(NamespacedUnderPackageName::IDENTIFIER).fail_never?
186
- end
187
-
188
- namespaces_to_packs = {}
189
- all_packs_enforcing_namespaces.each do |package|
190
- namespaces_to_packs[desired_zeitwerk_api.get_pack_based_namespace(package)] = package.name
191
- end
192
-
193
- namespaces_to_packs
194
- end
195
- end
196
108
  end
197
109
  end
198
110
  end
@@ -15,7 +15,7 @@ module RuboCop
15
15
  #
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
- class TypedPublicApi < Sorbet::StrictSigil
18
+ class TypedPublicApi < Modularization::TypedPublicApi
19
19
  extend T::Sig
20
20
 
21
21
  include ::PackageProtections::ProtectionInterface
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: 2.2.1
4
+ version: 2.3.1
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-09-23 00:00:00.000000000 Z
11
+ date: 2022-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-modularization
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rubocop-sorbet
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -192,7 +206,6 @@ files:
192
206
  - lib/package_protections/rubocop_protection_interface.rb
193
207
  - lib/package_protections/violation_behavior.rb
194
208
  - lib/rubocop/cop/package_protections/namespaced_under_package_name.rb
195
- - lib/rubocop/cop/package_protections/namespaced_under_package_name/desired_zeitwerk_api.rb
196
209
  - lib/rubocop/cop/package_protections/typed_public_api.rb
197
210
  homepage: https://github.com/rubyatscale/package_protections
198
211
  licenses:
@@ -210,7 +223,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
210
223
  requirements:
211
224
  - - ">="
212
225
  - !ruby/object:Gem::Version
213
- version: 2.5.0
226
+ version: 2.6.0
214
227
  required_rubygems_version: !ruby/object:Gem::Requirement
215
228
  requirements:
216
229
  - - ">="
@@ -1,110 +0,0 @@
1
- # typed: strict
2
-
3
- module RuboCop
4
- module Cop
5
- module PackageProtections
6
- class NamespacedUnderPackageName < Base
7
- #
8
- # This is a private class that represents API that we would prefer to be available somehow in Zeitwerk.
9
- #
10
- class DesiredZeitwerkApi
11
- extend T::Sig
12
-
13
- class NamespaceContext < T::Struct
14
- const :current_namespace, String
15
- const :expected_namespace, String
16
- const :expected_filepath, String
17
- end
18
-
19
- #
20
- # For now, this API includes `package_for_path`
21
- # If this were truly zeitwerk API, it wouldn't include any mention of packs and it would likely not need the package at all
22
- # Since it could get the actual namespace without knowing anything about packs.
23
- # However, we would need to pass to it the desired namespace based on the pack name for it to be able to suggest
24
- # a desired filepath.
25
- # Likely this means that our own cop should determine the desired namespace and pass that in
26
- # and this can determine actual namespace and how to get to expected.
27
- #
28
- sig { params(relative_filename: String, package_for_path: ParsePackwerk::Package).returns(T.nilable(NamespaceContext)) }
29
- def for_file(relative_filename, package_for_path)
30
- package_name = package_for_path.name
31
-
32
- # Zeitwerk establishes a standard convention by which namespaces are defined.
33
- # The package protections namespace checker is coupled to a specific assumption about how auto-loading works.
34
- #
35
- # Namely, we expect the following autoload paths: `packs/**/app/**/`
36
- # Examples:
37
- # 1) `packs/package_1/app/public/package_1/my_constant.rb` produces constant `Package1::MyConstant`
38
- # 2) `packs/package_1/app/services/package_1/my_service.rb` produces constant `Package1::MyService`
39
- # 3) `packs/package_1/app/services/package_1.rb` produces constant `Package1`
40
- # 4) `packs/package_1/app/public/package_1.rb` produces constant `Package1`
41
- #
42
- # Described another way, we expect any part of the directory labeled NAMESPACE to establish a portion of the fully qualified runtime constant:
43
- # `packs/**/app/**/NAMESPACE1/NAMESPACE2/[etc]`
44
- #
45
- # Therefore, for our implementation, we substitute out the non-namespace producing portions of the filename to count the number of namespaces.
46
- # Note this will *not work* properly in applications that have different assumptions about autoloading.
47
-
48
- path_without_package_base = relative_filename.gsub(%r{#{package_name}/app/}, '')
49
- if path_without_package_base.include?('concerns')
50
- autoload_folder_name = path_without_package_base.split('/').first(2).join('/')
51
- else
52
- autoload_folder_name = path_without_package_base.split('/').first
53
- end
54
-
55
- remaining_file_path = path_without_package_base.gsub(%r{\A#{autoload_folder_name}/}, '')
56
- actual_namespace = get_actual_namespace(remaining_file_path, package_name)
57
-
58
- if relative_filename.include?('app/')
59
- app_or_lib = 'app'
60
- elsif relative_filename.include?('lib/')
61
- app_or_lib = 'lib'
62
- end
63
-
64
- absolute_desired_path = root_pathname.join(
65
- package_name,
66
- T.must(app_or_lib),
67
- T.must(autoload_folder_name),
68
- get_package_last_name(package_for_path),
69
- remaining_file_path
70
- )
71
-
72
- relative_desired_path = absolute_desired_path.relative_path_from(root_pathname)
73
-
74
- NamespaceContext.new(
75
- current_namespace: actual_namespace,
76
- expected_namespace: get_pack_based_namespace(package_for_path),
77
- expected_filepath: relative_desired_path.to_s
78
- )
79
- end
80
-
81
- sig { params(pack: ParsePackwerk::Package).returns(String) }
82
- def get_pack_based_namespace(pack)
83
- get_package_last_name(pack).camelize
84
- end
85
-
86
- private
87
-
88
- sig { returns(Pathname) }
89
- def root_pathname
90
- Pathname.pwd
91
- end
92
-
93
- sig { params(pack: ParsePackwerk::Package).returns(String) }
94
- def get_package_last_name(pack)
95
- T.must(pack.name.split('/').last)
96
- end
97
-
98
- sig { params(remaining_file_path: String, package_name: String).returns(String) }
99
- def get_actual_namespace(remaining_file_path, package_name)
100
- # If the remaining file path is a ruby file (not a directory), then it establishes a global namespace
101
- # Otherwise, per Zeitwerk's conventions as listed above, its a directory that establishes another global namespace
102
- T.must(remaining_file_path.split('/').first).gsub('.rb', '').camelize
103
- end
104
- end
105
-
106
- private_constant :DesiredZeitwerkApi
107
- end
108
- end
109
- end
110
- end