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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fdc65115a894c678d04614ece5855c2089003d320fe6bf22cb9c9ef0aa5de50
4
- data.tar.gz: cb51570cf483f8a952daf0eb4798ac430c699f5357474c80d37cf4c75cc8b073
3
+ metadata.gz: 6e1244d297abc3e3ade0882ca3a8767b63701c42721a7199803ad75b46c566ed
4
+ data.tar.gz: c141c19397e2942a40bb049ca77790ca9b29a6de9e0cd30e218162b1e51c4ee7
5
5
  SHA512:
6
- metadata.gz: 669e9cd076e7be979fa8d145cf9c0413794eeddd650dc0c183a67f2341809217dcd41059050fd63143311b9afc8c0c44dae82ade79e94967929c080d55da9bc0
7
- data.tar.gz: 901f1b60e232b8dccc0fbd6bf36f50b906b702e7396fffec47fa8e180e3390629ef0e7733e736e341f2ea482fc255d0870a96cbbccaf74cbfe0988a2d6df0cc0
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 < 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.4.1"
4
+ VERSION = "0.6.0"
5
5
  end
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: [], 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
23
+
11
24
  RequireHooks.source_transform(
12
- patterns: include,
13
- exclude_patterns: exclude
25
+ patterns: EVERYTHING,
26
+ exclude_patterns: EMPTY_ARRAY
14
27
  ) do |path, source|
15
28
  source ||= File.read(path)
16
- Processor.call(source)
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
- 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.4.1
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