Dutchie-Style 2.0.11 → 2.1.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: 98940de414a26efc09108619c8421782a09bf51c25032a357e854856aaaa6505
4
- data.tar.gz: f6b9595bbf810983571ab1d531c0fb5d441ddb126e495df7d993c6bbd30ff398
3
+ metadata.gz: 66d1a3b386238c749fe8a2a57e69ac726ab64167e54fbf18fb3242e8e9988d21
4
+ data.tar.gz: 5590171816b08b40de46bf110d7d5e59eae4a8289a2dbd71cec88be6e94a5da7
5
5
  SHA512:
6
- metadata.gz: 82715d41789f44683bf8a58f6ec92f18d17ef50c00e9d13e994f5a3dc9f5b4fdfc606d2adcc35870568059a302c7ba1053356446c68ca69678f675d360c14ee6
7
- data.tar.gz: f05a26ac206b58de4b1e35777776fcb67e6a7578608c63e42e8cadd62fb386df722792146eb7ee69c3ffc2e44d083395f8f3c05cf238a3a4a3fb8d18ac661c91
6
+ metadata.gz: 95e9b22ce1d0f8016bb718f94145149f9f7f0daef5f8e431946559e3443e53e60935249884240dc8605293a64ee62156640bbc4923be535150213187bd6b516c
7
+ data.tar.gz: 906c6e5cb0961f91a2fae2b04628e42f73773f0e77e75e01cfc19a35fb4a52e6e7c403554e7abb4f1c2ae380b5c3e05d6e1af24cc1f2961e9e923d76d4114c9c
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  spec.summary = "Rubocop Settings for all dutchie Ruby Apps"
14
14
  spec.description = "Rubocop Settings for all dutchie Ruby Apps"
15
15
  spec.homepage = "https://github.com/GetDutchie/Dutchie-Style"
16
- spec.required_ruby_version = Gem::Requirement.new(">= 3.1.4")
16
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.6")
17
17
 
18
18
  spec.metadata["homepage_uri"] = spec.homepage
19
19
  spec.metadata["source_code_uri"] = "https://github.com/GetDutchie/Dutchie-Style"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- Dutchie-Style (2.0.11)
4
+ Dutchie-Style (2.1.0)
5
5
  rubocop (~> 1.72)
6
6
  rubocop-capybara (~> 2.18)
7
7
  rubocop-factory_bot (~> 2.24)
data/config/default.yml CHANGED
@@ -280,6 +280,10 @@ Rails/PluckId:
280
280
  Rails/PluckInWhere:
281
281
  Enabled: false
282
282
 
283
+ # Disable I18n locale texts cop
284
+ Rails/I18nLocaleTexts:
285
+ Enabled: false
286
+
283
287
  # RSpec
284
288
 
285
289
  # Start context description without 'when', 'with', or 'without'.
@@ -301,3 +305,22 @@ RSpec/NestedGroups:
301
305
  # Allows us to mark specs as "skip"
302
306
  RSpec/Pending:
303
307
  Enabled: false
308
+
309
+ # Dutchie Custom Cops
310
+
311
+ # Ensures all LaunchDarkly feature flag calls have default values
312
+ # Supports both Armageddon (DutchieFeatureFlags) and MenuConnector (ld_client.variation) patterns
313
+ Dutchie/LaunchDarklyDefaults:
314
+ Enabled: true
315
+ Severity: warning
316
+ AutoCorrect: true
317
+
318
+ # Flags questionable usage of safety_assured in migrations
319
+ # The Include directive overrides the global Exclude: db/**/* for this cop only
320
+ Dutchie/MigrationSafetyAssured:
321
+ Enabled: true
322
+ Severity: warning
323
+ Include:
324
+ - 'db/migrate/**/*.rb'
325
+ - 'lib/migration_helpers.rb'
326
+ - 'lib/**/migration_helpers.rb'
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Dutchie
4
4
  module Style
5
- VERSION = "2.0.11"
5
+ VERSION = "2.1.0"
6
6
  public_constant :VERSION
7
7
  end
8
8
  end
