strict_ivars 0.4.1 → 0.6.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 +4 -4
- data/README.md +26 -2
- data/lib/strict_ivars/base_processor.rb +57 -0
- data/lib/strict_ivars/configuration.rb +32 -0
- data/lib/strict_ivars/processor.rb +5 -45
- data/lib/strict_ivars/version.rb +1 -1
- data/lib/strict_ivars.rb +58 -6
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e1244d297abc3e3ade0882ca3a8767b63701c42721a7199803ad75b46c566ed
|
4
|
+
data.tar.gz: c141c19397e2942a40bb049ca77790ca9b29a6de9e0cd30e218162b1e51c4ee7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8cd2d471ca00c86a910843da2cacf803d71f30d48c946f469a5f18ef34c23b54d99d20897501a769695ff37114b677b8ce25c927a135876d99f1202324ad2e8f
|
7
|
+
data.tar.gz: ab6d3154078514bb0bd9615bae2cecdda743cb7a3dcd5f55d8571fae1f5201f94d090ebffd582881876f3b66bdcfd2035e4d600b77b7fa3350759f33deb4850a
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Strict Ivars
|
2
2
|
|
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.
|
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
|
|
@@ -26,6 +26,30 @@ The replacement happens on load, so you never see this in your source code. It
|
|
26
26
|
|
27
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
28
|
|
29
|
+
**Writes:**
|
30
|
+
|
31
|
+
Strict Ivars doesn’t apply to writes, since these are considered the authoritative source of the instance variable definitions.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
@foo = 1
|
35
|
+
```
|
36
|
+
|
37
|
+
**Or-writes:**
|
38
|
+
|
39
|
+
This is considered a definition and not guarded.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
@foo ||= 1
|
43
|
+
```
|
44
|
+
|
45
|
+
**And-writes:**
|
46
|
+
|
47
|
+
This is considered a definition and not guarded.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
@foo &&= 1
|
51
|
+
```
|
52
|
+
|
29
53
|
## Setup
|
30
54
|
|
31
55
|
Install the gem by adding it to your `Gemfile` and running `bundle install`.
|
@@ -41,7 +65,7 @@ Now the gem is installed, you should require and initialize the gem as early as
|
|
41
65
|
```ruby
|
42
66
|
require "strict_ivars"
|
43
67
|
|
44
|
-
StrictIvars.init(include: ["#{Dir.pwd}/**/*"])
|
68
|
+
StrictIvars.init(include: ["#{Dir.pwd}/**/*"], exclude: ["#{Dir.pwd}/vendor/**/*"])
|
45
69
|
```
|
46
70
|
|
47
71
|
You can pass an array of globs to `include:` and `exclude:`.
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
class StrictIvars::BaseProcessor < Prism::Visitor
|
6
|
+
EVAL_METHODS = Set[:class_eval, :module_eval, :instance_eval, :eval].freeze
|
7
|
+
|
8
|
+
#: (String) -> String
|
9
|
+
def self.call(source)
|
10
|
+
visitor = new
|
11
|
+
visitor.visit(Prism.parse(source).value)
|
12
|
+
buffer = source.dup
|
13
|
+
annotations = visitor.annotations
|
14
|
+
annotations.sort_by!(&:first)
|
15
|
+
|
16
|
+
annotations.reverse_each do |offset, string|
|
17
|
+
buffer.insert(offset, string)
|
18
|
+
end
|
19
|
+
|
20
|
+
buffer
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@context = Set[]
|
25
|
+
@annotations = []
|
26
|
+
end
|
27
|
+
|
28
|
+
#: Array[[Integer, :start_ivar_read | :end_ivar_read, Symbol]]
|
29
|
+
attr_reader :annotations
|
30
|
+
|
31
|
+
def visit_call_node(node)
|
32
|
+
name = node.name
|
33
|
+
|
34
|
+
if EVAL_METHODS.include?(name) && (arguments = node.arguments)
|
35
|
+
location = arguments.location
|
36
|
+
|
37
|
+
if node.receiver
|
38
|
+
receiver_local = "__eval_receiver_#{SecureRandom.hex(8)}__"
|
39
|
+
receiver_location = node.receiver.location
|
40
|
+
|
41
|
+
@annotations.push(
|
42
|
+
[receiver_location.start_character_offset, "(#{receiver_local} = "],
|
43
|
+
[receiver_location.end_character_offset, ")"],
|
44
|
+
[location.start_character_offset, "*(::StrictIvars.process_eval_args(#{receiver_local}, :#{name}, "],
|
45
|
+
[location.end_character_offset, "))"]
|
46
|
+
)
|
47
|
+
else
|
48
|
+
@annotations.push(
|
49
|
+
[location.start_character_offset, "*(::StrictIvars.process_eval_args(self, :#{name}, "],
|
50
|
+
[location.end_character_offset, "))"]
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
super
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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
|
+
return false unless String === path
|
27
|
+
path = File.absolute_path(path)
|
28
|
+
return false if @exclude.any? { |pattern| File.fnmatch?(pattern, path) }
|
29
|
+
return true if @include.any? { |pattern| File.fnmatch?(pattern, path) }
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
@@ -1,36 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class StrictIvars::Processor <
|
4
|
-
#: (String) -> String
|
5
|
-
def self.call(source)
|
6
|
-
visitor = new
|
7
|
-
visitor.visit(Prism.parse(source).value)
|
8
|
-
buffer = source.dup
|
9
|
-
annotations = visitor.annotations
|
10
|
-
annotations.sort_by!(&:first)
|
11
|
-
|
12
|
-
annotations.reverse_each do |offset, action, name|
|
13
|
-
case action
|
14
|
-
when :start
|
15
|
-
buffer.insert(offset, "(defined?(#{name}) ? ")
|
16
|
-
when :end
|
17
|
-
buffer.insert(offset, " : (::Kernel.raise(::StrictIvars::NameError.new('Undefined instance variable #{name}'))))")
|
18
|
-
else
|
19
|
-
raise "Invalid annotation"
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
buffer
|
24
|
-
end
|
25
|
-
|
26
|
-
def initialize
|
27
|
-
@context = Set[]
|
28
|
-
@annotations = []
|
29
|
-
end
|
30
|
-
|
31
|
-
#: Array[[Integer, :start | :end, Symbol]]
|
32
|
-
attr_reader :annotations
|
33
|
-
|
3
|
+
class StrictIvars::Processor < StrictIvars::BaseProcessor
|
34
4
|
#: (Prism::ClassNode) -> void
|
35
5
|
def visit_class_node(node)
|
36
6
|
new_context { super }
|
@@ -51,17 +21,6 @@ class StrictIvars::Processor < Prism::Visitor
|
|
51
21
|
new_context { super }
|
52
22
|
end
|
53
23
|
|
54
|
-
#: (Prism::DefinedNode) -> void
|
55
|
-
def visit_defined_node(node)
|
56
|
-
value = node.value
|
57
|
-
|
58
|
-
if Prism::InstanceVariableReadNode === value
|
59
|
-
@context << value.name
|
60
|
-
end
|
61
|
-
|
62
|
-
super
|
63
|
-
end
|
64
|
-
|
65
24
|
#: (Prism::DefNode) -> void
|
66
25
|
def visit_def_node(node)
|
67
26
|
new_context { super }
|
@@ -95,9 +54,10 @@ class StrictIvars::Processor < Prism::Visitor
|
|
95
54
|
|
96
55
|
@context << name
|
97
56
|
|
98
|
-
@annotations
|
99
|
-
[location.start_character_offset,
|
100
|
-
[location.end_character_offset, :
|
57
|
+
@annotations.push(
|
58
|
+
[location.start_character_offset, "(defined?(#{name}) ? "],
|
59
|
+
[location.end_character_offset, " : (::Kernel.raise(::StrictIvars::NameError.new('Undefined instance variable #{name}'))))"]
|
60
|
+
)
|
101
61
|
end
|
102
62
|
|
103
63
|
super
|
data/lib/strict_ivars/version.rb
CHANGED
data/lib/strict_ivars.rb
CHANGED
@@ -2,20 +2,72 @@
|
|
2
2
|
|
3
3
|
require "prism"
|
4
4
|
require "require-hooks/setup"
|
5
|
+
require "strict_ivars/version"
|
6
|
+
require "strict_ivars/base_processor"
|
7
|
+
require "strict_ivars/processor"
|
8
|
+
require "strict_ivars/configuration"
|
5
9
|
|
6
10
|
module StrictIvars
|
7
11
|
NameError = Class.new(::NameError)
|
8
12
|
|
13
|
+
EMPTY_ARRAY = [].freeze
|
14
|
+
EVERYTHING = ["**/*"].freeze
|
15
|
+
METHOD_METHOD = Module.instance_method(:method)
|
16
|
+
|
17
|
+
CONFIG = Configuration.new
|
18
|
+
|
9
19
|
#: (include: Array[String], exclude: Array[String]) -> void
|
10
|
-
def self.init(include:
|
20
|
+
def self.init(include: EMPTY_ARRAY, exclude: EMPTY_ARRAY)
|
21
|
+
CONFIG.include(*include) unless include.length == 0
|
22
|
+
CONFIG.exclude(*exclude) unless exclude.length == 0
|
23
|
+
|
11
24
|
RequireHooks.source_transform(
|
12
|
-
patterns:
|
13
|
-
exclude_patterns:
|
25
|
+
patterns: EVERYTHING,
|
26
|
+
exclude_patterns: EMPTY_ARRAY
|
14
27
|
) do |path, source|
|
15
28
|
source ||= File.read(path)
|
16
|
-
|
29
|
+
|
30
|
+
if CONFIG.match?(path)
|
31
|
+
Processor.call(source)
|
32
|
+
else
|
33
|
+
BaseProcessor.call(source)
|
34
|
+
end
|
17
35
|
end
|
18
36
|
end
|
19
|
-
end
|
20
37
|
|
21
|
-
|
38
|
+
def self.process_eval_args(receiver, method_name, *args)
|
39
|
+
method = METHOD_METHOD.bind_call(receiver, method_name)
|
40
|
+
owner = method.owner
|
41
|
+
|
42
|
+
source, file = nil
|
43
|
+
|
44
|
+
case method_name
|
45
|
+
when :class_eval, :module_eval
|
46
|
+
if Module == owner
|
47
|
+
source, file = args
|
48
|
+
end
|
49
|
+
when :instance_eval
|
50
|
+
if BasicObject == owner
|
51
|
+
source, file = args
|
52
|
+
end
|
53
|
+
when :eval
|
54
|
+
if Kernel == owner
|
55
|
+
source, binding, file = args
|
56
|
+
elsif Binding == owner
|
57
|
+
source, file = args
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
if String === source
|
62
|
+
file ||= caller_locations(1, 1).first&.path
|
63
|
+
|
64
|
+
if CONFIG.match?(file)
|
65
|
+
args[0] = Processor.call(source)
|
66
|
+
else
|
67
|
+
args[0] = BaseProcessor.call(source)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
args
|
72
|
+
end
|
73
|
+
end
|
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
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joel Drapper
|
@@ -47,6 +47,8 @@ files:
|
|
47
47
|
- LICENSE.txt
|
48
48
|
- README.md
|
49
49
|
- lib/strict_ivars.rb
|
50
|
+
- lib/strict_ivars/base_processor.rb
|
51
|
+
- lib/strict_ivars/configuration.rb
|
50
52
|
- lib/strict_ivars/processor.rb
|
51
53
|
- lib/strict_ivars/version.rb
|
52
54
|
homepage: https://github.com/joeldrapper/strict_ivars
|