low_type 1.1.8 → 1.1.10

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: 295c067ff64ba1d3ddb26243c1676a7bbe7d3a41fb65c4c51e868462a44e4f1b
4
- data.tar.gz: 2cff7d3f4f7f1dd8f6df9ab48e34737f562929cb802f798cf497a377034eee16
3
+ metadata.gz: 54e1e267e4ef98dc2364738a169dee20cb6364d9cb597c8f7c46084309b2296f
4
+ data.tar.gz: 1c1b44d5b7ba03b955d039bf62a7898897d2d04230dc89ff58df055de719d37d
5
5
  SHA512:
6
- metadata.gz: 1f697cb2610e1acb2bc89a41ec35cd1f78a381fea7f3fed90c1984166d4ae514cffb8adf4ba6ef4916cd32897b85ba22e4ee84d26e4b187cb21657d7dedc6553
7
- data.tar.gz: 54725a86fa96ac6bbf286d6c28d8a1af06090b64f1d75e5fe4ee21e398078d592aae71dd39b6c9d6170584f9ca4bd86175c827bf6d9ea1aaa0240540035080f7
6
+ metadata.gz: 39b512c4ff8c250659bdce0bb1be51c5f3e46e7a8ac30fa607deabdf9c933b7459bb78542211aafebf483dff3ecd8600ebb589dd4b91fb0d410c4dd9206e74fb
7
+ data.tar.gz: 0d4840867f0454b51b82f64807b691c4a4e4ac1380f5de113862ca8964f12fd18a62e0accaa51fad1105b94be403659ed5a80f486b4fc3ad4a1cddaf72abbf59
@@ -6,15 +6,12 @@ module Low
6
6
  module Adapter
7
7
  class Loader
8
8
  class << self
9
- def load(klass:, parser:, file_path:)
10
- adaptor = nil
11
-
9
+ def load(klass:, class_proxy:)
12
10
  ancestors = klass.ancestors.map(&:to_s)
13
- adaptor = Sinatra.new(klass:, parser:, file_path:) if ancestors.include?('Sinatra::Base')
14
11
 
15
- return if adaptor.nil?
12
+ return unless ancestors.include?('Sinatra::Base')
16
13
 
17
- adaptor
14
+ klass.prepend SinatraAdapter.new.module(file_path: class_proxy.file_path)
18
15
  end
19
16
  end
20
17
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'prism'
4
+ require 'lowkey'
4
5
 
5
- require_relative '../factories/proxy_factory'
6
6
  require_relative '../interfaces/adapter_interface'
7
7
  require_relative '../proxies/return_proxy'
8
8
  require_relative '../types/error_types'
@@ -10,70 +10,38 @@ require_relative '../types/error_types'
10
10
  module Low
11
11
  module Adapter
12
12
  # We don't use https://sinatrarb.com/extensions.html because we need to type check all Ruby methods (not just Sinatra) at a lower level.
13
- class Sinatra < AdapterInterface
14
- def initialize(klass:, parser:, file_path:)
15
- @klass = klass
16
- @parser = parser
17
- @file_path = file_path
18
- end
19
-
20
- def process # rubocop:disable Metrics/AbcSize
21
- method_calls = @parser.method_calls(method_names: %i[get post patch put delete options query])
22
-
23
- # Type check return values.
24
- method_calls.each do |method_call|
25
- arguments_node = method_call.compact_child_nodes.first
26
- next unless arguments_node.is_a?(Prism::ArgumentsNode)
27
-
28
- pattern = arguments_node.arguments.first.content
29
-
30
- file = ProxyFactory.file_proxy(node: method_call, path: @file_path, scope: "#{@klass}##{method_call.name}")
31
- next unless (return_proxy = return_proxy(method_node: method_call, pattern:, file:))
32
-
33
- route = "#{method_call.name.upcase} #{pattern}"
34
- params = [ParamProxy.new(expression: nil, name: :route, type: :req, position: 0, file:)]
35
- @klass.low_methods[route] = MethodProxy.new(name: method_call.name, params:, return_proxy:)
36
- end
37
- end
38
-
39
- def return_proxy(method_node:, pattern:, file:)
40
- return_type = FileParser.return_type(method_node:)
41
- return nil if return_type.nil?
42
-
43
- # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
44
- expression = eval(return_type.slice).call # rubocop:disable Security/Eval
45
- expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
46
-
47
- ReturnProxy.new(type_expression: expression, name: "#{method_node.name.upcase} #{pattern}", file:)
48
- end
49
- end
50
-
51
- module Methods
52
- # Unfortunately overriding invoke() is the best way to validate types for now. Though direct it's also very compute efficient.
53
- # I originally tried an after filter and it mostly worked but it only had access to Response which isn't the raw return value.
54
- # I suggest that Sinatra provide a hook that allows us to access the raw return value of a route before it becomes a Response.
55
- def invoke(&block)
56
- res = catch(:halt, &block)
57
-
58
- low_validate!(value: res) if res
59
-
60
- res = [res] if res.is_a?(Integer) || res.is_a?(String)
61
- if res.is_a?(::Array) && res.first.is_a?(Integer)
62
- res = res.dup
63
- status(res.shift)
64
- body(res.pop)
65
- headers(*res)
66
- elsif res.respond_to? :each
67
- body res
68
- end
69
-
70
- nil # avoid double setting the same response tuple twice
71
- end
72
-
73
- def low_validate!(value:)
74
- route = "#{request.request_method} #{request.path}"
75
- if (method_proxy = self.class.low_methods[route]) && (proxy = method_proxy.return_proxy)
76
- proxy.type_expression.validate!(value:, proxy:)
13
+ class SinatraAdapter < AdapterInterface
14
+ def module(file_path:) # rubocop:disable Metrics/AbcSize
15
+ Module.new do
16
+ @@file_path = file_path # rubocop:disable Style/ClassVars
17
+
18
+ # Unfortunately overriding invoke() is the best way to validate types for now. Though direct it's also very compute efficient.
19
+ # I originally tried an after filter and it mostly worked but it only had access to Response which isn't the raw return value.
20
+ # I suggest that Sinatra provide a hook that allows us to access the raw return value of a route before it becomes a Response.
21
+ def invoke(&block)
22
+ res = catch(:halt, &block)
23
+
24
+ lowtype_validate!(value: res) if res
25
+
26
+ res = [res] if res.is_a?(Integer) || res.is_a?(String)
27
+ if res.is_a?(::Array) && res.first.is_a?(Integer)
28
+ res = res.dup
29
+ status(res.shift)
30
+ body(res.pop)
31
+ headers(*res)
32
+ elsif res.respond_to? :each
33
+ body res
34
+ end
35
+
36
+ nil # avoid double setting the same response tuple twice
37
+ end
38
+
39
+ def lowtype_validate!(value:)
40
+ route = "#{request.request_method} #{request.path}"
41
+ if (method_proxy = Lowkey[@@file_path][self.class.name][route]) && (proxy = method_proxy.return_proxy)
42
+ proxy.expression.validate!(value:, proxy:)
43
+ end
44
+ end
77
45
  end
