betterlint 1.28.0 → 1.30.0

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: 070e58e2493975205fb3f5f52f1c6082109835db4388e67d7876bfd471861e59
4
- data.tar.gz: 73a4c6ddd11054bd6b2c3add1e3963f62b6849241b2eb4be2a41a1bec41cc2f7
3
+ metadata.gz: fd581451b52d71fae89c2aa596958a8bd87bbddfa13f7e8b9019dbbe34cf3c6d
4
+ data.tar.gz: '0198a7a5d5e9db377f8964db8a7cd3e05c0f227e3f7a047d2914d930103aec81'
5
5
  SHA512:
6
- metadata.gz: c212dd812b46431b40bea0876f5aeefe5d4f657ef59bb0355342a4e22d19e167cbe950ca7edd237ad581e17cc2400f7c08cabc84ce677b16731f56957ba517d6
7
- data.tar.gz: 486bdb3b54a34ee2363cf482e113a928840bda36ca94a9407e2b269748fa534a24d75e407df839d5b86561dedb50c9a1648558b507ae49acf6152b77866cfb8a
6
+ metadata.gz: c9c17ae11bc30e88ea612dd5a5b77dd8c7a89d9b2c0614a2c88dc53f5fc7099055286b1289c58015d7c8fad069df93f455a34000bf6820f32cb62a4241bc8025
7
+ data.tar.gz: ab5077e13a6044c53fc65654a57f282c14922a5288c1d2115c491cd35c063662f06e07572c7b26de750b4e73334ea26e8754ac1e3e8820c8821f312200960cec
data/README.md CHANGED
@@ -19,6 +19,10 @@ plugins:
19
19
  - betterlint
