rubocop-modularization 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -3
  3. data/lib/rubocop/cop/modularization/namespaced_under_package_name/desired_zeitwerk_api.rb +110 -0
  4. data/lib/rubocop/cop/modularization/namespaced_under_package_name.rb +119 -0
  5. data/lib/rubocop/cop/modularization/typed_public_api.rb +39 -0
  6. data/lib/rubocop/cop/modularization_cops.rb +1 -0
  7. data/lib/rubocop/modularization/inject.rb +4 -0
  8. data/lib/rubocop/modularization/private.rb +11 -0
  9. data/lib/rubocop/modularization.rb +8 -4
  10. data/lib/rubocop-modularization.rb +5 -2
  11. data/sorbet/rbi/gems/activesupport@7.0.4.rbi +15914 -0
  12. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  13. data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
  14. data/sorbet/rbi/gems/concurrent-ruby@1.1.10.rbi +11263 -0
  15. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1079 -0
  16. data/sorbet/rbi/gems/i18n@1.12.0.rbi +2296 -0
  17. data/sorbet/rbi/gems/json@2.6.2.rbi +1547 -0
  18. data/sorbet/rbi/gems/method_source@1.0.0.rbi +8 -0
  19. data/sorbet/rbi/gems/minitest@5.16.3.rbi +1459 -0
  20. data/sorbet/rbi/gems/netrc@0.11.0.rbi +161 -0
  21. data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
  22. data/sorbet/rbi/gems/parse_packwerk@0.12.1.rbi +203 -0
  23. data/sorbet/rbi/gems/parser@3.1.2.1.rbi +8944 -0
  24. data/sorbet/rbi/gems/pry@0.14.1.rbi +8 -0
  25. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +392 -0
  26. data/sorbet/rbi/gems/rake@13.0.6.rbi +2899 -0
  27. data/sorbet/rbi/gems/rbi@0.0.16.rbi +3007 -0
  28. data/sorbet/rbi/gems/regexp_parser@2.6.0.rbi +3498 -0
  29. data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
  30. data/sorbet/rbi/gems/rspec-core@3.11.0.rbi +10934 -0
  31. data/sorbet/rbi/gems/rspec-expectations@3.11.1.rbi +8090 -0
  32. data/sorbet/rbi/gems/rspec-mocks@3.11.1.rbi +5291 -0
  33. data/sorbet/rbi/gems/rspec-support@3.11.1.rbi +1610 -0
  34. data/sorbet/rbi/gems/rspec@3.11.0.rbi +88 -0
  35. data/sorbet/rbi/gems/rubocop-ast@1.21.0.rbi +6898 -0
  36. data/sorbet/rbi/gems/rubocop-extension-generator@0.5.1.rbi +86 -0
  37. data/sorbet/rbi/gems/rubocop-sorbet@0.6.11.rbi +1002 -0
  38. data/sorbet/rbi/gems/rubocop@1.36.0.rbi +52209 -0
  39. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1239 -0
  40. data/sorbet/rbi/gems/spoom@1.1.12.rbi +2369 -0
  41. data/sorbet/rbi/gems/tapioca@0.10.2.rbi +3439 -0
  42. data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
  43. data/sorbet/rbi/gems/tzinfo@2.0.5.rbi +5896 -0
  44. data/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi +48 -0
  45. data/sorbet/rbi/gems/unparser@0.6.5.rbi +4529 -0
  46. data/sorbet/rbi/gems/webrick@1.7.0.rbi +2586 -0
  47. data/sorbet/rbi/gems/yard-sorbet@0.7.0.rbi +389 -0
  48. data/sorbet/rbi/gems/yard@0.9.28.rbi +17775 -0
  49. data/sorbet/rbi/todo.rbi +0 -3
  50. metadata +104 -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: e6199f3d4749ec35027f1864c4cc3025c2e60f2c4ea5502efa971858f2ef7de1
4
+ data.tar.gz: 1acf6b3830411ab49e836f6f42e8fa6a59ba84ce50b38de33462ecde30b9bd96
5
5
  SHA512:
6
- metadata.gz: 0d94c4ee844d369e8b9a7ecc8cc1f0bab5f070cb7dbd413a5e3a14bb37cb4a03c88b743316e0316b2aed6ec2290d1f0c8d086253c1dbd00c625e06705e384d6f
7
- data.tar.gz: 0f11f26c9d0282dbe9b0b40738a66430b054b62a6c27d6e251eb3fa4ee8077a1766d9be549410ac9682dd223c055d947916802a51f32d10c2ca6ffaf45b1b435
6
+ metadata.gz: e916c36a627b63bd2eff77d9e61702b77920badc2fdc5d02a72d6fead67f219282d8b3a3e027de6cd5adab7a14a0adc2aa3e6987ddc0bd852a51dabcdd72c9fb
7
+ data.tar.gz: d55bab760856ad9e6314d23d8800b0567dd0e5e131ad7998de0cfaacb947cf01679ab3558111fe33ae8068ea5e708082ff8594e626bf66fc62aa0ed22b7ece30
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,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!