rubocop-gusto 10.3.0 → 10.6.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: 5b80d8e2199d3f26a7c573b61905ea30b866eb0a9f5ca2bf6eb0f6ef9891ecd4
4
- data.tar.gz: 88a6fac9e262c3c40caa6ce939b194badf71a84cd05e63165b11a3f36ed4b54d
3
+ metadata.gz: 8312e221e4e60856991b5771855411eda476845804f95a1b890944880a72c107
4
+ data.tar.gz: 48247a74fe3523f5d813fc1b36fe9f56782c166347bd1f45997876047d5a9bf4
5
5
  SHA512:
6
- metadata.gz: 830e8793b87b42c2a727dcbbda20a93de35f5a4887009455e8ff65bc8002dc9499d5fdfadd6971e2e5ce73660e47ddbd4cdf53eeae67c5c26d4840dadda0f092
7
- data.tar.gz: 1d6b49aa37716ac6095ae0e261f8673e5c2389bff7432ba5775d39b64a86f8f17725b2f35c4c1ffbea1e32cac36751c1ee7924d6251af30b5ea7bed60466bc1c
6
+ metadata.gz: b2b908c4b1f5f921371e6368956f57bce33995ab5127f346a59d4958909fd03469a8efde3ab035b52457c67bae2f8fc14efcb3c83250810ade31fe872b60948f
7
+ data.tar.gz: 5e0ce03d855c35c66a70fd0bbd3f940d3661e6d665fcac97d5c73c4fd57b10a3b5842b170ac9838d46fe6cec9ff042aedcc2e0950dfcfd231e035d3077fd9c27
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## Pending
2
2
 
3
+
4
+ ## 10.6.0
5
+
6
+ - Add `Rack/LowercaseHeaderKeys` cop to detect and autocorrect uppercase HTTP response header keys
7
+ - Enable Style/StringLiterals with double quotes enforced
8
+
9
+ ## 10.5.0
10
+
11
+ - Delete Object#in? cop
12
+
13
+ ## 10.4.0
14
+
15
+ - Add Gusto/DiscouragedGem cop with `timecop` as the first discouraged gem
16
+ - Update Gusto/PolymorphicTypeValidation settings to be scoped to `**/models/*.rb`
17
+
3
18
  ## 10.3.0
4
19
 
5
20
  - Add Gusto/RspecDateTimeMock cop
data/config/default.yml CHANGED
@@ -49,6 +49,11 @@ Gusto/DatadogConstant:
49
49
  - '**/spec/**/*'
50
50
  Description: 'Do not call Datadog directly, use an appropriate wrapper library.'
51
51
 
52
+ Gusto/DiscouragedGem:
53
+ Description: 'Flags installation of discouraged gems in Gemfiles and gemspecs.'
54
+ Enabled: false
55
+ Gems: {}
56
+
52
57
  Gusto/ExecuteMigration:
53
58
  Description: "Don't use `execute` in migrations. Use a backfill rake task instead."
54
59
  Include:
@@ -72,10 +77,8 @@ Gusto/NoRescueErrorMessageChecking:
72
77
 
73
78
  Gusto/NoSend:
74
79
  Description: 'Do not call a private method via `__send__`.'
75
-
76
- Gusto/ObjectIn:
77
- Description: 'Use `Range#cover?` instead of `Object#in?`.'
78
- Safe: false
80
+ Exclude:
81
+ - '**/spec/**/*'
79
82
 
80
83
  Gusto/PaperclipOrAttachable:
81
84
  Description: 'No more new paperclip or Attachable are allowed. Use ActiveStorage instead.'
@@ -89,6 +92,8 @@ Gusto/PerformClassMethod:
89
92
 
90
93
  Gusto/PolymorphicTypeValidation:
91
94
  Description: 'Ensures that polymorphic relations include a type validation, which is necessary for generating Sorbet types.'
95
+ Include:
96
+ - '**/models/**/*.rb'
92
97
 
93
98
  Gusto/PreferProcessLastStatus:
94
99
  Description: 'Prefer using `Process.last_status` instead of the global variables: `$?` and `$CHILD_STATUS`.'
@@ -424,6 +429,21 @@ RSpec/StubbedMock:
424
429
  RSpec/SubjectStub:
425
430
  Enabled: false
426
431
 
