ivar 0.2.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 +7 -0
- data/.augment-guidelines +10 -0
- data/.devcontainer/Dockerfile +21 -0
- data/.devcontainer/devcontainer.json +22 -0
- data/.standard.yml +3 -0
- data/.vscode/extensions.json +8 -0
- data/.vscode/settings.json +14 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +343 -0
- data/Rakefile +14 -0
- data/examples/sandwich_inheritance.rb +33 -0
- data/examples/sandwich_with_checked.rb +23 -0
- data/examples/sandwich_with_checked_once.rb +29 -0
- data/examples/sandwich_with_ivar_block.rb +43 -0
- data/examples/sandwich_with_ivar_macro.rb +37 -0
- data/examples/sandwich_with_kwarg.rb +45 -0
- data/ivar.gemspec +49 -0
- data/lib/ivar/auto_check.rb +77 -0
- data/lib/ivar/checked.rb +40 -0
- data/lib/ivar/macros.rb +123 -0
- data/lib/ivar/policies.rb +147 -0
- data/lib/ivar/prism_analysis.rb +102 -0
- data/lib/ivar/validation.rb +55 -0
- data/lib/ivar/version.rb +5 -0
- data/lib/ivar.rb +53 -0
- data/sig/ivar.rbs +4 -0
- data/test_file.rb +1 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4f9a15e9e7bb48429652a717400a82480167376802939f68873b575b3fca766f
|
4
|
+
data.tar.gz: f57d59b780e126287966c116fd83be4848bbbb05268591a3938262fb25395a08
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: acc557b2df216279c16bfa299a13017eae33f52622a8f5ef75729b253f00a414f37b8a2e44ae935f3d761a1a02477b3978297751f1e94d39b90a2005fbe6d5fd
|
7
|
+
data.tar.gz: 8acf31444752ee0ef75ac3a4d197448b37cac3f193e8844c57545d6d222e9899586129df849ea3a5e29628608d07a0390591455c82bdbfea60e5bf8826c347dc
|
data/.augment-guidelines
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
- tests are in minitest
|
2
|
+
- use modern Ruby 3.4 style and features, including pattern-matching, endless methods, etc., where appropriate.
|
3
|
+
- avoid over-engineering. Prefer basic Ruby data types to building new abstractions. (But extract abstractions when I tell you to.)
|
4
|
+
- avoid inline comments. Prefer "explaining variables", intention-revealing method names, and the "composed method" pattern where appropriate. Class, module, and method-documenting comments are fine.
|
5
|
+
- De-lint (with standardrb) before committing. Use auto-fix and manually fix the rest.
|
6
|
+
- Run all tests and fix failures before committing.
|
7
|
+
- Never "cheat" by making implementation code test-aware. But it is fine to make interactions with the world dependency-injectable for testing purposes.
|
8
|
+
- Include a quote of my instruction that led to the commit in the commit message
|
9
|
+
- Sign your commit messages with "-- Auggie".
|
10
|
+
- Commit and push after making successful changes.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
FROM ruby:3
|
2
|
+
|
3
|
+
ARG USERNAME=devcontainer
|
4
|
+
ARG USER_UID=1000
|
5
|
+
ARG USER_GID=$USER_UID
|
6
|
+
|
7
|
+
# Install basic development tools
|
8
|
+
RUN apt update && apt install -y less man-db sudo
|
9
|
+
|
10
|
+
# Set up unprivileged local user
|
11
|
+
RUN groupadd --gid $USER_GID $USERNAME \
|
12
|
+
&& groupadd bundler \
|
13
|
+
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME --shell /bin/bash --groups bundler \
|
14
|
+
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
|
15
|
+
&& chmod 0440 /etc/sudoers.d/$USERNAME
|
16
|
+
|
17
|
+
# Set unprivileged user as default user
|
18
|
+
USER $USERNAME
|
19
|
+
|
20
|
+
# Set `DEVCONTAINER` environment variable to help with orientation
|
21
|
+
ENV DEVCONTAINER=true
|
@@ -0,0 +1,22 @@
|
|
1
|
+
// See https://containers.dev/implementors/json_reference/ for configuration reference
|
2
|
+
{
|
3
|
+
"name": "ivar",
|
4
|
+
"build": {
|
5
|
+
"dockerfile": "Dockerfile"
|
6
|
+
},
|
7
|
+
"remoteUser": "devcontainer",
|
8
|
+
"features": {
|
9
|
+
"ghcr.io/devcontainers/features/github-cli:1": {},
|
10
|
+
"ghcr.io/jungaretti/features/ripgrep:1": {}
|
11
|
+
},
|
12
|
+
"customizations": {
|
13
|
+
"vscode": {
|
14
|
+
"extensions": [
|
15
|
+
"connorshea.vscode-ruby-test-adapter",
|
16
|
+
"stripe.endsmart",
|
17
|
+
"testdouble.vscode-standard-ruby",
|
18
|
+
"castwide.solargraph",
|
19
|
+
]
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
data/.standard.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
{
|
2
|
+
"rubyTestExplorer.testFramework": "minitest",
|
3
|
+
"[ruby]": {
|
4
|
+
"editor.formatOnType": true,
|
5
|
+
},
|
6
|
+
"editor.formatOnSave": true,
|
7
|
+
"standardRuby.mode": "enableUnconditionally",
|
8
|
+
"solargraph.diagnostics": true,
|
9
|
+
"solargraph.formatting": true,
|
10
|
+
"solargraph.useBundler": true,
|
11
|
+
"standardRuby.commandPath": "/usr/local/bundle/bin/standardrb",
|
12
|
+
"rubyLsp.formatter": "standard",
|
13
|
+
"standardRuby.autofix": true,
|
14
|
+
}
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
6
|
+
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
8
|
+
|
9
|
+
## Our Standards
|
10
|
+
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
12
|
+
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
18
|
+
|
19
|
+
Examples of unacceptable behavior include:
|
20
|
+
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or
|
22
|
+
advances of any kind
|
23
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
24
|
+
* Public or private harassment
|
25
|
+
* Publishing others' private information, such as a physical or email
|
26
|
+
address, without their explicit permission
|
27
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
28
|
+
professional setting
|
29
|
+
|
30
|
+
## Enforcement Responsibilities
|
31
|
+
|
32
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
33
|
+
|
34
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
35
|
+
|
36
|
+
## Scope
|
37
|
+
|
38
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
39
|
+
|
40
|
+
## Enforcement
|
41
|
+
|
42
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at avdi@avdi.codes. All complaints will be reviewed and investigated promptly and fairly.
|
43
|
+
|
44
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
45
|
+
|
46
|
+
## Enforcement Guidelines
|
47
|
+
|
48
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
49
|
+
|
50
|
+
### 1. Correction
|
51
|
+
|
52
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
53
|
+
|
54
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
55
|
+
|
56
|
+
### 2. Warning
|
57
|
+
|
58
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
59
|
+
|
60
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
61
|
+
|
62
|
+
### 3. Temporary Ban
|
63
|
+
|
64
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
65
|
+
|
66
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
67
|
+
|
68
|
+
### 4. Permanent Ban
|
69
|
+
|
70
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
71
|
+
|
72
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
73
|
+
|
74
|
+
## Attribution
|
75
|
+
|
76
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
77
|
+
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
78
|
+
|
79
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
80
|
+
|
81
|
+
[homepage]: https://www.contributor-covenant.org
|
82
|
+
|
83
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
84
|
+
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Avdi Grimm
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,343 @@
|
|
1
|
+
# Ivar
|
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.
|
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
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
# sandwich.rb
|
16
|
+
require "ivar"
|
17
|
+
|
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])
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"A #{@bread} sandwich with #{@chese} and #{@condiments.join(", ")}" \
|
30
|
+
(@side ? "and a side of #{@side}" : "")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Sandwich.new
|
35
|
+
```
|
36
|
+
|
37
|
+
```shell
|
38
|
+
$ ruby sandwich.rb -w
|
39
|
+
sandwich.rb:22: warning: unknown instance variable @chese. Did you mean: @cheese?
|
40
|
+
```
|
41
|
+
|
42
|
+
### Automatic Validation (Every Instance)
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
# sandwich_automatic.rb
|
46
|
+
require "ivar"
|
47
|
+
|
48
|
+
class Sandwich
|
49
|
+
include Ivar::Checked
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
@bread = "white"
|
53
|
+
@cheese = "havarti"
|
54
|
+
@condiments = ["mayo", "mustard"]
|
55
|
+
# no need for explicit check_ivars call
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
"A #{@bread} sandwich with #{@chese} and #{@condiments.join(", ")}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
Sandwich.new
|
64
|
+
```
|
65
|
+
|
66
|
+
```shell
|
67
|
+
$ ruby sandwich_automatic.rb -w
|
68
|
+
sandwich_automatic.rb:15: warning: unknown instance variable @chese. Did you mean: @cheese?
|
69
|
+
```
|
70
|
+
|
71
|
+
The `Checked` module automatically calls `check_ivars` after initialization, which means it will emit warnings for every instance of the class.
|
72
|
+
|
73
|
+
### Automatic Validation (Once Per Class)
|
74
|
+
|
75
|
+
Too many warnings? Try this:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
# sandwich_once.rb
|
79
|
+
require "ivar"
|
80
|
+
|
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
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_s
|
93
|
+
"A #{@bread} sandwich with #{@chese} and #{@condiments.join(", ")}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
Sandwich.new
|
98
|
+
```
|
99
|
+
|
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
|
+
|
107
|
+
### Pre-declaring Instance Variables
|
108
|
+
|
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
|
+
|
111
|
+
```ruby
|
112
|
+
# sandwich_with_ivar_macro.rb
|
113
|
+
require "ivar"
|
114
|
+
|
115
|
+
class SandwichWithIvarMacro
|
116
|
+
include Ivar::Checked
|
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
|
127
|
+
end
|
128
|
+
|
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
|
135
|
+
|
136
|
+
def add_side(side)
|
137
|
+
@side = side
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
sandwich = SandwichWithIvarMacro.new
|
142
|
+
puts sandwich.to_s # No warning about @side
|
143
|
+
|
144
|
+
sandwich.add_side("chips")
|
145
|
+
puts sandwich.to_s
|
146
|
+
```
|
147
|
+
|
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.
|
149
|
+
|
150
|
+
### Setting ivars from initializer keyword arguments
|
151
|
+
|
152
|
+
While we're messing around with ivars, let's fix Ruby's oldest missing convenience feature:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# sandwich_with_kwarg.rb
|
156
|
+
require "ivar"
|
157
|
+
|
158
|
+
class SandwichWithKwarg
|
159
|
+
include Ivar::Checked
|
160
|
+
|
161
|
+
ivar kwarg: [:@bread, :@cheese, :@condiments, :@pickles, :@side]
|
162
|
+
|
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
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
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
|
+
)
|
179
|
+
|
180
|
+
puts sandwich.to_s # Outputs: A wheat sandwich with muenster and mayo, mustard and a side of chips
|
181
|
+
```
|
182
|
+
|
183
|
+
Ta-da, no more tedious setting of instance variables from arguments of the same name.
|
184
|
+
|
185
|
+
TODO: Find a positional args version of this that makes sense.
|
186
|
+
|
187
|
+
### Inheritance
|
188
|
+
|
189
|
+
This stuff works with inheritance:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
class BaseSandwich
|
193
|
+
include Ivar::Checked
|
194
|
+
|
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
|
198
|
+
|
199
|
+
def initialize
|
200
|
+
@bread = "wheat"
|
201
|
+
@cheese = "muenster"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
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
|
+
|
210
|
+
def initialize
|
211
|
+
super
|
212
|
+
@condiments = ["mayo", "mustard"]
|
213
|
+
end
|
214
|
+
|
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
|
224
|
+
|
225
|
+
SpecialtySandwich.new
|
226
|
+
```
|
227
|
+
|
228
|
+
```shell
|
229
|
+
$ ruby inheritance_example.rb -w
|
230
|
+
inheritance_example.rb:17: warning: unknown instance variable @condimants. Did you mean: @condiments?
|
231
|
+
```
|
232
|
+
|
233
|
+
## Checking Policies
|
234
|
+
|
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.
|
236
|
+
|
237
|
+
### Available Policies
|
238
|
+
|
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
|
243
|
+
|
244
|
+
### Setting a Global Policy
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
# Set the global policy to raise exceptions
|
248
|
+
Ivar.check_policy = :raise
|
249
|
+
|
250
|
+
class Sandwich
|
251
|
+
include Ivar::Validation
|
252
|
+
|
253
|
+
def initialize
|
254
|
+
@bread = "wheat"
|
255
|
+
check_ivars
|
256
|
+
end
|
257
|
+
|
258
|
+
def to_s
|
259
|
+
"A #{@bread} sandwich with #{@chese}" # This will raise an exception
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
Sandwich.new # Raises: NameError: test_file.rb:2: unknown instance variable @chese. Did you mean: @cheese?
|
264
|
+
```
|
265
|
+
|
266
|
+
### Setting a Class-Level Policy
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
class Sandwich
|
270
|
+
include Ivar::Validation
|
271
|
+
extend Ivar::CheckPolicy
|
272
|
+
|
273
|
+
# Set the class-level policy to log
|
274
|
+
ivar_check_policy :log, logger: Logger.new($stderr)
|
275
|
+
|
276
|
+
def initialize
|
277
|
+
@bread = "wheat"
|
278
|
+
check_ivars
|
279
|
+
end
|
280
|
+
|
281
|
+
def to_s
|
282
|
+
"A #{@bread} sandwich with #{@chese}" # This will log a warning
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
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?
|
287
|
+
```
|
288
|
+
|
289
|
+
### Setting a Per-Check Policy
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
class Sandwich
|
293
|
+
include Ivar::Validation
|
294
|
+
|
295
|
+
def initialize
|
296
|
+
@bread = "wheat"
|
297
|
+
# Use the raise policy for this check
|
298
|
+
check_ivars(policy: :raise)
|
299
|
+
end
|
300
|
+
|
301
|
+
def to_s
|
302
|
+
"A #{@bread} sandwich with #{@chese}"
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
Sandwich.new # Raises: NameError: test_file.rb:2: unknown instance variable @chese. Did you mean: @cheese?
|
307
|
+
```
|
308
|
+
|
309
|
+
### Using the Checked Module with Policies
|
310
|
+
|
311
|
+
The `Checked` module sets a default policy:
|
312
|
+
|
313
|
+
- `Checked` sets the policy to `:warn`
|
314
|
+
|
315
|
+
You can override the default policy:
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
class Sandwich
|
319
|
+
include Ivar::Checked
|
320
|
+
|
321
|
+
# Override the default policy
|
322
|
+
ivar_check_policy :raise
|
323
|
+
|
324
|
+
def initialize
|
325
|
+
@bread = "wheat"
|
326
|
+
end
|
327
|
+
|
328
|
+
def to_s
|
329
|
+
"A #{@bread} sandwich with #{@chese}" # This will raise an exception
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
Sandwich.new # Raises: NameError: test_file.rb:2: unknown instance variable @chese. Did you mean: @cheese?
|
334
|
+
```
|
335
|
+
|
336
|
+
# Acknowledgements
|
337
|
+
|
338
|
+
Thank you to Joel Drapper, for inspiring me with his
|
339
|
+
|
340
|
+
# TODO
|
341
|
+
|
342
|
+
- Pre-declare "ghost" variables without setting them
|
343
|
+
- Add a module for dynamic checking of instance_variable_get/set
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "standard/rake"
|
13
|
+
|
14
|
+
task default: %i[test standard]
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ivar"
|
4
|
+
|
5
|
+
class BaseSandwich
|
6
|
+
include Ivar::Checked
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@bread = "wheat"
|
10
|
+
@cheese = "muenster"
|
11
|
+
end
|
12
|
+
|
13
|
+
def base_to_s
|
14
|
+
"A #{@bread} sandwich with #{@cheese}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class SpecialtySandwich < BaseSandwich
|
19
|
+
def initialize
|
20
|
+
super
|
21
|
+
@condiments = ["mayo", "mustard"]
|
22
|
+
@special_sauce = "secret sauce"
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
result = "#{base_to_s} with #{@condiments.join(", ")}"
|
27
|
+
result += " and #{@special_sause}" # Typo here
|
28
|
+
result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Create a specialty sandwich - this will automatically check instance variables
|
33
|
+
SpecialtySandwich.new
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ivar"
|
4
|
+
|
5
|
+
class SandwichWithChecked
|
6
|
+
include Ivar::Checked
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@bread = "wheat"
|
10
|
+
@cheese = "muenster"
|
11
|
+
@condiments = ["mayo", "mustard"]
|
12
|
+
# No need for explicit check_ivars call
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
result = "A #{@bread} sandwich with #{@chese} and #{@condiments.join(", ")}"
|
17
|
+
result += " and a side of #{@side}" if @side
|
18
|
+
result
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create a sandwich - this will automatically check instance variables
|
23
|
+
SandwichWithChecked.new
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ivar"
|
4
|
+
|
5
|
+
class SandwichWithCheckedOnce
|
6
|
+
include Ivar::Checked
|
7
|
+
ivar_check_policy :warn_once
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@bread = "wheat"
|
11
|
+
@cheese = "muenster"
|
12
|
+
@condiments = %w[mayo mustard]
|
13
|
+
# No need for explicit check_ivars call
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
result = "A #{@bread} sandwich with #{@chese} and #{@condiments.join(", ")}"
|
18
|
+
result += " and a side of #{@side}" if @side
|
19
|
+
result
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Create a sandwich - this will automatically check instance variables once
|
24
|
+
puts "Creating first sandwich..."
|
25
|
+
SandwichWithCheckedOnce.new
|
26
|
+
|
27
|
+
# Create another sandwich - this should not emit warnings
|
28
|
+
puts "Creating second sandwich..."
|
29
|
+
SandwichWithCheckedOnce.new
|