rubocop-modularization 0.0.1 → 0.0.3

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -3
  3. data/config/default.yml +5 -0
  4. data/lib/rubocop/cop/modularization/namespaced_under_package_name/desired_zeitwerk_api.rb +110 -0
  5. data/lib/rubocop/cop/modularization/namespaced_under_package_name.rb +119 -0
  6. data/lib/rubocop/cop/modularization/typed_public_api.rb +39 -0
  7. data/lib/rubocop/cop/modularization_cops.rb +1 -0
  8. data/lib/rubocop/modularization/inject.rb +4 -0
  9. data/lib/rubocop/modularization/private.rb +11 -0
  10. data/lib/rubocop/modularization.rb +8 -4
  11. data/lib/rubocop-modularization.rb +5 -2
  12. data/sorbet/rbi/gems/activesupport@7.0.4.rbi +15914 -0
  13. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  14. data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
  15. data/sorbet/rbi/gems/concurrent-ruby@1.1.10.rbi +11263 -0
  16. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1079 -0
  17. data/sorbet/rbi/gems/i18n@1.12.0.rbi +2296 -0
  18. data/sorbet/rbi/gems/json@2.6.2.rbi +1547 -0
  19. data/sorbet/rbi/gems/method_source@1.0.0.rbi +8 -0
  20. data/sorbet/rbi/gems/minitest@5.16.3.rbi +1459 -0
  21. data/sorbet/rbi/gems/netrc@0.11.0.rbi +161 -0
  22. data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
  23. data/sorbet/rbi/gems/parse_packwerk@0.12.1.rbi +203 -0
  24. data/sorbet/rbi/gems/parser@3.1.2.1.rbi +8944 -0
  25. data/sorbet/rbi/gems/pry@0.14.1.rbi +8 -0
  26. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +392 -0
  27. data/sorbet/rbi/gems/rake@13.0.6.rbi +2899 -0
  28. data/sorbet/rbi/gems/rbi@0.0.16.rbi +3007 -0
  29. data/sorbet/rbi/gems/regexp_parser@2.6.0.rbi +3498 -0
  30. data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
  31. data/sorbet/rbi/gems/rspec-core@3.11.0.rbi +10934 -0
  32. data/sorbet/rbi/gems/rspec-expectations@3.11.1.rbi +8090 -0
  33. data/sorbet/rbi/gems/rspec-mocks@3.11.1.rbi +5291 -0
  34. data/sorbet/rbi/gems/rspec-support@3.11.1.rbi +1610 -0
  35. data/sorbet/rbi/gems/rspec@3.11.0.rbi +88 -0
  36. data/sorbet/rbi/gems/rubocop-ast@1.21.0.rbi +6898 -0
  37. data/sorbet/rbi/gems/rubocop-extension-generator@0.5.1.rbi +86 -0
  38. data/sorbet/rbi/gems/rubocop-sorbet@0.6.11.rbi +1002 -0
  39. data/sorbet/rbi/gems/rubocop@1.36.0.rbi +52209 -0
  40. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1239 -0
  41. data/sorbet/rbi/gems/spoom@1.1.12.rbi +2369 -0
  42. data/sorbet/rbi/gems/tapioca@0.10.2.rbi +3439 -0
  43. data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
  44. data/sorbet/rbi/gems/tzinfo@2.0.5.rbi +5896 -0
  45. data/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi +48 -0
  46. data/sorbet/rbi/gems/unparser@0.6.5.rbi +4529 -0
  47. data/sorbet/rbi/gems/webrick@1.7.0.rbi +2586 -0
  48. data/sorbet/rbi/gems/yard-sorbet@0.7.0.rbi +389 -0
  49. data/sorbet/rbi/gems/yard@0.9.28.rbi +17775 -0
  50. data/sorbet/rbi/todo.rbi +0 -3
  51. metadata +105 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b0a6cf1f19e21271cc4aabe132a9fae31f38b15d7a2ed6602a29a15de72fde7
4
- data.tar.gz: d2e6d37536be643977a275a62d77e0c01ec86b20a57c9826d60d71128b628473
3
+ metadata.gz: a7be2735c3d928d9c4ccf2643f2d07c533926ded38120b0d50e825d021065d92
4
+ data.tar.gz: dc19628fa31d58ced5e4b93af213d31f475c23669da692722b41c3c12140fc71
5
5
  SHA512:
6
- metadata.gz: 0d94c4ee844d369e8b9a7ecc8cc1f0bab5f070cb7dbd413a5e3a14bb37cb4a03c88b743316e0316b2aed6ec2290d1f0c8d086253c1dbd00c625e06705e384d6f
7
- data.tar.gz: 0f11f26c9d0282dbe9b0b40738a66430b054b62a6c27d6e251eb3fa4ee8077a1766d9be549410ac9682dd223c055d947916802a51f32d10c2ca6ffaf45b1b435
6
+ metadata.gz: 55916f5fdcf893b864f41d69b69cb595f2b32427164e6cfd9f32cd2859d8ff6b2db012c7fc4a14a665b600efae8645a73bdab8f26f8b2455c0e4b461e21bcdb0
7
+ data.tar.gz: 5a558caa1855c9aeb2317c574d9c6983fe6ca35ef9a1fb34af4d285cc432810961273928cf628293452102dd1ba0d0c5d817ab6feb3fe6c833a49485fec1aa5f
data/README.md CHANGED
@@ -1,5 +1,84 @@
1
1
  # rubocop-modularization
2
2
 
3
- To finish off using this template:
4
- 1) Replace all instances of `rubocop-modularization` and `MyGem` with your gem's name.
5
- 2) Rename all files and folders from `rubocop-modularization` to your gem's name.
3
+ A collection of Rubocop rules for modularization..
4
+
5
+ ## Installation
6
+
7
+ Just install the `rubocop-modularization` gem
8
+
9
+ ```sh
10
+ gem install rubocop-modularization
11
+ ```
12
+ or, if you use `Bundler`, add this line your application's `Gemfile`:
13
+
14
+ ```ruby
15
+ gem 'rubocop-modularization', require: false
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ You need to tell RuboCop to load the Modularization extension. There are three ways to do this:
21
+
22
+ ### RuboCop configuration file
23
+
24
+ Put this into your `.rubocop.yml`:
25
+
26
+ ```yaml
27
+ require: rubocop-modularization
28
+ ```
29
+
30
+ Alternatively, use the following array notation when specifying multiple extensions:
31
+
32
+ ```yaml
33
+ require:
34
+ - rubocop-other-extension
35
+ - rubocop-modularization
36
+ ```
37
+
38
+ Now you can run `rubocop` and it will automatically load the RuboCop Modularization cops together with the standard cops.
39
+
40
+ ### Command line
41
+
42
+ ```sh
43
+ rubocop --require rubocop-modularization
44
+ ```
45
+
46
+ ### Rake task
47
+
48
+ ```ruby
49
+ RuboCop::RakeTask.new do |task|
50
+ task.requires << 'rubocop-modularization'
51
+ end
52
+ ```
53
+
54
+ ## The Cops
55
+ All cops are located under [`lib/rubocop/cop/modularization`](lib/rubocop/cop/modularization), and contain examples/documentation.
56
+
57
+ In your `.rubocop.yml`, you may treat the Modularization cops just like any other cop. For example:
58
+
59
+ ```yaml
60
+ Modularization/NamespacedUnderPackageName:
61
+ Exclude:
62
+ - lib/example.rb
63
+ ```
64
+ ## Contributing
65
+
66
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rubyatscale/rubocop-modularization. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Code Of Conduct](CODE_OF_CONDUCT.MD).
67
+
68
+ To contribute a new cop, please use the supplied generator like this:
69
+
70
+ ```sh
71
+ bundle exec rake new_cop[Modularization/NewCopName]
72
+ ```
73
+
74
+ which will create a skeleton cop, a skeleton spec, an entry in the default config file and will require the new cop so that it is properly exported from the gem.
75
+
76
+ Don't forget to update the documentation with:
77
+
78
+ ```sh
79
+ VERIFYING_DOCUMENTATION=1 bundle exec rake generate_cops_documentation
80
+ ```
81
+
82
+ ## License
83
+
84
+ The gem is available as open source under the terms of the [MIT License](https://github.com/Shopify/rubocop-modularization/blob/main/LICENSE.txt).
@@ -0,0 +1,5 @@
1
+ Modularization/NamespacedUnderPackageName:
2
+ Enabled: false
3
+
4
+ Modularization/TypedPublicApi:
5
+ Enabled: false
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Modularization
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
@@ -0,0 +1,119 @@
1
+ # typed: strict
2
+
3
+ # For String#camelize
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'rubocop/cop/modularization/namespaced_under_package_name/desired_zeitwerk_api'
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module Modularization
10
+ # This cop helps ensure that each pack exposes one namespace.
11
+ #
12
+ # @example
13
+ #
14
+ # # bad
15
+ # # packs/foo/app/services/blah/bar.rb
16
+ # class Blah::Bar; end
17
+ #
18
+ # # good
19
+ # # packs/foo/app/services/foo/blah/bar.rb
20
+ # class Foo::Blah::Bar; end
21
+ #
22
+ class NamespacedUnderPackageName < Base
23
+ extend T::Sig
24
+
25
+ include RangeHelp
26
+
27
+ sig { void }
28
+ def on_new_investigation
29
+ absolute_filepath = Pathname.new(processed_source.file_path)
30
+ relative_filepath = absolute_filepath.relative_path_from(Pathname.pwd)
31
+ relative_filename = relative_filepath.to_s
32
+
33
+ # This cop only works for files ruby files in `app`
34
+ return if !relative_filename.include?('app/') || relative_filepath.extname != '.rb'
35
+
36
+ relative_filename = relative_filepath.to_s
37
+ package_for_path = ParsePackwerk.package_from_path(relative_filename)
38
+ return if package_for_path.nil?
39
+
40
+ namespace_context = desired_zeitwerk_api.for_file(relative_filename, package_for_path)
41
+ return if namespace_context.nil?
42
+
43
+ allowed_global_namespaces = Set.new([
44
+ namespace_context.expected_namespace,
45
+ *cop_config['GloballyPermittedNamespaces']
46
+ ])
47
+
48
+ package_name = package_for_path.name
49
+ actual_namespace = namespace_context.current_namespace
50
+
51
+ if allowed_global_namespaces.include?(actual_namespace)
52
+ # No problem!
53
+ else
54
+ package_enforces_namespaces = cop_config['IncludePacks'].include?(package_for_path.name)
55
+ expected_namespace = namespace_context.expected_namespace
56
+ relative_desired_path = namespace_context.expected_filepath
57
+ pack_owning_this_namespace = namespaces_to_packs[actual_namespace]
58
+
59
+ if package_enforces_namespaces
60
+ add_offense(
61
+ source_range(processed_source.buffer, 1, 0),
62
+ message: format(
63
+ '`%<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.',
64
+ package_name: package_name,
65
+ expected_namespace: expected_namespace,
66
+ expected_path: relative_desired_path
67
+ )
68
+ )
69
+ elsif pack_owning_this_namespace
70
+ add_offense(
71
+ source_range(processed_source.buffer, 1, 0),
72
+ message: format(
73
+ '`%<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.',
74
+ package_name: package_name,
75
+ pack_owning_this_namespace: pack_owning_this_namespace,
76
+ expected_namespace: expected_namespace,
77
+ actual_namespace: actual_namespace,
78
+ expected_path: relative_desired_path
79
+ )
80
+ )
81
+ end
82
+ end
83
+ end
84
+
85
+ # In the future, we'd love this to support auto-correct.
86
+ # Perhaps by automatically renamespacing the file and changing its location?
87
+ sig { returns(T::Boolean) }
88
+ def support_autocorrect?
89
+ false
90
+ end
91
+
92
+ private
93
+
94
+ sig { returns(DesiredZeitwerkApi) }
95
+ def desired_zeitwerk_api
96
+ @desired_zeitwerk_api ||= T.let(nil, T.nilable(DesiredZeitwerkApi))
97
+ @desired_zeitwerk_api ||= DesiredZeitwerkApi.new
98
+ end
99
+
100
+ sig { returns(T::Hash[String, String]) }
101
+ def namespaces_to_packs
102
+ @namespaces_to_packs = T.let(nil, T.nilable(T::Hash[String, String]))
103
+ @namespaces_to_packs ||= begin
104
+ all_packs_enforcing_namespaces = ParsePackwerk.all.select do |p|
105
+ cop_config['IncludePacks'].include?(p.name)
106
+ end
107
+
108
+ namespaces_to_packs = {}
109
+ all_packs_enforcing_namespaces.each do |package|
110
+ namespaces_to_packs[desired_zeitwerk_api.get_pack_based_namespace(package)] = package.name
111
+ end
112
+
113
+ namespaces_to_packs
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+
3
+ require 'rubocop-sorbet'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Modularization
8
+ # This cop helps ensure that each pack's public API is strictly typed, enforcing strong boundaries.
9
+ #
10
+ # @example
11
+ #
12
+ # # bad
13
+ # # packs/foo/app/public/foo.rb
14
+ # # typed: false
15
+ # module Foo; end
16
+ #
17
+ # # good
18
+ # # packs/foo/app/public/foo.rb
19
+ # # typed: strict
20
+ # module Foo; end
21
+ #
22
+ class TypedPublicApi < Sorbet::StrictSigil
23
+ #
24
+ # This inherits from `Sorbet::StrictSigil` and doesn't change any behavior of it.
25
+ # The only reason we do this is so that configuration for this cop can live under a different cop namespace.
26
+ # This prevents this cop's configuration from clashing with other configurations for the same cop.
27
+ # A concrete example of this would be if a user is using this package protection to make sure public APIs are typed,
28
+ # and separately the application as a whole requiring strict typing in certain parts of the application.
29
+ #
30
+ # To prevent problems associated with needing to manage identical configurations for the same cop, we simply call it
31
+ # something else in the context of this protection.
32
+ #
33
+ # We can apply this same pattern if we want to use other cops in the context of package protections and prevent clashing.
34
+ #
35
+ extend T::Sig
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1 +1,2 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  # The original code is from https://github.com/rubocop/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
@@ -7,6 +8,9 @@ module RuboCop
7
8
  # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
