rubocop-packs 0.0.11 → 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/config/default.yml +8 -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/packwerk_lite.rb +15 -0
- data/lib/rubocop-packs.rb +1 -0
- metadata +8 -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/config/default.yml
CHANGED
@@ -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,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
|
@@ -194,10 +194,15 @@ files:
|
|
194
194
|
- lib/rubocop/cop/packs/namespace_convention/desired_zeitwerk_api.rb
|
195
195
|
- lib/rubocop/cop/packs/require_documented_public_apis.rb
|
196
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
|
197
201
|
- lib/rubocop/packs.rb
|
198
202
|
- lib/rubocop/packs/inject.rb
|
199
203
|
- lib/rubocop/packs/private.rb
|
200
204
|
- lib/rubocop/packs/private/configuration.rb
|
205
|
+
- lib/rubocop/packwerk_lite.rb
|
201
206
|
- sorbet/config
|
202
207
|
- sorbet/rbi/gems/activesupport@7.0.4.rbi
|
203
208
|
- sorbet/rbi/gems/ast@2.4.2.rbi
|
@@ -261,7 +266,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
261
266
|
- !ruby/object:Gem::Version
|
262
267
|
version: '0'
|
263
268
|
requirements: []
|
264
|
-
rubygems_version: 3.
|
269
|
+
rubygems_version: 3.1.6
|
265
270
|
signing_key:
|
266
271
|
specification_version: 4
|
267
272
|
summary: Fill this out!
|