ivar 0.2.0 → 0.4.6
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 +4 -4
- data/.augment-guidelines +5 -3
- data/.devcontainer/devcontainer.json +28 -20
- data/.devcontainer/post-create.sh +18 -0
- data/.editorconfig +35 -0
- data/.rubocop.yml +6 -0
- data/.standard.yml +1 -1
- data/.vscode/extensions.json +3 -1
- data/.vscode/launch.json +25 -0
- data/.vscode/settings.json +38 -2
- data/CHANGELOG.md +99 -1
- data/README.md +272 -207
- data/Rakefile +1 -1
- data/VERSION.md +46 -0
- data/examples/check_all_block_example.rb +84 -0
- data/examples/check_all_example.rb +42 -0
- data/examples/inheritance_with_kwarg_init.rb +156 -0
- data/examples/inheritance_with_positional_init.rb +142 -0
- data/examples/mixed_positional_and_kwarg_init.rb +125 -0
- data/examples/require_check_all_example.rb +23 -0
- data/examples/sandwich_inheritance.rb +1 -1
- data/examples/sandwich_with_accessors.rb +78 -0
- data/examples/sandwich_with_block_values.rb +54 -0
- data/examples/sandwich_with_checked.rb +0 -1
- data/examples/sandwich_with_checked_once.rb +0 -1
- data/examples/sandwich_with_initial_values.rb +52 -0
- data/examples/sandwich_with_ivar_block.rb +6 -9
- data/examples/sandwich_with_ivar_macro.rb +4 -4
- data/examples/sandwich_with_kwarg_init.rb +78 -0
- data/examples/sandwich_with_positional_init.rb +50 -0
- data/examples/sandwich_with_shared_values.rb +54 -0
- data/hooks/README.md +42 -0
- data/hooks/install.sh +12 -0
- data/hooks/pre-commit +54 -0
- data/lib/ivar/check_all.rb +7 -0
- data/lib/ivar/check_all_manager.rb +72 -0
- data/lib/ivar/check_policy.rb +29 -0
- data/lib/ivar/checked/class_methods.rb +19 -0
- data/lib/ivar/checked/instance_methods.rb +35 -0
- data/lib/ivar/checked.rb +17 -24
- data/lib/ivar/declaration.rb +30 -0
- data/lib/ivar/explicit_declaration.rb +56 -0
- data/lib/ivar/explicit_keyword_declaration.rb +24 -0
- data/lib/ivar/explicit_positional_declaration.rb +19 -0
- data/lib/ivar/macros.rb +48 -111
- data/lib/ivar/manifest.rb +124 -0
- data/lib/ivar/policies.rb +13 -1
- data/lib/ivar/project_root.rb +59 -0
- data/lib/ivar/targeted_prism_analysis.rb +144 -0
- data/lib/ivar/validation.rb +6 -29
- data/lib/ivar/version.rb +1 -1
- data/lib/ivar.rb +141 -9
- data/script/console +11 -0
- data/script/de-lint +2 -0
- data/script/de-lint-unsafe +2 -0
- data/script/lint +2 -0
- data/script/release +213 -0
- data/script/setup +8 -0
- data/script/test +2 -0
- metadata +46 -8
- data/examples/sandwich_with_kwarg.rb +0 -45
- data/ivar.gemspec +0 -49
- data/lib/ivar/auto_check.rb +0 -77
- data/lib/ivar/prism_analysis.rb +0 -102
data/README.md
CHANGED
@@ -1,343 +1,408 @@
|
|
1
1
|
# Ivar
|
2
2
|
|
3
|
-
|
3
|
+
Ivar is a Ruby gem that automatically checks for typos in instance variables.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
Ivar waits until an instance is created to do the checking, then uses Prism to look for variables that don't match what was set in initialization. So it's a little bit dynamic, a little bit static. It doesn't encumber your instance variable reads and writes with any extra checking. And with the `:warn_once` policy, it won't overwhelm you with output.
|
8
|
-
|
9
|
-
|
10
|
-
## Usage
|
11
|
-
|
12
|
-
### Manual Validation
|
5
|
+
## Synopsis
|
13
6
|
|
14
7
|
```ruby
|
15
|
-
|
16
|
-
require "ivar"
|
8
|
+
require "ivar/check_all" if $VERBOSE
|
17
9
|
|
18
|
-
class
|
19
|
-
|
20
|
-
|
21
|
-
def initialize
|
22
|
-
@bread = "wheat"
|
23
|
-
@cheese = "muenster"
|
24
|
-
@condiments = ["mayo", "mustard"]
|
25
|
-
check_ivars(add: [:@side])
|
10
|
+
class Pizza
|
11
|
+
def initialize(toppings)
|
12
|
+
@toppings = toppings
|
26
13
|
end
|
27
14
|
|
28
15
|
def to_s
|
29
|
-
"A
|
30
|
-
(@side ? "and a side of #{@side}" : "")
|
16
|
+
"A pizza with #{@topings.join(", ")}"
|
31
17
|
end
|
32
18
|
end
|
33
19
|
|
34
|
-
|
20
|
+
Pizza.new(["pepperoni", "mushrooms"])
|
35
21
|
```
|
36
22
|
|
37
23
|
```shell
|
38
|
-
$ ruby
|
39
|
-
|
24
|
+
$ ruby -w pizza.rb
|
25
|
+
pizza.rb:10: warning: unknown instance variable @topings. Did you mean: @toppings?
|
40
26
|
```
|
41
27
|
|
42
|
-
|
28
|
+
## Introduction
|
43
29
|
|
44
|
-
|
45
|
-
# sandwich_automatic.rb
|
46
|
-
require "ivar"
|
30
|
+
> OK I read the synopsis but I don't get it.
|
47
31
|
|
48
|
-
|
49
|
-
include Ivar::Checked
|
32
|
+
That's because what Ivar does seems so basic that it's almost a surprise it isn't part of the language. Do you see that warning about an unkown instance variable? That's Ivar, helping you avoid a bug.
|
50
33
|
|
51
|
-
|
52
|
-
@bread = "white"
|
53
|
-
@cheese = "havarti"
|
54
|
-
@condiments = ["mayo", "mustard"]
|
55
|
-
# no need for explicit check_ivars call
|
56
|
-
end
|
34
|
+
> Oh! Because in Ruby, any unset instance variable ("ivar") you reference just returns `nil`, with no error or warning.
|
57
35
|
|
58
|
-
|
59
|
-
|
36
|
+
Exactly.
|
37
|
+
|
38
|
+
> Yeah what's up with that anyway.
|
39
|
+
|
40
|
+
It's actually one of the conveniences of the language: when you realize you need to store a field you just do it, without having to go back and declare it somewhere else in the class:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class MyClass
|
44
|
+
# ...
|
45
|
+
def increment_usage_count!
|
46
|
+
(@usage_count ||= 0) += 1
|
60
47
|
end
|
48
|
+
# ...
|
61
49
|
end
|
62
|
-
|
63
|
-
Sandwich.new
|
64
50
|
```
|
65
51
|
|
66
|
-
|
67
|
-
|
68
|
-
|
52
|
+
> Right but there is no protection against typos.
|
53
|
+
|
54
|
+
Also true. If we later do this:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class MyClass
|
58
|
+
# ...
|
59
|
+
def usage_count
|
60
|
+
@usag_count
|
61
|
+
end
|
62
|
+
# ...
|
63
|
+
end
|
69
64
|
```
|
70
65
|
|
71
|
-
|
66
|
+
...there's nothing to tell us we got the variable wrong.
|
72
67
|
|
73
|
-
|
68
|
+
> I thought that's why we're supposed to use attr_reader and friends.
|
74
69
|
|
75
|
-
|
70
|
+
Yes, this is why a lot of people recommend using `attr_reader`/`_writer`/`attr_accessor` pervasively. Only ever reading or writing ivars through accessors. But this gives up the convenience, informality, and conciseness of Ruby's instance variables. And it also puts you at risk of Ruby's all-time favorite gotcha: forgetting to put `self.` in front a setter call.
|
76
71
|
|
77
72
|
```ruby
|
78
|
-
|
79
|
-
|
73
|
+
class MyClass
|
74
|
+
attr_accessor :usage_count
|
80
75
|
|
81
|
-
|
82
|
-
|
83
|
-
ivar_check_policy :warn_once
|
84
|
-
|
85
|
-
def initialize
|
86
|
-
@bread = "white"
|
87
|
-
@cheese = "havarti"
|
88
|
-
@condiments = ["mayo", "mustard"]
|
89
|
-
# no need for explicit check_ivars call
|
76
|
+
def increment_usage_count!
|
77
|
+
usage_count += 1 # oops, incremented a local, not the ivar
|
90
78
|
end
|
79
|
+
end
|
80
|
+
```
|
91
81
|
|
92
|
-
|
93
|
-
|
82
|
+
> Ouch, bad memories.
|
83
|
+
|
84
|
+
Yeah. Personally I've gone through phases with this. For many years I followed and advocated the advice to use accessors everywhere. But lately I've kind of gone back to my roots on using unadorned ivars directly when I'm not setting up a public interface.
|
85
|
+
|
86
|
+
Then I ran into Joel Drapper's [`strict_ivars`](https://github.com/joeldrapper/strict_ivars) gem and it got my wheels turning. I preferred a warning to a hard error, and I didn't necessarily want to have to change methods that intentionally referenced unset ivars. It got me wondering, though: what would it look like to have Ruby warn about possible typos in ivar names, the same way it warns about other potential oopsies? What even would the heuristic be to determine if an ivar reference might be a typo?
|
87
|
+
|
88
|
+
> Well obviously you found a way to do it. What heuristics does Ivar use?
|
89
|
+
|
90
|
+
At the moment there are two ways to give an ivar the stamp of approval. The first is the most un-intrusive: set it in the initializer.
|
91
|
+
|
92
|
+
> Oh, like in that first example, you set `@toppings` in the initializer.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
require "ivar/check_all" if $VERBOSE
|
96
|
+
|
97
|
+
class Pizza
|
98
|
+
def initialize(toppings)
|
99
|
+
@toppings = toppings
|
94
100
|
end
|
101
|
+
# ...
|
95
102
|
end
|
96
103
|
|
97
|
-
Sandwich.new
|
98
104
|
```
|
99
105
|
|
100
|
-
|
101
|
-
$ ruby sandwich_once.rb -w
|
102
|
-
sandwich_once.rb:15: warning: unknown instance variable @chese. Did you mean: @cheese?
|
103
|
-
```
|
104
|
-
|
105
|
-
Setting `ivar_check_policy :warn_once` makes `check_ivars` use the `warn_once` policy, which means it will emit warnings only for the first instance of each class.
|
106
|
+
Exactly.
|
106
107
|
|
107
|
-
|
108
|
+
> And then what... spooky magic happens?
|
108
109
|
|
109
|
-
|
110
|
+
Well, let's use a slightly more explicit version.
|
110
111
|
|
111
112
|
```ruby
|
112
|
-
# sandwich_with_ivar_macro.rb
|
113
113
|
require "ivar"
|
114
114
|
|
115
|
-
class
|
115
|
+
class Pizza
|
116
116
|
include Ivar::Checked
|
117
117
|
|
118
|
-
|
119
|
-
|
120
|
-
ivar :@side
|
121
|
-
|
122
|
-
def initialize
|
123
|
-
@bread = "wheat"
|
124
|
-
@cheese = "muenster"
|
125
|
-
@condiments = ["mayo", "mustard"]
|
126
|
-
# Note: @side is not set here, but it's pre-initialized to nil
|
118
|
+
def initialize(toppings)
|
119
|
+
@toppings = toppings
|
127
120
|
end
|
121
|
+
# ...
|
122
|
+
end
|
123
|
+
```
|
128
124
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
125
|
+
That's what `ivar/check_all` implicitly does under the hood: adds `Ivar::Checked` to classes.
|
126
|
+
|
127
|
+
> Which does what, exactly?
|
128
|
+
|
129
|
+
Let's use an even more explicit version to demonstrate:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
require "ivar"
|
135
133
|
|
136
|
-
|
137
|
-
|
134
|
+
class Pizza
|
135
|
+
include Ivar::Validation
|
136
|
+
|
137
|
+
def initialize(toppings)
|
138
|
+
@toppings = toppings
|
139
|
+
check_ivars
|
138
140
|
end
|
141
|
+
# ...
|
139
142
|
end
|
143
|
+
```
|
140
144
|
|
141
|
-
|
142
|
-
puts sandwich.to_s # No warning about @side
|
145
|
+
> So `check_ivars` is the magic method that does the checking?
|
143
146
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
+
Exactly. `Ivar::Checked` just arranges to automatically call it after your `initialize` methods finish.
|
148
|
+
|
149
|
+
> And what, precisely, does `check_ivars` do?
|
150
|
+
|
151
|
+
Well, it first notes all currently set ivars, and stamps them as "known". Then it kicks off a just-in-time static analysis of the class using [Prism](https://github.com/ruby/prism), to find all ivar references. And then it compares the two lists and generates warnings for any references that don't match a known ivar.
|
152
|
+
|
153
|
+
> And it does this when an instance is created?
|
147
154
|
|
148
|
-
|
155
|
+
Yep!
|
149
156
|
|
150
|
-
|
157
|
+
> OK I have some concerns about that but I'll save them for later. My next question is: what if I want to use an ivar without first initializing it in the `initialize` method?
|
151
158
|
|
152
|
-
|
159
|
+
Well, if you're using the explicit `check_ivars` version you can stamp some additional ivars as "known" by passing them in as an argument:
|
153
160
|
|
154
161
|
```ruby
|
155
|
-
# sandwich_with_kwarg.rb
|
156
162
|
require "ivar"
|
157
163
|
|
158
|
-
class
|
159
|
-
include Ivar::
|
160
|
-
|
161
|
-
ivar kwarg: [:@bread, :@cheese, :@condiments, :@pickles, :@side]
|
164
|
+
class Pizza
|
165
|
+
include Ivar::Validation
|
162
166
|
|
163
|
-
def
|
164
|
-
|
165
|
-
|
166
|
-
result += " with pickles" if @pickles
|
167
|
-
result += " and a side of #{@side}" if @side
|
168
|
-
result
|
167
|
+
def initialize(toppings)
|
168
|
+
@toppings = toppings
|
169
|
+
check_ivars(add: [:@extra_cheese])
|
169
170
|
end
|
171
|
+
# ...
|
170
172
|
end
|
173
|
+
```
|
171
174
|
|
172
|
-
|
173
|
-
sandwich = SandwichWithKwarg.new(
|
174
|
-
bread: "wheat",
|
175
|
-
cheese: "muenster",
|
176
|
-
condiments: ["mayo", "mustard"],
|
177
|
-
side: "chips"
|
178
|
-
)
|
175
|
+
But the canonical way to do it is with the `ivar` macro:
|
179
176
|
|
180
|
-
|
181
|
-
|
177
|
+
```ruby
|
178
|
+
require "ivar"
|
182
179
|
|
183
|
-
|
180
|
+
class Pizza
|
181
|
+
include Ivar::Checked
|
184
182
|
|
185
|
-
|
183
|
+
ivar :@minutes_waiting
|
186
184
|
|
187
|
-
|
185
|
+
def increment_wait_time
|
186
|
+
(@minutes_waiting ||= 0) += 1
|
187
|
+
end
|
188
|
+
# ...
|
189
|
+
end
|
190
|
+
```
|
188
191
|
|
189
|
-
This
|
192
|
+
This is purely a declaration: the variable will not be set. But as a convenience, you *can* also initialize it with a value:
|
190
193
|
|
191
194
|
```ruby
|
192
|
-
|
195
|
+
require "ivar"
|
196
|
+
|
197
|
+
class Pizza
|
193
198
|
include Ivar::Checked
|
194
199
|
|
195
|
-
|
196
|
-
# Variables set in initialize (@bread, @cheese) don't need to be pre-declared
|
197
|
-
ivar :@optional_topping
|
200
|
+
ivar :@minutes_waiting, value: 0
|
198
201
|
|
199
|
-
def
|
200
|
-
@
|
201
|
-
@cheese = "muenster"
|
202
|
+
def increment_wait_time
|
203
|
+
@minutes_waiting += 1
|
202
204
|
end
|
205
|
+
# ...
|
203
206
|
end
|
207
|
+
```
|
204
208
|
|
205
|
-
|
206
|
-
# Add more pre-declared instance variables as needed
|
207
|
-
# @condiments is set in initialize, so it doesn't need to be pre-declared
|
208
|
-
ivar :@special_sauce
|
209
|
+
> Can I initialize it with a different value for each instance?
|
209
210
|
|
210
|
-
|
211
|
-
super
|
212
|
-
@condiments = ["mayo", "mustard"]
|
213
|
-
end
|
211
|
+
Yes, you can pass a block that generates the value:
|
214
212
|
|
215
|
-
|
216
|
-
|
217
|
-
# @special_sauce is pre-declared, so this won't trigger a warning
|
218
|
-
result += " with #{@special_sauce}" if @special_sauce
|
219
|
-
# @optional_topping is inherited from the parent class
|
220
|
-
result += " and #{@optional_topping}" if @optional_topping
|
221
|
-
result
|
222
|
-
end
|
223
|
-
end
|
213
|
+
```ruby
|
214
|
+
require "ivar"
|
224
215
|
|
225
|
-
|
226
|
-
|
216
|
+
class Pizza
|
217
|
+
include Ivar::Checked
|
227
218
|
|
228
|
-
|
229
|
-
|
230
|
-
|
219
|
+
ivar(:@order_time) { Time.now }
|
220
|
+
# ...
|
221
|
+
end
|
231
222
|
```
|
232
223
|
|
233
|
-
|
224
|
+
This block will be passed the ivar name as an argument, if you want to do something fancy like share one dynamic initialization block between multiple ivars:
|
234
225
|
|
235
|
-
|
226
|
+
```ruby
|
227
|
+
require "ivar"
|
236
228
|
|
237
|
-
|
229
|
+
class Pizza
|
230
|
+
include Ivar::Checked
|
238
231
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
232
|
+
ivar :@order_time, :@delivery_time do |ivar_name|
|
233
|
+
Time.now + (ivar_name == :@order_time ? 0 : 30)
|
234
|
+
end
|
235
|
+
# ...
|
236
|
+
end
|
237
|
+
```
|
243
238
|
|
244
|
-
|
239
|
+
Which, yes, you can declare multiple ivars in one `ivar` call, if you want. You can also split them between individual `ivar` declarations.
|
240
|
+
|
241
|
+
> But what if I want to initialize an ivar from a constructor argument? Do I need to go back to the `initialize` method for that?
|
242
|
+
|
243
|
+
No you don't! One of the coolest conveniences that `ivar` adds is the ability to mark an ivar as initializable from a constructor argument:
|
245
244
|
|
246
245
|
```ruby
|
247
|
-
|
248
|
-
Ivar.check_policy = :raise
|
246
|
+
require "ivar"
|
249
247
|
|
250
248
|
class Sandwich
|
251
|
-
include Ivar::
|
249
|
+
include Ivar::Checked
|
252
250
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
end
|
251
|
+
ivar :@bread, init: :kwarg
|
252
|
+
ivar :@cheese, init: :kwarg
|
253
|
+
ivar :@condiments, init: :kwarg
|
257
254
|
|
258
255
|
def to_s
|
259
|
-
"A #{@bread} sandwich with #{@
|
256
|
+
"A #{@bread} sandwich with #{@cheese} and #{@condiments.join(", ")}"
|
260
257
|
end
|
261
258
|
end
|
262
259
|
|
263
|
-
Sandwich.new
|
260
|
+
s = Sandwich.new(bread: "wheat", cheese: "muenster", condiments: ["mayo"])
|
261
|
+
s.to_s # => "A wheat sandwich with muenster and mayo"
|
264
262
|
```
|
265
263
|
|
266
|
-
|
264
|
+
Notice the lack of an initialize method, and the lack of the usual Ruby repetition of `@ivar_name = ivar_name`.
|
265
|
+
|
266
|
+
> Whoah.
|
267
|
+
|
268
|
+
Right??? Oh yeah we've got positional arguments too if you like those better.
|
267
269
|
|
268
270
|
```ruby
|
269
|
-
|
270
|
-
include Ivar::Validation
|
271
|
-
extend Ivar::CheckPolicy
|
271
|
+
require "ivar"
|
272
272
|
|
273
|
-
|
274
|
-
|
273
|
+
class Sandwich
|
274
|
+
include Ivar::Checked
|
275
275
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
end
|
276
|
+
ivar :@bread, init: :arg
|
277
|
+
ivar :@cheese, init: :arg
|
278
|
+
ivar :@condiments, init: :arg
|
280
279
|
|
281
280
|
def to_s
|
282
|
-
"A #{@bread} sandwich with #{@
|
281
|
+
"A #{@bread} sandwich with #{@cheese} and #{@condiments.join(", ")}"
|
283
282
|
end
|
284
283
|
end
|
285
284
|
|
286
|
-
Sandwich.new
|
285
|
+
s = Sandwich.new("wheat", "muenster", ["mayo"])
|
286
|
+
s.to_s # => "A wheat sandwich with muenster and mayo"
|
287
287
|
```
|
288
288
|
|
289
|
-
|
289
|
+
> What if I also want external accessor methods?
|
290
|
+
|
291
|
+
Gotcha covered.
|
290
292
|
|
291
293
|
```ruby
|
292
|
-
|
293
|
-
include Ivar::Validation
|
294
|
+
require "ivar"
|
294
295
|
|
295
|
-
|
296
|
-
|
297
|
-
# Use the raise policy for this check
|
298
|
-
check_ivars(policy: :raise)
|
299
|
-
end
|
296
|
+
class Sandwich
|
297
|
+
include Ivar::Checked
|
300
298
|
|
301
|
-
|
302
|
-
|
303
|
-
|
299
|
+
ivar :@bread, init: :kwarg, reader: true
|
300
|
+
ivar :@cheese, init: :kwarg, reader: true
|
301
|
+
ivar :@condiments, init: :kwarg, accessor: true
|
304
302
|
end
|
305
303
|
|
306
|
-
Sandwich.new
|
304
|
+
s = Sandwich.new(bread: "wheat", cheese: "muenster", condiments: ["mayo"])
|
305
|
+
s.bread # => "wheat"
|
306
|
+
s.cheese # => "muenster"
|
307
|
+
s.condiments = ["mustard"]
|
308
|
+
s.condiments # => ["mustard"]
|
307
309
|
```
|
308
310
|
|
309
|
-
|
311
|
+
> This seems like it's more than just about detecting ivar typos at this point.
|
312
|
+
|
313
|
+
Yeah, well, I knew that in order to determine typos I'd have to have some kind of declaration mechanism. And once I have that, I might as well make it useful. And use it to gain back some of the convenience lost to having to write the declaration in the first place.
|
314
|
+
|
315
|
+
> Fair enough.
|
316
|
+
|
317
|
+
Any other questions?
|
318
|
+
|
319
|
+
> What about inheritance? Can I use these tools in both parent and child clases?
|
310
320
|
|
311
|
-
|
321
|
+
Yes, `ivar` goes to a fair amount of trouble to "just work" in ways you'll (hopefully) expect when it comes to inheritance.
|
312
322
|
|
313
|
-
|
323
|
+
More questions?
|
314
324
|
|
315
|
-
|
325
|
+
> Well, earlier you said that checking happens at object-instantiation time. Does this mean I'm going to be flooded with warnings if my code creates a lot of instances?
|
326
|
+
|
327
|
+
Excellent question! No, not out of the box. The default policy (`:warn_once`) is to warn only once per class, not per instance.
|
328
|
+
|
329
|
+
> Are there other policies?
|
330
|
+
|
331
|
+
Yeah, there's `:warn` for warning every time; `:log` for logging warnings, `:raise` for raising an exception, and `:none` for no checking at all.
|
332
|
+
|
333
|
+
Policies can be set program-wide:
|
316
334
|
|
317
335
|
```ruby
|
318
|
-
|
336
|
+
require "ivar"
|
337
|
+
Ivar.check_policy = :log
|
338
|
+
```
|
339
|
+
|
340
|
+
...or on a per-class basis:
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
require "ivar"
|
344
|
+
|
345
|
+
class Pizza
|
319
346
|
include Ivar::Checked
|
347
|
+
ivar_check_policy :none
|
348
|
+
# ...
|
349
|
+
end
|
350
|
+
```
|
320
351
|
|
321
|
-
|
322
|
-
ivar_check_policy :raise
|
352
|
+
...or when invoking `check_ivars`:
|
323
353
|
|
324
|
-
|
325
|
-
|
326
|
-
end
|
354
|
+
```ruby
|
355
|
+
require "ivar"
|
327
356
|
|
328
|
-
|
329
|
-
|
357
|
+
class Pizza
|
358
|
+
include Ivar::Validation
|
359
|
+
|
360
|
+
def initialize(toppings)
|
361
|
+
@toppings = toppings
|
362
|
+
check_ivars(policy: :raise)
|
330
363
|
end
|
364
|
+
# ...
|
331
365
|
end
|
366
|
+
```
|
367
|
+
|
368
|
+
You can check out the source code for more details about check policies.
|
369
|
+
|
370
|
+
> I still have some questions. Specifically... what exactly is going on behind the scenes with `ivar/check_all`? That seems... spooky.
|
371
|
+
|
372
|
+
Yeah. So when you require `ivar/check_all`, what you're doing is invoking `Ivar.check_all`.
|
373
|
+
|
374
|
+
This sets up a TracePoint that watches for class and module definitions. When it detects one, it includes `Ivar::Checked` into that the class or module.
|
375
|
+
|
376
|
+
> Wait so it infects every single class I load?
|
377
|
+
|
378
|
+
Not quite. It tries pretty hard to only do it for code from your project; not for stuff from gems or the standard library.
|
379
|
+
|
380
|
+
> Doesn't a TracePoint have a performance impact?
|
381
|
+
|
382
|
+
Yeah probably. That's why I don't necessarily recommend `ivar/check_all` for production use. But there are a couple of alternatives. For one, you can do it like we had in the opening example: only load it when `$VERBOSE` is true.
|
332
383
|
|
333
|
-
|
384
|
+
```ruby
|
385
|
+
require "ivar/check_all" if $VERBOSE
|
386
|
+
```
|
387
|
+
|
388
|
+
Or, you can use the block form of `Ivar.check_all`, which will only enable checking for classes defined within the block.
|
389
|
+
|
390
|
+
```ruby
|
391
|
+
require "ivar"
|
392
|
+
|
393
|
+
Ivar.check_all do
|
394
|
+
# load your code here
|
395
|
+
end
|
334
396
|
```
|
335
397
|
|
336
|
-
|
398
|
+
With this version the tracepoint will only be active for the duration of the `check_all` block.
|
399
|
+
|
400
|
+
## Acknowledgements
|
401
|
+
|
402
|
+
Thanks first to [Joel Draper](https://github.com/joeldrapper) for creating [strict_ivars](https://github.com/joeldrapper/strict_ivars), which inspired this gem. If `ivar` isn't quite what you're looking for, check out `strict_ivars` instead!
|
337
403
|
|
338
|
-
|
404
|
+
Thanks also to [Augment Code](https://www.augmentcode.com/), which served as my "hands" for building this. I'm not at a point in my life where I can actually afford the time to build random passion projects, so this project wouldn't exist without help from the robot.
|
339
405
|
|
340
|
-
|
406
|
+
## Contribution
|
341
407
|
|
342
|
-
|
343
|
-
- Add a module for dynamic checking of instance_variable_get/set
|
408
|
+
Contributions are welcome! Fair warning, if I accept a bunch of your PRs I may nominate you as a maintainer. I know my limits: I'm better at kicking off projects than at maintaining them.
|
data/Rakefile
CHANGED
data/VERSION.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# Versioning and Release Process
|
2
|
+
|
3
|
+
This project follows [Semantic Versioning](https://semver.org/) (SemVer).
|
4
|
+
|
5
|
+
## Version Numbers
|
6
|
+
|
7
|
+
Version numbers are in the format `MAJOR.MINOR.PATCH`:
|
8
|
+
|
9
|
+
- **MAJOR**: Incremented for incompatible API changes
|
10
|
+
- **MINOR**: Incremented for new functionality in a backward-compatible manner
|
11
|
+
- **PATCH**: Incremented for backward-compatible bug fixes
|
12
|
+
|
13
|
+
## Release Process
|
14
|
+
|
15
|
+
To release a new version:
|
16
|
+
|
17
|
+
1. Make sure all changes are documented in the `CHANGELOG.md` file under the "Unreleased" section
|
18
|
+
2. Run the release script with the appropriate version bump type:
|
19
|
+
```
|
20
|
+
script/release [major|minor|patch] [options]
|
21
|
+
```
|
22
|
+
|
23
|
+
Available options:
|
24
|
+
- `--yes` or `-y`: Skip confirmation prompt
|
25
|
+
- `--no-push`: Skip pushing changes to remote repository
|
26
|
+
|
27
|
+
3. The script will:
|
28
|
+
- Run tests and linter to ensure everything is working
|
29
|
+
- Update the version number in `lib/ivar/version.rb`
|
30
|
+
- Update the `CHANGELOG.md` file with the new version and date
|
31
|
+
- Commit these changes
|
32
|
+
- Create a git tag for the new version
|
33
|
+
- Push the changes and tag to GitHub (unless `--no-push` is specified)
|
34
|
+
4. The GitHub Actions workflow will automatically:
|
35
|
+
- Build the gem
|
36
|
+
- Run tests
|
37
|
+
- Publish the gem to RubyGems.org
|
38
|
+
|
39
|
+
## Setting Up RubyGems API Key
|
40
|
+
|
41
|
+
To allow GitHub Actions to publish to RubyGems.org, you need to add your RubyGems API key as a secret:
|
42
|
+
|
43
|
+
1. Get your API key from RubyGems.org (account settings)
|
44
|
+
2. Go to your GitHub repository settings
|
45
|
+
3. Navigate to "Secrets and variables" → "Actions"
|
46
|
+
4. Add a new repository secret named `RUBYGEMS_API_KEY` with your RubyGems API key as the value
|