rubocop-sane_conditionals 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 145264fffb5808ff6c7f5dfe04ffbf1340fd91fd1e86e150f471a9c41f086a39
4
+ data.tar.gz: fd868cb346b3296caee3e963831a43760ad7eb3c6f6ec430b818f3740c093034
5
+ SHA512:
6
+ metadata.gz: eb90cdf3cd716033916415e0ebe1fb2ea864a295828b6284cf93c17e3a418848f6f00f6fcabaf9977714edcdbc7fb9f4d9ba39f346589883938d96047fcea35e
7
+ data.tar.gz: 962ae39df0757a3702fdd2f413b7ac3f98a185031f84eb859a9a23cc787955e3bb582de437941a50092035ce746b70937b576356f7d701bf6bda13e2ed9b4e03
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Federico
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # rubocop-sane_conditionals
2
+
3
+ RuboCop cops for people who want to read code, not decode it.
4
+
5
+ ## The Problem
6
+
7
+ Ruby is a beautiful language. It is expressive, elegant, and reads almost like
8
+ English. This is exactly the problem.
9
+
10
+ At some point, someone decided that because Ruby *can* read like English, it
11
+ *should* read like English — and then they implemented the wrong kind of
12
+ English. The kind where the subject comes last. The kind you have to read
13
+ backwards. The kind that makes you go:
14
+
15
+ ```ruby
16
+ send_email unless already_sent?
17
+ ```
18
+
19
+ Cool. So do we send the email, or do we not? Let me just... hold that `send_email`
20
+ in working memory while I parse `unless`... flip the boolean in my head...
21
+ re-check the variable name... oh. OH. We send the email when `already_sent?` is
22
+ `false`. Got it. Only took 600ms and a small cortisol spike.
23
+
24
+ And then there is:
25
+
26
+ ```ruby
27
+ destroy! if admin? && confirmed?
28
+ ```
29
+
30
+ This one is great. Your eye lands on `destroy!`. Your adrenal gland fires.
31
+ Your hand moves toward the keyboard. Then you read `if admin? && confirmed?`
32
+ and you relax. But the damage is done. Your heart rate is up. You have aged
33
+ slightly. The code has won.
34
+
35
+ This gem fights back.
36
+
37
+ ## What It Does
38
+
39
+ ### `Style/NoUnless`
40
+
41
+ Bans `unless` in all its forms. Replaces it with a negated `if`.
42
+
43
+ `unless` is syntactic sugar. Sugar is fine in coffee. Sugar is not fine when
44
+ you are trying to understand whether a background job is going to run or not
45
+ at 11pm during an incident.
46
+
47
+ ```ruby
48
+ # Before (your colleagues wrote this)
49
+ unless user.banned?
50
+ grant_access
51
+ end
52
+
53
+ # After (the cop wrote this)
54
+ if !user.banned?
55
+ grant_access
56
+ end
57
+ ```
58
+
59
+ Yes, `!condition` looks slightly more aggressive. It is slightly more
60
+ aggressive. That is the point. You are negating something. Own it.
61
+
62
+ When the condition is compound, the parens come with it:
63
+
64
+ ```ruby
65
+ # Before (your colleagues wrote this)
66
+ unless user.banned? && account.locked?
67
+ grant_access
68
+ end
69
+
70
+ # After (the cop wrote this)
71
+ if !(user.banned? && account.locked?)
72
+ grant_access
73
+ end
74
+ ```
75
+
76
+ ```ruby
77
+ # Before (someone thought this was clever)
78
+ render_page unless maintenance_mode?
79
+
80
+ # After
81
+ if !maintenance_mode?
82
+ render_page
83
+ end
84
+ ```
85
+
86
+ **"But `unless foo` is so readable!"**
87
+
88
+ It is readable the first time. The fourteenth time you see it in a file, your
89
+ brain has learned to pattern-match it and skip the inversion step — which means
90
+ you are now *silently wrong* about what the code does until you stop and
91
+ actually re-read it. Congratulations on training yourself to make subtle errors
92
+ efficiently.
93
+
94
+ ### `Style/IfUnlessModifierMultiline`
95
+
96
+ Bans modifier `if`. Expands it to a normal `if` block.
97
+
98
+ Modifier `if` is a style borrowed from Perl. If you are taking style advice
99
+ from Perl, please close this README and reconsider your choices.
100
+
101
+ ```ruby
102
+ # Before (you thought you were saving lines)
103
+ do_the_thing if condition_is_met?
104
+
105
+ # After (you are saving brain cells instead)
106
+ if condition_is_met?
107
+ do_the_thing
108
+ end
109
+ ```
110
+
111
+ The argument for modifier `if` is always "it's more concise." This is true.
112
+ It is also more concise to write variable names as single letters, skip
113
+ comments entirely, and put everything on one line. Conciseness is not the
114
+ goal. Clarity is the goal.
115
+
116
+ When you write `do_thing if bar`, a reader must:
117
+
118
+ 1. Parse `do_thing`
119
+ 2. Begin forming the intention "ok, we do the thing"
120
+ 3. Hit `if`
121
+ 4. Backtrack
122
+ 5. Parse `bar`
123
+ 6. Decide whether to do the thing
124
+ 7. Wonder why this person hates them
125
+
126
+ When you write `if bar` on its own line, a reader:
127
+
128
+ 1. Parses `if bar`
129
+ 2. Decides whether to enter the block
130
+ 3. Does or does not enter the block
131
+ 4. Goes home at a reasonable hour
132
+
133
+ Note: modifier `unless` (`do_thing unless bar`) is also banned, but by
134
+ `Style/NoUnless`. It is doubly bad — it is both backwards *and* inverted — and
135
+ it gets its own cop because it deserves special attention.
136
+
137
+ ## Installation
138
+
139
+ Add this to your `Gemfile`:
140
+
141
+ ```ruby
142
+ gem "rubocop-sane_conditionals", require: false
143
+ ```
144
+
145
+ Then add this to your `.rubocop.yml`:
146
+
147
+ ```yaml
148
+ require:
149
+ - rubocop-sane_conditionals
150
+
151
+ Style/NoUnless:
152
+ Enabled: true
153
+
154
+ Style/IfUnlessModifierMultiline:
155
+ Enabled: true
156
+ ```
157
+
158
+ Run `bundle exec rubocop --autocorrect` and let the gem fix your codebase
159
+ while you make tea and quietly reflect on past decisions.
160
+
161
+ ## FAQ
162
+
163
+ **Q: Isn't `unless` idiomatic Ruby?**
164
+
165
+ A: Yes. So is `BEGIN`, `$PROGRAM_NAME`, and the flip-flop operator. Idiomatic
166
+ does not mean good. It means common. A lot of common things are bad.
167
+
168
+ **Q: My team loves `unless`. They will push back.**
169
+
170
+ A: Show them this gem. If they still push back, that is useful information
171
+ about your team.
172
+
173
+ **Q: What about `if !foo`? Isn't `unless foo` cleaner?**
174
+
175
+ A: No. `if !foo` is a complete, unambiguous statement. `unless foo` requires
176
+ the reader to know that `unless` means `if not`, apply that transformation,
177
+ and then continue. This is one extra step. Over a long career, these extra
178
+ steps add up to a measurable amount of time you could have spent doing
179
+ something else.
180
+
181
+ **Q: Why does `unless foo && bar` become `if !(foo && bar)` instead of `if !foo && !bar`?**
182
+
183
+ A: Because that would be wrong. De Morgan's law says `!(A && B)` equals
184
+ `!A || !B`, not `!A && !B`. The cop is not here to do your boolean algebra
185
+ for you. It is here to make the negation visible. The parens make it clear
186
+ that the whole compound condition is being negated, which is exactly what
187
+ `unless` was hiding.
188
+
189
+ **Q: You seem angry about this.**
190
+
191
+ A: I am not angry. I am tired. There is a difference. One of them requires
192
+ backtracking to determine the meaning.
193
+
194
+ ## License
195
+
196
+ MIT. Use it. Improve it. Inflict it on your colleagues.
@@ -0,0 +1,16 @@
1
+ ---
2
+ Style/NoUnless:
3
+ Description: >-
4
+ Use negated `if` instead of `unless`. Unless is cute. Unless is also
5
+ a brain trap. Please stop.
6
+ Enabled: true
7
+ VersionAdded: "0.1"
8
+ AutoCorrect: true
9
+
10
+ Style/IfUnlessModifierMultiline:
11
+ Description: >-
12
+ Expand modifier `if` to multi-line form. Your code is not a tweet.
13
+ You have all the lines you want.
14
+ Enabled: true
15
+ VersionAdded: "0.1"
16
+ AutoCorrect: true
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Disallows modifier `if` (i.e. `foo if bar`) and converts it to a
7
+ # standard multi-line `if` block.
8
+ #
9
+ # Modifier `if` reads right-to-left: your eye hits `foo`, your brain
10
+ # starts processing "ok, we do foo", then hits `if bar` and has to
11
+ # backtrack. Congratulations, you have implemented a for-loop in a
12
+ # human brain. It will crash.
13
+ #
14
+ # Note: modifier `unless` is handled separately by `Style/NoUnless`.
15
+ # Don't worry, it's also banned.
16
+ #
17
+ # @example
18
+ # # bad
19
+ # do_something if condition
20
+ #
21
+ # # good
22
+ # if condition
23
+ # do_something
24
+ # end
25
+ class IfUnlessModifierMultiline < Base
26
+ extend AutoCorrector
27
+
28
+ MSG = 'Convert modifier `if` to multi-line form. Reading left-to-right ' \
29
+ 'is a feature, not a suggestion.'
30
+
31
+ def on_if(node)
32
+ if !node.modifier_form?
33
+ return
34
+ end
35
+
36
+ if node.unless?
37
+ return
38
+ end
39
+
40
+ add_offense(node) do |corrector|
41
+ autocorrect(corrector, node)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def autocorrect(corrector, node)
48
+ condition = node.condition.source
49
+ body = node.body.source
50
+ indentation = ' ' * node.loc.column
51
+
52
+ replacement = <<~RUBY.chomp
53
+ if #{condition}
54
+ #{indentation} #{body}
55
+ #{indentation}end
56
+ RUBY
57
+
58
+ corrector.replace(node, replacement)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Disallows `unless` in all its forms and replaces it with a negated `if`.
7
+ #
8
+ # `unless` is syntactic sugar that requires the reader to mentally invert
9
+ # the condition. This is fine if your reader is a compiler. Your reader is
10
+ # not a compiler. Your reader is a tired human at 4pm on a Friday who just
11
+ # wants to know whether the code runs the thing or does not run the thing.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # unless foo
16
+ # bar
17
+ # end
18
+ #
19
+ # # good
20
+ # if !foo
21
+ # bar
22
+ # end
23
+ #
24
+ # # bad
25
+ # unless foo && bar
26
+ # baz
27
+ # end
28
+ #
29
+ # # good
30
+ # if !(foo && bar)
31
+ # baz
32
+ # end
33
+ #
34
+ # # bad
35
+ # bar unless foo
36
+ #
37
+ # # good
38
+ # if !foo
39
+ # bar
40
+ # end
41
+ class NoUnless < Base
42
+ extend AutoCorrector
43
+
44
+ MSG = 'Use negated `if` instead of `unless`. Your future self will thank you. ' \
45
+ 'Your colleagues will thank you. English teachers need not apply.'
46
+
47
+ def on_if(node)
48
+ if !node.unless?
49
+ return
50
+ end
51
+
52
+ add_offense(node) do |corrector|
53
+ autocorrect(corrector, node)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def autocorrect(corrector, node)
60
+ condition_node = node.condition
61
+ condition_source = condition_node.source
62
+
63
+ negated_condition = if requires_parens?(condition_node)
64
+ "!(#{condition_source})"
65
+ else
66
+ "!#{condition_source}"
67
+ end
68
+
69
+ if node.modifier_form?
70
+ autocorrect_modifier(corrector, node, negated_condition)
71
+ else
72
+ autocorrect_multiline(corrector, node, negated_condition)
73
+ end
74
+ end
75
+
76
+ def requires_parens?(condition_node)
77
+ condition_node.and_type? || condition_node.or_type?
78
+ end
79
+
80
+ def autocorrect_modifier(corrector, node, negated_condition)
81
+ body = node.body.source
82
+ indentation = ' ' * node.loc.column
83
+ replacement = "if #{negated_condition}\n#{indentation} #{body}\n#{indentation}end"
84
+ corrector.replace(node, replacement)
85
+ end
86
+
87
+ def autocorrect_multiline(corrector, node, negated_condition)
88
+ body_source = node.body.source
89
+ indentation = ' ' * node.loc.column
90
+
91
+ replacement = if node.else_branch
92
+ else_body = node.else_branch.source
93
+ <<~RUBY.chomp
94
+ if #{negated_condition}
95
+ #{indentation} #{body_source}
96
+ #{indentation}else
97
+ #{indentation} #{else_body}
98
+ #{indentation}end
99
+ RUBY
100
+ else
101
+ <<~RUBY.chomp
102
+ if #{negated_condition}
103
+ #{indentation} #{body_source}
104
+ #{indentation}end
105
+ RUBY
106
+ end
107
+
108
+ corrector.replace(node, replacement)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module SaneConditionals
5
+ VERSION = '1.0.0'
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+ require_relative 'rubocop-sane_conditionals/version'
5
+ require_relative 'rubocop/cop/style/no_unless'
6
+ require_relative 'rubocop/cop/style/if_unless_modifier_multiline'
7
+
8
+ if defined?(Rake)
9
+ RuboCop::RakeTask
10
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-sane_conditionals
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Federico
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rubocop
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ description: |
28
+ Enforces conditional style that does not require you to hold the entire
29
+ sentence in working memory while your brain tries to figure out what
30
+ "do_thing if !foo unless bar" means. It means you hate your colleagues.
31
+ email:
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - LICENSE.txt
37
+ - README.md
38
+ - config/default.yml
39
+ - lib/rubocop-sane_conditionals.rb
40
+ - lib/rubocop-sane_conditionals/version.rb
41
+ - lib/rubocop/cop/style/if_unless_modifier_multiline.rb
42
+ - lib/rubocop/cop/style/no_unless.rb
43
+ homepage: https://github.com/jmfederico/rubocop-sane_conditionals
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ rubygems_mfa_required: 'true'
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '3.0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.4.10
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: RuboCop cops for humans who can read.
67
+ test_files: []