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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.augment-guidelines +5 -3
  3. data/.devcontainer/devcontainer.json +28 -20
  4. data/.devcontainer/post-create.sh +18 -0
  5. data/.editorconfig +35 -0
  6. data/.rubocop.yml +6 -0
  7. data/.standard.yml +1 -1
  8. data/.vscode/extensions.json +3 -1
  9. data/.vscode/launch.json +25 -0
  10. data/.vscode/settings.json +38 -2
  11. data/CHANGELOG.md +99 -1
  12. data/README.md +272 -207
  13. data/Rakefile +1 -1
  14. data/VERSION.md +46 -0
  15. data/examples/check_all_block_example.rb +84 -0
  16. data/examples/check_all_example.rb +42 -0
  17. data/examples/inheritance_with_kwarg_init.rb +156 -0
  18. data/examples/inheritance_with_positional_init.rb +142 -0
  19. data/examples/mixed_positional_and_kwarg_init.rb +125 -0
  20. data/examples/require_check_all_example.rb +23 -0
  21. data/examples/sandwich_inheritance.rb +1 -1
  22. data/examples/sandwich_with_accessors.rb +78 -0
  23. data/examples/sandwich_with_block_values.rb +54 -0
  24. data/examples/sandwich_with_checked.rb +0 -1
  25. data/examples/sandwich_with_checked_once.rb +0 -1
  26. data/examples/sandwich_with_initial_values.rb +52 -0
  27. data/examples/sandwich_with_ivar_block.rb +6 -9
  28. data/examples/sandwich_with_ivar_macro.rb +4 -4
  29. data/examples/sandwich_with_kwarg_init.rb +78 -0
  30. data/examples/sandwich_with_positional_init.rb +50 -0
  31. data/examples/sandwich_with_shared_values.rb +54 -0
  32. data/hooks/README.md +42 -0
  33. data/hooks/install.sh +12 -0
  34. data/hooks/pre-commit +54 -0
  35. data/lib/ivar/check_all.rb +7 -0
  36. data/lib/ivar/check_all_manager.rb +72 -0
  37. data/lib/ivar/check_policy.rb +29 -0
  38. data/lib/ivar/checked/class_methods.rb +19 -0
  39. data/lib/ivar/checked/instance_methods.rb +35 -0
  40. data/lib/ivar/checked.rb +17 -24
  41. data/lib/ivar/declaration.rb +30 -0
  42. data/lib/ivar/explicit_declaration.rb +56 -0
  43. data/lib/ivar/explicit_keyword_declaration.rb +24 -0
  44. data/lib/ivar/explicit_positional_declaration.rb +19 -0
  45. data/lib/ivar/macros.rb +48 -111
  46. data/lib/ivar/manifest.rb +124 -0
  47. data/lib/ivar/policies.rb +13 -1
  48. data/lib/ivar/project_root.rb +59 -0
  49. data/lib/ivar/targeted_prism_analysis.rb +144 -0
  50. data/lib/ivar/validation.rb +6 -29
  51. data/lib/ivar/version.rb +1 -1
  52. data/lib/ivar.rb +141 -9
  53. data/script/console +11 -0
  54. data/script/de-lint +2 -0
  55. data/script/de-lint-unsafe +2 -0
  56. data/script/lint +2 -0
  57. data/script/release +213 -0
  58. data/script/setup +8 -0
  59. data/script/test +2 -0
  60. metadata +46 -8
  61. data/examples/sandwich_with_kwarg.rb +0 -45
  62. data/ivar.gemspec +0 -49
  63. data/lib/ivar/auto_check.rb +0 -77
  64. data/lib/ivar/prism_analysis.rb +0 -102
data/README.md CHANGED
@@ -1,343 +1,408 @@
1
1
  # Ivar
2
2
 
3
- Ruby instance variables are so convenient - you don't even need to declare them! But... they are also dangerous, because a mispelled variable name results in `nil` instead of an error.
3
+ Ivar is a Ruby gem that automatically checks for typos in instance variables.
4
4
 
5
- Why not have the best of both worlds? Ivar lets you use plain-old instance variables, and automatically checks for typos.
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
- # sandwich.rb
16
- require "ivar"
8
+ require "ivar/check_all" if $VERBOSE
17
9
 
18
- class Sandwich
19
- include Ivar::Validation
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 #{@bread} sandwich with #{@chese} and #{@condiments.join(", ")}" \
30
- (@side ? "and a side of #{@side}" : "")
16
+ "A pizza with #{@topings.join(", ")}"
31
17
  end
32
18
  end
33
19
 
34
- Sandwich.new
20
+ Pizza.new(["pepperoni", "mushrooms"])
35
21
  ```
36
22
 
37
23
  ```shell
