low_type 1.1.2 → 1.1.4

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: f2ff86050dbdbbaf494210b5ccb0ca36735305cae2a90701d406ee41830eb8b1
4
- data.tar.gz: 5345157740d454fa13dbf8d78c37e989b1e1a5a7e0e76cf914b5d9b8da324c58
3
+ metadata.gz: 2ed9039d493d3be677095a358691d03a7edf2dfd6416b48dfa352d03fec7cef7
4
+ data.tar.gz: 832ac8f63eec8a4e03d616286bea41f9945e0c30fbe55afe41f88857a82e01e9
5
5
  SHA512:
6
- metadata.gz: 75b594af3ac1947a5adda7a9b56d1e31a2fecb95098f48be0b0af54f16364d3c43cad27412e19f3f525f06864a9c5e7b74f45f6f41734641287bb76160e4c17d
7
- data.tar.gz: 423c63f4242bb7c4bf3fdbea6786eba6fc8408fd1c16ba26cd2400e6963e86e2f5ba38882bbe1cba167e42a334bf8fd85a5b320b7d5a1975b0c8a756886643f8
6
+ metadata.gz: b3eb8482d26eed780072944c51a00afb5e0d6cb360d754f37208453728102a07ed227ed13d17735e096ff107709b06e61d215ee6b0d89b3b9cdf5099048a643e
7
+ data.tar.gz: 3710dfca9e28556e2d8a781585adc809992b6469e0f3cd4f7c26fcd82e54d149a303f020522531b1350adaf954e09ffc8f02813c88c83f5718f5b334c59d4939
@@ -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 = create_method_proxies(method_nodes:, klass: class_proxy.klass, file_path:)
25
- define_methods(method_proxies:, class_proxy:)
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 create_method_proxies(method_nodes:, klass:, file_path:)
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 define_methods(method_proxies:, class_proxy:) # rubocop:disable Metrics
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
- # NOTE: You are now in the binding of the includer class (`name` is also available here).
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.type_expression.default_value != :LOW_TYPE_UNDEFINED
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 param_proxies(method_node:, file:)
97
- return [] if method_node.parameters.nil?
98
-
99
- params = method_node.parameters.slice
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
- if expression.is_a?(TypeExpression)
114
- param_proxies << ParamProxy.new(type_expression: expression, name:, type:, position:, file:)
115
- elsif ::LowType::TypeQuery.type?(expression)
116
- param_proxies << ParamProxy.new(type_expression: TypeExpression.new(type: expression), name:, type:, position:, file:)
117
- end
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
- param_proxies
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,89 @@ 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
+ params_without_block = method_node.parameters.slice.delete_suffix(', &block')
29
+
30
+ # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
31
+ ruby_method = eval("-> (#{params_without_block}) {}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
32
+
33
+ # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
34
+ # Local variable names are prefixed with __lt or __rb where necessary to avoid being overridden by method parameters.
35
+ typed_method = <<~RUBY
36
+ -> (#{params_without_block}, __rb_method:, __lt_file:) {
37
+ param_proxies_for_type_expressions(ruby_method: __rb_method, file: __lt_file, method_binding: binding)
38
+ }
39
+ RUBY
40
+
41
+ # Called with only required args (as nil) and optional args omitted, to evaluate type expressions (from default values).
42
+ # Passes internal variables with namespaced names to avoid conflicts with the method parameters.
43
+ required_args, required_kwargs = required_args(ruby_method:)
44
+ eval(typed_method, binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
45
+ .call(*required_args, **required_kwargs, __rb_method: ruby_method, __lt_file: file)
46
+
47
+ # TODO: Unit test this.
48
+ rescue ArgumentError => e
49
+ raise ArgumentError, "Incorrect param syntax: #{e.message}"
50
+ end
51
+
18
52
  def return_proxy(method_node:, file:)
19
53
  return_type = FileParser.return_type(method_node:)
20
54
  return nil if return_type.nil?
21
55
 
22
- # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
23
- expression = eval(return_type.slice, binding, __FILE__, __LINE__).call # rubocop:disable Security/Eval
56
+ begin
57
+ # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
58
+ expression = eval(return_type.slice, binding, __FILE__, __LINE__).call # rubocop:disable Security/Eval
59
+ rescue NameError
60
+ raise NameError, "Unknown return type '#{return_type.slice}' for #{file.scope} at #{file.path}:#{file.start_line}"
61
+ end
62
+
24
63
  expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
25
64
 
26
65
  ReturnProxy.new(type_expression: expression, name: method_node.name, file:)
27
66
  end
67
+
68
+ private
69
+
70
+ def required_args(ruby_method:)
71
+ required_args = []
72
+ required_kwargs = {}
73
+
74
+ ruby_method.parameters.each do |param|
75
+ param_type, param_name = param
76
+
77
+ case param_type
78
+ when :req
79
+ required_args << nil
80
+ when :keyreq
81
+ required_kwargs[param_name] = nil
82
+ end
83
+ end
84
+
85
+ [required_args, required_kwargs]
86
+ end
87
+
88
+ def param_proxies_for_type_expressions(ruby_method:, file:, method_binding:)
89
+ param_proxies = []
90
+
91
+ ruby_method.parameters.each_with_index do |param, position|
92
+ type, name = param
93
+ position = nil unless %i[opt req rest].include?(type)
94
+ expression = method_binding.local_variable_get(name)
95
+
96
+ type_expression = nil
97
+ if expression.is_a?(TypeExpression)
98
+ type_expression = expression
99
+ elsif ::LowType::TypeQuery.type?(expression)
100
+ type_expression = TypeExpression.new(type: expression)
101
+ end
102
+
103
+ param_proxies << ParamProxy.new(type_expression:, name:, type:, position:, file:) if type_expression
104
+ end
105
+
106
+ param_proxies
107
+ end
28
108
  end
29
109
  end
30
110
  end
data/lib/low_type.rb CHANGED
@@ -11,7 +11,7 @@ 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.
14
- def self.included(klass)
14
+ def self.included(klass) # rubocop:disable Metrics/AbcSize
15
15
  require_relative 'syntax/union_types' if LowType.config.union_type_expressions
16
16
 
17
17
  class << klass
@@ -21,6 +21,8 @@ module LowType
21
21
  end
22
22
 
23
23
  file_path = FileQuery.file_path(klass:)
24
+ return unless File.exist?(file_path)
25
+
24
26
  parser = FileParser.new(klass:, file_path:)
25
27
 
26
28
  klass.extend TypeAccessors
@@ -36,8 +38,8 @@ module LowType
36
38
 
37
39
  class << self
38
40
  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)
41
+ config = Struct.new(:type_checking, :error_mode, :output_mode, :output_size, :deep_type_check, :union_type_expressions)
42
+ @config ||= config.new(true, :error, :type, 100, false, true)
41
43
  end
42
44
 
43
45
  def configure
@@ -17,6 +17,10 @@ module LowType
17
17
  @file = file
18
18
  end
19
19
 
20
+ def required?
21
+ @type_expression.default_value == :LOW_TYPE_UNDEFINED
22
+ end
23
+
20
24
  def error_type
21
25
  ArgumentTypeError
22
26
  end
@@ -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 [type_expression/type/value].
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LowType
4
- VERSION = '1.1.2'
4
+ VERSION = '1.1.4'
5
5
  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.2
4
+ version: 1.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi