rubocop-packs 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +84 -0
- data/config/default.yml +16 -0
- data/lib/rubocop/cop/packs/class_methods_as_public_apis.rb +71 -0
- data/lib/rubocop/cop/packs/namespaced_under_package_name/desired_zeitwerk_api.rb +110 -0
- data/lib/rubocop/cop/packs/namespaced_under_package_name.rb +119 -0
- data/lib/rubocop/cop/packs/require_documented_public_apis.rb +34 -0
- data/lib/rubocop/cop/packs/typed_public_api.rb +39 -0
- data/lib/rubocop/packs/inject.rb +24 -0
- data/lib/rubocop/packs/private.rb +11 -0
- data/lib/rubocop/packs.rb +18 -0
- data/lib/rubocop-packs.rb +15 -0
- data/sorbet/config +3 -0
- data/sorbet/rbi/gems/activesupport@7.0.4.rbi +15914 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
- data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
- data/sorbet/rbi/gems/concurrent-ruby@1.1.10.rbi +11263 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1079 -0
- data/sorbet/rbi/gems/i18n@1.12.0.rbi +2296 -0
- data/sorbet/rbi/gems/json@2.6.2.rbi +1547 -0
- data/sorbet/rbi/gems/method_source@1.0.0.rbi +8 -0
- data/sorbet/rbi/gems/minitest@5.16.3.rbi +1459 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +161 -0
- data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
- data/sorbet/rbi/gems/parse_packwerk@0.12.1.rbi +203 -0
- data/sorbet/rbi/gems/parser@3.1.2.1.rbi +8944 -0
- data/sorbet/rbi/gems/pry@0.14.1.rbi +8 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +392 -0
- data/sorbet/rbi/gems/rake@13.0.6.rbi +2899 -0
- data/sorbet/rbi/gems/rbi@0.0.16.rbi +3007 -0
- data/sorbet/rbi/gems/regexp_parser@2.6.0.rbi +3498 -0
- data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
- data/sorbet/rbi/gems/rspec-core@3.11.0.rbi +10934 -0
- data/sorbet/rbi/gems/rspec-expectations@3.11.1.rbi +8090 -0
- data/sorbet/rbi/gems/rspec-mocks@3.11.1.rbi +5291 -0
- data/sorbet/rbi/gems/rspec-support@3.11.1.rbi +1610 -0
- data/sorbet/rbi/gems/rspec@3.11.0.rbi +88 -0
- data/sorbet/rbi/gems/rubocop-ast@1.21.0.rbi +6898 -0
- data/sorbet/rbi/gems/rubocop-extension-generator@0.5.1.rbi +86 -0
- data/sorbet/rbi/gems/rubocop-sorbet@0.6.11.rbi +1002 -0
- data/sorbet/rbi/gems/rubocop@1.36.0.rbi +52209 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1239 -0
- data/sorbet/rbi/gems/spoom@1.1.12.rbi +2369 -0
- data/sorbet/rbi/gems/tapioca@0.10.2.rbi +3439 -0
- data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
- data/sorbet/rbi/gems/tzinfo@2.0.5.rbi +5896 -0
- data/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi +48 -0
- data/sorbet/rbi/gems/unparser@0.6.5.rbi +4529 -0
- data/sorbet/rbi/gems/webrick@1.7.0.rbi +2586 -0
- data/sorbet/rbi/gems/yard-sorbet@0.7.0.rbi +389 -0
- data/sorbet/rbi/gems/yard@0.9.28.rbi +17775 -0
- data/sorbet/rbi/todo.rbi +5 -0
- metadata +266 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bfc93294dd3f42e1dece0b632adba1a4a9ac811d0328c682dd121f2f11063d98
|
4
|
+
data.tar.gz: df7a631fa820a30b672461e106882e7221555ddd3f64ba690dd8f7407d172ca4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ff8d5fbd4ab6233223b705ab097bed58aa9e45139e148c084c071a299720a12759fe2173ba6f557d5794b7e097c5dbc0ef87db3744bd47903662721fb4a8831e
|
7
|
+
data.tar.gz: cf7e59a3a2aa75057cee35ed0d5938b0c3131eff0d5c9657d9705833f26bff636ebc2d2ecf21fe69d04a49121e4772fa32457fe70225345587b3baaca77e1350
|
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# rubocop-packs
|
2
|
+
|
3
|
+
A collection of Rubocop rules for modularizing ruby applications that conform to the `packs` standard.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Just install the `rubocop-packs` gem
|
8
|
+
|
9
|
+
```sh
|
10
|
+
gem install rubocop-packs
|
11
|
+
```
|
12
|
+
or, if you use `Bundler`, add this line your application's `Gemfile`:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'rubocop-packs', require: false
|
16
|
+
```
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
You need to tell RuboCop to load the Packs 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-packs
|
28
|
+
```
|
29
|
+
|
30
|
+
Alternatively, use the following array notation when specifying multiple extensions:
|
31
|
+
|
32
|
+
```yaml
|
33
|
+
require:
|
34
|
+
- rubocop-other-extension
|
35
|
+
- rubocop-packs
|
36
|
+
```
|
37
|
+
|
38
|
+
Now you can run `rubocop` and it will automatically load the RuboCop Packs cops together with the standard cops.
|
39
|
+
|
40
|
+
### Command line
|
41
|
+
|
42
|
+
```sh
|
43
|
+
rubocop --require rubocop-packs
|
44
|
+
```
|
45
|
+
|
46
|
+
### Rake task
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
RuboCop::RakeTask.new do |task|
|
50
|
+
task.requires << 'rubocop-packs'
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
## The Cops
|
55
|
+
All cops are located under [`lib/rubocop/cop/packs`](lib/rubocop/cop/packs), and contain examples/documentation.
|
56
|
+
|
57
|
+
In your `.rubocop.yml`, you may treat the Packs cops just like any other cop. For example:
|
58
|
+
|
59
|
+
```yaml
|
60
|
+
Packs/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-packs. 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[Packs/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-packs/blob/main/LICENSE.txt).
|
data/config/default.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Packs/ClassMethodsAsPublicApis:
|
2
|
+
Enabled: true
|
3
|
+
AcceptableParentClasses:
|
4
|
+
- T::Enum
|
5
|
+
- T::Struct
|
6
|
+
- Struct
|
7
|
+
- OpenStruct
|
8
|
+
|
9
|
+
Packs/NamespacedUnderPackageName:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Packs/TypedPublicApi:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Packs/RequireDocumentedPublicApis:
|
16
|
+
Enabled: false
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RuboCop
|
5
|
+
module Cop
|
6
|
+
module Packs
|
7
|
+
# This cop states that public API should live on class methods, which are more easily statically analyzable,
|
8
|
+
# searchable, and typically hold less state.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# # bad
|
13
|
+
# # packs/foo/app/public/foo.rb
|
14
|
+
# module Foo
|
15
|
+
# def blah
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # good
|
20
|
+
# # packs/foo/app/public/foo.rb
|
21
|
+
# module Foo
|
22
|
+
# def self.blah
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @example AcceptableParentClasses: [T::Enum, T::Struct, Struct, OpenStruct] (default)
|
27
|
+
# You can define `AcceptableParentClasses` which are a list of classes that, if inherited from, non-class methods are permitted.
|
28
|
+
# This is useful when value objects are a part of your public API.
|
29
|
+
#
|
30
|
+
# # good
|
31
|
+
# # packs/foo/app/public/foo.rb
|
32
|
+
# class Foo < T::Enum
|
33
|
+
# const :blah
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
class ClassMethodsAsPublicApis < Base
|
37
|
+
extend T::Sig
|
38
|
+
|
39
|
+
sig { returns(T::Boolean) }
|
40
|
+
def support_autocorrect?
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { params(node: T.untyped).void }
|
45
|
+
def on_def(node)
|
46
|
+
# This cop only applies for ruby files in `app/public`
|
47
|
+
return if !processed_source.file_path.include?('app/public')
|
48
|
+
|
49
|
+
# Looked at https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Lint/MissingSuper source code as inspiration for htis part.
|
50
|
+
class_node = node.each_ancestor(:class).first
|
51
|
+
module_node = node.each_ancestor(:module).first
|
52
|
+
parent_class = class_node&.parent_class || module_node&.parent
|
53
|
+
|
54
|
+
acceptable_parent_classes = cop_config['AcceptableParentClasses'] || []
|
55
|
+
|
56
|
+
# Used this PR as inspiration to check if we're within a `class << self` block
|
57
|
+
uses_implicit_static_methods = node.each_ancestor(:sclass).first&.identifier&.source == 'self'
|
58
|
+
class_is_allowed_to_have_instance_methods = acceptable_parent_classes.include?(parent_class&.const_name)
|
59
|
+
return if uses_implicit_static_methods || class_is_allowed_to_have_instance_methods
|
60
|
+
|
61
|
+
add_offense(
|
62
|
+
node.source_range,
|
63
|
+
message: format(
|
64
|
+
'Top-level files in the public/ folder may only define class methods.'
|
65
|
+
)
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Packs
|
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/packs/namespaced_under_package_name/desired_zeitwerk_api'
|
6
|
+
|
7
|
+
module RuboCop
|
8
|
+
module Cop
|
9
|
+
module Packs
|
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,34 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Packs
|
6
|
+
class RequireDocumentedPublicApis < Style::DocumentationMethod
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { returns(T::Boolean) }
|
10
|
+
def support_autocorrect?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { params(node: T.untyped).void }
|
15
|
+
def check(node)
|
16
|
+
# This cop only applies for ruby files in `app/public`
|
17
|
+
return if !processed_source.file_path.include?('app/public')
|
18
|
+
return if non_public?(node) && !require_for_non_public_methods?
|
19
|
+
|
20
|
+
left_sibling = node.left_sibling
|
21
|
+
left_sibling_is_sig = left_sibling && (left_sibling.source.include?('sig do') || left_sibling.source.include?('sig {'))
|
22
|
+
# Is there a better way to check if the left sibling is a sorbet signature? Probably!
|
23
|
+
if left_sibling_is_sig
|
24
|
+
return if documentation_comment?(node.left_sibling)
|
25
|
+
elsif documentation_comment?(node)
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
29
|
+
add_offense(node)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
require 'rubocop-sorbet'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module Packs
|
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
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# The original code is from https://github.com/rubocop/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
|
5
|
+
# See https://github.com/rubocop/rubocop-rspec/blob/master/MIT-LICENSE.md
|
6
|
+
module RuboCop
|
7
|
+
module Packs
|
8
|
+
# Because RuboCop doesn't yet support plugins, we have to monkey patch in a
|
9
|
+
# bit of our configuration.
|
10
|
+
module Inject
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { void }
|
14
|
+
def self.defaults!
|
15
|
+
path = CONFIG_DEFAULT.to_s
|
16
|
+
hash = ConfigLoader.send(:load_yaml_configuration, path)
|
17
|
+
config = Config.new(hash, path).tap(&:make_excludes_absolute)
|
18
|
+
puts "configuration from #{path}" if ConfigLoader.debug?
|
19
|
+
config = ConfigLoader.merge_with_default(config, path)
|
20
|
+
ConfigLoader.instance_variable_set(:@default_configuration, config)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'rubocop/packs/private'
|
5
|
+
|
6
|
+
module RuboCop
|
7
|
+
module Packs
|
8
|
+
class Error < StandardError; end
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
# Your code goes here...
|
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)
|
15
|
+
|
16
|
+
private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'rubocop'
|
5
|
+
require 'parse_packwerk'
|
6
|
+
|
7
|
+
require_relative 'rubocop/packs'
|
8
|
+
require_relative 'rubocop/packs/inject'
|
9
|
+
|
10
|
+
require 'rubocop/cop/packs/namespaced_under_package_name'
|
11
|
+
require 'rubocop/cop/packs/typed_public_api'
|
12
|
+
require 'rubocop/cop/packs/class_methods_as_public_apis'
|
13
|
+
require 'rubocop/cop/packs/require_documented_public_apis'
|
14
|
+
|
15
|
+
RuboCop::Packs::Inject.defaults!
|
data/sorbet/config
ADDED