432
+ Rack/LowercaseHeaderKeys:
433
+ Description: 'HTTP response header keys should be lowercase for consistency and compatibility with HTTP/2.'
434
+ Enabled: true
435
+ Include:
436
+ - 'lib/middleware/**/*.rb'
437
+ - 'app/middleware/**/*.rb'
438
+ - 'app/controllers/**/*.rb'
439
+ - 'packs/**/middleware/**/*.rb'
440
+ - 'packs/**/controllers/**/*.rb'
441
+ - 'components/**/middleware/**/*.rb'
442
+ - 'components/**/controllers/**/*.rb'
443
+ - 'config/initializers/**/*.rb'
444
+ Exclude:
445
+ - '**/spec/**/*'
446
+
427
447
  Rake/ClassDefinitionInTask:
428
448
  Enabled: false
429
449
 
@@ -481,6 +501,10 @@ Style/Alias:
481
501
  Enabled: true
482
502
  EnforcedStyle: prefer_alias_method
483
503
 
504
+ Style/ArgumentsForwarding:
505
+ # This is incompatible with Sorbet
506
+ Enabled: false
507
+
484
508
  Style/AsciiComments:
485
509
  Enabled: true
486
510
 
@@ -569,6 +593,10 @@ Style/IfUnlessModifier:
569
593
  Style/ImplicitRuntimeError:
570
594
  Enabled: false
571
595
 
596
+ Style/ItBlockParameter:
597
+ Enabled: true
598
+ EnforcedStyle: only_numbered_parameters
599
+
572
600
  Style/Lambda:
573
601
  EnforcedStyle: literal
574
602
 
data/config/rails.yml CHANGED
@@ -17,6 +17,14 @@ AllCops:
17
17
  - 'db/**/*schema.rb'
18
18
  - 'db/seeds{.rb,/**/*}'
19
19
 
20
+ Gusto/DiscouragedGem:
21
+ Enabled: true
22
+ Include:
23
+ - '**/*.gemspec'
24
+ - '**/Gemfile'
25
+ Gems:
26
+ timecop: "Use Rails' time helpers (e.g., freeze_time, travel_to) instead of Timecop."
27
+
20
28
  Performance/DoubleStartEndWith:
21
29
  IncludeActiveSupportAliases: true
22
30
 
@@ -33,6 +33,7 @@ module RuboCop
33
33
  add_offense(node, message: "Use #{constant_node.source}.load_file(#{file_path_node.source}) to improve load time with bootsnap")
34
34
  end
35
35
  end
36
+ alias_method :on_itblock, :on_block
36
37
  alias_method :on_numblock, :on_block
37
38
 
38
39
  def on_send(node)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Gusto
