strict_ivars 0.5.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65516e69e17c893c02c57e0cc0be683c57c9dc3fd91b80f56942453d5df43649
4
- data.tar.gz: 16d3c581474be1f5a7500eb24e442ac8bcd54f55ee5d7afef6cdea3ef986381f
3
+ metadata.gz: 6e1244d297abc3e3ade0882ca3a8767b63701c42721a7199803ad75b46c566ed
4
+ data.tar.gz: c141c19397e2942a40bb049ca77790ca9b29a6de9e0cd30e218162b1e51c4ee7
5
5
  SHA512:
6
- metadata.gz: b92fa82ca1acbcef116a5ab531bb1a4ec08301db0b17eb9d5f5a99af47c29face2c82a51d0f63b905eec0a7506e2453b040731848a69afff951f6f44b18e789e
7
- data.tar.gz: b650231f2312548af22bd11ecc031dbdcbf178a5ce4f501b29f48687a59209a6533c12a4ef8fe78a351cc03e591bbd3e01c1edf67f03568620279b3990ab0e63
6
+ metadata.gz: 8cd2d471ca00c86a910843da2cacf803d71f30d48c946f469a5f18ef34c23b54d99d20897501a769695ff37114b677b8ce25c927a135876d99f1202324ad2e8f
7
+ data.tar.gz: ab6d3154078514bb0bd9615bae2cecdda743cb7a3dcd5f55d8571fae1f5201f94d090ebffd582881876f3b66bdcfd2035e4d600b77b7fa3350759f33deb4850a
data/README.md CHANGED
@@ -26,26 +26,6 @@ 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
- **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
29
  **Writes:**
50
30
 
51
31
  Strict Ivars doesn’t apply to writes, since these are considered the authoritative source of the instance variable definitions.
@@ -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
@@ -22,7 +22,8 @@ class StrictIvars::Configuration
22
22
  end
23
23
 
24
24
  #: (String) -> bool
25
- def match(path)
25
+ def match?(path)
26
+ return false unless String === path
26
27
  path = File.absolute_path(path)
27
28
  return false if @exclude.any? { |pattern| File.fnmatch?(pattern, path) }
28
29
  return true if @include.any? { |pattern| File.fnmatch?(pattern, path) }
@@ -1,36 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class StrictIvars::Processor < Prism::Visitor
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, :start, name] <<
100
- [location.end_character_offset, :end, name]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StrictIvars
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/strict_ivars.rb CHANGED
@@ -3,28 +3,71 @@
3
3
  require "prism"
4
4
  require "require-hooks/setup"
5
5
  require "strict_ivars/version"
6
+ require "strict_ivars/base_processor"
7
+ require "strict_ivars/processor"
6
8
  require "strict_ivars/configuration"
7
9
 
8
10
  module StrictIvars
9
11
  NameError = Class.new(::NameError)
10
12
 
13
+ EMPTY_ARRAY = [].freeze
14
+ EVERYTHING = ["**/*"].freeze
15
+ METHOD_METHOD = Module.instance_method(:method)
16
+
11
17
  CONFIG = Configuration.new
12
18
 
13
19
  #: (include: Array[String], exclude: Array[String]) -> void
14
- def self.init(include: [], exclude: [])
15
- require "strict_ivars/patch_eval"
16
-
17
- CONFIG.include(*include)
18
- CONFIG.exclude(*exclude)
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
19
23
 
20
24
  RequireHooks.source_transform(
21
- patterns: include,
22
- exclude_patterns: exclude
25
+ patterns: EVERYTHING,
26
+ exclude_patterns: EMPTY_ARRAY
23
27
  ) do |path, source|
24
28
  source ||= File.read(path)
25
- Processor.call(source)
29
+
30
+ if CONFIG.match?(path)
31
+ Processor.call(source)
32
+ else
33
+ BaseProcessor.call(source)
34
+ end
26
35
  end
27
36
  end
28
- end
29
37
 
30
- require "strict_ivars/processor"
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Drapper
@@ -47,8 +47,8 @@ files:
47
47
  - LICENSE.txt
48
48
  - README.md
49
49
  - lib/strict_ivars.rb
50
+ - lib/strict_ivars/base_processor.rb
50
51
  - lib/strict_ivars/configuration.rb
51
- - lib/strict_ivars/patch_eval.rb
52
52
  - lib/strict_ivars/processor.rb
53
53
  - lib/strict_ivars/version.rb
54
54
  homepage: https://github.com/joeldrapper/strict_ivars
@@ -1,84 +0,0 @@
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