low_type 1.1.1 → 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/adapters/sinatra_adapter.rb +2 -3
- data/lib/definitions/redefiner.rb +123 -0
- data/lib/definitions/repository.rb +32 -0
- data/lib/{instance_types.rb → definitions/type_accessors.rb} +6 -6
- data/lib/{local_types.rb → expressions/expressions.rb} +6 -6
- data/lib/{type_expression.rb → expressions/type_expression.rb} +3 -3
- data/lib/factories/expression_factory.rb +2 -2
- data/lib/factories/proxy_factory.rb +89 -3
- data/lib/interfaces/error_interface.rb +1 -1
- data/lib/low_type.rb +11 -12
- data/lib/proxies/class_proxy.rb +20 -0
- data/lib/proxies/file_proxy.rb +9 -3
- data/lib/proxies/local_proxy.rb +1 -1
- data/lib/proxies/method_proxy.rb +10 -2
- data/lib/proxies/param_proxy.rb +4 -0
- data/lib/queries/file_parser.rb +18 -21
- data/lib/queries/type_query.rb +1 -1
- data/lib/syntax/union_types.rb +1 -1
- data/lib/version.rb +1 -1
- metadata +8 -6
- data/lib/redefiner.rb +0 -147
- /data/lib/{value_expression.rb → expressions/value_expression.rb} +0 -0
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
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'prism'
|
|
4
4
|
|
|
5
|
+
require_relative '../factories/proxy_factory'
|
|
5
6
|
require_relative '../interfaces/adapter_interface'
|
|
6
|
-
require_relative '../proxies/file_proxy'
|
|
7
7
|
require_relative '../proxies/return_proxy'
|
|
8
8
|
require_relative '../types/error_types'
|
|
9
9
|
|
|
@@ -27,8 +27,7 @@ module LowType
|
|
|
27
27
|
|
|
28
28
|
pattern = arguments_node.arguments.first.content
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
file = FileProxy.new(path: @file_path, line:, scope: "#{@klass}##{method_call.name}")
|
|
30
|
+
file = ProxyFactory.file_proxy(node: method_call, path: @file_path, scope: "#{@klass}##{method_call.name}")
|
|
32
31
|
next unless (return_proxy = return_proxy(method_node: method_call, pattern:, file:))
|
|
33
32
|
|
|
34
33
|
route = "#{method_call.name.upcase} #{pattern}"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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'
|
|
8
|
+
|
|
9
|
+
module LowType
|
|
10
|
+
# Redefine methods to have their arguments and return values type checked.
|
|
11
|
+
class Redefiner
|
|
12
|
+
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
|
+
|
|
16
|
+
if LowType.config.type_checking
|
|
17
|
+
typed_methods(method_proxies:, class_proxy:)
|
|
18
|
+
else
|
|
19
|
+
untyped_methods(method_proxies:, class_proxy:)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def redefinable?(method_proxy:, class_proxy:)
|
|
24
|
+
# Method has no types.
|
|
25
|
+
if method_proxy.params == [] && method_proxy.return_proxy.nil?
|
|
26
|
+
LowType::Repository.delete(name: method_proxy.name, klass: class_proxy.klass)
|
|
27
|
+
return false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Method outside class bounds.
|
|
31
|
+
within_bounds = method_proxy.start_line > class_proxy.start_line && method_proxy.end_line <= class_proxy.end_line
|
|
32
|
+
if method_proxy.lines? && class_proxy.lines? && !within_bounds
|
|
33
|
+
LowType::Repository.delete(name: method_proxy.name, klass: class_proxy.klass)
|
|
34
|
+
return false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
end
|
|
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
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def build_methods(method_nodes:, klass:, file_path:)
|
|
58
|
+
method_nodes.each do |name, method_node|
|
|
59
|
+
file = ProxyFactory.file_proxy(path: file_path, node: method_node, scope: "#{klass}##{name}")
|
|
60
|
+
|
|
61
|
+
param_proxies = ProxyFactory.param_proxies(method_node:, file:)
|
|
62
|
+
return_proxy = ProxyFactory.return_proxy(method_node:, file:)
|
|
63
|
+
method_proxy = MethodProxy.new(name:, params: param_proxies, return_proxy:, file:)
|
|
64
|
+
|
|
65
|
+
Repository.save(method: method_proxy, klass:)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Repository.all(klass:)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def typed_methods(method_proxies:, class_proxy:) # rubocop:disable Metrics
|
|
72
|
+
Module.new do
|
|
73
|
+
method_proxies.each do |name, method_proxy|
|
|
74
|
+
next unless LowType::Redefiner.redefinable?(method_proxy:, class_proxy:)
|
|
75
|
+
|
|
76
|
+
# You are now in the binding of the includer class (`name` is also available here).
|
|
77
|
+
define_method(name) do |*args, **kwargs|
|
|
78
|
+
# Inlined version of Repository.load() for performance increase.
|
|
79
|
+
method_proxy = instance_of?(Class) ? low_methods[name] : self.class.low_methods[name] || Object.low_methods[name]
|
|
80
|
+
|
|
81
|
+
method_proxy.params.each do |param_proxy|
|
|
82
|
+
value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
|
|
83
|
+
value = param_proxy.type_expression.default_value if value.nil? && !param_proxy.required?
|
|
84
|
+
|
|
85
|
+
param_proxy.type_expression.validate!(value:, proxy: param_proxy)
|
|
86
|
+
value = value.value if value.is_a?(ValueExpression)
|
|
87
|
+
param_proxy.position ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if (return_proxy = method_proxy.return_proxy)
|
|
91
|
+
return_value = super(*args, **kwargs)
|
|
92
|
+
return_proxy.type_expression.validate!(value: return_value, proxy: return_proxy)
|
|
93
|
+
return return_value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
super(*args, **kwargs)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
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:)
|
|
108
|
+
|
|
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)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LowType
|
|
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,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
4
|
-
require_relative '
|
|
5
|
-
require_relative '
|
|
3
|
+
require_relative '../expressions/type_expression'
|
|
4
|
+
require_relative '../proxies/return_proxy'
|
|
5
|
+
require_relative '../queries/type_query'
|
|
6
6
|
|
|
7
7
|
module LowType
|
|
8
|
-
module
|
|
8
|
+
module TypeAccessors
|
|
9
9
|
def type_reader(named_expressions)
|
|
10
10
|
named_expressions.each do |name, expression|
|
|
11
11
|
last_caller = caller_locations(1, 1).first
|
|
12
12
|
type_expression = type_expression(expression)
|
|
13
|
-
file = FileProxy.new(path: last_caller.path,
|
|
13
|
+
file = FileProxy.new(path: last_caller.path, start_line: last_caller.lineno, scope: "#{self}##{name}")
|
|
14
14
|
|
|
15
15
|
@low_methods[name] = MethodProxy.new(name:, return_proxy: ReturnProxy.new(type_expression:, name:, file:))
|
|
16
16
|
|
|
@@ -27,7 +27,7 @@ module LowType
|
|
|
27
27
|
named_expressions.each do |name, expression|
|
|
28
28
|
last_caller = caller_locations(1, 1).first
|
|
29
29
|
type_expression = type_expression(expression)
|
|
30
|
-
file = FileProxy.new(path: last_caller.path,
|
|
30
|
+
file = FileProxy.new(path: last_caller.path, start_line: last_caller.lineno, scope: "#{self}##{name}")
|
|
31
31
|
|
|
32
32
|
@low_methods["#{name}="] = MethodProxy.new(name:, params: [ParamProxy.new(type_expression:, name:, type: :hashreq, file:)])
|
|
33
33
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
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'
|
|
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
7
|
|
|
8
8
|
module LowType
|
|
9
|
-
module
|
|
9
|
+
module Expressions
|
|
10
10
|
def type(type_expression)
|
|
11
11
|
value = type_expression.default_value
|
|
12
12
|
|
|
13
13
|
last_caller = caller_locations(1, 1).first
|
|
14
|
-
file = FileProxy.new(path: last_caller.path,
|
|
14
|
+
file = FileProxy.new(path: last_caller.path, start_line: last_caller.lineno, scope: 'local type')
|
|
15
15
|
proxy = LocalProxy.new(type_expression:, name: self, file:)
|
|
16
16
|
|
|
17
17
|
type_expression.validate!(value:, proxy:)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'proxies/param_proxy'
|
|
4
|
-
require_relative 'queries/type_query'
|
|
3
|
+
require_relative '../proxies/param_proxy'
|
|
4
|
+
require_relative '../queries/type_query'
|
|
5
5
|
|
|
6
6
|
module LowType
|
|
7
7
|
root_path = File.expand_path(__dir__)
|
|
8
8
|
adapter_paths = Dir.chdir(root_path) { Dir.glob('adapters/*') }.map { |path| File.join(root_path, path) }
|
|
9
|
-
module_paths = %w[instance_types
|
|
9
|
+
module_paths = %w[expressions/expressions instance_types redefiner].map { |path| File.join(root_path, "#{path}.rb") }
|
|
10
10
|
|
|
11
11
|
HIDDEN_PATHS = [File.expand_path(__FILE__), *adapter_paths, *module_paths].freeze
|
|
12
12
|
|
|
@@ -1,22 +1,108 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../expressions/expressions'
|
|
4
|
+
require_relative '../expressions/type_expression'
|
|
5
|
+
require_relative '../proxies/file_proxy'
|
|
6
|
+
require_relative '../proxies/param_proxy'
|
|
3
7
|
require_relative '../proxies/return_proxy'
|
|
4
8
|
require_relative '../queries/file_parser'
|
|
5
|
-
require_relative '../
|
|
9
|
+
require_relative '../syntax/syntax'
|
|
6
10
|
|
|
7
11
|
module LowType
|
|
8
12
|
class ProxyFactory
|
|
13
|
+
using ::LowType::Syntax
|
|
14
|
+
|
|
9
15
|
class << self
|
|
16
|
+
include Expressions
|
|
17
|
+
|
|
18
|
+
def file_proxy(node:, path:, scope:)
|
|
19
|
+
start_line = node.respond_to?(:start_line) ? node.start_line : nil
|
|
20
|
+
end_line = node.respond_to?(:end_line) ? node.end_line : nil
|
|
21
|
+
|
|
22
|
+
FileProxy.new(path:, start_line:, end_line:, scope:)
|
|
23
|
+
end
|
|
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
|
+
|
|
10
50
|
def return_proxy(method_node:, file:)
|
|
11
51
|
return_type = FileParser.return_type(method_node:)
|
|
12
52
|
return nil if return_type.nil?
|
|
13
53
|
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
|
|
16
61
|
expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
|
|
17
62
|
|
|
18
63
|
ReturnProxy.new(type_expression: expression, name: method_node.name, file:)
|
|
19
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
|
|
20
106
|
end
|
|
21
107
|
end
|
|
22
108
|
end
|
|
@@ -35,7 +35,7 @@ module LowType
|
|
|
35
35
|
filtered_backtrace = backtrace.reject { |line| hidden_paths.find { |file_path| line.include?(file_path) } }
|
|
36
36
|
|
|
37
37
|
# Add the proxied file to the backtrace.
|
|
38
|
-
proxy_file_backtrace = "#{file.path}:#{file.
|
|
38
|
+
proxy_file_backtrace = "#{file.path}:#{file.start_line}:in '#{file.scope}'"
|
|
39
39
|
from_prefix = filtered_backtrace.first.match(/\s+from /)
|
|
40
40
|
proxy_file_backtrace = "#{from_prefix}#{proxy_file_backtrace}" if from_prefix
|
|
41
41
|
|
data/lib/low_type.rb
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'adapters/adapter_loader'
|
|
4
|
-
require_relative '
|
|
5
|
-
require_relative '
|
|
4
|
+
require_relative 'definitions/redefiner'
|
|
5
|
+
require_relative 'definitions/type_accessors'
|
|
6
|
+
require_relative 'expressions/expressions'
|
|
6
7
|
require_relative 'queries/file_parser'
|
|
7
8
|
require_relative 'queries/file_query'
|
|
8
|
-
require_relative '
|
|
9
|
-
require_relative '
|
|
10
|
-
require_relative 'redefiner'
|
|
9
|
+
require_relative 'syntax/syntax'
|
|
10
|
+
require_relative 'types/complex_types'
|
|
11
11
|
|
|
12
12
|
module LowType
|
|
13
13
|
# We do as much as possible on class load rather than on instantiation to be thread-safe and efficient.
|
|
@@ -22,12 +22,11 @@ module LowType
|
|
|
22
22
|
|
|
23
23
|
file_path = FileQuery.file_path(klass:)
|
|
24
24
|
parser = FileParser.new(klass:, file_path:)
|
|
25
|
-
line_numbers = parser.line_numbers
|
|
26
25
|
|
|
27
|
-
klass.extend
|
|
28
|
-
klass.include
|
|
29
|
-
klass.prepend Redefiner.redefine(method_nodes: parser.instance_methods,
|
|
30
|
-
klass.singleton_class.prepend Redefiner.redefine(method_nodes: parser.class_methods,
|
|
26
|
+
klass.extend TypeAccessors
|
|
27
|
+
klass.include Expressions
|
|
28
|
+
klass.prepend Redefiner.redefine(method_nodes: parser.instance_methods, class_proxy: parser.class_proxy, file_path:)
|
|
29
|
+
klass.singleton_class.prepend Redefiner.redefine(method_nodes: parser.class_methods, class_proxy: parser.class_proxy, file_path:)
|
|
31
30
|
|
|
32
31
|
if (adapter = Adapter::Loader.load(klass:, parser:, file_path:))
|
|
33
32
|
adapter.process
|
|
@@ -37,8 +36,8 @@ module LowType
|
|
|
37
36
|
|
|
38
37
|
class << self
|
|
39
38
|
def config
|
|
40
|
-
config = Struct.new(:error_mode, :output_mode, :output_size, :deep_type_check, :union_type_expressions)
|
|
41
|
-
@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)
|
|
42
41
|
end
|
|
43
42
|
|
|
44
43
|
def configure
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
module LowType
|
|
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
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module LowType
|
|
4
4
|
class FileProxy
|
|
5
|
-
attr_reader :path, :
|
|
5
|
+
attr_reader :path, :scope
|
|
6
|
+
attr_accessor :start_line, :end_line
|
|
6
7
|
|
|
7
|
-
def initialize(path:,
|
|
8
|
+
def initialize(path:, scope:, start_line:, end_line: nil)
|
|
8
9
|
@path = path
|
|
9
|
-
@
|
|
10
|
+
@start_line = start_line
|
|
11
|
+
@end_line = end_line || start_line
|
|
10
12
|
@scope = scope
|
|
11
13
|
end
|
|
14
|
+
|
|
15
|
+
def lines?
|
|
16
|
+
start_line && end_line
|
|
17
|
+
end
|
|
12
18
|
end
|
|
13
19
|
end
|
data/lib/proxies/local_proxy.rb
CHANGED
|
@@ -20,7 +20,7 @@ module LowType
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def error_message(value:)
|
|
23
|
-
"Invalid variable type #{output(value:)} in '#{
|
|
23
|
+
"Invalid variable type #{output(value:)} in '#{name.class}:#{@file.start_line}'. Valid types: '#{type_expression.valid_types}'"
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
data/lib/proxies/method_proxy.rb
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
3
5
|
module LowType
|
|
4
6
|
class MethodProxy
|
|
5
|
-
|
|
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?
|
|
6
13
|
|
|
7
|
-
def initialize(name:, params: [], return_proxy: nil)
|
|
14
|
+
def initialize(name:, file: nil, params: [], return_proxy: nil)
|
|
8
15
|
@name = name
|
|
16
|
+
@file = file
|
|
9
17
|
@params = params
|
|
10
18
|
@return_proxy = return_proxy
|
|
11
19
|
end
|
data/lib/proxies/param_proxy.rb
CHANGED
data/lib/queries/file_parser.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'prism'
|
|
4
|
+
require_relative '../proxies/class_proxy'
|
|
4
5
|
|
|
5
6
|
module LowType
|
|
6
7
|
class FileParser
|
|
7
|
-
attr_reader :parent_map, :instance_methods, :class_methods, :
|
|
8
|
+
attr_reader :parent_map, :instance_methods, :class_methods, :class_proxy
|
|
8
9
|
|
|
9
10
|
def initialize(klass:, file_path:)
|
|
10
11
|
@root_node = Prism.parse_file(file_path).value
|
|
@@ -13,12 +14,12 @@ module LowType
|
|
|
13
14
|
parent_mapper.visit(@root_node)
|
|
14
15
|
@parent_map = parent_mapper.parent_map
|
|
15
16
|
|
|
16
|
-
method_visitor = MethodDefVisitor.new(root_node: @root_node, parent_map:, klass:)
|
|
17
|
+
method_visitor = MethodDefVisitor.new(root_node: @root_node, parent_map:, klass:, file_path:)
|
|
17
18
|
@root_node.accept(method_visitor)
|
|
18
19
|
|
|
19
20
|
@instance_methods = method_visitor.instance_methods
|
|
20
21
|
@class_methods = method_visitor.class_methods
|
|
21
|
-
@
|
|
22
|
+
@class_proxy = ClassProxy.new(klass:, file: method_visitor.file_proxy, private_start_line: method_visitor.private_start_line)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def method_calls(method_names:)
|
|
@@ -46,50 +47,46 @@ module LowType
|
|
|
46
47
|
|
|
47
48
|
nil
|
|
48
49
|
end
|
|
49
|
-
|
|
50
|
-
def line_number(node:)
|
|
51
|
-
node.respond_to?(:start_line) ? node.start_line : nil
|
|
52
|
-
end
|
|
53
50
|
end
|
|
54
51
|
end
|
|
55
52
|
|
|
56
53
|
class MethodDefVisitor < Prism::Visitor
|
|
57
|
-
attr_reader :class_methods, :instance_methods, :
|
|
54
|
+
attr_reader :class_methods, :instance_methods, :file_proxy, :private_start_line
|
|
58
55
|
|
|
59
|
-
def initialize(root_node:, parent_map:, klass:)
|
|
56
|
+
def initialize(root_node:, parent_map:, klass:, file_path:)
|
|
60
57
|
@parent_map = parent_map
|
|
61
58
|
@klass = klass
|
|
62
59
|
|
|
63
|
-
@instance_methods =
|
|
64
|
-
@class_methods =
|
|
65
|
-
|
|
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
66
|
end
|
|
67
67
|
|
|
68
68
|
def visit_def_node(node)
|
|
69
69
|
if class_method?(node)
|
|
70
|
-
@class_methods
|
|
70
|
+
@class_methods[node.name] = node
|
|
71
71
|
else
|
|
72
|
-
@instance_methods
|
|
72
|
+
@instance_methods[node.name] = node
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
super # Continue walking the tree.
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def visit_call_node(node)
|
|
79
|
-
|
|
80
|
-
class_start = @line_numbers[:class_start]
|
|
81
|
-
class_end = @line_numbers[:class_end]
|
|
79
|
+
return super unless node.name == :private && node.respond_to?(:start_line) && file_proxy.start_line && file_proxy.end_line
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
@line_numbers[:private_start] = node.start_line
|
|
85
|
-
end
|
|
81
|
+
@private_start_line = node.start_line if node.start_line > file_proxy.start_line && node.start_line < file_proxy.end_line
|
|
86
82
|
|
|
87
83
|
super
|
|
88
84
|
end
|
|
89
85
|
|
|
90
86
|
def visit_class_node(node)
|
|
91
87
|
if node.name == @klass.to_s.to_sym
|
|
92
|
-
|
|
88
|
+
file_proxy.start_line = node.class_keyword_loc.start_line
|
|
89
|
+
file_proxy.end_line = node.end_keyword_loc.end_line
|
|
93
90
|
end
|
|
94
91
|
|
|
95
92
|
super
|
data/lib/queries/type_query.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
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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
@@ -19,14 +19,19 @@ extra_rdoc_files: []
|
|
|
19
19
|
files:
|
|
20
20
|
- lib/adapters/adapter_loader.rb
|
|
21
21
|
- lib/adapters/sinatra_adapter.rb
|
|
22
|
+
- lib/definitions/redefiner.rb
|
|
23
|
+
- lib/definitions/repository.rb
|
|
24
|
+
- lib/definitions/type_accessors.rb
|
|
25
|
+
- lib/expressions/expressions.rb
|
|
26
|
+
- lib/expressions/type_expression.rb
|
|
27
|
+
- lib/expressions/value_expression.rb
|
|
22
28
|
- lib/factories/expression_factory.rb
|
|
23
29
|
- lib/factories/proxy_factory.rb
|
|
24
30
|
- lib/factories/type_factory.rb
|
|
25
|
-
- lib/instance_types.rb
|
|
26
31
|
- lib/interfaces/adapter_interface.rb
|
|
27
32
|
- lib/interfaces/error_interface.rb
|
|
28
|
-
- lib/local_types.rb
|
|
29
33
|
- lib/low_type.rb
|
|
34
|
+
- lib/proxies/class_proxy.rb
|
|
30
35
|
- lib/proxies/file_proxy.rb
|
|
31
36
|
- lib/proxies/local_proxy.rb
|
|
32
37
|
- lib/proxies/method_proxy.rb
|
|
@@ -35,13 +40,10 @@ files:
|
|
|
35
40
|
- lib/queries/file_parser.rb
|
|
36
41
|
- lib/queries/file_query.rb
|
|
37
42
|
- lib/queries/type_query.rb
|
|
38
|
-
- lib/redefiner.rb
|
|
39
43
|
- lib/syntax/syntax.rb
|
|
40
44
|
- lib/syntax/union_types.rb
|
|
41
|
-
- lib/type_expression.rb
|
|
42
45
|
- lib/types/complex_types.rb
|
|
43
46
|
- lib/types/error_types.rb
|
|
44
|
-
- lib/value_expression.rb
|
|
45
47
|
- lib/version.rb
|
|
46
48
|
homepage: https://codeberg.org/low_ruby/low_type
|
|
47
49
|
licenses: []
|
data/lib/redefiner.rb
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'factories/expression_factory'
|
|
4
|
-
require_relative 'factories/proxy_factory'
|
|
5
|
-
require_relative 'proxies/file_proxy'
|
|
6
|
-
require_relative 'proxies/method_proxy'
|
|
7
|
-
require_relative 'proxies/param_proxy'
|
|
8
|
-
require_relative 'queries/type_query'
|
|
9
|
-
require_relative 'syntax/syntax'
|
|
10
|
-
require_relative 'type_expression'
|
|
11
|
-
require_relative 'value_expression'
|
|
12
|
-
|
|
13
|
-
module LowType
|
|
14
|
-
# Redefine methods to have their arguments and return values type checked.
|
|
15
|
-
class Redefiner
|
|
16
|
-
using Syntax
|
|
17
|
-
|
|
18
|
-
class << self
|
|
19
|
-
def redefine(method_nodes:, klass:, line_numbers:, file_path:)
|
|
20
|
-
create_proxies(method_nodes:, klass:, file_path:)
|
|
21
|
-
define_methods(method_nodes:, line_numbers:)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def create_proxies(method_nodes:, klass:, file_path:)
|
|
27
|
-
method_nodes.each do |method_node|
|
|
28
|
-
name = method_node.name
|
|
29
|
-
line = FileParser.line_number(node: method_node)
|
|
30
|
-
file = FileProxy.new(path: file_path, line:, scope: "#{klass}##{name}")
|
|
31
|
-
params = param_proxies(method_node:, file:)
|
|
32
|
-
return_proxy = ProxyFactory.return_proxy(method_node:, file:)
|
|
33
|
-
|
|
34
|
-
klass.low_methods[name] = MethodProxy.new(name:, params:, return_proxy:)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def define_methods(method_nodes:, line_numbers:) # rubocop:disable Metrics
|
|
39
|
-
class_start = line_numbers[:class_start]
|
|
40
|
-
class_end = line_numbers[:class_end]
|
|
41
|
-
private_start = line_numbers[:private_start]
|
|
42
|
-
|
|
43
|
-
Module.new do
|
|
44
|
-
method_nodes.each do |method_node|
|
|
45
|
-
method_start = method_node.respond_to?(:start_line) ? method_node.start_line : nil
|
|
46
|
-
method_end = method_node.respond_to?(:end_line) ? method_node.end_line : nil
|
|
47
|
-
|
|
48
|
-
next if method_start && method_end && class_end && !(method_start > class_start && method_end <= class_end)
|
|
49
|
-
|
|
50
|
-
name = method_node.name
|
|
51
|
-
|
|
52
|
-
define_method(name) do |*args, **kwargs|
|
|
53
|
-
method_proxy = instance_of?(Class) ? low_methods[name] : self.class.low_methods[name] || Object.low_methods[name]
|
|
54
|
-
|
|
55
|
-
method_proxy.params.each do |param_proxy|
|
|
56
|
-
# Get argument value or default value.
|
|
57
|
-
value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
|
|
58
|
-
if value.nil? && param_proxy.type_expression.default_value != :LOW_TYPE_UNDEFINED
|
|
59
|
-
value = param_proxy.type_expression.default_value
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
param_proxy.type_expression.validate!(value:, proxy: param_proxy)
|
|
63
|
-
value = value.value if value.is_a?(ValueExpression)
|
|
64
|
-
param_proxy.position ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
if (return_proxy = method_proxy.return_proxy)
|
|
68
|
-
return_value = super(*args, **kwargs)
|
|
69
|
-
return_proxy.type_expression.validate!(value: return_value, proxy: return_proxy)
|
|
70
|
-
return return_value
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
super(*args, **kwargs)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private name if private_start && method_start > private_start
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def param_proxies(method_node:, file:)
|
|
82
|
-
return [] if method_node.parameters.nil?
|
|
83
|
-
|
|
84
|
-
params = method_node.parameters.slice
|
|
85
|
-
proxy_method = proxy_method(method_node:)
|
|
86
|
-
required_args, required_kwargs = required_args(proxy_method:)
|
|
87
|
-
|
|
88
|
-
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
89
|
-
typed_method = <<~RUBY
|
|
90
|
-
-> (#{params}) {
|
|
91
|
-
param_proxies = []
|
|
92
|
-
|
|
93
|
-
proxy_method.parameters.each_with_index do |param, position|
|
|
94
|
-
type, name = param
|
|
95
|
-
position = nil unless [:opt, :req, :rest].include?(type)
|
|
96
|
-
expression = binding.local_variable_get(name)
|
|
97
|
-
|
|
98
|
-
if expression.is_a?(TypeExpression)
|
|
99
|
-
param_proxies << ParamProxy.new(type_expression: expression, name:, type:, position:, file:)
|
|
100
|
-
elsif ::LowType::TypeQuery.type?(expression)
|
|
101
|
-
param_proxies << ParamProxy.new(type_expression: TypeExpression.new(type: expression), name:, type:, position:, file:)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
param_proxies
|
|
106
|
-
}
|
|
107
|
-
RUBY
|
|
108
|
-
|
|
109
|
-
# Called with only required args (as nil) and optional args omitted, to evaluate type expressions (from default values).
|
|
110
|
-
eval(typed_method, binding, __FILE__, __LINE__).call(*required_args, **required_kwargs) # rubocop:disable Security/Eval
|
|
111
|
-
|
|
112
|
-
# TODO: Write spec for this.
|
|
113
|
-
rescue ArgumentError => e
|
|
114
|
-
raise ArgumentError, "Incorrect param syntax: #{e.message}"
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def proxy_method(method_node:)
|
|
118
|
-
params = method_node.parameters.slice
|
|
119
|
-
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
120
|
-
eval("-> (#{params}) {}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def required_args(proxy_method:)
|
|
124
|
-
required_args = []
|
|
125
|
-
required_kwargs = {}
|
|
126
|
-
|
|
127
|
-
proxy_method.parameters.each do |param|
|
|
128
|
-
param_type, param_name = param
|
|
129
|
-
|
|
130
|
-
case param_type
|
|
131
|
-
when :req
|
|
132
|
-
required_args << nil
|
|
133
|
-
when :keyreq
|
|
134
|
-
required_kwargs[param_name] = nil
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
[required_args, required_kwargs]
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Value expressions are eval()'d in the context of this module class (the instance doesn't exist yet) so alias API.
|
|
142
|
-
def value(type)
|
|
143
|
-
ExpressionFactory.type_expression_with_value(type:)
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
end
|
|
File without changes
|