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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e9fa5d49d58c37b01f9c2e11b74ebea9809bfd880a4a3a86fc37894727bedb6
4
- data.tar.gz: acd5fcdc8320f9f1c2f972915b0d72ed7f1e21516d1790f77f91bcd345f5e7d9
3
+ metadata.gz: 2fdc65115a894c678d04614ece5855c2089003d320fe6bf22cb9c9ef0aa5de50
4
+ data.tar.gz: cb51570cf483f8a952daf0eb4798ac430c699f5357474c80d37cf4c75cc8b073
5
5
  SHA512:
6
- metadata.gz: c382d756d7d25ad5ce12706a0e80a886e4d8759616e8c2d505cdd40a7dd735d21e09a1c07c325062edd452e66a3aeae4b9a560cb08296a654e027b8d07949b84
7
- data.tar.gz: 415b00bd3f828d6b8f211774476f88e0f37ee4ed4c14d1211daf2ae68d75aa027bf60bcc79fd0ee84cd7958422780a60b715a69f8a93502ff3f63acb33b01052
6
+ metadata.gz: 669e9cd076e7be979fa8d145cf9c0413794eeddd650dc0c183a67f2341809217dcd41059050fd63143311b9afc8c0c44dae82ade79e94967929c080d55da9bc0
7
+ data.tar.gz: 901f1b60e232b8dccc0fbd6bf36f50b906b702e7396fffec47fa8e180e3390629ef0e7733e736e341f2ea482fc255d0870a96cbbccaf74cbfe0988a2d6df0cc0
data/README.md CHANGED
@@ -1,39 +1,79 @@
1
- # StrictIvars
1
+ # Strict Ivars
2
2
 
3
- StrictIvars is a tiny pre-processor for Ruby that guards your instance variable reads, ensuring the instance variable is actually defined. This helps catch typos early.
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 StrictIvars 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.
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
- foo if @bar
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
- def example
21
- foo if (raise unless defined?(@bar); @bar)
22
- end
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`. You may want to set it to `require: false` because you need to require it at the right moment.
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
- Then require and initialize the gem as early as possible in your boot process. Ideally, this should be right after bootsnap.
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
- visitor.annotations.sort_by(&:first).reverse_each do |offset, action, name|
12
+ annotations.reverse_each do |offset, action, name|
11
13
  case action
12
14
  when :start
13
- buffer.insert(offset, "((::Kernel.raise ::StrictIvars::NameError.new('Undefined instance variable #{name}') unless defined?(#{name})); ")
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
- if Prism::InstanceVariableReadNode === node.value
49
- @context << node.value.name
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 << [location.start_character_offset, :start, name]
85
- @annotations << [location.end_character_offset, :end, name]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StrictIvars
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/strict_ivars.rb CHANGED
@@ -6,6 +6,7 @@ require "require-hooks/setup"
6
6
  module StrictIvars
7
7
  NameError = Class.new(::NameError)
8
8
 
9
+ #: (include: Array[String], exclude: Array[String]) -> void
9
10
  def self.init(include: [], exclude: [])
10
11
  RequireHooks.source_transform(
11
12
  patterns: include,
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.0
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