rubocop-packs 0.0.10 → 0.0.12
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 +4 -4
- data/README.md +16 -0
- data/config/default.yml +10 -0
- data/config/pack_config.yml +6 -0
- data/lib/rubocop/cop/packs/namespace_convention/desired_zeitwerk_api.rb +2 -0
- data/lib/rubocop/cop/packs/require_documented_public_apis.rb +3 -4
- data/lib/rubocop/cop/packs/typed_public_api.rb +7 -0
- data/lib/rubocop/cop/packwerk_lite/constant_resolver.rb +84 -0
- data/lib/rubocop/cop/packwerk_lite/dependency_checker.rb +77 -0
- data/lib/rubocop/cop/packwerk_lite/privacy_checker.rb +78 -0
- data/lib/rubocop/cop/packwerk_lite/private.rb +38 -0
- data/lib/rubocop/packs/private/configuration.rb +25 -0
- data/lib/rubocop/packs/private.rb +30 -0
- data/lib/rubocop/packs.rb +150 -0
- data/lib/rubocop/packwerk_lite.rb +15 -0
- data/lib/rubocop-packs.rb +1 -0
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23ba4ef8b1719f4a38ac54f304efb11c88df874e3ba15888ad83fd6e3d0b78e1
|
4
|
+
data.tar.gz: 1b2c369b9f7f34dfa6439724a5b2a4dc39addeb1843f8d24d001a9dc55be1860
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 16a8330e4f3649d6e37178759e23eb500dec1d152b13b8a46e1c0b33ebb8c6f4369c1e4683d1c8ab44ca58b987cedcb567a7e0692aae80744022cc0496d781c8
|
7
|
+
data.tar.gz: c022df83da7b9c811a0add0c6a13950fbb65b7289f2e687093df1aa2c3900b23aacac0e50f72d3d89deb350d94c055b98a198001692498a394ee31136c3e3e65
|
data/README.md
CHANGED
@@ -61,6 +61,22 @@ Packs/NamespacedUnderPackageName:
|
|
61
61
|
Exclude:
|
62
62
|
- lib/example.rb
|
63
63
|
```
|
64
|
+
|
65
|
+
## Other Utilities
|
66
|
+
`rubocop-packs` also has some API that help you use rubocop in a pack-based context.
|
67
|
+
|
68
|
+
### `RuboCop::Packs.auto_generate_rubocop_todo(packs: ParsePackwerk.all)`
|
69
|
+
This API will auto-generate a `packs/some_pack/.rubocop_todo.yml`. This allows a pack to own its own exception list. Note that you need to configure `rubocop-packs` with an allow list of cops that can live in per-pack `.rubocop_todo.yml` files:
|
70
|
+
```
|
71
|
+
# `config/rubocop_packs.rb`
|
72
|
+
RuboCop::Packs.configure do |config|
|
73
|
+
# For example:
|
74
|
+
config.permitted_pack_level_cops = ['Packs/NamespaceConvention']
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
There is a supporting validation to ensure these `packs/*/.rubocop_todo.yml` files only add exceptions to the allow listed set of rules. Run this validation with `RuboCop::packs.validate`, which returns an array of errors.
|
79
|
+
|
64
80
|
## Contributing
|
65
81
|
|
66
82
|
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).
|
data/config/default.yml
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
inherit_from: ./pack_config.yml
|
2
|
+
|
1
3
|
Packs/ClassMethodsAsPublicApis:
|
2
4
|
Enabled: true
|
3
5
|
AcceptableParentClasses:
|
@@ -14,3 +16,11 @@ Packs/TypedPublicApi:
|
|
14
16
|
|
15
17
|
Packs/RequireDocumentedPublicApis:
|
16
18
|
Enabled: true
|
19
|
+
|
20
|
+
PackwerkLite/Privacy:
|
21
|
+
# It is recommended to use packwerk
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
PackwerkLite/Dependency:
|
25
|
+
# It is recommended to use packwerk
|
26
|
+
Enabled: false
|
@@ -0,0 +1,6 @@
|
|
1
|
+
# Relevant documentation:
|
2
|
+
# - Inheriting config from a gem:
|
3
|
+
# - https://docs.rubocop.org/rubocop/configuration.html#inheriting-configuration-from-a-dependency-gem
|
4
|
+
# - ERB in a .rubocop.yml file
|
5
|
+
# - https://docs.rubocop.org/rubocop/configuration.html#pre-processing
|
6
|
+
<%= RuboCop::Packs.pack_based_rubocop_todos %>
|
@@ -6,6 +6,8 @@ module RuboCop
|
|
6
6
|
class NamespaceConvention < Base
|
7
7
|
#
|
8
8
|
# This is a private class that represents API that we would prefer to be available somehow in Zeitwerk.
|
9
|
+
# However, the boundaries between systems (packwerk/zeitwerk, rubocop/zeitwerk) are poor in this class, so
|
10
|
+
# that would need to be separated prior to proposing any API changes in zeitwerk.
|
9
11
|
#
|
10
12
|
class DesiredZeitwerkApi
|
11
13
|
extend T::Sig
|
@@ -4,22 +4,21 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module Packs
|
6
6
|
# This cop helps ensure that each pack has a documented public API
|
7
|
+
# The following examples assume this basic setup.
|
7
8
|
#
|
8
9
|
# @example
|
9
10
|
#
|
10
11
|
# # bad
|
11
12
|
# # packs/foo/app/public/foo.rb
|
12
13
|
# class Foo
|
13
|
-
#
|
14
|
-
# def bar
|
14
|
+
# def bar; end
|
15
15
|
# end
|
16
16
|
#
|
17
17
|
# # packs/foo/app/public/foo.rb
|
18
18
|
# class Foo
|
19
19
|
# # This is a documentation comment.
|
20
20
|
# # It can live below or below a sorbet type signature.
|
21
|
-
#
|
22
|
-
# def bar
|
21
|
+
# def bar; end
|
23
22
|
# end
|
24
23
|
#
|
25
24
|
class RequireDocumentedPublicApis < Style::DocumentationMethod
|
@@ -33,6 +33,13 @@ module RuboCop
|
|
33
33
|
# We can apply this same pattern if we want to use other cops in the context of package protections and prevent clashing.
|
34
34
|
#
|
35
35
|
extend T::Sig
|
36
|
+
|
37
|
+
sig { params(processed_source: T.untyped).void }
|
38
|
+
def investigate(processed_source)
|
39
|
+
return unless processed_source.path.include?('app/public')
|
40
|
+
|
41
|
+
super
|
42
|
+
end
|
36
43
|
end
|
37
44
|
end
|
38
45
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module PackwerkLite
|
6
|
+
#
|
7
|
+
# This is a private class that represents API that we would prefer to be available somehow in Zeitwerk.
|
8
|
+
# However, the boundaries between systems (packwerk/zeitwerk, rubocop/zeitwerk) are poor in this class, so
|
9
|
+
# that would need to be separated prior to proposing any API changes in zeitwerk.
|
10
|
+
#
|
11
|
+
class ConstantResolver
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
class ConstantReference < T::Struct
|
15
|
+
extend T::Sig
|
16
|
+
|
17
|
+
const :constant_name, String
|
18
|
+
const :global_namespace, String
|
19
|
+
const :source_package, ParsePackwerk::Package
|
20
|
+
const :constant_definition_location, Pathname
|
21
|
+
const :referencing_file, Pathname
|
22
|
+
|
23
|
+
sig { returns(ParsePackwerk::Package) }
|
24
|
+
def referencing_package
|
25
|
+
ParsePackwerk.package_from_path(referencing_file)
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { returns(T::Boolean) }
|
29
|
+
def public_api?
|
30
|
+
# ParsePackwerk::Package should have a method to take in a path and determine if the file is public.
|
31
|
+
# For now we put it here and only support the public folder (and not specific private constants).
|
32
|
+
constant_definition_location.to_s.include?('/public/')
|
33
|
+
end
|
34
|
+
|
35
|
+
sig { params(node: RuboCop::AST::ConstNode, processed_source: RuboCop::AST::ProcessedSource).returns(T.nilable(ConstantReference)) }
|
36
|
+
def self.resolve(node, processed_source)
|
37
|
+
constant_name = node.const_name
|
38
|
+
namespaces = constant_name.split('::')
|
39
|
+
global_namespace = namespaces.first
|
40
|
+
|
41
|
+
expected_containing_pack_last_name = global_namespace.underscore
|
42
|
+
|
43
|
+
# We don't use ParsePackwerk.find(...) here because we want to look for nested packs, and this pack could be a child pack in a nested pack too.
|
44
|
+
# In the future, we might want `find` to be able to take a glob or a regex to look for packs with a specific name structure.
|
45
|
+
expected_containing_pack = ParsePackwerk.all.find { |p| p.name.include?("/#{expected_containing_pack_last_name}") }
|
46
|
+
return if expected_containing_pack.nil?
|
47
|
+
|
48
|
+
if namespaces.count == 1
|
49
|
+
found_files = expected_containing_pack.directory.glob("app/*/#{expected_containing_pack_last_name}.rb")
|
50
|
+
else
|
51
|
+
expected_location_in_pack = namespaces[1..].map(&:underscore).join('/')
|
52
|
+
found_files = expected_containing_pack.directory.glob("app/*/#{expected_containing_pack_last_name}/#{expected_location_in_pack}.rb")
|
53
|
+
end
|
54
|
+
|
55
|
+
# Because of how Zietwerk works, we know two things:
|
56
|
+
# 1) Since namespaces map one to one with files, Zeitwerk does not permit multiple files to define the same fully-qualified class/module.
|
57
|
+
# (Note it does permit multiple files to open up portions of other namespaces)
|
58
|
+
# 2) If a file *could* define a fully qualified constant, then it *must* define that constant!
|
59
|
+
#
|
60
|
+
# Therefore when we've found possible files, we can sanity check there is only one,
|
61
|
+
# and then assume the found pack defines the constant!
|
62
|
+
raise if found_files.count > 1
|
63
|
+
|
64
|
+
expected_pack_contains_constant = found_files.any?
|
65
|
+
|
66
|
+
return if !expected_pack_contains_constant
|
67
|
+
|
68
|
+
found_file = found_files.first
|
69
|
+
|
70
|
+
ConstantReference.new(
|
71
|
+
constant_name: constant_name,
|
72
|
+
global_namespace: global_namespace,
|
73
|
+
source_package: expected_containing_pack,
|
74
|
+
constant_definition_location: T.must(found_file),
|
75
|
+
referencing_file: Pathname.new(processed_source.path).relative_path_from(Pathname.pwd)
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private_constant :ConstantResolver
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module PackwerkLite
|
6
|
+
# This cop helps ensure that packs are depending on packs explicitly.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
#
|
10
|
+
# # bad
|
11
|
+
# # packs/foo/app/services/foo.rb
|
12
|
+
# class Foo
|
13
|
+
# def bar
|
14
|
+
# Bar
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # packs/foo/package.yml
|
19
|
+
# # enforces_dependencies: true
|
20
|
+
# # enforces_privacy: false
|
21
|
+
# # dependencies:
|
22
|
+
# # - packs/baz
|
23
|
+
#
|
24
|
+
# # good
|
25
|
+
# # packs/foo/app/services/foo.rb
|
26
|
+
# class Foo
|
27
|
+
# def bar
|
28
|
+
# Bar
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # packs/foo/package.yml
|
33
|
+
# # enforces_dependencies: true
|
34
|
+
# # enforces_privacy: false
|
35
|
+
# # dependencies:
|
36
|
+
# # - packs/baz
|
37
|
+
# # - packs/bar
|
38
|
+
#
|
39
|
+
class Dependency < Base
|
40
|
+
extend T::Sig
|
41
|
+
|
42
|
+
sig { returns(T::Boolean) }
|
43
|
+
def support_autocorrect?
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { params(node: RuboCop::AST::ConstNode).void }
|
48
|
+
def on_const(node)
|
49
|
+
return if Private.partial_const_reference?(node)
|
50
|
+
|
51
|
+
constant_reference = ConstantResolver::ConstantReference.resolve(node, processed_source)
|
52
|
+
return if constant_reference.nil?
|
53
|
+
return if constant_reference.referencing_package.name == constant_reference.source_package.name
|
54
|
+
|
55
|
+
# These are cases that don't work yet!!
|
56
|
+
# I'll need to look into this more. It's related to inflections but not sure how yet!
|
57
|
+
return if constant_reference.constant_name.include?('PncApi')
|
58
|
+
|
59
|
+
is_new_violation = [
|
60
|
+
!constant_reference.referencing_package.dependencies.include?(constant_reference.source_package.name),
|
61
|
+
constant_reference.referencing_package.enforces_dependencies?,
|
62
|
+
!Private.violation_in_deprecated_references_yml?(constant_reference, type: 'dependency')
|
63
|
+
].all?
|
64
|
+
|
65
|
+
if is_new_violation
|
66
|
+
add_offense(
|
67
|
+
node.source_range,
|
68
|
+
message: format(
|
69
|
+
'Dependency violation detected. See https://github.com/Shopify/packwerk/blob/main/RESOLVING_VIOLATIONS.md for help'
|
70
|
+
)
|
71
|
+
)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module PackwerkLite
|
6
|
+
# This cop helps ensure that packs are using public API of other systems
|
7
|
+
# The following examples assume this basic setup.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # packs/bar/app/public/bar.rb
|
11
|
+
# class Bar
|
12
|
+
# def my_public_api; end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# # packs/bar/app/services/private.rb
|
16
|
+
# class Private
|
17
|
+
# def my_private_api; end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # packs/bar/package.yml
|
21
|
+
# # enforces_dependencies: false
|
22
|
+
# # enforces_privacy: true
|
23
|
+
#
|
24
|
+
# # bad
|
25
|
+
# # packs/foo/app/services/foo.rb
|
26
|
+
# class Foo
|
27
|
+
# def bar
|
28
|
+
# Private.my_private_api
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # good
|
33
|
+
# # packs/foo/app/services/foo.rb
|
34
|
+
# class Bar
|
35
|
+
# def bar
|
36
|
+
# Bar.my_public_api
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
class Privacy < Base
|
41
|
+
extend T::Sig
|
42
|
+
|
43
|
+
sig { returns(T::Boolean) }
|
44
|
+
def support_autocorrect?
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { params(node: RuboCop::AST::ConstNode).void }
|
49
|
+
def on_const(node)
|
50
|
+
# See https://github.com/rubocop/rubocop/blob/master/lib/rubocop/cop/lint/constant_resolution.rb source code as an example
|
51
|
+
return if Private.partial_const_reference?(node)
|
52
|
+
|
53
|
+
constant_reference = ConstantResolver::ConstantReference.resolve(node, processed_source)
|
54
|
+
|
55
|
+
# If we can't determine a constant reference, we can just early return. This could be beacuse the constant is defined
|
56
|
+
# in a gem OR because it's not abiding by the namespace convention we've established.
|
57
|
+
return if constant_reference.nil?
|
58
|
+
return if constant_reference.referencing_package.name == constant_reference.source_package.name
|
59
|
+
|
60
|
+
is_new_violation = [
|
61
|
+
!constant_reference.public_api?,
|
62
|
+
constant_reference.source_package.enforces_privacy?,
|
63
|
+
!Private.violation_in_deprecated_references_yml?(constant_reference)
|
64
|
+
].all?
|
65
|
+
|
66
|
+
if is_new_violation
|
67
|
+
add_offense(
|
68
|
+
node.source_range,
|
69
|
+
message: format(
|
70
|
+
'Privacy violation detected. See https://github.com/Shopify/packwerk/blob/main/RESOLVING_VIOLATIONS.md for help'
|
71
|
+
)
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module PackwerkLite
|
6
|
+
module Private
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { params(node: RuboCop::AST::ConstNode).returns(T::Boolean) }
|
10
|
+
def self.partial_const_reference?(node)
|
11
|
+
# This is a bit whacky, but if I have a reference in the code like this: Foo::Bar::Baz.any_method, `on_const` will be called three times:
|
12
|
+
# One with `Foo`, one with `Foo::Bar`, and one with `Foo::Bar::Baz`.
|
13
|
+
# As far as I can tell, there is no way to direct Rubocop to only look at the full constant name.
|
14
|
+
# In order to ensure we're only operating on fully constant names, I check the "right sibling" of the `node`, which is the portion of the AST
|
15
|
+
# immediately following the node.
|
16
|
+
# If that right sibling is `nil` OR it's a lowercase string, we assume that it's the full constant.
|
17
|
+
# If the right sibling is a non-nil capitalized string, we assume it's a part of the constant, because by convention, constants
|
18
|
+
# start with capital letters and methods start with lowercase letters.
|
19
|
+
# RegularRateOfPay::Types::HourlyEarningWithDate
|
20
|
+
right_sibling = node.right_sibling
|
21
|
+
return false if right_sibling.nil?
|
22
|
+
|
23
|
+
right_sibling.to_s[0].capitalize == right_sibling.to_s[0]
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { params(constant_reference: ConstantResolver::ConstantReference, type: String).returns(T::Boolean) }
|
27
|
+
def self.violation_in_deprecated_references_yml?(constant_reference, type: 'privacy')
|
28
|
+
existing_violations = ParsePackwerk::DeprecatedReferences.for(constant_reference.referencing_package).violations
|
29
|
+
existing_violations.any? do |v|
|
30
|
+
v.class_name == "::#{constant_reference.constant_name}" && (type == 'privacy' ? v.privacy? : v.dependency?)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private_constant :Private
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RuboCop
|
5
|
+
module Packs
|
6
|
+
module Private
|
7
|
+
class Configuration
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { returns(T::Array[String]) }
|
11
|
+
attr_accessor :permitted_pack_level_cops
|
12
|
+
|
13
|
+
sig { void }
|
14
|
+
def initialize
|
15
|
+
@permitted_pack_level_cops = T.let([], T::Array[String])
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { void }
|
19
|
+
def bust_cache!
|
20
|
+
@permitted_pack_level_cops = []
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,9 +1,39 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require 'rubocop/packs/private/configuration'
|
5
|
+
|
4
6
|
module RuboCop
|
5
7
|
module Packs
|
6
8
|
module Private
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { void }
|
12
|
+
def self.bust_cache!
|
13
|
+
@rubocop_todo_ymls = nil
|
14
|
+
@loaded_client_configuration = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { void }
|
18
|
+
def self.load_client_configuration
|
19
|
+
@loaded_client_configuration ||= T.let(false, T.nilable(T::Boolean))
|
20
|
+
return if @loaded_client_configuration
|
21
|
+
|
22
|
+
@loaded_client_configuration = true
|
23
|
+
client_configuration = Pathname.pwd.join('config/rubocop_packs.rb')
|
24
|
+
require client_configuration.to_s if client_configuration.exist?
|
25
|
+
end
|
26
|
+
|
27
|
+
sig { returns(T::Array[T::Hash[T.untyped, T.untyped]]) }
|
28
|
+
def self.rubocop_todo_ymls
|
29
|
+
@rubocop_todo_ymls = T.let(@rubocop_todo_ymls, T.nilable(T::Array[T::Hash[T.untyped, T.untyped]]))
|
30
|
+
@rubocop_todo_ymls ||= begin
|
31
|
+
todo_files = Pathname.glob('**/.rubocop_todo.yml')
|
32
|
+
todo_files.map do |todo_file|
|
33
|
+
YAML.load_file(todo_file)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
7
37
|
end
|
8
38
|
|
9
39
|
private_constant :Private
|
data/lib/rubocop/packs.rb
CHANGED
@@ -14,5 +14,155 @@ module RuboCop
|
|
14
14
|
CONFIG = T.let(YAML.safe_load(CONFIG_DEFAULT.read).freeze, T.untyped)
|
15
15
|
|
16
16
|
private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
|
17
|
+
|
18
|
+
#
|
19
|
+
# Ideally, this is API that is available to us via `rubocop` itself.
|
20
|
+
# That is: the ability to preserve the location of `.rubocop_todo.yml` files and associate
|
21
|
+
# exclusions with the closest ancestor `.rubocop_todo.yml`
|
22
|
+
#
|
23
|
+
sig { params(packs: T::Array[ParsePackwerk::Package]).void }
|
24
|
+
def self.auto_generate_rubocop_todo(packs:)
|
25
|
+
pack_arguments = packs.map(&:name).join(' ')
|
26
|
+
cop_arguments = config.permitted_pack_level_cops.join(',')
|
27
|
+
command = "bundle exec rubocop #{pack_arguments} --only=#{cop_arguments} --format=json"
|
28
|
+
puts "Executing: #{command}"
|
29
|
+
json = JSON.parse(`#{command}`)
|
30
|
+
new_rubocop_todo_exclusions = {}
|
31
|
+
json['files'].each do |file_hash|
|
32
|
+
filepath = file_hash['path']
|
33
|
+
pack = ParsePackwerk.package_from_path(filepath)
|
34
|
+
next if pack.name == ParsePackwerk::ROOT_PACKAGE_NAME
|
35
|
+
|
36
|
+
file_hash['offenses'].each do |offense_hash|
|
37
|
+
cop_name = offense_hash['cop_name']
|
38
|
+
next unless config.permitted_pack_level_cops.include?(cop_name)
|
39
|
+
|
40
|
+
new_rubocop_todo_exclusions[pack.name] ||= {}
|
41
|
+
new_rubocop_todo_exclusions[pack.name][filepath] ||= []
|
42
|
+
new_rubocop_todo_exclusions[pack.name][filepath] << cop_name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
new_rubocop_todo_exclusions.each do |pack_name, file_hash|
|
47
|
+
pack = T.must(ParsePackwerk.find(pack_name))
|
48
|
+
rubocop_todo_yml = pack.directory.join('.rubocop_todo.yml')
|
49
|
+
if rubocop_todo_yml.exist?
|
50
|
+
rubocop_todo = YAML.load_file(rubocop_todo_yml)
|
51
|
+
else
|
52
|
+
rubocop_todo = {}
|
53
|
+
end
|
54
|
+
file_hash.each do |file, failing_cops|
|
55
|
+
failing_cops.each do |failing_cop|
|
56
|
+
rubocop_todo[failing_cop] ||= { 'Exclude' => [] }
|
57
|
+
rubocop_todo[failing_cop]['Exclude'] << file
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
next if rubocop_todo.empty?
|
62
|
+
|
63
|
+
rubocop_todo_yml.write(YAML.dump(rubocop_todo))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
sig { params(root_pathname: String).returns(String) }
|
68
|
+
# It would be great if rubocop (upstream) could take in a glob for `inherit_from`, which
|
69
|
+
# would allow us to delete this method and this additional complexity.
|
70
|
+
def self.pack_based_rubocop_todos(root_pathname: Bundler.root)
|
71
|
+
rubocop_todos = {}
|
72
|
+
# We do this because when the ERB is evaluated Dir.pwd is at the directory containing the YML.
|
73
|
+
# Ideally rubocop wouldn't change the PWD before invoking this method.
|
74
|
+
Dir.chdir(root_pathname) do
|
75
|
+
ParsePackwerk.all.each do |package|
|
76
|
+
next if package.name == ParsePackwerk::ROOT_PACKAGE_NAME
|
77
|
+
|
78
|
+
rubocop_todo = package.directory.join('.rubocop_todo.yml')
|
79
|
+
next unless rubocop_todo.exist?
|
80
|
+
|
81
|
+
loaded_rubocop_todo = YAML.load_file(rubocop_todo)
|
82
|
+
loaded_rubocop_todo.each do |protection_key, key_config|
|
83
|
+
rubocop_todos[protection_key] ||= { 'Exclude' => [] }
|
84
|
+
rubocop_todos[protection_key]['Exclude'] += key_config['Exclude']
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
YAML.dump(rubocop_todos)
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { void }
|
93
|
+
def self.bust_cache!
|
94
|
+
config.bust_cache!
|
95
|
+
Private.bust_cache!
|
96
|
+
end
|
97
|
+
|
98
|
+
sig { params(blk: T.proc.params(arg0: Private::Configuration).void).void }
|
99
|
+
def self.configure(&blk)
|
100
|
+
yield(config)
|
101
|
+
end
|
102
|
+
|
103
|
+
sig { returns(Private::Configuration) }
|
104
|
+
def self.config
|
105
|
+
Private.load_client_configuration
|
106
|
+
@config = T.let(@config, T.nilable(Private::Configuration))
|
107
|
+
@config ||= Private::Configuration.new
|
108
|
+
end
|
109
|
+
|
110
|
+
sig { params(rule: String).returns(T::Set[String]) }
|
111
|
+
def self.exclude_for_rule(rule)
|
112
|
+
excludes = T.let(Set.new, T::Set[String])
|
113
|
+
|
114
|
+
Private.rubocop_todo_ymls.each do |todo_yml|
|
115
|
+
next if !todo_yml
|
116
|
+
|
117
|
+
config = todo_yml[rule]
|
118
|
+
next if config.nil?
|
119
|
+
|
120
|
+
exclude_list = config['Exclude']
|
121
|
+
next if exclude_list.nil?
|
122
|
+
|
123
|
+
excludes += exclude_list
|
124
|
+
end
|
125
|
+
|
126
|
+
excludes
|
127
|
+
end
|
128
|
+
|
129
|
+
sig { returns(T::Array[String]) }
|
130
|
+
def self.validate
|
131
|
+
errors = []
|
132
|
+
ParsePackwerk.all.each do |package|
|
133
|
+
next if package.name == ParsePackwerk::ROOT_PACKAGE_NAME
|
134
|
+
|
135
|
+
rubocop_todo = package.directory.join('.rubocop_todo.yml')
|
136
|
+
next unless rubocop_todo.exist?
|
137
|
+
|
138
|
+
loaded_rubocop_todo = YAML.load_file(rubocop_todo)
|
139
|
+
loaded_rubocop_todo.each_key do |key|
|
140
|
+
if !config.permitted_pack_level_cops.include?(key)
|
141
|
+
errors << <<~ERROR_MESSAGE
|
142
|
+
#{rubocop_todo} contains invalid configuration for #{key}.
|
143
|
+
Please ensure the only configuration is for package protection exclusions, which are one of the following cops: #{config.permitted_pack_level_cops.inspect}"
|
144
|
+
For ignoring other cops, please instead modify the top-level .rubocop_todo.yml file.
|
145
|
+
ERROR_MESSAGE
|
146
|
+
elsif loaded_rubocop_todo[key].keys != ['Exclude']
|
147
|
+
errors << <<~ERROR_MESSAGE
|
148
|
+
#{rubocop_todo} contains invalid configuration for #{key}.
|
149
|
+
Please ensure the only configuration for #{key} is `Exclude`
|
150
|
+
ERROR_MESSAGE
|
151
|
+
else
|
152
|
+
loaded_rubocop_todo[key]['Exclude'].each do |filepath|
|
153
|
+
next unless ParsePackwerk.package_from_path(filepath).name != package.name
|
154
|
+
|
155
|
+
errors << <<~ERROR_MESSAGE
|
156
|
+
#{rubocop_todo} contains invalid configuration for #{key}.
|
157
|
+
#{filepath} does not belong to #{package.name}. Please ensure you only add exclusions
|
158
|
+
for files within this pack.
|
159
|
+
ERROR_MESSAGE
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
errors
|
166
|
+
end
|
17
167
|
end
|
18
168
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'rubocop/cop/packwerk_lite/private'
|
5
|
+
require 'rubocop/cop/packwerk_lite/constant_resolver'
|
6
|
+
require 'rubocop/cop/packwerk_lite/privacy_checker'
|
7
|
+
require 'rubocop/cop/packwerk_lite/dependency_checker'
|
8
|
+
|
9
|
+
module RuboCop
|
10
|
+
# See docs/packwerk_lite.md
|
11
|
+
module PackwerkLite
|
12
|
+
class Error < StandardError; end
|
13
|
+
extend T::Sig
|
14
|
+
end
|
15
|
+
end
|
data/lib/rubocop-packs.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubocop-packs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.12
|
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-10-
|
11
|
+
date: 2022-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -187,15 +187,22 @@ extra_rdoc_files: []
|
|
187
187
|
files:
|
188
188
|
- README.md
|
189
189
|
- config/default.yml
|
190
|
+
- config/pack_config.yml
|
190
191
|
- lib/rubocop-packs.rb
|
191
192
|
- lib/rubocop/cop/packs/class_methods_as_public_apis.rb
|
192
193
|
- lib/rubocop/cop/packs/namespace_convention.rb
|
193
194
|
- lib/rubocop/cop/packs/namespace_convention/desired_zeitwerk_api.rb
|
194
195
|
- lib/rubocop/cop/packs/require_documented_public_apis.rb
|
195
196
|
- lib/rubocop/cop/packs/typed_public_api.rb
|
197
|
+
- lib/rubocop/cop/packwerk_lite/constant_resolver.rb
|
198
|
+
- lib/rubocop/cop/packwerk_lite/dependency_checker.rb
|
199
|
+
- lib/rubocop/cop/packwerk_lite/privacy_checker.rb
|
200
|
+
- lib/rubocop/cop/packwerk_lite/private.rb
|
196
201
|
- lib/rubocop/packs.rb
|
197
202
|
- lib/rubocop/packs/inject.rb
|
198
203
|
- lib/rubocop/packs/private.rb
|
204
|
+
- lib/rubocop/packs/private/configuration.rb
|
205
|
+
- lib/rubocop/packwerk_lite.rb
|
199
206
|
- sorbet/config
|
200
207
|
- sorbet/rbi/gems/activesupport@7.0.4.rbi
|
201
208
|
- sorbet/rbi/gems/ast@2.4.2.rbi
|
@@ -259,7 +266,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
259
266
|
- !ruby/object:Gem::Version
|
260
267
|
version: '0'
|
261
268
|
requirements: []
|
262
|
-
rubygems_version: 3.
|
269
|
+
rubygems_version: 3.1.6
|
263
270
|
signing_key:
|
264
271
|
specification_version: 4
|
265
272
|
summary: Fill this out!
|