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 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!