low_type 1.1.8 → 1.1.9
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/lib/adapters/adapter_loader.rb +2 -2
- data/lib/adapters/sinatra_adapter.rb +16 -20
- data/lib/definitions/redefiner.rb +25 -23
- data/lib/definitions/type_accessors.rb +16 -10
- data/lib/expressions/expressions.rb +3 -3
- data/lib/factories/proxy_factory.rb +12 -18
- data/lib/interfaces/error_interface.rb +9 -5
- data/lib/low_type.rb +7 -5
- data/lib/proxies/local_proxy.rb +3 -4
- data/lib/proxies/method_proxy.rb +7 -10
- data/lib/proxies/param_proxy.rb +3 -3
- data/lib/proxies/return_proxy.rb +2 -3
- data/lib/version.rb +1 -1
- metadata +15 -4
- data/lib/proxies/class_proxy.rb +0 -20
- data/lib/proxies/file_proxy.rb +0 -19
- data/lib/queries/file_parser.rb +0 -147
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 372ec6574de86edfe13eba87a457e290934635e88b092a711798df69d04aaa30
|
|
4
|
+
data.tar.gz: 3afaccd3fed8508ca0064cc9552b80bd05cf7b09ecfdd1906a93a043034ec978
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d59f9cf98881584fb19d3ca79748d67fdc71c5a9b55ff4ff3c4324c5658cedc88567b4037876e1d8ed5a087aafb76deff459f9493f2194ccbf002ff855a1f88b
|
|
7
|
+
data.tar.gz: be0a567f9212cb628c7efe852f71901ea4495cb9eb75681a259e68e7ef2fa545cfdfa8df0b01e4774872158db607f57fd950f274a4dcad32910c87f743de6208
|
|
@@ -6,11 +6,11 @@ module Low
|
|
|
6
6
|
module Adapter
|
|
7
7
|
class Loader
|
|
8
8
|
class << self
|
|
9
|
-
def load(klass:,
|
|
9
|
+
def load(klass:, class_proxy:)
|
|
10
10
|
adaptor = nil
|
|
11
11
|
|
|
12
12
|
ancestors = klass.ancestors.map(&:to_s)
|
|
13
|
-
adaptor = Sinatra.new(klass:,
|
|
13
|
+
adaptor = Sinatra.new(klass:, class_proxy:) if ancestors.include?('Sinatra::Base')
|
|
14
14
|
|
|
15
15
|
return if adaptor.nil?
|
|
16
16
|
|
|
@@ -11,41 +11,37 @@ 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
13
|
class Sinatra < AdapterInterface
|
|
14
|
-
def initialize(klass:,
|
|
14
|
+
def initialize(klass:, class_proxy:)
|
|
15
15
|
@klass = klass
|
|
16
|
-
@
|
|
17
|
-
@file_path = file_path
|
|
16
|
+
@class_proxy = class_proxy
|
|
17
|
+
@file_path = class_proxy.file_path
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def process # rubocop:disable Metrics/AbcSize
|
|
21
|
-
method_calls = @
|
|
21
|
+
method_calls = @class_proxy.method_calls(%i[get post patch put delete options query])
|
|
22
22
|
|
|
23
23
|
# Type check return values.
|
|
24
|
-
method_calls.each do |
|
|
25
|
-
arguments_node =
|
|
24
|
+
method_calls.each do |method_node|
|
|
25
|
+
arguments_node = method_node.compact_child_nodes.first
|
|
26
26
|
next unless arguments_node.is_a?(Prism::ArgumentsNode)
|
|
27
27
|
|
|
28
28
|
pattern = arguments_node.arguments.first.content
|
|
29
|
+
name = "#{method_node.name.upcase} #{pattern}"
|
|
30
|
+
scope = name
|
|
31
|
+
start_line = method_node.start_line
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
next unless (return_proxy = return_proxy(method_node: method_call, pattern:, file:))
|
|
33
|
+
next unless (return_proxy = ProxyFactory.return_proxy(method_node:, name:, file_path:, scope: pattern))
|
|
32
34
|
|
|
33
|
-
route = "#{
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
route = "#{method_node.name.upcase} #{pattern}"
|
|
36
|
+
name = method_node.name
|
|
37
|
+
param_proxies = [ParamProxy.new(expression: nil, name: :route, type: :req, file_path:, start_line:, scope:, position: 0)]
|
|
38
|
+
@klass.low_methods[route] = MethodProxy.new(file_path:, start_line:, scope:, name:, param_proxies:, return_proxy:)
|
|
36
39
|
end
|
|
37
40
|
end
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
return_type = FileParser.return_type(method_node:)
|
|
41
|
-
return nil if return_type.nil?
|
|
42
|
+
private
|
|
42
43
|
|
|
43
|
-
|
|
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
|
|
44
|
+
attr_reader :file_path
|
|
49
45
|
end
|
|
50
46
|
|
|
51
47
|
module Methods
|
|
@@ -10,22 +10,22 @@ module Low
|
|
|
10
10
|
# Redefine methods to have their arguments and return values type checked.
|
|
11
11
|
class Redefiner
|
|
12
12
|
class << self
|
|
13
|
-
def redefine(method_nodes:, class_proxy:,
|
|
14
|
-
method_proxies = build_methods(method_nodes:, klass: class_proxy.
|
|
13
|
+
def redefine(method_nodes:, class_proxy:, klass:)
|
|
14
|
+
method_proxies = build_methods(method_nodes:, klass:, file_path: class_proxy.file_path)
|
|
15
15
|
|
|
16
16
|
if LowType.config.type_checking
|
|
17
|
-
typed_methods(method_proxies:, class_proxy:)
|
|
17
|
+
typed_methods(method_proxies:, class_proxy:, klass:)
|
|
18
18
|
else
|
|
19
|
-
untyped_methods(method_proxies:, class_proxy:)
|
|
19
|
+
untyped_methods(method_proxies:, class_proxy:, klass:)
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def redefinable?(method_proxy:, class_proxy:)
|
|
24
|
-
method_has_types?(method_proxy:,
|
|
23
|
+
def redefinable?(method_proxy:, class_proxy:, klass:)
|
|
24
|
+
method_has_types?(method_proxy:, klass:) && method_within_class_bounds?(method_proxy:, class_proxy:, klass:)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def untyped_args(args:, kwargs:, method_proxy:) # rubocop:disable Metrics/AbcSize
|
|
28
|
-
method_proxy.
|
|
28
|
+
method_proxy.param_proxies.each do |param_proxy|
|
|
29
29
|
value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
|
|
30
30
|
|
|
31
31
|
next unless value.nil?
|
|
@@ -44,11 +44,12 @@ module Low
|
|
|
44
44
|
def build_methods(method_nodes:, klass:, file_path:)
|
|
45
45
|
method_nodes.each do |name, method_node|
|
|
46
46
|
begin # rubocop:disable Style/RedundantBegin
|
|
47
|
-
|
|
47
|
+
name = method_node.name
|
|
48
|
+
scope = name
|
|
48
49
|
|
|
49
|
-
param_proxies = ProxyFactory.param_proxies(method_node:,
|
|
50
|
-
return_proxy = ProxyFactory.return_proxy(method_node:,
|
|
51
|
-
method_proxy = MethodProxy.new(
|
|
50
|
+
param_proxies = ProxyFactory.param_proxies(method_node:, file_path:, scope:)
|
|
51
|
+
return_proxy = ProxyFactory.return_proxy(method_node:, name:, file_path:, scope:)
|
|
52
|
+
method_proxy = MethodProxy.new(file_path:, start_line: method_node.start_line, scope:, name:, param_proxies:, return_proxy:)
|
|
52
53
|
|
|
53
54
|
Repository.save(method: method_proxy, klass:)
|
|
54
55
|
# When we can't parse the method's params or return type then skip it.
|
|
@@ -60,17 +61,17 @@ module Low
|
|
|
60
61
|
Repository.all(klass:)
|
|
61
62
|
end
|
|
62
63
|
|
|
63
|
-
def typed_methods(method_proxies:, class_proxy:) # rubocop:disable Metrics
|
|
64
|
+
def typed_methods(method_proxies:, class_proxy:, klass:) # rubocop:disable Metrics
|
|
64
65
|
Module.new do
|
|
65
66
|
method_proxies.each do |name, method_proxy|
|
|
66
|
-
next unless Low::Redefiner.redefinable?(method_proxy:, class_proxy:)
|
|
67
|
+
next unless Low::Redefiner.redefinable?(method_proxy:, class_proxy:, klass:)
|
|
67
68
|
|
|
68
69
|
# You are now in the binding of the includer class (`name` is also available here).
|
|
69
70
|
define_method(name) do |*args, **kwargs|
|
|
70
71
|
# Inlined version of Repository.load() for performance increase.
|
|
71
72
|
method_proxy = instance_of?(Class) ? low_methods[name] : self.class.low_methods[name] || Object.low_methods[name]
|
|
72
73
|
|
|
73
|
-
method_proxy.
|
|
74
|
+
method_proxy.param_proxies.each do |param_proxy|
|
|
74
75
|
value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
|
|
75
76
|
value = param_proxy.expression.default_value if value.nil? && !param_proxy.required?
|
|
76
77
|
|
|
@@ -93,10 +94,10 @@ module Low
|
|
|
93
94
|
end
|
|
94
95
|
end
|
|
95
96
|
|
|
96
|
-
def untyped_methods(method_proxies:, class_proxy:)
|
|
97
|
+
def untyped_methods(method_proxies:, class_proxy:, klass:)
|
|
97
98
|
Module.new do
|
|
98
99
|
method_proxies.each do |name, method_proxy|
|
|
99
|
-
next unless Low::Redefiner.redefinable?(method_proxy:, class_proxy:)
|
|
100
|
+
next unless Low::Redefiner.redefinable?(method_proxy:, class_proxy:, klass:)
|
|
100
101
|
|
|
101
102
|
# You are now in the binding of the includer class (`name` is also available here).
|
|
102
103
|
define_method(name) do |*args, **kwargs|
|
|
@@ -111,19 +112,20 @@ module Low
|
|
|
111
112
|
end
|
|
112
113
|
end
|
|
113
114
|
|
|
114
|
-
def method_has_types?(method_proxy:,
|
|
115
|
-
if method_proxy.
|
|
116
|
-
Low::Repository.delete(name: method_proxy.name, klass:
|
|
115
|
+
def method_has_types?(method_proxy:, klass:)
|
|
116
|
+
if method_proxy.param_proxies == [] && method_proxy.return_proxy.nil?
|
|
117
|
+
Low::Repository.delete(name: method_proxy.name, klass:)
|
|
117
118
|
return false
|
|
118
119
|
end
|
|
119
120
|
|
|
120
121
|
true
|
|
121
122
|
end
|
|
122
123
|
|
|
123
|
-
def method_within_class_bounds?(method_proxy:, class_proxy:)
|
|
124
|
-
within_bounds = method_proxy.start_line > class_proxy.start_line && method_proxy.
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
def method_within_class_bounds?(method_proxy:, class_proxy:, klass:)
|
|
125
|
+
within_bounds = method_proxy.start_line > class_proxy.start_line && method_proxy.start_line <= class_proxy.end_line
|
|
126
|
+
|
|
127
|
+
unless within_bounds
|
|
128
|
+
Low::Repository.delete(name: method_proxy.name, klass:)
|
|
127
129
|
return false
|
|
128
130
|
end
|
|
129
131
|
|
|
@@ -9,31 +9,37 @@ 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
|
-
|
|
12
|
+
file_path = last_caller.path
|
|
13
|
+
start_line = last_caller.lineno
|
|
14
|
+
scope = "#{self}##{name}"
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
type_expression = type_expression(exp)
|
|
17
|
+
return_proxy = ReturnProxy.new(type_expression:, name:, file_path:, start_line:, scope:)
|
|
18
|
+
|
|
19
|
+
@low_methods[name] = MethodProxy.new(file_path:, start_line:, scope:, name:, return_proxy:)
|
|
16
20
|
|
|
17
21
|
define_method(name) do
|
|
18
22
|
method_proxy = self.class.low_methods[name]
|
|
19
23
|
value = instance_variable_get("@#{name}")
|
|
20
|
-
|
|
24
|
+
type_expression.validate!(value:, proxy: method_proxy.return_proxy)
|
|
21
25
|
value
|
|
22
26
|
end
|
|
23
27
|
end
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def type_writer(named_expressions) # rubocop:disable Metrics/AbcSize
|
|
27
|
-
named_expressions.each do |name,
|
|
31
|
+
named_expressions.each do |name, expression|
|
|
28
32
|
last_caller = caller_locations(1, 1).first
|
|
29
|
-
|
|
33
|
+
file_path = last_caller.path
|
|
34
|
+
start_line = last_caller.lineno
|
|
35
|
+
scope = "#{self}##{name}"
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
@low_methods["#{name}="] = MethodProxy.new(name:,
|
|
37
|
+
param_proxies = [ParamProxy.new(expression: type_expression(expression), name:, type: :hashreq, file_path:, start_line:, scope:)]
|
|
38
|
+
@low_methods["#{name}="] = MethodProxy.new(file_path:, start_line:, scope:, name:, param_proxies:)
|
|
33
39
|
|
|
34
40
|
define_method("#{name}=") do |value|
|
|
35
41
|
method_proxy = self.class.low_methods["#{name}="]
|
|
36
|
-
method_proxy.
|
|
42
|
+
method_proxy.param_proxies.first.expression.validate!(value:, proxy: method_proxy.param_proxies.first)
|
|
37
43
|
instance_variable_set("@#{name}", value)
|
|
38
44
|
end
|
|
39
45
|
end
|
|
@@ -48,7 +54,7 @@ module Low
|
|
|
48
54
|
|
|
49
55
|
private
|
|
50
56
|
|
|
51
|
-
def
|
|
57
|
+
def type_expression(expression)
|
|
52
58
|
if expression.is_a?(::Expressions::Expression)
|
|
53
59
|
expression
|
|
54
60
|
elsif ::Low::TypeQuery.type?(expression)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../factories/expression_factory'
|
|
4
|
-
require_relative '../proxies/file_proxy'
|
|
5
4
|
require_relative '../proxies/local_proxy'
|
|
6
5
|
require_relative '../types/error_types'
|
|
7
6
|
|
|
@@ -11,8 +10,9 @@ module Low
|
|
|
11
10
|
value = type_expression.default_value
|
|
12
11
|
|
|
13
12
|
last_caller = caller_locations(1, 1).first
|
|
14
|
-
|
|
15
|
-
|
|
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
16
|
|
|
17
17
|
type_expression.validate!(value:, proxy:)
|
|
18
18
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'expressions'
|
|
4
|
+
require 'lowkey'
|
|
4
5
|
|
|
5
6
|
require_relative '../expressions/expressions'
|
|
6
7
|
require_relative '../expressions/type_expression'
|
|
7
|
-
require_relative '../proxies/file_proxy'
|
|
8
8
|
require_relative '../proxies/param_proxy'
|
|
9
9
|
require_relative '../proxies/return_proxy'
|
|
10
|
-
require_relative '../queries/file_parser'
|
|
11
10
|
require_relative '../syntax/syntax'
|
|
12
11
|
require_relative '../types/complex_types'
|
|
13
12
|
require_relative '../types/status'
|
|
@@ -20,15 +19,8 @@ module Low
|
|
|
20
19
|
include Low::Expressions
|
|
21
20
|
include Low::Types
|
|
22
21
|
|
|
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
22
|
# 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:,
|
|
23
|
+
def param_proxies(method_node:, file_path:, scope:)
|
|
32
24
|
return [] if method_node.parameters.nil?
|
|
33
25
|
|
|
34
26
|
params_without_block = method_node.parameters.slice.delete_suffix(', &block')
|
|
@@ -38,7 +30,7 @@ module Low
|
|
|
38
30
|
# Local variable names are prefixed with __lt or __rb where needed to avoid being overridden by method parameters.
|
|
39
31
|
typed_method = <<~RUBY
|
|
40
32
|
-> (#{params_without_block}, __rb_method:, __lt_file:) {
|
|
41
|
-
param_proxies_for_expressions(ruby_method: __rb_method,
|
|
33
|
+
param_proxies_for_expressions(ruby_method: __rb_method, file_path: __lt_file, start_line: method_node.start_line, scope:, method_binding: binding)
|
|
42
34
|
}
|
|
43
35
|
RUBY
|
|
44
36
|
|
|
@@ -46,27 +38,29 @@ module Low
|
|
|
46
38
|
|
|
47
39
|
# Called with only required args (as nil) and optional args omitted, to evaluate expressions stored as default values.
|
|
48
40
|
eval(typed_method, binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
|
|
49
|
-
.call(*required_args, **required_kwargs, __rb_method: ruby_method, __lt_file:
|
|
41
|
+
.call(*required_args, **required_kwargs, __rb_method: ruby_method, __lt_file: file_path)
|
|
50
42
|
|
|
51
43
|
# TODO: Unit test this.
|
|
52
44
|
rescue ArgumentError => e
|
|
53
45
|
raise ArgumentError, "Incorrect param syntax: #{e.message}"
|
|
54
46
|
end
|
|
55
47
|
|
|
56
|
-
def return_proxy(method_node:,
|
|
57
|
-
return_type =
|
|
48
|
+
def return_proxy(method_node:, name:, file_path:, scope:)
|
|
49
|
+
return_type = Lowkey::ClassProxy.return_type(method_node:)
|
|
58
50
|
return nil if return_type.nil?
|
|
59
51
|
|
|
52
|
+
start_line = method_node.start_line
|
|
53
|
+
|
|
60
54
|
begin
|
|
61
55
|
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
62
56
|
expression = eval(return_type.slice, binding, __FILE__, __LINE__).call # rubocop:disable Security/Eval
|
|
63
57
|
rescue NameError
|
|
64
|
-
raise NameError, "Unknown return type '#{return_type.slice}' for #{
|
|
58
|
+
raise NameError, "Unknown return type '#{return_type.slice}' for #{scope} at #{file_path}:#{start_line}"
|
|
65
59
|
end
|
|
66
60
|
|
|
67
61
|
expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
|
|
68
62
|
|
|
69
|
-
ReturnProxy.new(type_expression: expression, name
|
|
63
|
+
ReturnProxy.new(type_expression: expression, name:, file_path:, start_line:, scope:)
|
|
70
64
|
end
|
|
71
65
|
|
|
72
66
|
private
|
|
@@ -89,7 +83,7 @@ module Low
|
|
|
89
83
|
[required_args, required_kwargs]
|
|
90
84
|
end
|
|
91
85
|
|
|
92
|
-
def param_proxies_for_expressions(ruby_method:,
|
|
86
|
+
def param_proxies_for_expressions(ruby_method:, file_path:, start_line:, scope:, method_binding:)
|
|
93
87
|
param_proxies = []
|
|
94
88
|
|
|
95
89
|
ruby_method.parameters.each_with_index do |param, position|
|
|
@@ -110,7 +104,7 @@ module Low
|
|
|
110
104
|
expression = TypeExpression.new(type: local_variable)
|
|
111
105
|
end
|
|
112
106
|
|
|
113
|
-
param_proxies << ParamProxy.new(expression:, name:, type:,
|
|
107
|
+
param_proxies << ParamProxy.new(expression:, name:, type:, file_path:, start_line:, scope:, position:) if expression
|
|
114
108
|
end
|
|
115
109
|
|
|
116
110
|
param_proxies
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Low
|
|
4
|
+
# Used by proxies to output errors.
|
|
4
5
|
class ErrorInterface
|
|
5
|
-
attr_reader :
|
|
6
|
+
attr_reader :file_path, :start_line, :scope
|
|
7
|
+
|
|
8
|
+
def initialize(file_path:, start_line:, scope:)
|
|
9
|
+
@file_path = file_path
|
|
10
|
+
@start_line = start_line
|
|
11
|
+
@scope = scope
|
|
6
12
|
|
|
7
|
-
def initialize
|
|
8
|
-
@file = nil
|
|
9
13
|
@output_mode = LowType.config.output_mode
|
|
10
14
|
@output_size = LowType.config.output_size
|
|
11
15
|
end
|
|
@@ -34,8 +38,8 @@ module Low
|
|
|
34
38
|
# Remove LowType defined method file paths from the backtrace.
|
|
35
39
|
filtered_backtrace = backtrace.reject { |line| hidden_paths.find { |file_path| line.include?(file_path) } }
|
|
36
40
|
|
|
37
|
-
# Add the proxied
|
|
38
|
-
proxy_file_backtrace = "#{
|
|
41
|
+
# Add the proxied entity to the backtrace.
|
|
42
|
+
proxy_file_backtrace = "#{file_path}:#{start_line}:in '#{scope}'"
|
|
39
43
|
from_prefix = filtered_backtrace.first.match(/\s+from /)
|
|
40
44
|
proxy_file_backtrace = "#{from_prefix}#{proxy_file_backtrace}" if from_prefix
|
|
41
45
|
|
data/lib/low_type.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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
8
|
require_relative 'expressions/expressions'
|
|
7
|
-
require_relative 'queries/file_parser'
|
|
8
9
|
require_relative 'queries/file_query'
|
|
9
10
|
require_relative 'syntax/syntax'
|
|
10
11
|
require_relative 'types/complex_types'
|
|
@@ -23,15 +24,16 @@ module LowType
|
|
|
23
24
|
file_path = Low::FileQuery.file_path(klass:)
|
|
24
25
|
return unless File.exist?(file_path)
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
file_proxy = Lowkey.load(file_path:)
|
|
28
|
+
class_proxy = file_proxy.definitions[klass.name]
|
|
27
29
|
|
|
28
30
|
klass.extend Low::TypeAccessors
|
|
29
31
|
klass.include Low::Types
|
|
30
32
|
klass.include Low::Expressions
|
|
31
|
-
klass.prepend Low::Redefiner.redefine(method_nodes:
|
|
32
|
-
klass.singleton_class.prepend Low::Redefiner.redefine(method_nodes:
|
|
33
|
+
klass.prepend Low::Redefiner.redefine(method_nodes: class_proxy.instance_methods, class_proxy:, klass:)
|
|
34
|
+
klass.singleton_class.prepend Low::Redefiner.redefine(method_nodes: class_proxy.class_methods, class_proxy:, klass:)
|
|
33
35
|
|
|
34
|
-
if (adapter = Low::Adapter::Loader.load(klass:,
|
|
36
|
+
if (adapter = Low::Adapter::Loader.load(klass:, class_proxy:))
|
|
35
37
|
adapter.process
|
|
36
38
|
klass.prepend Low::Adapter::Methods
|
|
37
39
|
end
|
data/lib/proxies/local_proxy.rb
CHANGED
|
@@ -7,12 +7,11 @@ module Low
|
|
|
7
7
|
class LocalProxy < ErrorInterface
|
|
8
8
|
attr_reader :type_expression, :name
|
|
9
9
|
|
|
10
|
-
def initialize(type_expression:, name:,
|
|
11
|
-
super()
|
|
10
|
+
def initialize(type_expression:, name:, file_path:, start_line:, scope:)
|
|
11
|
+
super(file_path:, start_line:, scope:)
|
|
12
12
|
|
|
13
13
|
@type_expression = type_expression
|
|
14
14
|
@name = name
|
|
15
|
-
@file = file
|
|
16
15
|
end
|
|
17
16
|
|
|
18
17
|
def error_type
|
|
@@ -20,7 +19,7 @@ module Low
|
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
def error_message(value:)
|
|
23
|
-
"Invalid variable type #{output(value:)} in '#{name.class}:#{@
|
|
22
|
+
"Invalid variable type #{output(value:)} in '#{name.class}:#{@start_line}'. Valid types: '#{type_expression.valid_types}'"
|
|
24
23
|
end
|
|
25
24
|
end
|
|
26
25
|
end
|
data/lib/proxies/method_proxy.rb
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'forwardable'
|
|
4
|
-
|
|
5
3
|
module Low
|
|
6
4
|
class MethodProxy
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
attr_reader :name, :file, :params, :return_proxy
|
|
5
|
+
attr_reader :file_path, :start_line, :scope, :name, :param_proxies, :return_proxy
|
|
10
6
|
|
|
11
|
-
#
|
|
12
|
-
|
|
7
|
+
# TODO: Refactor file path, start line and scope into "meta scope" model.
|
|
8
|
+
def initialize(file_path:, start_line:, scope:, name:, param_proxies: [], return_proxy: nil) # rubocop:disable Metrics/ParameterLists
|
|
9
|
+
@file_path = file_path
|
|
10
|
+
@start_line = start_line
|
|
11
|
+
@scope = scope
|
|
13
12
|
|
|
14
|
-
def initialize(name:, file: nil, params: [], return_proxy: nil)
|
|
15
13
|
@name = name
|
|
16
|
-
@
|
|
17
|
-
@params = params
|
|
14
|
+
@param_proxies = param_proxies
|
|
18
15
|
@return_proxy = return_proxy
|
|
19
16
|
end
|
|
20
17
|
end
|
data/lib/proxies/param_proxy.rb
CHANGED
|
@@ -7,14 +7,14 @@ module Low
|
|
|
7
7
|
class ParamProxy < ErrorInterface
|
|
8
8
|
attr_reader :expression, :name, :type, :position
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
# TODO: Refactor file path, start line and scope into "meta scope" model.
|
|
11
|
+
def initialize(expression:, name:, type:, file_path:, start_line:, scope:, position: nil) # rubocop:disable Metrics/ParameterLists
|
|
12
|
+
super(file_path:, start_line:, scope:)
|
|
12
13
|
|
|
13
14
|
@expression = expression
|
|
14
15
|
@name = name
|
|
15
16
|
@type = type
|
|
16
17
|
@position = position
|
|
17
|
-
@file = file
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def required?
|
data/lib/proxies/return_proxy.rb
CHANGED
|
@@ -7,12 +7,11 @@ module Low
|
|
|
7
7
|
class ReturnProxy < ErrorInterface
|
|
8
8
|
attr_reader :type_expression, :name
|
|
9
9
|
|
|
10
|
-
def initialize(type_expression:, name:,
|
|
11
|
-
super()
|
|
10
|
+
def initialize(type_expression:, name:, file_path:, start_line:, scope:)
|
|
11
|
+
super(file_path:, start_line:, scope:)
|
|
12
12
|
|
|
13
13
|
@type_expression = type_expression
|
|
14
14
|
@name = name
|
|
15
|
-
@file = file
|
|
16
15
|
end
|
|
17
16
|
|
|
18
17
|
def error_type
|
data/lib/version.rb
CHANGED
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.
|
|
4
|
+
version: 1.1.9
|
|
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.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.2'
|
|
26
40
|
description: An elegant and simple way to define types in Ruby, only when you need
|
|
27
41
|
them.
|
|
28
42
|
email:
|
|
@@ -45,13 +59,10 @@ files:
|
|
|
45
59
|
- lib/interfaces/adapter_interface.rb
|
|
46
60
|
- lib/interfaces/error_interface.rb
|
|
47
61
|
- lib/low_type.rb
|
|
48
|
-
- lib/proxies/class_proxy.rb
|
|
49
|
-
- lib/proxies/file_proxy.rb
|
|
50
62
|
- lib/proxies/local_proxy.rb
|
|
51
63
|
- lib/proxies/method_proxy.rb
|
|
52
64
|
- lib/proxies/param_proxy.rb
|
|
53
65
|
- lib/proxies/return_proxy.rb
|
|
54
|
-
- lib/queries/file_parser.rb
|
|
55
66
|
- lib/queries/file_query.rb
|
|
56
67
|
- lib/queries/type_query.rb
|
|
57
68
|
- lib/syntax/syntax.rb
|
data/lib/proxies/class_proxy.rb
DELETED
|
@@ -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
|
data/lib/proxies/file_proxy.rb
DELETED
|
@@ -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
|
data/lib/queries/file_parser.rb
DELETED
|
@@ -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
|