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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2ff86050dbdbbaf494210b5ccb0ca36735305cae2a90701d406ee41830eb8b1
4
- data.tar.gz: 5345157740d454fa13dbf8d78c37e989b1e1a5a7e0e76cf914b5d9b8da324c58
3
+ metadata.gz: 31fd9fee4aba001196fa20d6aaf51428121c4512ec85093bd3c68a1103b0ba83
4
+ data.tar.gz: 6ddc9528e3f5b0b6ac3fb841bf620874ab3ef08f0cd7acb0d6e0f6aebe3d20d7
5
5
  SHA512:
6
- metadata.gz: 75b594af3ac1947a5adda7a9b56d1e31a2fecb95098f48be0b0af54f16364d3c43cad27412e19f3f525f06864a9c5e7b74f45f6f41734641287bb76160e4c17d
7
- data.tar.gz: 423c63f4242bb7c4bf3fdbea6786eba6fc8408fd1c16ba26cd2400e6963e86e2f5ba38882bbe1cba167e42a334bf8fd85a5b320b7d5a1975b0c8a756886643f8
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 = 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,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
- # 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
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
@@ -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.3'
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.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi