strict_ivars 0.4.0 → 0.4.1
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/README.md +50 -10
- data/lib/strict_ivars/processor.rb +25 -8
- data/lib/strict_ivars/version.rb +1 -1
- data/lib/strict_ivars.rb +1 -0
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2fdc65115a894c678d04614ece5855c2089003d320fe6bf22cb9c9ef0aa5de50
|
4
|
+
data.tar.gz: cb51570cf483f8a952daf0eb4798ac430c699f5357474c80d37cf4c75cc8b073
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 669e9cd076e7be979fa8d145cf9c0413794eeddd650dc0c183a67f2341809217dcd41059050fd63143311b9afc8c0c44dae82ade79e94967929c080d55da9bc0
|
7
|
+
data.tar.gz: 901f1b60e232b8dccc0fbd6bf36f50b906b702e7396fffec47fa8e180e3390629ef0e7733e736e341f2ea482fc255d0870a96cbbccaf74cbfe0988a2d6df0cc0
|
data/README.md
CHANGED
@@ -1,39 +1,79 @@
|
|
1
|
-
#
|
1
|
+
# Strict Ivars
|
2
2
|
|
3
|
-
|
3
|
+
Strict Ivars is a tiny pre-processor for Ruby that guards your instance variable reads, ensuring the instance variable is actually defined. This helps catch typos nice and early.
|
4
4
|
|
5
5
|
## How does it work?
|
6
6
|
|
7
|
-
When
|
7
|
+
When Strict Ivars detects that you are loading code from paths its configured to handle, it quickly looks for instance variable reads and guards them with a `defined?` check.
|
8
8
|
|
9
9
|
For example, it will replace this:
|
10
10
|
|
11
11
|
```ruby
|
12
12
|
def example
|
13
|
-
|
13
|
+
foo if @bar
|
14
14
|
end
|
15
15
|
```
|
16
16
|
|
17
|
-
with something like this:
|
17
|
+
...with something like this:
|
18
18
|
|
19
19
|
```ruby
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
def example
|
21
|
+
foo if (defined?(@bar) ? @bar : raise)
|
22
|
+
end
|
23
23
|
```
|
24
24
|
|
25
|
+
The replacement happens on load, so you never see this in your source code. It’s also always wrapped in parentheses and takes up a single line, so it won’t mess up the line numbers in exceptions.
|
26
|
+
|
27
|
+
The real guard is a little uglier than this. It uses `::Kernel.raise` so it’s compatible with `BasicObject`. It also raises a `StrictIvars::NameError` with a helpful message mentioning the name of the instance variable, and that inherits from `NameError`, allowing you to rescue either `NameError` or `StrictIvars::NameError`.
|
28
|
+
|
25
29
|
## Setup
|
26
30
|
|
27
|
-
Install the gem by adding it to your `Gemfile` and running `bundle install`.
|
31
|
+
Install the gem by adding it to your `Gemfile` and running `bundle install`.
|
32
|
+
|
33
|
+
You may want to set it to `require: false` here because you should require it manually at precisely the right moment.
|
28
34
|
|
29
35
|
```ruby
|
30
36
|
gem "strict_ivars", require: false
|
31
37
|
```
|
32
38
|
|
33
|
-
|
39
|
+
Now the gem is installed, you should require and initialize the gem as early as possible in your boot process. Ideally, this should be right after Bootsnap is set up. In Rails, this will be in your `boot.rb` file.
|
34
40
|
|
35
41
|
```ruby
|
36
42
|
require "strict_ivars"
|
37
43
|
|
38
44
|
StrictIvars.init(include: ["#{Dir.pwd}/**/*"])
|
39
45
|
```
|
46
|
+
|
47
|
+
You can pass an array of globs to `include:` and `exclude:`.
|
48
|
+
|
49
|
+
## Compatibility
|
50
|
+
|
51
|
+
Because Strict Ivars only transforms the source code that matches your include paths and becuase the check happens at runtime, it’s completely compatible with the rest of the Ruby ecosystem.
|
52
|
+
|
53
|
+
#### For apps
|
54
|
+
|
55
|
+
Strict Ivars is really designed for apps, where you control the boot process and you want some extra safety in the code you and your team writes.
|
56
|
+
|
57
|
+
#### For libraries
|
58
|
+
|
59
|
+
You could use Strict Ivars as a dev dependency in your gem’s test suite, but I don’t recommend initializing Strict Ivars in a library directly.
|
60
|
+
|
61
|
+
## Performance
|
62
|
+
|
63
|
+
#### Startup performance
|
64
|
+
|
65
|
+
Using Strict Ivars will impact startup performance since it needs to process each Ruby file you require. However, if you are using Bootsnap, the processed RubyVM::InstructionSequences will be cached and you probably won’t notice the incremental cache misses day-to-day.
|
66
|
+
|
67
|
+
#### Runtime performance
|
68
|
+
|
69
|
+
In my benchmarks on Ruby 3.4 with YJIT, it’s difficult to tell if there is any performance difference with or without the `defined?` guards at runtime. Sometimes it’s about 1% faster with the guards than without. Sometimes the other way around.
|
70
|
+
|
71
|
+
On my laptop, a method that returns an instance varible takes about 15ns and a method that checks if an instance varible is defined and then returns it takes about 15ns.
|
72
|
+
|
73
|
+
All this is to say, I don’t think there will be any measurable runtime performance impact.
|
74
|
+
|
75
|
+
## Uninstall
|
76
|
+
|
77
|
+
Becuase Strict Ivars only ever makes your code safer, you can always back out without anything breaking.
|
78
|
+
|
79
|
+
To uninstall Strict Ivars, first remove the require and initialization code from wherever you added it and then remove the gem from your `Gemfile`. If you were using Bootsnap, there’s a good chance it cached some pre-processed code with the instance variable read guards in it. To clear this, you’ll need to delete your bootsnap cache, which should be in `tmp/cache/bootsnap`.
|
@@ -1,18 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class StrictIvars::Processor < Prism::Visitor
|
4
|
+
#: (String) -> String
|
4
5
|
def self.call(source)
|
5
6
|
visitor = new
|
6
7
|
visitor.visit(Prism.parse(source).value)
|
7
|
-
|
8
8
|
buffer = source.dup
|
9
|
+
annotations = visitor.annotations
|
10
|
+
annotations.sort_by!(&:first)
|
9
11
|
|
10
|
-
|
12
|
+
annotations.reverse_each do |offset, action, name|
|
11
13
|
case action
|
12
14
|
when :start
|
13
|
-
buffer.insert(offset, "(
|
15
|
+
buffer.insert(offset, "(defined?(#{name}) ? ")
|
14
16
|
when :end
|
15
|
-
buffer.insert(offset, ")")
|
17
|
+
buffer.insert(offset, " : (::Kernel.raise(::StrictIvars::NameError.new('Undefined instance variable #{name}'))))")
|
16
18
|
else
|
17
19
|
raise "Invalid annotation"
|
18
20
|
end
|
@@ -26,36 +28,46 @@ class StrictIvars::Processor < Prism::Visitor
|
|
26
28
|
@annotations = []
|
27
29
|
end
|
28
30
|
|
31
|
+
#: Array[[Integer, :start | :end, Symbol]]
|
29
32
|
attr_reader :annotations
|
30
33
|
|
34
|
+
#: (Prism::ClassNode) -> void
|
31
35
|
def visit_class_node(node)
|
32
36
|
new_context { super }
|
33
37
|
end
|
34
38
|
|
39
|
+
#: (Prism::ModuleNode) -> void
|
35
40
|
def visit_module_node(node)
|
36
41
|
new_context { super }
|
37
42
|
end
|
38
43
|
|
44
|
+
#: (Prism::BlockNode) -> void
|
39
45
|
def visit_block_node(node)
|
40
46
|
new_context { super }
|
41
47
|
end
|
42
48
|
|
49
|
+
#: (Prism::SingletonClassNode) -> void
|
43
50
|
def visit_singleton_class_node(node)
|
44
51
|
new_context { super }
|
45
52
|
end
|
46
53
|
|
54
|
+
#: (Prism::DefinedNode) -> void
|
47
55
|
def visit_defined_node(node)
|
48
|
-
|
49
|
-
|
56
|
+
value = node.value
|
57
|
+
|
58
|
+
if Prism::InstanceVariableReadNode === value
|
59
|
+
@context << value.name
|
50
60
|
end
|
51
61
|
|
52
62
|
super
|
53
63
|
end
|
54
64
|
|
65
|
+
#: (Prism::DefNode) -> void
|
55
66
|
def visit_def_node(node)
|
56
67
|
new_context { super }
|
57
68
|
end
|
58
69
|
|
70
|
+
#: (Prism::IfNode) -> void
|
59
71
|
def visit_if_node(node)
|
60
72
|
visit(node.predicate)
|
61
73
|
|
@@ -63,6 +75,7 @@ class StrictIvars::Processor < Prism::Visitor
|
|
63
75
|
branch { visit(node.subsequent) }
|
64
76
|
end
|
65
77
|
|
78
|
+
#: (Prism::CaseNode) -> void
|
66
79
|
def visit_case_node(node)
|
67
80
|
visit(node.predicate)
|
68
81
|
|
@@ -73,6 +86,7 @@ class StrictIvars::Processor < Prism::Visitor
|
|
73
86
|
branch { visit(node.else_clause) }
|
74
87
|
end
|
75
88
|
|
89
|
+
#: (Prism::InstanceVariableReadNode) -> void
|
76
90
|
def visit_instance_variable_read_node(node)
|
77
91
|
name = node.name
|
78
92
|
|
@@ -81,13 +95,15 @@ class StrictIvars::Processor < Prism::Visitor
|
|
81
95
|
|
82
96
|
@context << name
|
83
97
|
|
84
|
-
@annotations <<
|
85
|
-
|
98
|
+
@annotations <<
|
99
|
+
[location.start_character_offset, :start, name] <<
|
100
|
+
[location.end_character_offset, :end, name]
|
86
101
|
end
|
87
102
|
|
88
103
|
super
|
89
104
|
end
|
90
105
|
|
106
|
+
#: () { () -> void } -> void
|
91
107
|
private def new_context
|
92
108
|
original_context = @context
|
93
109
|
|
@@ -100,6 +116,7 @@ class StrictIvars::Processor < Prism::Visitor
|
|
100
116
|
end
|
101
117
|
end
|
102
118
|
|
119
|
+
#: () { () -> void } -> void
|
103
120
|
private def branch
|
104
121
|
original_context = @context
|
105
122
|
@context = original_context.dup
|
data/lib/strict_ivars/version.rb
CHANGED
data/lib/strict_ivars.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strict_ivars
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joel Drapper
|
@@ -13,16 +13,16 @@ dependencies:
|
|
13
13
|
name: require-hooks
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
|
-
- - "
|
16
|
+
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: '0'
|
18
|
+
version: '0.2'
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
|
-
- - "
|
23
|
+
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: '0'
|
25
|
+
version: '0.2'
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: prism
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|