38
- $ ruby sandwich.rb -w
39
- sandwich.rb:22: warning: unknown instance variable @chese. Did you mean: @cheese?
24
+ $ ruby -w pizza.rb
25
+ pizza.rb:10: warning: unknown instance variable @topings. Did you mean: @toppings?
40
26
  ```
41
27
 
42
- ### Automatic Validation (Every Instance)
28
+ ## Introduction
43
29
 
44
- ```ruby
45
- # sandwich_automatic.rb
46
- require "ivar"
30
+ > OK I read the synopsis but I don't get it.
47
31
 
48
- class Sandwich
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
- def initialize
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
- def to_s
59
- "A #{@bread} sandwich with #{@chese} and #{@condiments.join(", ")}"
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
- ```shell
67
- $ ruby sandwich_automatic.rb -w
68
- sandwich_automatic.rb:15: warning: unknown instance variable @chese. Did you mean: @cheese?
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
- The `Checked` module automatically calls `check_ivars` after initialization, which means it will emit warnings for every instance of the class.
66
+ ...there's nothing to tell us we got the variable wrong.
72
67
 
73
- ### Automatic Validation (Once Per Class)
68
+ > I thought that's why we're supposed to use attr_reader and friends.
74
69
 
75
- Too many warnings? Try this:
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
- # sandwich_once.rb
79
- require "ivar"
73
+ class MyClass
74
+ attr_accessor :usage_count
80
75
 
81
- class Sandwich
82
- include Ivar::Checked
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
- def to_s
93
- "A #{@bread} sandwich with #{@chese} and #{@condiments.join(", ")}"
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
- ```shell
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
- ### Pre-declaring Instance Variables
108
+ > And then what... spooky magic happens?
108
109
 
109
- Normally we "declare" variables by setting them in `initialize`. But if you don't have any reason to set them in the initializer, you can still declare them so they won't be flagged.
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 SandwichWithIvarMacro
115
+ class Pizza
116
116
  include Ivar::Checked
117
117
 
118
- # Pre-declare only instance variables that might be referenced before being set
119
- # You don't need to include variables that are always set in initialize
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
- def to_s
130
- result = "A #{@bread} sandwich with #{@cheese} and #{@condiments.join(", ")}"
131
- # This won't trigger a warning because @side is pre-initialized
132
- result += " and a side of #{@side}" if @side
133
- result
134
- end
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
- def add_side(side)
137
- @side = side
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
- sandwich = SandwichWithIvarMacro.new
142
- puts sandwich.to_s # No warning about @side
145
+ > So `check_ivars` is the magic method that does the checking?
143
146
 
144
- sandwich.add_side("chips")
145
- puts sandwich.to_s
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
- Note: this WILL set the variable to `nil` before `initialize` runs, so if you have code that depends on `defined?(@var)` it may break. If folks want it we might look into non-setting predeclaration.
155
+ Yep!
149
156
 
150
- ### Setting ivars from initializer keyword arguments
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
- While we're messing around with ivars, let's fix Ruby's oldest missing convenience feature:
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 SandwichWithKwarg
159
- include Ivar::Checked
160
-
161
- ivar kwarg: [:@bread, :@cheese, :@condiments, :@pickles, :@side]
164
+ class Pizza
165
+ include Ivar::Validation
162
166
 
163
- def to_s
164
- result = "A #{@bread} sandwich with #{@cheese}"
165
- result += " and #{@condiments.join(", ")}" unless @condiments.empty?
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
- # Create a sandwich with keyword arguments
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
- puts sandwich.to_s # Outputs: A wheat sandwich with muenster and mayo, mustard and a side of chips
181
- ```
177
+ ```ruby
178
+ require "ivar"
182
179
 
183
- Ta-da, no more tedious setting of instance variables from arguments of the same name.
180
+ class Pizza
181
+ include Ivar::Checked
184
182
 
185
- TODO: Find a positional args version of this that makes sense.
183
+ ivar :@minutes_waiting
186
184
 
187
- ### Inheritance
185
+ def increment_wait_time
186
+ (@minutes_waiting ||= 0) += 1
187
+ end
188
+ # ...
189
+ end
190
+ ```
188
191
 
189
- This stuff works with inheritance:
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
- class BaseSandwich
195
+ require "ivar"
196
+
197
+ class Pizza
193
198
  include Ivar::Checked
194
199
 
195
- # Pre-declare only variables that might be referenced before being set
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 initialize
200
- @bread = "wheat"
201
- @cheese = "muenster"
202
+ def increment_wait_time
203
+ @minutes_waiting += 1
202
204
  end
205
+ # ...
203
206
  end
207
+ ```
204
208
 
205
- class SpecialtySandwich < BaseSandwich
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
- def initialize
211
- super
212
- @condiments = ["mayo", "mustard"]
213
- end
211
+ Yes, you can pass a block that generates the value:
214
212
 
215
- def to_s
216
- result = "A #{@bread} sandwich with #{@cheese} and #{@condimants.join(", ")}"
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
- SpecialtySandwich.new
226
- ```
216
+ class Pizza
217
+ include Ivar::Checked
227
218
 
228
- ```shell
229
- $ ruby inheritance_example.rb -w
230
- inheritance_example.rb:17: warning: unknown instance variable @condimants. Did you mean: @condiments?
219
+ ivar(:@order_time) { Time.now }
220
+ # ...
221
+ end
231
222
  ```
232
223
 
233
- ## Checking Policies
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
- Ivar supports different policies for handling unknown instance variables. You can specify a policy at the global level, class level, or per-check level.
226
+ ```ruby
227
+ require "ivar"
236
228
 
237
- ### Available Policies
229
+ class Pizza
230
+ include Ivar::Checked
238
231
 
239
- - `:warn` - Emit warnings for all unknown instance variables (default)
240
- - `:warn_once` - Emit warnings only once per class
241
- - `:raise` - Raise an exception for unknown instance variables
242
- - `:log` - Log unknown instance variables to a logger
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
- ### Setting a Global Policy
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
- # Set the global policy to raise exceptions
248
- Ivar.check_policy = :raise
246
+ require "ivar"
249
247
 
250
248
  class Sandwich
251
- include Ivar::Validation
249
+ include Ivar::Checked
252
250
 
253
- def initialize
254
- @bread = "wheat"
255
- check_ivars
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 #{@chese}" # This will raise an exception
256
+ "A #{@bread} sandwich with #{@cheese} and #{@condiments.join(", ")}"
260
257
  end
261
258
  end
262
259
 
263
- Sandwich.new # Raises: NameError: test_file.rb:2: unknown instance variable @chese. Did you mean: @cheese?
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
- ### Setting a Class-Level Policy
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
- class Sandwich
270
- include Ivar::Validation
271
- extend Ivar::CheckPolicy
271
+ require "ivar"
272
272
 
273
- # Set the class-level policy to log
274
- ivar_check_policy :log, logger: Logger.new($stderr)
273
+ class Sandwich
274
+ include Ivar::Checked
275
275
 
276
- def initialize
277
- @bread = "wheat"
278
- check_ivars
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 #{@chese}" # This will log a warning
281
+ "A #{@bread} sandwich with #{@cheese} and #{@condiments.join(", ")}"
283
282
  end
284
283
  end
285
284
 
286
- Sandwich.new # Logs: W, [2023-06-01T12:34:56.789123 #12345] WARN -- : test_file.rb:2: unknown instance variable @chese. Did you mean: @cheese?
285
+ s = Sandwich.new("wheat", "muenster", ["mayo"])
286
+ s.to_s # => "A wheat sandwich with muenster and mayo"
287
287
  ```
288
288
 
289
- ### Setting a Per-Check Policy
289
+ > What if I also want external accessor methods?
290
+
291
+ Gotcha covered.
290
292
 
291
293
  ```ruby
292
- class Sandwich
293
- include Ivar::Validation
294
+ require "ivar"
294
295
 
295
- def initialize
296
- @bread = "wheat"
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
- def to_s
302
- "A #{@bread} sandwich with #{@chese}"
303
- end
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 # Raises: NameError: test_file.rb:2: unknown instance variable @chese. Did you mean: @cheese?
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
- ### Using the Checked Module with Policies
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
- The `Checked` module sets a default policy:
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
- - `Checked` sets the policy to `:warn`
323
+ More questions?
314
324
 
315
- You can override the default policy:
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
- class Sandwich
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
- # Override the default policy
322
- ivar_check_policy :raise
352
+ ...or when invoking `check_ivars`:
323
353
 
324
- def initialize
325
- @bread = "wheat"
326
- end
354
+ ```ruby
355
+ require "ivar"
327
356
 
328
- def to_s
329
- "A #{@bread} sandwich with #{@chese}" # This will raise an exception
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
- Sandwich.new # Raises: NameError: test_file.rb:2: unknown instance variable @chese. Did you mean: @cheese?
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
- # Acknowledgements
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
- Thank you to Joel Drapper, for inspiring me with his
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
- # TODO
406
+ ## Contribution
341
407
 
342
- - Pre-declare "ghost" variables without setting them
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
@@ -6,7 +6,7 @@ require "rake/testtask"
6
6
  Rake::TestTask.new(:test) do |t|
7
7
  t.libs << "test"
8
8
  t.libs << "lib"
9
- t.test_files = FileList["test/**/test_*.rb"]
9
+ t.test_files = FileList["test/**/test_*.rb"].exclude("test/fixtures/**/*")
10
10
  end
11
11
 
12
12
  require "standard/rake"
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