78
46
  end
79
47
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'expressions'
4
+ require 'lowkey'
5
+
6
+ require_relative '../expressions/expression_helpers'
7
+ require_relative '../expressions/type_expression'
8
+ require_relative '../syntax/syntax'
9
+ require_relative '../types/complex_types'
10
+ require_relative '../types/status'
11
+
12
+ module Low
13
+ # Evaluate code stored in strings into constants and values.
14
+ # ┌────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────┐ ┌─────────┐
15
+ # │ Lowkey │ │ Proxies │ │ Expressions │ │ LowType │ │ Methods │
16
+ # └────┬───┘ └────┬────┘ └──────┬──────┘ └────┬────┘ └────┬────┘
17
+ # │ │ │ │ │
18
+ # │ Parses AST │ │ │ │
19
+ # ├─────────────►│ │ │ │
20
+ # │ │ │ │ │
21
+ # │ │ Stores │ │ │
22
+ # │ ├────────────────►│ │ │
23
+ # │ │ │ │ │
24
+ # │ │ │ Evaluates <-- YOU ARE HERE. |
25
+ # │ │ │◄────────────────┤ │
26
+ # │ │ │ │ │
27
+ # │ │ │ │ Redefines │
28
+ # │ │ │ ├──────────────►│
29
+ # │ │ │ │ │
30
+ # │ │ │ Validates │ │
31
+ # │ │ │◄┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
32
+ # │ │ │ │ │
33
+ class Evaluator
34
+ include ExpressionHelpers
35
+ include Types
36
+ using LowType::Syntax
37
+
38
+ def instance_evaluate(proxy:)
39
+ # Not a security risk because the code comes from a trusted source; the file that included lowtype.
40
+ eval(proxy.value, binding, proxy.file_path, proxy.start_line) # rubocop:disable Security/Eval
41
+ end
42
+
43
+ class << self
44
+ def evaluate(method_proxies:)
45
+ require_relative '../syntax/union_types' if LowType.config.union_type_expressions
46
+
47
+ method_proxies.each_value do |method_proxy|
48
+ evaluate_param_proxy_expressions(method_proxy:)
49
+ evaluate_return_proxy_expression(return_proxy: method_proxy.return_proxy) if method_proxy.return_proxy
50
+ end
51
+ end
52
+
53
+ def evaluate_param_proxy_expressions(method_proxy:)
54
+ begin # rubocop:disable Style/RedundantBegin
55
+ method_proxy.tagged_params(:value).each do |param_proxy|
56
+ # TODO: Evaluate in the binding of the class that included LowType if not a type managed by LowType.
57
+ expression = new.instance_evaluate(proxy: param_proxy)
58
+ param_proxy.expression = cast_type_expression(expression:, method_proxy:)
59
+ end
60
+ rescue NameError
61
+ mp = method_proxy
62
+ raise NameError, "Unknown type '#{mp.value}' for #{mp.scope} at #{mp.file_path}:#{mp.start_line}"
63
+ end
64
+ end
65
+
66
+ def evaluate_return_proxy_expression(return_proxy:)
67
+ begin
68
+ expression = new.instance_evaluate(proxy: return_proxy)
69
+ rescue NameError
70
+ rp = return_proxy
71
+ raise NameError, "Unknown return type '#{rp.value}' for #{rp.scope} at #{rp.file_path}:#{rp.start_line}"
72
+ end
73
+
74
+ expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
75
+
76
+ return_proxy.expression = expression
77
+ end
78
+
79
+ private
80
+
81
+ def cast_type_expression(expression:, method_proxy:)
82
+ if expression.is_a?(::Expressions::Expression)
83
+ return expression
84
+ elsif expression.instance_of?(Class) && expression.name == 'Low::Dependency'
85
+ return expression.new(provider_key: method_proxy.name)
86
+ elsif TypeQuery.type?(expression)
87
+ return TypeExpression.new(type: expression)
88
+ end
89
+
90
+ nil
91
+ end
92
+ end
93
+ end
94
+ end
@@ -1,18 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../expressions/value_expression'
4
- require_relative '../factories/proxy_factory'
5
- require_relative '../proxies/method_proxy'
6
- require_relative '../queries/type_query'
7
- require_relative 'repository'
4
+ require_relative '../definitions/evaluator'
8
5
 
9
6
  module Low
10
7
  # Redefine methods to have their arguments and return values type checked.
8
+ # ┌────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────┐ ┌─────────┐
9
+ # │ Lowkey │ │ Proxies │ │ Expressions │ │ LowType │ │ Methods │
10
+ # └────┬───┘ └────┬────┘ └──────┬──────┘ └────┬────┘ └────┬────┘
11
+ # │ │ │ │ │
12
+ # │ Parses AST │ │ │ │
13
+ # ├─────────────►│ │ │ │
14
+ # │ │ │ │ │
15
+ # │ │ Stores │ │ │
16
+ # │ ├────────────────►│ │ │
17
+ # │ │ │ │ │
18
+ # │ │ │ Evaluates │ │
19
+ # │ │ │◄────────────────┤ │
20
+ # │ │ │ │ │
21
+ # │ │ │ │ Redefines <-- YOU ARE HERE.
22
+ # │ │ │ ├──────────────►│
23
+ # │ │ │ │ │
24
+ # │ │ │ Validates │ │
25
+ # │ │ │◄┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
26
+ # │ │ │ │ │
11
27
  class Redefiner
