strict_ivars 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e9fa5d49d58c37b01f9c2e11b74ebea9809bfd880a4a3a86fc37894727bedb6
4
- data.tar.gz: acd5fcdc8320f9f1c2f972915b0d72ed7f1e21516d1790f77f91bcd345f5e7d9
3
+ metadata.gz: 65516e69e17c893c02c57e0cc0be683c57c9dc3fd91b80f56942453d5df43649
4
+ data.tar.gz: 16d3c581474be1f5a7500eb24e442ac8bcd54f55ee5d7afef6cdea3ef986381f
5
5
  SHA512:
6
- metadata.gz: c382d756d7d25ad5ce12706a0e80a886e4d8759616e8c2d505cdd40a7dd735d21e09a1c07c325062edd452e66a3aeae4b9a560cb08296a654e027b8d07949b84
7
- data.tar.gz: 415b00bd3f828d6b8f211774476f88e0f37ee4ed4c14d1211daf2ae68d75aa027bf60bcc79fd0ee84cd7958422780a60b715a69f8a93502ff3f63acb33b01052
6
+ metadata.gz: b92fa82ca1acbcef116a5ab531bb1a4ec08301db0b17eb9d5f5a99af47c29face2c82a51d0f63b905eec0a7506e2453b040731848a69afff951f6f44b18e789e
7
+ data.tar.gz: b650231f2312548af22bd11ecc031dbdcbf178a5ce4f501b29f48687a59209a6533c12a4ef8fe78a351cc03e591bbd3e01c1edf67f03568620279b3990ab0e63
data/README.md CHANGED
@@ -1,39 +1,123 @@
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. It‘s especially good when used with [Literal](https://literal.fun).
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
+ ```
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
+
29
+ **When you check defined already**
30
+
31
+ Within the same context (e.g. class definition, module definition, block, method), if you check `defined?`, Strict Ivars will not add a guard to reads of the checked instance variable.
32
+
33
+ ```ruby
34
+ if defined?(@foo)
35
+ @foo
36
+ end
37
+ ```
38
+
39
+ This applies even if the read is not in one of the conditional’s branches since you’ve indicated local awareness of the potential for this instance variable not being defined.
40
+
41
+ ```ruby
42
+ if defined?(@foo)
43
+ # anything
44
+ end
45
+
46
+ @foo
47
+ ```
48
+
49
+ **Writes:**
50
+
51
+ Strict Ivars doesn’t apply to writes, since these are considered the authoritative source of the instance variable definitions.
52
+
53
+ ```ruby
54
+ @foo = 1
55
+ ```
56
+
57
+ **Or-writes:**
58
+
59
+ This is considered a definition and not guarded.
60
+
61
+ ```ruby
62
+ @foo ||= 1
63
+ ```
64
+
65
+ **And-writes:**
66
+
67
+ This is considered a definition and not guarded.
68
+
69
+ ```ruby
70
+ @foo &&= 1
23
71
  ```
24
72
 
25
73
  ## Setup
26
74
 
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.
75
+ Install the gem by adding it to your `Gemfile` and running `bundle install`.
76
+
77
+ You may want to set it to `require: false` here because you should require it manually at precisely the right moment.
28
78
 
29
79
  ```ruby
30
80
  gem "strict_ivars", require: false
31
81
  ```
32
82
 
33
- Then require and initialize the gem as early as possible in your boot process. Ideally, this should be right after bootsnap.
83
+ 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
84
 
35
85
  ```ruby
36
86
  require "strict_ivars"
37
87
 
38
- StrictIvars.init(include: ["#{Dir.pwd}/**/*"])
88
+ StrictIvars.init(include: ["#{Dir.pwd}/**/*"], exclude: ["#{Dir.pwd}/vendor/**/*"])
39
89
  ```
90
+
91
+ You can pass an array of globs to `include:` and `exclude:`.
92
+
93
+ ## Compatibility
94
+
95
+ 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.
96
+
97
+ #### For apps
98
+
99
+ 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.
100
+
101
+ #### For libraries
102
+
103
+ 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.
104
+
105
+ ## Performance
106
+
107
+ #### Startup performance
108
+
109
+ 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.
110
+
111
+ #### Runtime performance
112
+
113
+ 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.
114
+
115
+ 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.
116
+
117
+ All this is to say, I don’t think there will be any measurable runtime performance impact.
118
+
119
+ ## Uninstall
120
+
121
+ Becuase Strict Ivars only ever makes your code safer, you can always back out without anything breaking.
122
+
123
+ 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`.
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class StrictIvars::Configuration
4
+ def initialize
5
+ @mutex = Mutex.new
6
+ @include = []
7
+ @exclude = []
8
+ end
9
+
10
+ #: (*String) -> void
11
+ def include(*patterns)
12
+ @mutex.synchronize do
13
+ @include.concat(patterns)
14
+ end
15
+ end
16
+
17
+ #: (*String) -> void
18
+ def exclude(*patterns)
19
+ @mutex.synchronize do
20
+ @exclude.concat(patterns)
21
+ end
22
+ end
23
+
24
+ #: (String) -> bool
25
+ def match(path)
26
+ path = File.absolute_path(path)
27
+ return false if @exclude.any? { |pattern| File.fnmatch?(pattern, path) }
28
+ return true if @include.any? { |pattern| File.fnmatch?(pattern, path) }
29
+ false
30
+ end
31
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrictIvars
4
+ module ModuleEvalPatch
5
+ #: (String, ?String, ?Integer) -> void
6
+ #: () { () -> void } -> void
7
+ def class_eval(*args)
8
+ source, file, lineno = args
9
+
10
+ file ||= caller_locations(1, 1).first.path
11
+
12
+ if source && file && CONFIG.match(file)
13
+ args[0] = Processor.call(source)
14
+ end
15
+
16
+ super
17
+ end
18
+
19
+ #: (String, ?String, ?Integer) -> void
20
+ #: () { () -> void } -> void
21
+ def module_eval(*args)
22
+ source, file, lineno = args
23
+
24
+ file ||= caller_locations(1, 1).first.path
25
+
26
+ if source && file && CONFIG.match(file)
27
+ args[0] = Processor.call(source)
28
+ end
29
+
30
+ super
31
+ end
32
+ end
33
+
34
+ module InstanceEvalPatch
35
+ #: (String, ?String, ?Integer) -> void
36
+ #: () { () -> void } -> void
37
+ def instance_eval(*args)
38
+ source, file, lineno = args
39
+
40
+ file ||= caller_locations(1, 1).first.path
41
+
42
+ if source && file && CONFIG.match(file)
43
+ args[0] = Processor.call(source)
44
+ end
45
+
46
+ super
47
+ end
48
+ end
49
+
50
+ module KernelEvalPatch
51
+ #: (String, Binding, ?String, ?Integer) -> void
52
+ def eval(*args)
53
+ source, binding, file, lineno = args
54
+
55
+ file ||= caller_locations(1, 1).first.path
56
+
57
+ if source && file && CONFIG.match(file)
58
+ args[0] = Processor.call(source.to_s)
59
+ end
60
+
61
+ super
62
+ end
63
+ end
64
+
65
+ module BindingEvalPatch
66
+ #: (String, ?String, ?Integer) -> void
67
+ def eval(*args)
68
+ source, file, lineno = args
69
+
70
+ file ||= caller_locations(1, 1).first.path
71
+
72
+ if source && file && CONFIG.match(file)
73
+ args[0] = Processor.call(source.to_s)
74
+ end
75
+
76
+ super
77
+ end
78
+ end
79
+
80
+ Kernel.prepend(KernelEvalPatch)
81
+ Module.prepend(ModuleEvalPatch)
82
+ Binding.prepend(BindingEvalPatch)
83
+ BasicObject.prepend(InstanceEvalPatch)
84
+ end
@@ -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.5.0"
5
5
  end
data/lib/strict_ivars.rb CHANGED
@@ -2,11 +2,21 @@
2
2
 
3
3
  require "prism"
4
4
  require "require-hooks/setup"
5
+ require "strict_ivars/version"
6
+ require "strict_ivars/configuration"
5
7
 
6
8
  module StrictIvars
7
9
  NameError = Class.new(::NameError)
8
10
 
11
+ CONFIG = Configuration.new
12
+
13
+ #: (include: Array[String], exclude: Array[String]) -> void
9
14
  def self.init(include: [], exclude: [])
15
+ require "strict_ivars/patch_eval"
16
+
17
+ CONFIG.include(*include)
18
+ CONFIG.exclude(*exclude)
19
+
10
20
  RequireHooks.source_transform(
11
21
  patterns: include,
12
22
  exclude_patterns: exclude
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.5.0
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
@@ -47,6 +47,8 @@ files:
47
47
  - LICENSE.txt
48
48
  - README.md
49
49
  - lib/strict_ivars.rb
50
+ - lib/strict_ivars/configuration.rb
51
+ - lib/strict_ivars/patch_eval.rb
50
52
  - lib/strict_ivars/processor.rb
51
53
  - lib/strict_ivars/version.rb
52
54
  homepage: https://github.com/joeldrapper/strict_ivars