rubocop-packs 0.0.10 → 0.0.11

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: 3aa6a83103635cffaf57bbe3187211f14b0981633a55cf04cd73f6261df3d83a
4
- data.tar.gz: 8816eab16fe3dd069a3327b8f1569e910ea07a012bb97bead1baf0f7f6cdfc66
3
+ metadata.gz: caa9570071b5d3b21d447db0959789fbdf5747b352f5b1d5705bd95f0552f722
4
+ data.tar.gz: 3e739760cb2850057189f4f355417921b0be120cb723f8444e567bdf86ad7392
5
5
  SHA512:
6
- metadata.gz: 37e828c802da1563049ce07a05e224187bfe8a3d61861f0cc05e774c311a3627d66d6ff116480d44100a9d31ef756bde1ea16777e02e5517e8f31fb0aab77595
7
- data.tar.gz: 2e54cf17766e69bfd81663a488f1171a74335dd71c55fbfc55f9d1334f6eeae5d0b0fd5e7134349b42fb61c749520a760fd1902f0dfe19c457c311cba55f10ca
6
+ metadata.gz: 851f5ca37b72f15b5b763a5272094c0996cd4ae2c1daf6924209eaefc4787e0c9a68c9336c71db949995f7fc95eae89d0597a36af486127d87665b5d7c8431fd
7
+ data.tar.gz: ebac8a9f6f263d7a3898f12dce5ace40980f52789fe7f6763e56d54ad9de7238b7b0b56eb16bbdbe18af13ddda4b698903c37a11946530c1d2fd4763a4265c96
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:
@@ -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 %>
@@ -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
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.10
4
+ version: 0.0.11
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-07 00:00:00.000000000 Z
11
+ date: 2022-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -187,6 +187,7 @@ 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
@@ -196,6 +197,7 @@ files:
196
197
  - lib/rubocop/packs.rb
197
198
  - lib/rubocop/packs/inject.rb
198
199
  - lib/rubocop/packs/private.rb
200
+ - lib/rubocop/packs/private/configuration.rb
199
201
  - sorbet/config
200
202
  - sorbet/rbi/gems/activesupport@7.0.4.rbi
201
203
  - sorbet/rbi/gems/ast@2.4.2.rbi