12
28
  class << self
13
- def redefine(method_nodes:, class_proxy:, file_path:)
14
- method_proxies = build_methods(method_nodes:, klass: class_proxy.klass, file_path:)
15
-
29
+ # TODO: Pass in "klass" and use it to class_eval/eval methods in the binding of the class that included LowType.
30
+ def redefine(method_proxies:, class_proxy:)
16
31
  if LowType.config.type_checking
17
32
  typed_methods(method_proxies:, class_proxy:)
18
33
  else
@@ -20,16 +35,12 @@ module Low
20
35
  end
21
36
  end
22
37
 
23
- def redefinable?(method_proxy:, class_proxy:)
24
- method_has_types?(method_proxy:, class_proxy:) && method_within_class_bounds?(method_proxy:, class_proxy:)
25
- end
26
-
27
38
  def untyped_args(args:, kwargs:, method_proxy:) # rubocop:disable Metrics/AbcSize
28
- method_proxy.params.each do |param_proxy|
39
+ method_proxy.params_with_expressions.each do |param_proxy|
29
40
  value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
30
41
 
31
42
  next unless value.nil?
32
- raise param_proxy.error_type, param_proxy.error_message(value:) if param_proxy.required?
43
+ raise param_proxy.error_type, param_proxy.error_message(value:) if param_proxy.expression.required?
33
44
 
34
45
  value = param_proxy.expression.default_value # Default value can still be `nil`.
35
46
  value = value.value if value.is_a?(ValueExpression)
@@ -41,94 +52,51 @@ module Low
41
52
 
42
53
  private
43
54
 
44
- def build_methods(method_nodes:, klass:, file_path:)
45
- method_nodes.each do |name, method_node|
46
- begin # rubocop:disable Style/RedundantBegin
47
- file = ProxyFactory.file_proxy(path: file_path, node: method_node, scope: "#{klass}##{name}")
48
-
49
- param_proxies = ProxyFactory.param_proxies(method_node:, file:)
50
- return_proxy = ProxyFactory.return_proxy(method_node:, file:)
51
- method_proxy = MethodProxy.new(name:, params: param_proxies, return_proxy:, file:)
52
-
53
- Repository.save(method: method_proxy, klass:)
54
- # When we can't parse the method's params or return type then skip it.
55
- rescue SyntaxError
56
- next
57
- end
58
- end
59
-
60
- Repository.all(klass:)
61
- end
62
-
63
55
  def typed_methods(method_proxies:, class_proxy:) # rubocop:disable Metrics
64
56
  Module.new do
65
- method_proxies.each do |name, method_proxy|
66
- next unless Low::Redefiner.redefinable?(method_proxy:, class_proxy:)
67
-
68
- # You are now in the binding of the includer class (`name` is also available here).
69
- define_method(name) do |*args, **kwargs|
70
- # Inlined version of Repository.load() for performance increase.
71
- method_proxy = instance_of?(Class) ? low_methods[name] : self.class.low_methods[name] || Object.low_methods[name]
72
-
73
- method_proxy.params.each do |param_proxy|
74
- value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
75
- value = param_proxy.expression.default_value if value.nil? && !param_proxy.required?
57
+ method_proxies.values.filter(&:expressions?).each do |method_proxy|
58
+ define_method(method_proxy.name) do |*args, **kwargs|
59
+ method_proxy.params_with_expressions.each do |param_proxy|
60
+ positional = %i[pos_req pos_opt].include?(param_proxy.type)
76
61
 
62
+ value = positional ? args[param_proxy.position] : kwargs[param_proxy.name]
63
+ value = param_proxy.expression.default_value if value.nil? && !param_proxy.expression.required?
77
64
  param_proxy.expression.validate!(value:, proxy: param_proxy)
78
65
  value = value.value if value.is_a?(ValueExpression)
79
- param_proxy.position ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
66
+
67
+ positional ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
80
68
  end
81
69
 
82
70
  if (return_proxy = method_proxy.return_proxy)
83
71
  return_value = super(*args, **kwargs)
84
- return_proxy.type_expression.validate!(value: return_value, proxy: return_proxy)
72
+ return_proxy.expression.validate!(value: return_value, proxy: return_proxy)
85
73
  return return_value
86
74
  end
87
75
 
88
76
  super(*args, **kwargs)
89
77
  end
90
78
 
91
- private name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
79
+ private method_proxy.name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
92
80
  end
93
81
  end
94
82
  end
95
83
 
96
84
  def untyped_methods(method_proxies:, class_proxy:)
97
85
  Module.new do
98
- method_proxies.each do |name, method_proxy|
99
- next unless Low::Redefiner.redefinable?(method_proxy:, class_proxy:)
100
-
101
- # You are now in the binding of the includer class (`name` is also available here).
102
- define_method(name) do |*args, **kwargs|
86
+ method_proxies.values.filter(&:expressions?).each do |method_proxy|
87
+ # You are now in the binding of the includer class.
88
+ define_method(method_proxy.name) do |*args, **kwargs|
103
89
  # NOTE: Type checking is currently disabled. See 'config.type_checking'.
104
- method_proxy = instance_of?(Class) ? low_methods[name] : self.class.low_methods[name] || Object.low_methods[name]
90
+ method_proxy = Lowkey[class_proxy.file_path][class_proxy.namespace][__method__]
91
+
105
92
  args, kwargs = Low::Redefiner.untyped_args(args:, kwargs:, method_proxy:)
106
93
  super(*args, **kwargs)
107
94
  end
108
95
 
109
- private name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
96
+ private method_proxy.name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
110
97
  end
111
98
  end
112
99
  end