data/lib/dutchie-style.rb CHANGED
@@ -1,3 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "Dutchie/Style"
4
+ require_relative "rubocop/cop/dutchie/launchdarkly_defaults"
5
+ require_relative "rubocop/cop/dutchie/migration_safety_assured"
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Dutchie
8
+ # Ensures all LaunchDarkly feature flag calls have default values
9
+ # to prevent failures during LaunchDarkly service outages
10
+ #
11
+ # This cop supports two LaunchDarkly patterns:
12
+ # 1. Armageddon pattern: DutchieFeatureFlags.flag(), .context_flag(), etc.
13
+ # 2. MenuConnector pattern: ld_client.variation(), MenuConnector::App[:launchdarkly].variation()
14
+ #
15
+ # @example Armageddon pattern
16
+ # # bad
17
+ # DutchieFeatureFlags.flag("some.flag")
18
+ # DutchieFeatureFlags.context_flag("some.flag", dispensary_id: id)
19
+ # DutchieFeatureFlags.on?("some.flag")
20
+ #
21
+ # # good
22
+ # DutchieFeatureFlags.flag("some.flag", false)
23
+ # DutchieFeatureFlags.context_flag("some.flag", default: false, dispensary_id: id)
24
+ # DutchieFeatureFlags.on?("some.flag", default: false)
25
+ #
26
+ # @example MenuConnector pattern
27
+ # # bad
28
+ # ld_client.variation("flag", context)
29
+ # MenuConnector::App[:launchdarkly].variation("flag", context)
30
+ #
31
+ # # good
32
+ # ld_client.variation("flag", context, false)
33
+ # MenuConnector::App[:launchdarkly].variation("flag", context, false)
34
+ #
35
+ class LaunchDarklyDefaults < ::RuboCop::Cop::Base
36
+ extend AutoCorrector
37
+
38
+ MSG_FLAG = 'DutchieFeatureFlags.flag must have a default value as 2nd parameter'
39
+ MSG_CONTEXT_FLAG = 'DutchieFeatureFlags.context_flag must have a default: parameter'
40
+ MSG_DISPENSARY_FLAG = 'DutchieFeatureFlags.dispensary_flag must have a default: parameter'
41
+ MSG_ENTERPRISE_FLAG = 'DutchieFeatureFlags.enterprise_flag must have a default: parameter'
42
+ MSG_ON = 'DutchieFeatureFlags.on? should have a default: parameter'
43
+ MSG_OFF = 'DutchieFeatureFlags.off? should have a default: parameter'
44
+ MSG_VARIATION = 'LaunchDarkly variation must have a default value as 3rd parameter'
45
+
46
+ # DutchieFeatureFlags.flag("key", default, ...)
47
+ def_node_matcher :dutchie_flag_call?, <<~PATTERN
48
+ (send
49
+ (const nil? :DutchieFeatureFlags) :flag
50
+ $_
51
+ $...
52
+ )
53
+ PATTERN
54
+
55
+ # DutchieFeatureFlags.context_flag("key", ...)
56
+ def_node_matcher :context_flag_call?, <<~PATTERN
57
+ (send
58
+ (const nil? :DutchieFeatureFlags) :context_flag
59
+ $_
60
+ $...
61
+ )
62
+ PATTERN
63
+
64
+ # DutchieFeatureFlags.dispensary_flag("key", ...)
65
+ def_node_matcher :dispensary_flag_call?, <<~PATTERN
66
+ (send
67
+ (const nil? :DutchieFeatureFlags) :dispensary_flag
68
+ $_
69
+ $...
70
+ )
71
+ PATTERN
72
+
73
+ # DutchieFeatureFlags.enterprise_flag("key", ...)
74
+ def_node_matcher :enterprise_flag_call?, <<~PATTERN
75
+ (send
76
+ (const nil? :DutchieFeatureFlags) :enterprise_flag
77
+ $_
78
+ $...
79
+ )
80
+ PATTERN
81
+
82
+ # DutchieFeatureFlags.on?("key", ...)
83
+ def_node_matcher :on_call?, <<~PATTERN
84
+ (send
85
+ (const nil? :DutchieFeatureFlags) :on?
86
+ $_
87
+ $...
88
+ )
89
+ PATTERN
90
+
91
+ # DutchieFeatureFlags.off?("key", ...)
92
+ def_node_matcher :off_call?, <<~PATTERN
93
+ (send
94
+ (const nil? :DutchieFeatureFlags) :off?
95
+ $_
96
+ $...
97
+ )
98
+ PATTERN
99
+
100
+ # Direct variation calls on ld_client or MenuConnector::App[:launchdarkly]
101
+ def_node_matcher :variation_call?, <<~PATTERN
102
+ (send
103
+ {
104
+ (send nil? :ld_client)
105
+ (send
106
+ (const (const nil? :MenuConnector) :App)
107
+ :[]
108
+ (sym :launchdarkly)
109
+ )
110
+ (send _ :ld_client)
111
+ }
112
+ :variation
113
+ $_
114
+ $...
115
+ )
116
+ PATTERN
117
+
118
+ # MockLaunchDarkly variation calls (for test environments)
119
+ def_node_matcher :mock_variation_call?, <<~PATTERN
120
+ (send
121
+ (const (const nil? :MenuConnector) :MockLaunchDarkly)
122
+ :variation
123
+ $_
124
+ $...
125
+ )
126
+ PATTERN
127
+
128
+ def on_send(node)
129
+ # Check Armageddon patterns
130
+ check_flag_method(node)
131
+ check_context_flag_method(node)
132
+ check_dispensary_flag_method(node)
133
+ check_enterprise_flag_method(node)
134
+ check_on_method(node)
135
+ check_off_method(node)
136
+
137
+ # Check MenuConnector patterns
138
+ check_variation_method(node)
139
+ check_mock_variation_method(node)
140
+ end
141
+
142
+ private
143
+
144
+ # Armageddon pattern checks
145
+
146
+ def check_flag_method(node)
147
+ flag_key, *args = dutchie_flag_call?(node)
148
+ return unless flag_key
149
+
150
+ # Need at least one more argument after the key (the default value)
151
+ return if args.any?
152
+
153
+ add_offense(node, message: MSG_FLAG) do |corrector|
154
+ corrector.insert_after(flag_key, ', false')
155
+ end
156
+ end
157
+
158
+ def check_context_flag_method(node)
159
+ flag_key, *args = context_flag_call?(node)
160
+ return unless flag_key
161
+
162
+ return if has_default_kwarg?(args)
163
+
164
+ add_offense(node, message: MSG_CONTEXT_FLAG) do |corrector|
165
+ insert_default_kwarg(corrector, flag_key, args)
166
+ end
167
+ end
168
+
169
+ def check_dispensary_flag_method(node)
170
+ flag_key, *args = dispensary_flag_call?(node)
171
+ return unless flag_key
172
+
173
+ return if has_default_kwarg?(args)
174
+
175
+ add_offense(node, message: MSG_DISPENSARY_FLAG) do |corrector|
176
+ insert_default_kwarg(corrector, flag_key, args)
177
+ end
178
+ end
179
+
180
+ def check_enterprise_flag_method(node)
181
+ flag_key, *args = enterprise_flag_call?(node)
182
+ return unless flag_key
183
+
184
+ return if has_default_kwarg?(args)
185
+
186
+ add_offense(node, message: MSG_ENTERPRISE_FLAG) do |corrector|
187
+ insert_default_kwarg(corrector, flag_key, args)
188
+ end
189
+ end
190
+
191
+ def check_on_method(node)
192
+ flag_key, *args = on_call?(node)
193
+ return unless flag_key
194
+
195
+ # on? accepts default: as keyword or as second positional argument
196
+ # Return if there's a positional default (non-hash arg) or default: kwarg
197
+ return if has_positional_default?(args) || has_default_kwarg?(args)
198
+
199
+ add_offense(node, message: MSG_ON) do |corrector|
200
+ insert_default_kwarg(corrector, flag_key, args)
201
+ end
202
+ end
203
+
204
+ def check_off_method(node)
205
+ flag_key, *args = off_call?(node)
206
+ return unless flag_key
207
+
208
+ # off? accepts default: as keyword or as second positional argument
209
+ # Return if there's a positional default (non-hash arg) or default: kwarg
210
+ return if has_positional_default?(args) || has_default_kwarg?(args)
211
+
212
+ add_offense(node, message: MSG_OFF) do |corrector|
213
+ insert_default_kwarg(corrector, flag_key, args)
214
+ end
215
+ end
216
+
217
+ # MenuConnector pattern checks
218
+
219
+ def check_variation_method(node)
220
+ flag_key, *args = variation_call?(node)
221
+ return unless flag_key
222
+
223
+ # variation(flag, context, default) - need at least 2 args after flag
224
+ return if args.size >= 2
225
+
226
+ add_offense(node, message: MSG_VARIATION) do |corrector|
227
+ if args.empty?
228
+ # No context provided, add both context and default
229
+ corrector.insert_after(flag_key, ', nil, false')
230
+ elsif args.size == 1
231
+ # Context provided but no default
232
+ corrector.insert_after(args.last, ', false')
233
+ end
234
+ end
235
+ end
236
+
237
+ def check_mock_variation_method(node)
238
+ # Don't check mock variation calls in test code
239
+ return if in_test_file?(node)
240
+
241
+ flag_key, *args = mock_variation_call?(node)
242
+ return unless flag_key
243
+
244
+ # Same logic as regular variation
245
+ return if args.size >= 2
246
+
247
+ add_offense(node, message: MSG_VARIATION) do |corrector|
248
+ if args.empty?
249
+ corrector.insert_after(flag_key, ', nil, false')
250
+ elsif args.size == 1
251
+ corrector.insert_after(args.last, ', false')
252
+ end
253
+ end
254
+ end
255
+
256
+ # Helper methods
257
+
258
+ def has_default_kwarg?(args)
259
+ args.any? do |arg|
260
+ next false unless arg.hash_type?
261
+
262
+ arg.pairs.any? do |pair|
263
+ pair.key.sym_type? && pair.key.value == :default
264
+ end
265
+ end
266
+ end
267
+
268
+ def has_positional_default?(args)
269
+ args.any? { |arg| !arg.hash_type? }
270
+ end
271
+
272
+ def insert_default_kwarg(corrector, flag_key, args)
273
+ if args.any? && args.last.hash_type?
274
+ # Add to existing hash
275
+ corrector.insert_after(args.last.source_range.end.adjust(begin_pos: -1), ', default: false')
276
+ elsif args.any?
277
+ # Add as new keyword argument after existing args
278
+ corrector.insert_after(args.last, ', default: false')
279
+ else
280
+ # Add as first argument after flag key
281
+ corrector.insert_after(flag_key, ', default: false')
282
+ end
283
+ end
284
+
285
+ def in_test_file?(node)
286
+ processed_source.file_path.include?('spec/')
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Dutchie
8
+ # Flags usage of safety_assured in migrations
9
+ #
10
+ # The safety_assured block bypasses strong_migrations safety checks,
11
+ # which should only be used when you're certain the operation is safe
12
+ # and have documented why it's necessary.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # def change
17
+ # safety_assured do
18
+ # remove_column :users, :email
19
+ # end
20
+ # end
21
+ #
22
+ # # good - avoid safety_assured when possible
23
+ # def change
24
+ # # Use strong_migrations approved patterns instead
25
+ # remove_column :users, :email, type: :string
26
+ # end
27
+ #
28
+ # # acceptable - with clear documentation
29
+ # def change
30
+ # # This operation is safe because:
31
+ # # 1. The column was already removed from the model
32
+ # # 2. No code references this column anymore
33
+ # # 3. We've verified in production logs that no queries use this column
34
+ # safety_assured do
35
+ # remove_column :users, :deprecated_field
36
+ # end
37
+ # end
38
+ #
39
+ class MigrationSafetyAssured < ::RuboCop::Cop::Base
40
+ MSG = 'Avoid `safety_assured` in migrations. It bypasses strong_migrations safety checks. ' \
41
+ 'Ensure this operation is truly safe, consider safer alternatives, and document why ' \
42
+ 'safety_assured is necessary.'
43
+
44
+ # Matches: safety_assured do...end or safety_assured {...}
45
+ def_node_matcher :safety_assured_block?, <<~PATTERN
46
+ (block
47
+ (send nil? :safety_assured)
48
+ ...
49
+ )
50
+ PATTERN
51
+
52
+ def on_block(node)
53
+ return unless safety_assured_block?(node)
54
+
55
+ add_offense(node)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: Dutchie-Style
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.11
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Ostrowski
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 2025-11-07 00:00:00.000000000 Z
11
+ date: 2026-02-07 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rubocop
@@ -110,6 +111,8 @@ files:
110
111
  - lib/Dutchie/Style.rb
