rubocop-packs 0.0.11 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: caa9570071b5d3b21d447db0959789fbdf5747b352f5b1d5705bd95f0552f722
4
- data.tar.gz: 3e739760cb2850057189f4f355417921b0be120cb723f8444e567bdf86ad7392
3
+ metadata.gz: 23ba4ef8b1719f4a38ac54f304efb11c88df874e3ba15888ad83fd6e3d0b78e1
4
+ data.tar.gz: 1b2c369b9f7f34dfa6439724a5b2a4dc39addeb1843f8d24d001a9dc55be1860
5
5
  SHA512:
6
- metadata.gz: 851f5ca37b72f15b5b763a5272094c0996cd4ae2c1daf6924209eaefc4787e0c9a68c9336c71db949995f7fc95eae89d0597a36af486127d87665b5d7c8431fd
7
- data.tar.gz: ebac8a9f6f263d7a3898f12dce5ace40980f52789fe7f6763e56d54ad9de7238b7b0b56eb16bbdbe18af13ddda4b698903c37a11946530c1d2fd4763a4265c96
6
+ metadata.gz: 16a8330e4f3649d6e37178759e23eb500dec1d152b13b8a46e1c0b33ebb8c6f4369c1e4683d1c8ab44ca58b987cedcb567a7e0692aae80744022cc0496d781c8
7
+ data.tar.gz: c022df83da7b9c811a0add0c6a13950fbb65b7289f2e687093df1aa2c3900b23aacac0e50f72d3d89deb350d94c055b98a198001692498a394ee31136c3e3e65
data/config/default.yml CHANGED
@@ -16,3 +16,11 @@ Packs/TypedPublicApi:
16
16
 
17
17
  Packs/RequireDocumentedPublicApis:
18
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
@@ -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
- # sig { void }
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
- # sig { void }
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
@@ -6,6 +6,7 @@ require 'parse_packwerk'
6
6
 
7
7
  require_relative 'rubocop/packs'
8
8
  require_relative 'rubocop/packs/inject'
9
+ require_relative 'rubocop/packwerk_lite'
9
10
 
10
11
  require 'rubocop/cop/packs/namespace_convention'
11
12
  require 'rubocop/cop/packs/typed_public_api'
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.11
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-09 00:00:00.000000000 Z
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.3.7
269
+ rubygems_version: 3.1.6
265
270
  signing_key:
266
271
  specification_version: 4
267
272
  summary: Fill this out!