113
-
114
- def method_has_types?(method_proxy:, class_proxy:)
115
- if method_proxy.params == [] && method_proxy.return_proxy.nil?
116
- Low::Repository.delete(name: method_proxy.name, klass: class_proxy.klass)
117
- return false
118
- end
119
-
120
- true
121
- end
122
-
123
- def method_within_class_bounds?(method_proxy:, class_proxy:)
124
- within_bounds = method_proxy.start_line > class_proxy.start_line && method_proxy.end_line <= class_proxy.end_line
125
- if method_proxy.lines? && class_proxy.lines? && !within_bounds
126
- Low::Repository.delete(name: method_proxy.name, klass: class_proxy.klass)
127
- return false
128
- end
129
-
130
- true
131
- end
132
100
  end
133
101
  end
134
102
  end
@@ -9,31 +9,35 @@ module Low
9
9
  def type_reader(named_expressions)
10
10
  named_expressions.each do |name, exp|
11
11
  last_caller = caller_locations(1, 1).first
12
- file = FileProxy.new(path: last_caller.path, start_line: last_caller.lineno, scope: "#{self}##{name}")
13
12
 
14
- expression = expression(exp)
15
- @low_methods[name] = MethodProxy.new(name:, return_proxy: ReturnProxy.new(type_expression: expression, name:, file:))
13
+ file_path = last_caller.path
14
+ start_line = last_caller.lineno
15
+ scope = "#{self}##{name}"
16
+
17
+ expression = cast_type_expression(exp)
18
+ proxy = ::Lowkey::ReturnProxy.new(file_path:, start_line:, scope:, name:, expression:)
16
19
 
17
20
  define_method(name) do
18
- method_proxy = self.class.low_methods[name]
19
21
  value = instance_variable_get("@#{name}")
20
- expression.validate!(value:, proxy: method_proxy.return_proxy)
22
+ expression.validate!(value:, proxy:)
21
23
  value
22
24
  end
23
25
  end
24
26
  end
25
27
 
26
- def type_writer(named_expressions) # rubocop:disable Metrics/AbcSize
27
- named_expressions.each do |name, exp|
28
+ def type_writer(named_expressions)
29
+ named_expressions.each do |name, expression|
28
30
  last_caller = caller_locations(1, 1).first
29
- file = FileProxy.new(path: last_caller.path, start_line: last_caller.lineno, scope: "#{self}##{name}")
30
31
 
31
- params = [ParamProxy.new(expression: expression(exp), name:, type: :hashreq, file:)]
32
- @low_methods["#{name}="] = MethodProxy.new(name:, params:)
32
+ file_path = last_caller.path
33
+ start_line = last_caller.lineno
34
+ scope = "#{self}##{name}"
35
+
36
+ expression = cast_type_expression(expression)
37
+ proxy = ::Lowkey::ParamProxy.new(file_path:, start_line:, scope:, name:, type: :key_req, expression:)
33
38
 
34
39
  define_method("#{name}=") do |value|
35
- method_proxy = self.class.low_methods["#{name}="]
36
- method_proxy.params.first.expression.validate!(value:, proxy: method_proxy.params.first)
40
+ expression.validate!(value:, proxy:)
37
41
  instance_variable_set("@#{name}", value)
38
42
  end
39
43
  end
@@ -48,10 +52,10 @@ module Low
48
52
 
49
53
  private
50
54
 
51
- def expression(expression)
55
+ def cast_type_expression(expression)
52
56
  if expression.is_a?(::Expressions::Expression)
53
57
  expression
54
- elsif ::Low::TypeQuery.type?(expression)
58
+ elsif TypeQuery.type?(expression)
55
59
  TypeExpression.new(type: expression)
56
60
  end
57
61
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'type_expression'
4
+ require_relative 'value_expression'
5
+ require_relative '../proxies/local_proxy'
6
+
7
+ module Low
8
+ module ExpressionHelpers
9
+ def type(type_expression)
10
+ value = type_expression.default_value
11
+
12
+ last_caller = caller_locations(1, 1).first
13
+ file_path = last_caller.path
14
+ start_line = last_caller.lineno
15
+ proxy = LocalProxy.new(type_expression:, name: self, file_path:, start_line:, scope: 'local type')
16
+
17
+ type_expression.validate!(value:, proxy:)
18
+
19
+ return value.value if value.is_a?(ValueExpression)
20
+
21
+ value
22
+ rescue NoMethodError
23
+ raise ConfigError, "Invalid type expression. Did you add 'using LowType::Syntax'?"
24
+ end
25
+ alias low_type type
26
+
27
+ def value(type)
28
+ TypeExpression.new(default_value: ValueExpression.new(value: type))
29
+ end
30
+ alias low_value value
31
+ end
32
+ end
@@ -22,7 +22,7 @@ module Low
22
22
  @types << type unless type.nil?
23
23
  @default_value = default_value
24
24
  # TODO: Override per type expression with a config expression.
25
- @deep_type_check = LowType.config.deep_type_check
25
+ @deep_type_check = nil
26
26
  end
27
27
 
28
28
  def required?
@@ -76,7 +76,7 @@ module Low
76
76
 
77
77
  # Override Expressions as LowType supports complex types which are implemented as values.
78
78
  def value?(expression)
79
- ::Low::TypeQuery.value?(expression) || expression.nil?
79
+ TypeQuery.value?(expression) || expression.nil?
80
80
  end
81
81
 
82
82
  def valid_subtype(subtype:)
@@ -141,7 +141,9 @@ module Low
141
141
  end
142
142
 
143
143
  def deep_type_check?
144
- @deep_type_check || LowType.config.deep_type_check || false
144
+ return @deep_type_check unless @deep_type_check.nil?
145
+
146
+ LowType.config.deep_type_check
145
147
  end
146
148
  end
147
149
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A value expression converts a type to a value in the eyes of LowType.
3
+ # A value expression presents a type as a value:
4
+ # 1. It is an instance
5
+ # 2. It has a class method
4
6
  class ValueExpression
5
7
  attr_reader :value
6
8
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Low
4
4
  class AdapterInterface