111
112
  - lib/Dutchie/Style/version.rb
112
113
  - lib/dutchie-style.rb
114
+ - lib/rubocop/cop/dutchie/launchdarkly_defaults.rb
115
+ - lib/rubocop/cop/dutchie/migration_safety_assured.rb
113
116
  - old-default.yml
114
117
  homepage: https://github.com/GetDutchie/Dutchie-Style
115
118
  licenses:
@@ -119,6 +122,7 @@ metadata:
119
122
  source_code_uri: https://github.com/GetDutchie/Dutchie-Style
120
123
  changelog_uri: https://github.com/GetDutchie/Dutchie-Style/releases
121
124
  rubygems_mfa_required: 'true'
125
+ post_install_message:
122
126
  rdoc_options: []
123
127
  require_paths:
124
128
  - lib
@@ -126,14 +130,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
126
130
  requirements:
127
131
  - - ">="
128
132
  - !ruby/object:Gem::Version
129
- version: 3.1.4
133
+ version: 3.0.6
130
134
  required_rubygems_version: !ruby/object:Gem::Requirement
131
135
  requirements:
132
136
  - - ">="
133
137
  - !ruby/object:Gem::Version
134
138
  version: '0'
135
139
  requirements: []
136
- rubygems_version: 3.6.2
140
+ rubygems_version: 3.0.3.1
141
+ signing_key:
137
142
  specification_version: 4
138
143
  summary: Rubocop Settings for all dutchie Ruby Apps
139
144
  test_files: []