low_type 1.1.2 → 1.1.3
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/definitions/redefiner.rb +38 -72
- data/lib/factories/proxy_factory.rb +80 -2
- data/lib/low_type.rb +2 -2
- data/lib/proxies/param_proxy.rb +4 -0
- data/lib/syntax/union_types.rb +1 -1
- data/lib/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31fd9fee4aba001196fa20d6aaf51428121c4512ec85093bd3c68a1103b0ba83
|
|
4
|
+
data.tar.gz: 6ddc9528e3f5b0b6ac3fb841bf620874ab3ef08f0cd7acb0d6e0f6aebe3d20d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6eb3e48691817f849f922e5f59af5157eb107b9cf960cdd7e147eb49561a0617ad6e6669d8f7a8319f8b39ea46e98f66cb4ac91e4c4a35fe269b2db115811a2f
|
|
7
|
+
data.tar.gz: 4ad23e5c8e4c5be264713f4dc6ea6deff8e66019db967ff09bbe92d4b0e7d3d46796bc123fa188a17d3b6904e76268e29a9def75b2c34304d0712c785cfee4ee
|
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../expressions/expressions'
|
|
4
|
-
require_relative '../expressions/type_expression'
|
|
5
3
|
require_relative '../expressions/value_expression'
|
|
6
|
-
require_relative '../factories/expression_factory'
|
|
7
4
|
require_relative '../factories/proxy_factory'
|
|
8
|
-
require_relative '../proxies/file_proxy'
|
|
9
5
|
require_relative '../proxies/method_proxy'
|
|
10
|
-
require_relative '../proxies/param_proxy'
|
|
11
6
|
require_relative '../queries/type_query'
|
|
12
|
-
require_relative '../syntax/syntax'
|
|
13
7
|
require_relative 'repository'
|
|
14
8
|
|
|
15
9
|
module LowType
|
|
16
10
|
# Redefine methods to have their arguments and return values type checked.
|
|
17
11
|
class Redefiner
|
|
18
|
-
using Syntax
|
|
19
|
-
|
|
20
12
|
class << self
|
|
21
|
-
include Expressions
|
|
22
|
-
|
|
23
13
|
def redefine(method_nodes:, class_proxy:, file_path:)
|
|
24
|
-
method_proxies =
|
|
25
|
-
|
|
14
|
+
method_proxies = build_methods(method_nodes:, klass: class_proxy.klass, file_path:)
|
|
15
|
+
|
|
16
|
+
if LowType.config.type_checking
|
|
17
|
+
typed_methods(method_proxies:, class_proxy:)
|
|
18
|
+
else
|
|
19
|
+
untyped_methods(method_proxies:, class_proxy:)
|
|
20
|
+
end
|
|
26
21
|
end
|
|
27
22
|
|
|
28
23
|
def redefinable?(method_proxy:, class_proxy:)
|
|
@@ -42,13 +37,28 @@ module LowType
|
|
|
42
37
|
true
|
|
43
38
|
end
|
|
44
39
|
|
|
40
|
+
def untyped_args(args:, kwargs:, method_proxy:) # rubocop:disable Metrics/AbcSize
|
|
41
|
+
method_proxy.params.each do |param_proxy|
|
|
42
|
+
value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
|
|
43
|
+
|
|
44
|
+
next unless value.nil?
|
|
45
|
+
raise param_proxy.error_type, param_proxy.error_message(value:) if param_proxy.required?
|
|
46
|
+
|
|
47
|
+
value = param_proxy.type_expression.default_value # Default value can still be `nil`.
|
|
48
|
+
value = value.value if value.is_a?(ValueExpression)
|
|
49
|
+
param_proxy.position ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[args, kwargs]
|
|
53
|
+
end
|
|
54
|
+
|
|
45
55
|
private
|
|
46
56
|
|
|
47
|
-
def
|
|
57
|
+
def build_methods(method_nodes:, klass:, file_path:)
|
|
48
58
|
method_nodes.each do |name, method_node|
|
|
49
59
|
file = ProxyFactory.file_proxy(path: file_path, node: method_node, scope: "#{klass}##{name}")
|
|
50
60
|
|
|
51
|
-
param_proxies = param_proxies(method_node:, file:)
|
|
61
|
+
param_proxies = ProxyFactory.param_proxies(method_node:, file:)
|
|
52
62
|
return_proxy = ProxyFactory.return_proxy(method_node:, file:)
|
|
53
63
|
method_proxy = MethodProxy.new(name:, params: param_proxies, return_proxy:, file:)
|
|
54
64
|
|
|
@@ -58,21 +68,19 @@ module LowType
|
|
|
58
68
|
Repository.all(klass:)
|
|
59
69
|
end
|
|
60
70
|
|
|
61
|
-
def
|
|
71
|
+
def typed_methods(method_proxies:, class_proxy:) # rubocop:disable Metrics
|
|
62
72
|
Module.new do
|
|
63
73
|
method_proxies.each do |name, method_proxy|
|
|
64
74
|
next unless LowType::Redefiner.redefinable?(method_proxy:, class_proxy:)
|
|
65
75
|
|
|
66
|
-
#
|
|
76
|
+
# You are now in the binding of the includer class (`name` is also available here).
|
|
67
77
|
define_method(name) do |*args, **kwargs|
|
|
68
78
|
# Inlined version of Repository.load() for performance increase.
|
|
69
79
|
method_proxy = instance_of?(Class) ? low_methods[name] : self.class.low_methods[name] || Object.low_methods[name]
|
|
70
80
|
|
|
71
81
|
method_proxy.params.each do |param_proxy|
|
|
72
82
|
value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
|
|
73
|
-
if value.nil? && param_proxy.
|
|
74
|
-
value = param_proxy.type_expression.default_value
|
|
75
|
-
end
|
|
83
|
+
value = param_proxy.type_expression.default_value if value.nil? && !param_proxy.required?
|
|
76
84
|
|
|
77
85
|
param_proxy.type_expression.validate!(value:, proxy: param_proxy)
|
|
78
86
|
value = value.value if value.is_a?(ValueExpression)
|
|
@@ -93,64 +101,22 @@ module LowType
|
|
|
93
101
|
end
|
|
94
102
|
end
|
|
95
103
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
proxy_method = proxy_method(method_node:)
|
|
101
|
-
required_args, required_kwargs = required_args(proxy_method:)
|
|
102
|
-
|
|
103
|
-
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
104
|
-
typed_method = <<~RUBY
|
|
105
|
-
-> (#{params}) {
|
|
106
|
-
param_proxies = []
|
|
107
|
-
|
|
108
|
-
proxy_method.parameters.each_with_index do |param, position|
|
|
109
|
-
type, name = param
|
|
110
|
-
position = nil unless [:opt, :req, :rest].include?(type)
|
|
111
|
-
expression = binding.local_variable_get(name)
|
|
104
|
+
def untyped_methods(method_proxies:, class_proxy:)
|
|
105
|
+
Module.new do
|
|
106
|
+
method_proxies.each do |name, method_proxy|
|
|
107
|
+
next unless LowType::Redefiner.redefinable?(method_proxy:, class_proxy:)
|
|
112
108
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
# You are now in the binding of the includer class (`name` is also available here).
|
|
110
|
+
define_method(name) do |*args, **kwargs|
|
|
111
|
+
# NOTE: Type checking is currently disabled. See 'config.type_checking'.
|
|
112
|
+
method_proxy = instance_of?(Class) ? low_methods[name] : self.class.low_methods[name] || Object.low_methods[name]
|
|
113
|
+
args, kwargs = LowType::Redefiner.untyped_args(args:, kwargs:, method_proxy:)
|
|
114
|
+
super(*args, **kwargs)
|
|
118
115
|
end
|
|
119
116
|
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
RUBY
|
|
123
|
-
|
|
124
|
-
# Called with only required args (as nil) and optional args omitted, to evaluate type expressions (from default values).
|
|
125
|
-
eval(typed_method, binding, __FILE__, __LINE__).call(*required_args, **required_kwargs) # rubocop:disable Security/Eval
|
|
126
|
-
|
|
127
|
-
# TODO: Write spec for this.
|
|
128
|
-
rescue ArgumentError => e
|
|
129
|
-
raise ArgumentError, "Incorrect param syntax: #{e.message}"
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def proxy_method(method_node:)
|
|
133
|
-
params = method_node.parameters.slice
|
|
134
|
-
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
135
|
-
eval("-> (#{params}) {}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def required_args(proxy_method:)
|
|
139
|
-
required_args = []
|
|
140
|
-
required_kwargs = {}
|
|
141
|
-
|
|
142
|
-
proxy_method.parameters.each do |param|
|
|
143
|
-
param_type, param_name = param
|
|
144
|
-
|
|
145
|
-
case param_type
|
|
146
|
-
when :req
|
|
147
|
-
required_args << nil
|
|
148
|
-
when :keyreq
|
|
149
|
-
required_kwargs[param_name] = nil
|
|
117
|
+
private name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
|
|
150
118
|
end
|
|
151
119
|
end
|
|
152
|
-
|
|
153
|
-
[required_args, required_kwargs]
|
|
154
120
|
end
|
|
155
121
|
end
|
|
156
122
|
end
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../expressions/expressions'
|
|
3
4
|
require_relative '../expressions/type_expression'
|
|
4
5
|
require_relative '../proxies/file_proxy'
|
|
6
|
+
require_relative '../proxies/param_proxy'
|
|
5
7
|
require_relative '../proxies/return_proxy'
|
|
6
8
|
require_relative '../queries/file_parser'
|
|
9
|
+
require_relative '../syntax/syntax'
|
|
7
10
|
|
|
8
11
|
module LowType
|
|
9
12
|
class ProxyFactory
|
|
13
|
+
using ::LowType::Syntax
|
|
14
|
+
|
|
10
15
|
class << self
|
|
16
|
+
include Expressions
|
|
17
|
+
|
|
11
18
|
def file_proxy(node:, path:, scope:)
|
|
12
19
|
start_line = node.respond_to?(:start_line) ? node.start_line : nil
|
|
13
20
|
end_line = node.respond_to?(:end_line) ? node.end_line : nil
|
|
@@ -15,16 +22,87 @@ module LowType
|
|
|
15
22
|
FileProxy.new(path:, start_line:, end_line:, scope:)
|
|
16
23
|
end
|
|
17
24
|
|
|
25
|
+
def param_proxies(method_node:, file:)
|
|
26
|
+
return [] if method_node.parameters.nil?
|
|
27
|
+
|
|
28
|
+
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
29
|
+
ruby_method = eval("-> (#{method_node.parameters.slice}) {}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
|
|
30
|
+
|
|
31
|
+
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
32
|
+
# Local variable names are prefixed with __lt or __rb where necessary to avoid being overridden by method parameters.
|
|
33
|
+
typed_method = <<~RUBY
|
|
34
|
+
-> (#{method_node.parameters.slice}, __rb_method:, __lt_file:) {
|
|
35
|
+
param_proxies_for_type_expressions(ruby_method: __rb_method, file: __lt_file, method_binding: binding)
|
|
36
|
+
}
|
|
37
|
+
RUBY
|
|
38
|
+
|
|
39
|
+
# Called with only required args (as nil) and optional args omitted, to evaluate type expressions (from default values).
|
|
40
|
+
# Passes internal variables with namespaced names to avoid conflicts with the method parameters.
|
|
41
|
+
required_args, required_kwargs = required_args(ruby_method:)
|
|
42
|
+
eval(typed_method, binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
|
|
43
|
+
.call(*required_args, **required_kwargs, __rb_method: ruby_method, __lt_file: file)
|
|
44
|
+
|
|
45
|
+
# TODO: Unit test this.
|
|
46
|
+
rescue ArgumentError => e
|
|
47
|
+
raise ArgumentError, "Incorrect param syntax: #{e.message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
18
50
|
def return_proxy(method_node:, file:)
|
|
19
51
|
return_type = FileParser.return_type(method_node:)
|
|
20
52
|
return nil if return_type.nil?
|
|
21
53
|
|
|
22
|
-
|
|
23
|
-
|
|
54
|
+
begin
|
|
55
|
+
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
56
|
+
expression = eval(return_type.slice, binding, __FILE__, __LINE__).call # rubocop:disable Security/Eval
|
|
57
|
+
rescue NameError
|
|
58
|
+
raise NameError, "Unknown return type '#{return_type.slice}' for #{file.scope} at #{file.path}:#{file.start_line}"
|
|
59
|
+
end
|
|
60
|
+
|
|
24
61
|
expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
|
|
25
62
|
|
|
26
63
|
ReturnProxy.new(type_expression: expression, name: method_node.name, file:)
|
|
27
64
|
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def required_args(ruby_method:)
|
|
69
|
+
required_args = []
|
|
70
|
+
required_kwargs = {}
|
|
71
|
+
|
|
72
|
+
ruby_method.parameters.each do |param|
|
|
73
|
+
param_type, param_name = param
|
|
74
|
+
|
|
75
|
+
case param_type
|
|
76
|
+
when :req
|
|
77
|
+
required_args << nil
|
|
78
|
+
when :keyreq
|
|
79
|
+
required_kwargs[param_name] = nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
[required_args, required_kwargs]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def param_proxies_for_type_expressions(ruby_method:, file:, method_binding:)
|
|
87
|
+
param_proxies = []
|
|
88
|
+
|
|
89
|
+
ruby_method.parameters.each_with_index do |param, position|
|
|
90
|
+
type, name = param
|
|
91
|
+
position = nil unless %i[opt req rest].include?(type)
|
|
92
|
+
expression = method_binding.local_variable_get(name)
|
|
93
|
+
|
|
94
|
+
type_expression = nil
|
|
95
|
+
if expression.is_a?(TypeExpression)
|
|
96
|
+
type_expression = expression
|
|
97
|
+
elsif ::LowType::TypeQuery.type?(expression)
|
|
98
|
+
type_expression = TypeExpression.new(type: expression)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
param_proxies << ParamProxy.new(type_expression:, name:, type:, position:, file:) if type_expression
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
param_proxies
|
|
105
|
+
end
|
|
28
106
|
end
|
|
29
107
|
end
|
|
30
108
|
end
|
data/lib/low_type.rb
CHANGED
|
@@ -36,8 +36,8 @@ module LowType
|
|
|
36
36
|
|
|
37
37
|
class << self
|
|
38
38
|
def config
|
|
39
|
-
config = Struct.new(:error_mode, :output_mode, :output_size, :deep_type_check, :union_type_expressions)
|
|
40
|
-
@config ||= config.new(:error, :type, 100, false, true)
|
|
39
|
+
config = Struct.new(:type_checking, :error_mode, :output_mode, :output_size, :deep_type_check, :union_type_expressions)
|
|
40
|
+
@config ||= config.new(true, :error, :type, 100, false, true)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def configure
|
data/lib/proxies/param_proxy.rb
CHANGED
data/lib/syntax/union_types.rb
CHANGED
|
@@ -19,7 +19,7 @@ class Object
|
|
|
19
19
|
expression | self
|
|
20
20
|
expression
|
|
21
21
|
else
|
|
22
|
-
# We turn our type into a type expression and pass in their
|
|
22
|
+
# We turn our type into a type expression and pass in their type/value.
|
|
23
23
|
type_expression = ::LowType::TypeExpression.new(type: self)
|
|
24
24
|
type_expression | expression
|
|
25
25
|
end
|
data/lib/version.rb
CHANGED