5
- def process
6
- raise NotImplementedError
5
+ def module
6
+ nil
7
7
  end
8
8
  end
9
9
  end
@@ -1,15 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Low
4
- class ErrorInterface
5
- attr_reader :file
6
-
7
- def initialize
8
- @file = nil
9
- @output_mode = LowType.config.output_mode
10
- @output_size = LowType.config.output_size
11
- end
12
-
4
+ # Used by proxies to output errors.
5
+ module ErrorHandling
13
6
  def error_type
14
7
  raise NotImplementedError
15
8
  end
@@ -19,12 +12,12 @@ module Low
19
12
  end
20
13
 
21
14
  def output(value:)
22
- case @output_mode
15
+ case LowType.config.output_mode
23
16
  when :type
24
17
  # TODO: Show full type structure in error output instead of just the type of the supertype.
25
18
  value.class
26
19
  when :value
27
- value.inspect[0...@output_size]
20
+ value.inspect[0...LowType.config.output_size]
28
21
  else
29
22
  'REDACTED'
30
23
  end
@@ -34,8 +27,8 @@ module Low
34
27
  # Remove LowType defined method file paths from the backtrace.
35
28
  filtered_backtrace = backtrace.reject { |line| hidden_paths.find { |file_path| line.include?(file_path) } }
36
29
 
37
- # Add the proxied file to the backtrace.
38
- proxy_file_backtrace = "#{file.path}:#{file.start_line}:in '#{file.scope}'"
30
+ # Add the proxied entity to the backtrace.
31
+ proxy_file_backtrace = "#{file_path}:#{start_line}:in '#{scope}'"
39
32
  from_prefix = filtered_backtrace.first.match(/\s+from /)
40
33
  proxy_file_backtrace = "#{from_prefix}#{proxy_file_backtrace}" if from_prefix
41
34
 
data/lib/low_type.rb CHANGED
@@ -1,40 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'lowkey'
4
+
3
5
  require_relative 'adapters/adapter_loader'
4
6
  require_relative 'definitions/redefiner'
5
7
  require_relative 'definitions/type_accessors'
6
- require_relative 'expressions/expressions'
7
- require_relative 'queries/file_parser'
8
+ require_relative 'expressions/expression_helpers'
8
9
  require_relative 'queries/file_query'
9
10
  require_relative 'syntax/syntax'
10
11
  require_relative 'types/complex_types'
11
12
 
13
+ # Architecture:
14
+ # ┌────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────┐ ┌─────────┐
15
+ # │ Lowkey │ │ Proxies │ │ Expressions │ │ LowType │ │ Methods │
16
+ # └────┬───┘ └────┬────┘ └──────┬──────┘ └────┬────┘ └────┬────┘
17
+ # │ │ │ │ │
18
+ # │ Parses AST │ │ │ │
19
+ # ├─────────────►│ │ │ │
20
+ # │ │ │ │ │
21
+ # │ │ Stores │ │ │
22
+ # │ ├────────────────►│ │ │
23
+ # │ │ │ │ │
24
+ # │ │ │ Evaluates │ │
25
+ # │ │ │◄────────────────┤ │
26
+ # │ │ │ │ │
27
+ # │ │ │ │ Redefines │
28
+ # │ │ │ ├──────────────►│
29
+ # │ │ │ │ │
30
+ # │ │ │ Validates │ │
31
+ # │ │ │◄┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
32
+ # │ │ │ │ │
12
33
  module LowType
13
- # We do as much as possible on class load rather than on instantiation to be thread-safe and efficient.
14
- def self.included(klass) # rubocop:disable Metrics/AbcSize
15
- require_relative 'syntax/union_types' if LowType.config.union_type_expressions
16
-
17
- class << klass
18
- def low_methods
19
- @low_methods ||= {}
20
- end
21
- end
22
-
34
+ # We do as much as possible on class load rather than on object instantiation to be thread-safe and efficient.
35
+ def self.included(klass)
23
36
  file_path = Low::FileQuery.file_path(klass:)
24
- return unless File.exist?(file_path)
37
+ file_proxy = Lowkey.load(file_path:)
38
+ class_proxy = file_proxy[klass.name]
25
39
 
26
- parser = Low::FileParser.new(klass:, file_path:)
40
+ Low::Evaluator.evaluate(method_proxies: class_proxy.keyed_methods)
27
41
 
42
+ klass.include Low::ExpressionHelpers
43
+ klass.extend Low::ExpressionHelpers
28
44
  klass.extend Low::TypeAccessors
29
- klass.include Low::Types
30
- klass.include Low::Expressions
31
- klass.prepend Low::Redefiner.redefine(method_nodes: parser.instance_methods, class_proxy: parser.class_proxy, file_path:)
32
- klass.singleton_class.prepend Low::Redefiner.redefine(method_nodes: parser.class_methods, class_proxy: parser.class_proxy, file_path:)
33
-
34
- if (adapter = Low::Adapter::Loader.load(klass:, parser:, file_path:))
35
- adapter.process
36
- klass.prepend Low::Adapter::Methods
37
- end
45
+ klass.extend Low::Types
46
+
47
+ klass.prepend Low::Redefiner.redefine(method_proxies: class_proxy.instance_methods, class_proxy:)
48
+ klass.singleton_class.prepend Low::Redefiner.redefine(method_proxies: class_proxy.class_methods, class_proxy:)
49
+
50
+ Low::Adapter::Loader.load(klass:, class_proxy:)
38
51
  end
39
52
 
40
53
  class << self
@@ -47,7 +60,7 @@ module LowType
47
60
  :deep_type_check,
48
61
  :union_type_expressions
49
62
  )
50
- @config ||= config.new(true, :error, :type, 100, false, true)
63
+ @config ||= config.new(true, :error, :type, 100, true, true)
51
64
  end
52
65
 
53
66
  def configure
@@ -1,18 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/error_interface'
3
+ require_relative '../interfaces/error_handling'
4
4
  require_relative '../types/error_types'
5
5
 
6
6
  module Low