6
+ # Flags installation of discouraged gems (e.g., timecop) in Gemfiles and gemspecs.
7
+ #
8
+ # Configuration:
9
+ # Gems:
10
+ # timecop: "Use Rails' time helpers (e.g., freeze_time, travel_to) instead of Timecop."
11
+ #
12
+ # This cop is intended to be enabled in Rails projects via config/rails.yml.
13
+ class DiscouragedGem < Base
14
+ MSG = "Avoid using the '%{gem}' gem. %{advice}"
15
+
16
+ RESTRICT_ON_SEND = %i(gem add_dependency add_development_dependency).freeze
17
+
18
+ def on_send(node)
19
+ check_gem_usage(node)
20
+ end
21
+
22
+ private
23
+
24
+ def check_gem_usage(node)
25
+ return unless node.first_argument&.type?(:str, :sym)
26
+ return unless discouraged_gems.include?(node.first_argument.value.to_s)
27
+
28
+ add_offense(node, message: message_for(node.first_argument.value.to_s))
29
+ # No autocorrect: removing dependencies is a project decision.
30
+ end
31
+
32
+ def discouraged_gems
33
+ @discouraged_gems ||= gems_config.keys.map(&:to_s)
34
+ end
35
+
36
+ def message_for(gem)
37
+ format(MSG, gem: gem, advice: advice_for(gem))
38
+ end
39
+
40
+ def advice_for(gem)
41
+ gems_config[gem]
42
+ end
43
+
44
+ def gems_config
45
+ cop_config["Gems"] || {}
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rack
6
+ # Detects HTTP response headers with uppercase characters.
7
+ # HTTP response header keys should be lowercase for consistency
8
+ # and compatibility with HTTP/2 and modern web standards.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # headers['Content-Type'] = 'application/json'
13
+ # response.headers['Location'] = '/redirect'
14
+ #
15
+ # # good
16
+ # headers['content-type'] = 'application/json'
17
+ # response.headers['location'] = '/redirect'
18
+ #
19
+ class LowercaseHeaderKeys < Base
20
+ extend AutoCorrector
21
+
22
+ MSG = "HTTP response header keys should be lowercase. Use `%{downcased}` instead of `%{original}`."
23
+ RESTRICT_ON_SEND = %i([]=).freeze
24
+
25
+ # Known HTTP headers (case-insensitive check)
26
+ KNOWN_HEADERS = Set.new(
27
+ %w(
28
+ Accept Accept-Charset Accept-Encoding Accept-Language Accept-Ranges
29
+ Access-Control-Allow-Credentials Access-Control-Allow-Headers
30
+ Access-Control-Allow-Methods Access-Control-Allow-Origin
31
+ Access-Control-Allow-Private-Network Access-Control-Expose-Headers
32
+ Access-Control-Max-Age Access-Control-Request-Headers
33
+ Access-Control-Request-Method Age Allow Authorization
34
+ Cache-Control Connection Content-Disposition Content-Encoding
35
+ Content-Language Content-Length Content-Location Content-Range
36
+ Content-Security-Policy Content-Security-Policy-Report-Only
37
+ Content-Type Cookie Date ETag Expect
38
+ Expires Forwarded From Host If-Match If-Modified-Since
39
+ If-None-Match If-Range If-Unmodified-Since Last-Modified
40
+ Link Location Max-Forwards Origin Pragma Proxy-Authenticate
41
+ Proxy-Authorization Range Referer Referrer-Policy Retry-After
42
+ Server Set-Cookie SOAPAction Strict-Transport-Security TE Trailer
43
+ Transfer-Encoding Upgrade User-Agent Vary Via Warning
44
+ WWW-Authenticate X-Content-Type-Options X-Frame-Options
45
+ X-XSS-Protection X-Forwarded-For X-Forwarded-Host X-Forwarded-Proto
46
+ X-Real-IP X-Request-ID X-Request-Start X-Requested-With
47
+ ).map(&:downcase)
48
+ ).freeze
49
+
50
+ def on_send(node)
51
+ return unless headers_assignment?(node)
52
+
53
+ key_node = node.first_argument
54
+ return unless key_node.str_type?
55
+
56
+ key_value = key_node.value
57
+ return unless uppercase_known_header?(key_value)
58
+
59
+ add_offense_for_header(key_node, key_value)
60
+ end
61
+ alias_method :on_csend, :on_send
62
+
63
+ private
64
+
65
+ def uppercase_known_header?(key)
66
+ return false if key.empty?
67
+ return false if key == key.downcase
68
+
69
+ KNOWN_HEADERS.include?(key.downcase)
70
+ end
71
+
72
+ # Matches:
73
+ # headers['...'] = value (bare method call in controller)
74
+ # response.headers['...'] = value
75
+ # Does NOT match:
76
+ # conn.headers, request.headers, client.headers, etc.
77
+ def headers_assignment?(node)
78
+ receiver = node.receiver
79
+ # RESTRICT_ON_SEND ensures we only see []=, which always has a receiver
80
+ return false unless receiver.send_type?
81
+
82
+ headers_method_receiver?(receiver)
83
+ end
84
+
85
+ def headers_method_receiver?(receiver)
86
+ return false unless receiver.method?(:headers)
87
+
88
+ inner = receiver.receiver
89
+ if inner.nil?
90
+ # Bare `headers` method call (Rails controller helper)
91
+ true
92
+ elsif inner.send_type? && inner.receiver.nil? && inner.method?(:response)
93
+ # `response.headers`
94
+ true
95
+ elsif inner.lvar_type? && inner.children.first == :response
96
+ # `response.headers` where response is a local var
97
+ true
98
+ else
99
+ false
100
+ end
101
+ end
102
+
103
+ def add_offense_for_header(node, key_value)
104
+ downcased = key_value.downcase
105
+ message = format(MSG, downcased: downcased, original: key_value)
106
+
107
+ add_offense(node, message: message) do |corrector|
108
+ corrector.replace(node, "'#{downcased}'")
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -18,7 +18,7 @@ module RuboCop
18
18
 