8
9
  # bit of our configuration.
9
10
  module Inject
11
+ extend T::Sig
12
+
13
+ sig { void }
10
14
  def self.defaults!
11
15
  path = CONFIG_DEFAULT.to_s
12
16
  hash = ConfigLoader.send(:load_yaml_configuration, path)
@@ -0,0 +1,11 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RuboCop
5
+ module Modularization
6
+ module Private
7
+ end
8
+
9
+ private_constant :Private
10
+ end
11
+ end
@@ -1,14 +1,18 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
4
+ require 'rubocop/modularization/private'
5
+
3
6
  module RuboCop
4
7
  module Modularization
5
8
  class Error < StandardError; end
9
+ extend T::Sig
10
+
6
11
  # Your code goes here...
7
- PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
8
- CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
9
- CONFIG = YAML.safe_load(CONFIG_DEFAULT.read).freeze
12
+ PROJECT_ROOT = T.let(Pathname.new(__dir__).parent.parent.expand_path.freeze, Pathname)
13
+ CONFIG_DEFAULT = T.let(PROJECT_ROOT.join('config', 'default.yml').freeze, Pathname)
14
+ CONFIG = T.let(YAML.safe_load(CONFIG_DEFAULT.read).freeze, T.untyped)
10
15
 
11
16
  private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
12
17
  end
13
18
  end
14
-
@@ -1,10 +1,13 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'rubocop'
5
+ require 'parse_packwerk'
4
6
 
5
7
  require_relative 'rubocop/modularization'
6
8
  require_relative 'rubocop/modularization/inject'
7
9
 
8
- RuboCop::Modularization::Inject.defaults!
10
+ require 'rubocop/cop/modularization/namespaced_under_package_name'
11
+ require 'rubocop/cop/modularization/typed_public_api'
9
12
 
10
- require_relative 'rubocop/cop/modularization_cops'
13
+ RuboCop::Modularization::Inject.defaults!