7
- class LocalProxy < ErrorInterface
8
- attr_reader :type_expression, :name
7
+ class LocalProxy
8
+ include ErrorHandling
9
9
 
10
- def initialize(type_expression:, name:, file:)
11
- super()
10
+ attr_reader :type_expression, :name, :file_path, :start_line, :scope
11
+
12
+ def initialize(type_expression:, name:, file_path:, start_line:, scope:)
13
+ @file_path = file_path
14
+ @start_line = start_line
15
+ @scope = scope
12
16
 
13
17
  @type_expression = type_expression
14
18
  @name = name
15
- @file = file
16
19
  end
17
20
 
18
21
  def error_type
@@ -20,7 +23,7 @@ module Low
20
23
  end
21
24
 
22
25
  def error_message(value:)
23
- "Invalid variable type #{output(value:)} in '#{name.class}:#{@file.start_line}'. Valid types: '#{type_expression.valid_types}'"
26
+ "Invalid variable type #{output(value:)} in '#{name.class}:#{@start_line}'. Valid types: '#{type_expression.valid_types}'"
24
27
  end
25
28
  end
26
29
  end
@@ -1,28 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/error_interface'
4
- require_relative '../types/error_types'
5
-
6
- module Low
7
- class ParamProxy < ErrorInterface
8
- attr_reader :expression, :name, :type, :position
9
-
10
- def initialize(expression:, name:, type:, file:, position: nil)
11
- super()
3
+ require 'lowkey'
12
4
 
13
- @expression = expression
14
- @name = name
15
- @type = type
16
- @position = position
17
- @file = file
18
- end
5
+ require_relative '../interfaces/error_handling'
6
+ require_relative '../types/error_types'
19
7
 
20
- def required?
21
- @expression.required?
22
- end
8
+ module ::Lowkey
9
+ class ParamProxy
10
+ include ::Low::ErrorHandling
23
11
 
24
12
  def error_type
25
- ArgumentTypeError
13
+ ::Low::ArgumentTypeError
26
14
  end
27
15
 
28
16
  def error_message(value:)
@@ -1,26 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/error_interface'
4
- require_relative '../types/error_types'
5
-
6
- module Low
7
- class ReturnProxy < ErrorInterface
8
- attr_reader :type_expression, :name
3
+ require 'lowkey'
9
4
 
10
- def initialize(type_expression:, name:, file:)
11
- super()
5
+ require_relative '../interfaces/error_handling'
6
+ require_relative '../types/error_types'
12
7
 
13
- @type_expression = type_expression
14
- @name = name
15
- @file = file
16
- end
8
+ module ::Lowkey
9
+ class ReturnProxy
10
+ include ::Low::ErrorHandling
17
11
 
18
12
  def error_type
19
- ReturnTypeError
13
+ ::Low::ReturnTypeError
20
14
  end
21
15
 
22
16
  def error_message(value:)
23
- "Invalid return type '#{output(value:)}' for method '#{@name}'. Valid types: '#{@type_expression.valid_types}'"
17
+ "Invalid return type '#{output(value:)}' for method '#{@name}'. Valid types: '#{@expression.valid_types}'"
24
18
  end
25
19
  end
26
20
  end
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Low
4
+ class MissingFileError < StandardError; end
5
+
4
6
  class FileQuery
5
7
  class << self
6
8
  def file_path(klass:)
7
9
  includer_line = line_from_class(klass:) || line_from_include || ''
8
- includer_line.split(':').first || ''
10
+ file_path = includer_line.split(':').first || ''
11
+
12
+ return file_path if File.exist?(file_path)
13
+
14
+ raise MissingFileError, "No file found at path '#{file_path}'"
9
15
  end
10
16
 
11
17
  private
data/lib/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Low
4
4
  module Type
5
- VERSION = '1.1.8'
5
+ VERSION = '1.1.10'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: low_type
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.8
4
+ version: 1.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: lowkey
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.3'
26
40
  description: An elegant and simple way to define types in Ruby, only when you need
27
41
  them.
28
42
  email:
@@ -33,25 +47,19 @@ extra_rdoc_files: []
33
47
  files:
34
48
  - lib/adapters/adapter_loader.rb
35
49
  - lib/adapters/sinatra_adapter.rb
50
+ - lib/definitions/evaluator.rb
36
51
  - lib/definitions/redefiner.rb
37
- - lib/definitions/repository.rb
38
52
  - lib/definitions/type_accessors.rb
39
- - lib/expressions/expressions.rb
53
+ - lib/expressions/expression_helpers.rb
40
54
  - lib/expressions/type_expression.rb
41
55
  - lib/expressions/value_expression.rb
42
- - lib/factories/expression_factory.rb
43
- - lib/factories/proxy_factory.rb
44
56
  - lib/factories/type_factory.rb
45
57
  - lib/interfaces/adapter_interface.rb
46
- - lib/interfaces/error_interface.rb
58
+ - lib/interfaces/error_handling.rb
47
59
  - lib/low_type.rb
48
- - lib/proxies/class_proxy.rb
49
- - lib/proxies/file_proxy.rb
50
60
  - lib/proxies/local_proxy.rb
51
- - lib/proxies/method_proxy.rb
52
61
  - lib/proxies/param_proxy.rb
53
62
  - lib/proxies/return_proxy.rb
54
- - lib/queries/file_parser.rb
55
63
  - lib/queries/file_query.rb
56
64
  - lib/queries/type_query.rb