20
20
  ```
21
21
 
22
+ ## Upgrading to 1.30
23
+
24
+ Migration files are included in normal lint runs. Set `AllCops/MigratedSchemaVersion` to the last existing migration version to [skip historical files](#bettermentnodownmigrations).
25
+
22
26
  ## Dependencies
23
27
 
24
28
  This gem depends on the following other gems:
@@ -235,6 +239,49 @@ Betterment/UnsafeJob:
235
239
 
236
240
  It may make sense to consult your application's values for `Rails.application.config.filter_parameters`; if the application is filtering specific parameters from being logged, it might be a good idea to prevent these values from being stored in plaintext in a database as well.
237
241
 
242
+ ### Betterment/NoDownMigrations
243
+
244
+ This cop enforces [the rule against down migrations](https://the-book.betterment.io/rules/ruby/#do-not-write-down-migrations)
245
+ by flagging `down` methods and `direction.down` calls in migration `reversible` blocks.
246
+ It allows `change` and standalone `up` methods and does not autocorrect.
247
+
248
+ **Bad**
249
+
250
+ ```ruby
251
+ class AddNickname < ActiveRecord::Migration[8.0]
252
+ def up
253
+ add_column :users, :nickname, :string
254
+ end
255
+
256
+ def down
257
+ remove_column :users, :nickname
258
+ end
259
+ end
260
+ ```
261
+
262
+ **Good**
263
+
264
+ ```ruby
265
+ class AddNickname < ActiveRecord::Migration[8.0]
266
+ def change
267
+ add_column :users, :nickname, :string
268
+ end
269
+ end
270
+ ```
271
+
272
+ The cop is disabled by default. Enable it for migrations newer than a chosen version:
273
+
274
+ ```yaml
275
+ AllCops:
276
+ MigratedSchemaVersion: '20261030120000'
277
+
278
+ Betterment/NoDownMigrations:
279
+ Enabled: true
280
+ ```
281
+
282
+ Set `MigratedSchemaVersion` under this cop to ignore additional migrations.
283
+ Leave `Rails/ReversibleMigrationMethodDefinition` disabled: it requires `up` and `down`.
284
+
238
285
  ### Betterment/NonStandardActions
239
286
 
240
287
  This cop looks at Rails route files and flags routes that go to non-standard controller actions.
@@ -367,3 +414,59 @@ the `coder` keyword argument.
367
414
  - And you've opted into 7.1 defaults (namely, `config.active_record.default_column_serializer = nil`)
368
415
 
369
416
  ...you can safely disable this cop, since failing to pass a deserializer will raise an exception.
417
+
418
+ ### Betterment/SpecDescribeMethodName
419
+
420
+ This cop requires each spec to name the class under test and the method under test using `describe` blocks. The outermost `describe` blocks should be the class constant, and second level `describe` blocks should be of the form `"#instance_method"` or `".class_method"`. This structure works with [mutant](https://github.com/mbj/mutant) and creates consistently shaped specs
421
+
422
+ ```ruby
423
+ # BAD - the outer describe is a string
424
+ RSpec.describe "Invoice" do
425
+ # ...
426
+ end
427
+
428
+ # BAD - no method label
429
+ RSpec.describe Invoice do
430
+ it "sums the line items" do
431
+ # ...
432
+ end
433
+ end
434
+
435
+ # BAD - the context block breaks the "Invoice#total" label
436
+ RSpec.describe Invoice do
437
+ context "when the account is delinquent" do
438
+ describe "#total" do
439
+ # ...
440
+ end
441
+ end
442
+ end
443
+
444
+ # BAD - a second class describe breaks the "Invoice::Item#total" label.
445
+ # Give Invoice::Item its own spec file.
446
+ RSpec.describe Invoice do
447
+ describe Invoice::Item do
448
+ describe "#total" do
449
+ # ...
450
+ end
451
+ end
452
+ end
453
+
454
+ # GOOD - shared setup above the method labels
455
+ RSpec.describe Invoice do
456
+ let(:account) { create(:account, :delinquent) }
457
+
458
+ describe "#total" do
459
+ it "adds the late fee" do
460
+ # ...
461
+ end
462
+ end
463
+
464
+ describe ".open" do
465
+ it "excludes paid invoices" do
466
+ # ...
467
+ end
468
+ end
469
+ end
470
+ ```
471
+
472
+ This cop is disabled by default, because it reports many offenses in applications with older specs. To use it, enable it in your `.rubocop.yml`. By default it examines all files in `spec/`, except for the `features`, `requests`, `routing`, `system`, and `views` directories, where integration style tests that don't test one class and method are common. Examples in shared example groups are also ignored.
data/config/default.yml CHANGED
@@ -20,7 +20,7 @@ AllCops:
20
20
  - Gemfile
21
21
  - bin/**/*
22
22
  - build/**/*
23
- - db/**/*
23
+ - db/*schema.rb
24
24
  - frontend/**/*
25
25
  - node_modules/**/*
26
26
  - tmp/**/*
@@ -62,6 +62,14 @@ Betterment/InternalsProtection:
62
62
  StyleGuide: '#bettermentinternalsprotection'
63
63
  Enabled: false
64
64
 
65
+ Betterment/NoDownMigrations:
66
+ Description: Disallows explicit rollback code in database migrations.
67
+ Enabled: false
68
+ MigratedSchemaVersion:
69
+ StyleGuide: '#bettermentnodownmigrations'
70
+ Include:
71
+ - db/migrate/**/*.rb
72
+
65
73
  Betterment/NonNamespacedClass:
66
74
  Description: Detects classes that are not namespaced under a module or constant
67
75
  StyleGuide: '#bettermentnonamespacedclass'
@@ -114,6 +122,19 @@ Betterment/SitePrismLoaded:
114
122
  - spec/features/**/*_spec.rb
115
123
  - spec/system/**/*_spec.rb
116
124
 
125
+ Betterment/SpecDescribeMethodName:
126
+ Description: Requires spec `describe` blocks to be labeled with a class constant and method names.
127
+ Enabled: false
128
+ StyleGuide: '#bettermentspecdescribemethodname'
129
+ Include:
130
+ - spec/**/*_spec.rb
131
+ Exclude:
132
+ - spec/features/**/*_spec.rb
133
+ - spec/requests/**/*_spec.rb
134
+ - spec/routing/**/*_spec.rb
135
+ - spec/system/**/*_spec.rb
136
+ - spec/views/**/*_spec.rb
137
+
117
138
  Betterment/UnsafeJob:
118
139
  Enabled: false
119
140
  StyleGuide: '#bettermentunsafejob'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Betterlint
4
- VERSION = '1.28.0'
4
+ VERSION = '1.30.0'
5
5
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop/cop/mixin/migrations_helper'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Betterment
8
+ class NoDownMigrations < Base
9
+ include MigrationsHelper
10
+
11
+ MSG = 'Do not write explicit rollback code. Correct deployed migrations with a new forward migration.'
12
+ RESTRICT_ON_SEND = %i(down).freeze
13
+
14
+ def on_def(node)
15
+ add_offense(node.loc.name) if node.method?(:down) && migration_to_check?(node)
16
+ end
17
+ alias on_defs on_def
18
+
19
+ def on_send(node)
20
+ if migration_to_check?(node) && reversible_direction?(node.receiver)
21
+ add_offense(node.loc.selector)
22
+ end
23
+ end
24
+ alias on_csend on_send
25
+
26
+ private
27
+
28
+ def migration_to_check?(node)
29
+ return false unless migration_class?(node.each_ancestor(:class).first)
30
+
31
+ cutoff = cop_config.fetch('MigratedSchemaVersion')
32
+ version = File.basename(processed_source.file_path)[/\A\d+(?=_)/]
33
+ !cutoff || !version || version.to_i > Integer(cutoff)
34
+ end
35
+
36
+ def reversible_direction?(receiver)
37
+ return false unless receiver&.lvar_type?
38
+
39
+ binding = receiver.each_ancestor(:any_block).find do |block|
40
+ block.argument_list.any? { |argument| argument.name == receiver.children.first }
41
+ end
42
+
43
+ binding&.method?(:reversible) && (binding.receiver.nil? || binding.receiver.self_type?)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Betterment
6
+ class SpecDescribeMethodName < Base
7
+ MSG_CLASS_LABEL = 'Label the outer `describe` with the class constant, not a string.'
8
+ MSG_METHOD_LABEL = 'Put examples inside a `describe` labeled with a method name, such as `"#instance_method"` or `".class_method"`.'
9
+ MSG_DIRECTLY_INSIDE = 'Put the method `describe` directly inside the class `describe`, and share setup with `let` or `before`.'
10
+ MSG_ONE_CLASS = 'Describe one class in each spec file, and put the method `describe` directly inside the outer `describe`.'
11
+
12
+ METHOD_LABEL = /\A[#.]\S/
13
+
14
+ DESCRIBE_METHODS = %i(describe fdescribe xdescribe).freeze
15
+ EXAMPLE_GROUP_METHODS = %i(describe fdescribe xdescribe context fcontext xcontext).freeze
16
+ SHARED_GROUP_METHODS = %i(shared_examples shared_examples_for shared_context).freeze
17
+ EXAMPLE_METHODS = %i(
18
+ it its specify example scenario
19
+ fit fspecify fexample fscenario
20
+ xit xspecify xexample xscenario
21
+ ).freeze
22
+
23
+ RESTRICT_ON_SEND = (DESCRIBE_METHODS + EXAMPLE_METHODS).freeze
24
+
25
+ def on_send(node)
26
+ if DESCRIBE_METHODS.include?(node.method_name)
27
+ check_describe(node) if rspec_receiver?(node)
28
+ elsif node.receiver.nil?
29
+ check_example(node)
30
+ end
31
+ end
32
+ alias on_csend on_send
33
+
34
+ private
35
+
36
+ def check_describe(node)
37
+ groups = enclosing_groups(node)
38
+ return if groups.any? { |group| shared_group?(group) }
39
+
40
+ check_class_label(node) if groups.empty?
41
+ check_method_describe_placement(node, groups)
42
+ end
43
+
44
+ def check_example(node)
45
+ groups = enclosing_groups(node)
46
+ return if groups.any? { |group| shared_group?(group) || method_describe?(group) }
47
+ return unless groups.any? { |group| class_describe?(group) }
48
+
49
+ add_offense(node, message: MSG_METHOD_LABEL)
50
+ end
51
+
52
+ def check_class_label(send_node)
53
+ label = send_node.first_argument
54
+ return if label.nil? || label.const_type?
55
+
56
+ add_offense(label, message: MSG_CLASS_LABEL)
57
+ end
58
+
59
+ def check_method_describe_placement(send_node, groups)
60
+ return unless method_describe?(send_node)
61
+
62
+ parent = groups.first
63
+ return if parent.nil? || (groups.one? && class_describe?(parent))
64
+ return unless groups.any? { |group| class_describe?(group) }
65
+
66
+ add_offense(send_node, message: class_describe?(parent) ? MSG_ONE_CLASS : MSG_DIRECTLY_INSIDE)
67
+ end
68
+
69
+ def enclosing_groups(node)
70
+ node.each_ancestor(:block, :numblock).filter_map do |ancestor|
71
+ send_node = ancestor.send_node
72
+ next if send_node.equal?(node)
73
+
74
+ send_node if example_group?(send_node) || shared_group?(send_node)
75
+ end
76
+ end
77
+
78
+ def example_group?(send_node)
79
+ EXAMPLE_GROUP_METHODS.include?(send_node.method_name) && rspec_receiver?(send_node)
80
+ end
81
+
82
+ def shared_group?(send_node)
83
+ SHARED_GROUP_METHODS.include?(send_node.method_name) && rspec_receiver?(send_node)
84
+ end
85
+
86
+ def describe?(send_node)
87
+ DESCRIBE_METHODS.include?(send_node.method_name) && rspec_receiver?(send_node)
88
+ end
89
+
90
+ def class_describe?(send_node)
91
+ return false unless describe?(send_node)
92
+
93
+ send_node.first_argument&.const_type? || false
94
+ end
95
+
96
+ def method_describe?(send_node)
97
+ return false unless describe?(send_node)
98
+
99
+ label = send_node.first_argument
100
+ return false unless label&.str_type?
101
+
102
+ METHOD_LABEL.match?(label.value)
103
+ end
104
+
105
+ def rspec_receiver?(send_node)
106
+ receiver = send_node.receiver
107
+ return true if receiver.nil?
108
+
109
+ receiver.const_type? && receiver.short_name == :RSpec && (receiver.namespace.nil? || receiver.namespace.cbase_type?)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -12,6 +12,7 @@ require 'rubocop/cop/betterment/hardcoded_id'
12
12
  require 'rubocop/cop/betterment/implicit_redirect_type'
13
13
  require 'rubocop/cop/betterment/internals_protection'
14
14
  require 'rubocop/cop/betterment/memoization_with_arguments'
15
+ require 'rubocop/cop/betterment/no_down_migrations'
15
16
  require 'rubocop/cop/betterment/non_namespaced_class'
16
17
  require 'rubocop/cop/betterment/non_standard_actions'
17
18
  require 'rubocop/cop/betterment/non_standard_controller'
@@ -21,6 +22,7 @@ require 'rubocop/cop/betterment/render_status'
21
22
  require 'rubocop/cop/betterment/server_error_assertion'
22
23
  require 'rubocop/cop/betterment/simple_delegator'
23
24
  require 'rubocop/cop/betterment/site_prism_loaded'
25
+ require 'rubocop/cop/betterment/spec_describe_method_name'
24
26
  require 'rubocop/cop/betterment/spec_helper_required_outside_spec_dir'
25
27
  require 'rubocop/cop/betterment/timeout'
26
28
  require 'rubocop/cop/betterment/unsafe_job'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: betterlint
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.28.0
4
+ version: 1.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Development
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-01-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: lint_roller
@@ -106,14 +105,14 @@ dependencies:
106
105
  requirements:
107
106
  - - "~>"
108
107
  - !ruby/object:Gem::Version
109
- version: '2.34'
108
+ version: '2.36'
110
109
  type: :runtime
111
110
  prerelease: false
112
111
  version_requirements: !ruby/object:Gem::Requirement
113
112
  requirements:
114
113
  - - "~>"
115
114
  - !ruby/object:Gem::Version
116
- version: '2.34'
115
+ version: '2.36'
117
116
  - !ruby/object:Gem::Dependency
118
117
  name: rubocop-rake
119
118
  requirement: !ruby/object:Gem::Requirement
@@ -180,6 +179,7 @@ files:
180
179
  - lib/rubocop/cop/betterment/implicit_redirect_type.rb
181
180
  - lib/rubocop/cop/betterment/internals_protection.rb
182
181
  - lib/rubocop/cop/betterment/memoization_with_arguments.rb
182
+ - lib/rubocop/cop/betterment/no_down_migrations.rb
183
183
  - lib/rubocop/cop/betterment/non_namespaced_class.rb
184
184
  - lib/rubocop/cop/betterment/non_standard_actions.rb
185
185
  - lib/rubocop/cop/betterment/non_standard_controller.rb
@@ -189,6 +189,7 @@ files:
189
189
  - lib/rubocop/cop/betterment/server_error_assertion.rb
190
190
  - lib/rubocop/cop/betterment/simple_delegator.rb
191
191
  - lib/rubocop/cop/betterment/site_prism_loaded.rb
192
+ - lib/rubocop/cop/betterment/spec_describe_method_name.rb
192
193
  - lib/rubocop/cop/betterment/spec_helper_required_outside_spec_dir.rb
193
194
  - lib/rubocop/cop/betterment/timeout.rb
194
195
  - lib/rubocop/cop/betterment/unsafe_job.rb
@@ -203,13 +204,12 @@ licenses:
203
204
  - MIT
204
205
  metadata:
205
206
  homepage_uri: https://github.com/Betterment/betterlint
206
- source_code_uri: https://github.com/Betterment/betterlint/tree/v1.28.0
207
- changelog_uri: https://github.com/Betterment/betterlint/blob/v1.28.0/CHANGELOG.md
207
+ source_code_uri: https://github.com/Betterment/betterlint/tree/v1.30.0
208
+ changelog_uri: https://github.com/Betterment/betterlint/blob/v1.30.0/CHANGELOG.md
208
209
  bug_tracker_uri: https://github.com/Betterment/betterlint/issues
209
- documentation_uri: https://www.rubydoc.info/gems/betterlint/1.28.0
210
+ documentation_uri: https://www.rubydoc.info/gems/betterlint/1.30.0
210
211
  rubygems_mfa_required: 'true'
211
212
  default_lint_roller_plugin: Betterlint::Plugin
212
- post_install_message:
213
213
  rdoc_options: []
214
214
  require_paths:
215
215
  - lib
@@ -224,8 +224,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
224
224
  - !ruby/object:Gem::Version
225
225
  version: '0'
226
226
  requirements: []
227
- rubygems_version: 3.4.10
228
- signing_key:
227
+ rubygems_version: 4.0.21
229
228
  specification_version: 4
230
229
  summary: Betterment rubocop configuration
231
230
  test_files: []