19
19
  # @param [String] file_path the path to the .rubocop.yml file
20
20
  def self.load_file(file_path = ".rubocop.yml")
21
- new(File.readlines(file_path))
21
+ new(File.readlines(file_path, encoding: "UTF-8"))
22
22
  rescue Errno::ENOENT
23
23
  new([])
24
24
  end
@@ -75,9 +75,10 @@ module RuboCop
75
75
 
76
76
  def sort!
77
77
  # Sort the preamble chunks by our preferred order, falling back to key name
78
+ # Comment-only chunks (nil key) sort to the end with "ZZZZZ"
78
79
  preamble.sort_by! do |chunk|
79
80
  key = chunk_name(chunk)
80
- PREAMBLE_KEYS.index(key)&.to_s || key
81
+ PREAMBLE_KEYS.index(key)&.to_s || key || "ZZZZZ"
81
82
  end
82
83
 
83
84
  # Sort the cops by their key name, putting comments at the top
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Gusto
5
- VERSION = "10.3.0"
5
+ VERSION = "10.6.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-gusto
3
3
  version: !ruby/object:Gem::Version
4
- version: 10.3.0
4
+ version: 10.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineering
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-02-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: lint_roller
@@ -125,13 +125,13 @@ files:
125
125
  - lib/rubocop-gusto.rb
126
126
  - lib/rubocop/cop/gusto/bootsnap_load_file.rb
127
127
  - lib/rubocop/cop/gusto/datadog_constant.rb
128
+ - lib/rubocop/cop/gusto/discouraged_gem.rb
128
129
  - lib/rubocop/cop/gusto/execute_migration.rb
129
130
  - lib/rubocop/cop/gusto/factory_classes_or_modules.rb
130
131
  - lib/rubocop/cop/gusto/min_by_max_by.rb
131
132
  - lib/rubocop/cop/gusto/no_metaprogramming.rb
132
133
  - lib/rubocop/cop/gusto/no_rescue_error_message_checking.rb
133
134
  - lib/rubocop/cop/gusto/no_send.rb
134
- - lib/rubocop/cop/gusto/object_in.rb
135
135
  - lib/rubocop/cop/gusto/paperclip_or_attachable.rb
136
136
  - lib/rubocop/cop/gusto/perform_class_method.rb
137
137
  - lib/rubocop/cop/gusto/polymorphic_type_validation.rb
@@ -147,6 +147,7 @@ files:
147
147
  - lib/rubocop/cop/gusto/vcr_recordings.rb
148
148
  - lib/rubocop/cop/internal_affairs/assignment_first.rb
149
149
  - lib/rubocop/cop/internal_affairs/require_restrict_on_send.rb
150
+ - lib/rubocop/cop/rack/lowercase_header_keys.rb
150
151
  - lib/rubocop/gusto.rb
151
152
  - lib/rubocop/gusto/cli.rb
152
153
  - lib/rubocop/gusto/config_yml.rb
@@ -173,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
173
174
  - !ruby/object:Gem::Version
174
175
  version: '0'
175
176
  requirements: []
176
- rubygems_version: 3.7.1
177
+ rubygems_version: 3.6.2
177
178
  specification_version: 4
178
179
  summary: A gem for sharing gusto rubocop rules
179
180
  test_files: []
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Cop
5
- module Gusto
6
- # Identifies uses of `Object#in?`, which iterates over each
7
- # item in a `Range` to see if a specified item is there. In contrast,
8
- # `Range#cover?` simply compares the target item with the beginning and
9
- # end points of the `Range`. In a great majority of cases, this is what
10
- # is wanted.
11
- #
12
- # @safety
13
- # This cop is unsafe. Here is an example of a case where `Range#cover?`
14
- # may not provide the desired result:
15
- #
16
- # ('a'..'z').cover?('yellow') # => true
17
- #
18
- class ObjectIn < Base
19
- MSG = "Use `Range#cover?` instead of `Object#in?`."
20
- RESTRICT_ON_SEND = [:in?].freeze
21
-
22
- # @!method object_in(node)
23
- def_node_matcher :object_in, <<-PATTERN
24
- (call _ :in? {range (begin range)})
25
- PATTERN
26
-
27
- def on_send(node)
28
- return unless object_in(node)
29
-
30
- add_offense(node)
31
- end
32
- alias_method :on_csend, :on_send
33
- end
34
- end
35
- end
36
- end