57
65
  - lib/syntax/syntax.rb
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Low
4
- class Repository
5
- class << self
6
- def all(klass:)
7
- klass.low_methods
8
- end
9
-
10
- def save(method:, klass:)
11
- klass.low_methods[method.name] = method
12
- end
13
-
14
- def delete(name:, klass:)
15
- klass.low_methods.delete(name)
16
- end
17
-
18
- # Redefiner inlines this method in define_method() for better performance. TODO: Test this assumption.
19
- def load(name:, object:)
20
- singleton(object:).low_methods[name]
21
- end
22
-
23
- # TODO: export() to RBS
24
-
25
- private
26
-
27
- def singleton(object:)
28
- object.instance_of?(Class) ? object : object.class || Object
29
- end
30
- end
31
- end
32
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../factories/expression_factory'
4
- require_relative '../proxies/file_proxy'
5
- require_relative '../proxies/local_proxy'
6
- require_relative '../types/error_types'
7
-
8
- module Low
9
- module Expressions
10
- def type(type_expression)
11
- value = type_expression.default_value
12
-
13
- last_caller = caller_locations(1, 1).first
14
- file = FileProxy.new(path: last_caller.path, start_line: last_caller.lineno, scope: 'local type')
15
- proxy = LocalProxy.new(type_expression:, name: self, file:)
16
-
17
- type_expression.validate!(value:, proxy:)
18
-
19
- return value.value if value.is_a?(ValueExpression)
20
-
21
- value
22
- rescue NoMethodError
23
- raise ConfigError, "Invalid type expression, likely because you didn't add 'using LowType::Syntax'"
24
- end
25
- alias low_type type
26
-
27
- def value(type)
28
- ExpressionFactory.type_expression_with_value(type:)
29
- end
30
- alias low_value value
31
- end
32
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../expressions/type_expression'
4
- require_relative '../expressions/value_expression'
5
-
6
- module Low
7
- class ExpressionFactory
8
- class << self
9
- def type_expression_with_value(type:)
10
- TypeExpression.new(default_value: ValueExpression.new(value: type))
11
- end
12
- end
13
- end
14
- end
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'expressions'
4
-
5
- require_relative '../expressions/expressions'
6
- require_relative '../expressions/type_expression'
7
- require_relative '../proxies/file_proxy'
8
- require_relative '../proxies/param_proxy'
9
- require_relative '../proxies/return_proxy'
10
- require_relative '../queries/file_parser'
11
- require_relative '../syntax/syntax'
12
- require_relative '../types/complex_types'
13
- require_relative '../types/status'
14
-
15
- module Low
16
- class ProxyFactory
17
- using ::LowType::Syntax
18
-
19
- class << self
20
- include Low::Expressions
21
- include Low::Types
22
-
23
- def file_proxy(node:, path:, scope:)
24
- start_line = node.respond_to?(:start_line) ? node.start_line : nil
25
- end_line = node.respond_to?(:end_line) ? node.end_line : nil
26
-
27
- FileProxy.new(path:, start_line:, end_line:, scope:)
28
- end
29
-
30
- # The evals below aren't a security risk because the code comes from a trusted source; the file itself that did the include.
31
- def param_proxies(method_node:, file:)
32
- return [] if method_node.parameters.nil?
33
-
34
- params_without_block = method_node.parameters.slice.delete_suffix(', &block')
35
-
36
- ruby_method = eval("-> (#{params_without_block}) {}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
37
-
38
- # Local variable names are prefixed with __lt or __rb where needed to avoid being overridden by method parameters.
39
- typed_method = <<~RUBY
40
- -> (#{params_without_block}, __rb_method:, __lt_file:) {
41
- param_proxies_for_expressions(ruby_method: __rb_method, file: __lt_file, method_binding: binding)
42
- }
43
- RUBY
44
-
45
- required_args, required_kwargs = required_args(ruby_method:)
46
-
47
- # Called with only required args (as nil) and optional args omitted, to evaluate expressions stored as default values.
48
- eval(typed_method, binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
49
- .call(*required_args, **required_kwargs, __rb_method: ruby_method, __lt_file: file)
50
-
51
- # TODO: Unit test this.
52
- rescue ArgumentError => e
53
- raise ArgumentError, "Incorrect param syntax: #{e.message}"
54
- end
55
-
56
- def return_proxy(method_node:, file:)
57
- return_type = FileParser.return_type(method_node:)
58
- return nil if return_type.nil?
59
-
60
- begin
61
- # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
62
- expression = eval(return_type.slice, binding, __FILE__, __LINE__).call # rubocop:disable Security/Eval
63
- rescue NameError
64
- raise NameError, "Unknown return type '#{return_type.slice}' for #{file.scope} at #{file.path}:#{file.start_line}"
65
- end
66
-
67
- expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
68
-
69
- ReturnProxy.new(type_expression: expression, name: method_node.name, file:)
70
- end
71
-
72
- private
73
-
74
- def required_args(ruby_method:)
75
- required_args = []
76
- required_kwargs = {}
77
-
78
- ruby_method.parameters.each do |param|
79
- param_type, param_name = param
80
-
81
- case param_type
82
- when :req
83
- required_args << nil
84
- when :keyreq
85
- required_kwargs[param_name] = nil
86
- end
87
- end
88
-
89
- [required_args, required_kwargs]
90
- end
91
-
92
- def param_proxies_for_expressions(ruby_method:, file:, method_binding:)
93
- param_proxies = []
94
-
95
- ruby_method.parameters.each_with_index do |param, position|
96
- type, name = param
97
-
98
- # We don't support splatted *positional and **keyword arguments as by definition they are untyped.
99
- next if type == :rest
100
-
101
- position = nil unless %i[opt req].include?(type)
102
- local_variable = method_binding.local_variable_get(name)
103
-
104
- expression = nil
105
- if local_variable.is_a?(::Expressions::Expression)
106
- expression = local_variable
107
- elsif local_variable.instance_of?(Class) && local_variable < ::Expressions::Expression
108
- expression = local_variable.new(provider_key: name)
109
- elsif ::Low::TypeQuery.type?(local_variable)
110
- expression = TypeExpression.new(type: local_variable)
111
- end
112
-
113
- param_proxies << ParamProxy.new(expression:, name:, type:, position:, file:) if expression
114
- end
115
-
116
- param_proxies
117
- end
118
- end
119
- end
120
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'forwardable'
4
-
5
- module Low
6
- class ClassProxy
7
- extend Forwardable
8
-
9
- attr_reader :name, :klass, :file, :private_start_line
10
-
11
- def_delegators :@file, :start_line, :end_line, :lines?
12
-
13
- def initialize(klass:, file:, private_start_line:)
14
- @name = klass.to_s
15
- @klass = klass
16
- @file = file
17
- @private_start_line = private_start_line
18
- end
19
- end
20
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Low
4
- class FileProxy
5
- attr_reader :path, :scope
6
- attr_accessor :start_line, :end_line
7
-
8
- def initialize(path:, scope:, start_line:, end_line: nil)
9
- @path = path
10
- @start_line = start_line
11
- @end_line = end_line || start_line
12
- @scope = scope
13
- end
14
-
15
- def lines?
16
- start_line && end_line
17
- end
18
- end
19
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'forwardable'
4
-
5
- module Low
6
- class MethodProxy
7
- extend Forwardable
8
-
9
- attr_reader :name, :file, :params, :return_proxy
10
-
11
- # File is queried by redefiner but not sinatra adapter nor type accessors.
12
- def_delegators :@file, :start_line, :end_line, :lines?
13
-
14
- def initialize(name:, file: nil, params: [], return_proxy: nil)
15
- @name = name
16
- @file = file
17
- @params = params
18
- @return_proxy = return_proxy
19
- end
20
- end
21
- end
@@ -1,147 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'prism'
4
- require_relative '../proxies/class_proxy'
5
-
6
- module Low
7
- class FileParser
8
- attr_reader :parent_map, :instance_methods, :class_methods, :class_proxy
9
-
10
- def initialize(klass:, file_path:)
11
- @root_node = Prism.parse_file(file_path).value
12
-
13
- parent_mapper = ParentMapper.new
14
- parent_mapper.visit(@root_node)
15
- @parent_map = parent_mapper.parent_map
16
-
17
- method_visitor = MethodDefVisitor.new(root_node: @root_node, parent_map:, klass:, file_path:)
18
- @root_node.accept(method_visitor)
19
-
20
- @instance_methods = method_visitor.instance_methods
21
- @class_methods = method_visitor.class_methods
22
- @class_proxy = ClassProxy.new(klass:, file: method_visitor.file_proxy, private_start_line: method_visitor.private_start_line)
23
- end
24
-
25
- def method_calls(method_names:)
26
- block_visitor = MethodCallVisitor.new(parent_map:, method_names:)
27
- @root_node.accept(block_visitor)
28
- block_visitor.method_calls
29
- end
30
-
31
- class << self
32
- # Only a lambda defined immediately after a method's parameters/block is considered a return type expression.
33
- def return_type(method_node:)
34
- # Method statements.
35
- statements_node = method_node.compact_child_nodes.find { |node| node.is_a?(Prism::StatementsNode) }
36
-
37
- # Block statements.
38
- if statements_node.nil?
39
- block_node = method_node.compact_child_nodes.find { |node| node.is_a?(Prism::BlockNode) }
40
- statements_node = block_node.compact_child_nodes.find { |node| node.is_a?(Prism::StatementsNode) } if block_node
41
- end
42
-
43
- return nil if statements_node.nil? # Sometimes developers define methods without code inside them.
44
-
45
- node = statements_node.body.first
46
- return node if node.is_a?(Prism::LambdaNode)
47
-
48
- nil
49
- end
50
- end
51
- end
52
-
53
- class MethodDefVisitor < Prism::Visitor
54
- attr_reader :class_methods, :instance_methods, :file_proxy, :private_start_line
55
-
56
- def initialize(root_node:, parent_map:, klass:, file_path:)
57
- @parent_map = parent_map
58
- @klass = klass
59
-
60
- @instance_methods = {}
61
- @class_methods = {}
62
-
63
- end_line = root_node.respond_to?(:end_line) ? root_node.end_line : nil
64
- @file_proxy = FileProxy.new(path: file_path, start_line: 0, end_line:, scope: klass.to_s)
65
- @private_start_line = nil
66
- end
67
-
68
- def visit_def_node(node)
69
- if class_method?(node)
70
- @class_methods[node.name] = node
71
- else
72
- @instance_methods[node.name] = node
73
- end
74
-
75
- super # Continue walking the tree.
76
- end
77
-
78
- def visit_call_node(node)
79
- return super unless node.name == :private && node.respond_to?(:start_line) && file_proxy.start_line && file_proxy.end_line
80
-
81
- @private_start_line = node.start_line if node.start_line > file_proxy.start_line && node.start_line < file_proxy.end_line
82
-
83
- super
84
- end
85
-
86
- def visit_class_node(node)
87
- if node.name == @klass.to_s.to_sym
88
- file_proxy.start_line = node.class_keyword_loc.start_line
89
- file_proxy.end_line = node.end_keyword_loc.end_line
90
- end
91
-
92
- super
93
- end
94
-
95
- private
96
-
97
- def class_method?(node)
98
- return true if node.is_a?(::Prism::DefNode) && node.receiver.instance_of?(Prism::SelfNode) # self.method_name
99
- return true if node.is_a?(::Prism::SingletonClassNode) # class << self
100
-
101
- if (parent_node = @parent_map[node])
102
- return class_method?(parent_node)
103
- end
104
-
105
- false
106
- end
107
- end
108
-
109
- class MethodCallVisitor < Prism::Visitor
110
- attr_reader :method_calls
111
-
112
- def initialize(parent_map:, method_names:)
113
- @parent_map = parent_map
114
- @method_names = method_names
115
-
116
- @method_calls = []
117
- end
118
-
119
- def visit_call_node(node)
120
- @method_calls << node if @method_names.include?(node.name)
121
-
122
- super # Continue walking the tree.
123
- end
124
- end
125
-
126
- class ParentMapper < Prism::Visitor
127
- attr_reader :parent_map
128
-
129
- def initialize
130
- @parent_map = {}
131
- @current_parent = nil
132
- end
133
-
134
- def visit(node)
135
- @parent_map[node] = @current_parent
136
-
137
- old_parent = @current_parent
138
- @current_parent = node
139
-
140
- node.compact_child_nodes.each do |n|
141
- visit(n)
142
- end
143
-
144
- @current_parent = old_parent
145
- end
146
- end
147
- end