package_protections 2.2.1 → 2.3.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: 6797406abd58b1f207223519e6351b0c26ee54c7c79d7400f74400b71a16e556
4
- data.tar.gz: e7a583cebe8634d730f665c31aaee955d6d1d2b83e5322a07c0d1c1f917abda1
3
+ metadata.gz: ba0b3c74ccdd20872e0cf789b6cf062e918ff25d6aa2c70515939f3cd57c4f17
4
+ data.tar.gz: '08c71c84b5a76786ff42b5c9e065afdea30a3ceb38775d0ca0b20c9e10ca568b'
5
5
  SHA512:
6
- metadata.gz: f40cb6189fc8d3b4364fce64e2933f929af1ad35a2a956cc9cea4ff46042cf2719379e0e8f8e6e82e1df68dcf1cf07a3326733c68e55bfe50a2d1914a9a3b34b
7
- data.tar.gz: ff10108a8577060d176820a74a939da8897bc566117a9f68e5c6f7c832cbd7047448a216913972bb7b47801c524dddd088c10a6f55a750b60ffd2b915a6d0764
6
+ metadata.gz: ccea3e0cf794040c52d283816e3905571973917e4d777b655dabbc15f871bb76672097f5c00897972e239a5146b14c6aa10b5ecdec1c6e6998be93bf2d0654ce
7
+ data.tar.gz: 7c33132a1fb7d27aaaa154254315776498dbe58654ebe6f3cfbbd2d3e8efe4b2067fa3dcde1cf6cb8ef06e1d5be79b00915bd7bc2343b8bd7544e7328607213c
@@ -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
9
  class NamespacedUnderPackageName < Base
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.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-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