low_type 0.8.10 → 0.8.11

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: afb83ff40c25644eeb106f67afff8e4f3ab31ae960bf51ec55daa0154dfe2a53
4
- data.tar.gz: f57573081ca5725606904a4cfe0e7ba85a937b6639330938975c05d161841df0
3
+ metadata.gz: 2f0c69a8ca233198b383a056a7c78819a2dae7944b73d681846924ef8bfe9355
4
+ data.tar.gz: fb04f1df5257523e0a1cb266500ff9d106c30723ee8aa46e66929ffa80198340
5
5
  SHA512:
6
- metadata.gz: eecb035a898e3e9f0020b619fdbe44460967dc5233023fe7bb4d3e5d950f7d3832b07d6512594ba4ccd74216bd70e739ebb39d0eac8d5b34f5410a67645035b9
7
- data.tar.gz: dbcce2c671faceb757b8a549404ec27662a63f1c5df71b2f0d711d023d3c949920e9fa09b17c6ec135d567fbe3cc376218fbfab6028a84339bc2830867b0b2f5
6
+ metadata.gz: a8eaaeed7102693d813d82fde2da76124b050eba9afa042bfaea2e94f40923629596c4fe613fce66f8cfba3342b86499bd257e14094c40ae8e399962dec2fe74
7
+ data.tar.gz: '0519fec1df0f3e1b80403e376aa41cdc2e25a6d1dbd87d85c3c76cc185340c3c937255c84136f3d467bbd63d01ce8bfbc78e0c253e817324338a7a29e12dddae'
@@ -1,17 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'sinatra_adapter'
2
4
 
3
5
  module LowType
4
- class AdapterLoader
5
- class << self
6
- def load(klass:, parser:, file_path:)
7
- adaptor = nil
6
+ module Adapter
7
+ class Loader
8
+ class << self
9
+ def load(klass:, parser:, file_path:)
10
+ adaptor = nil
8
11
 
9
- ancestors = klass.ancestors.map(&:to_s)
10
- adaptor = SinatraAdapter.new(klass:, parser:, file_path:) if ancestors.include?('Sinatra::Base')
12
+ ancestors = klass.ancestors.map(&:to_s)
13
+ adaptor = Sinatra.new(klass:, parser:, file_path:) if ancestors.include?('Sinatra::Base')
11
14
 
12
- return if adaptor.nil?
15
+ return if adaptor.nil?
13
16
 
14
- adaptor
17
+ adaptor
18
+ end
15
19
  end
16
20
  end
17
21
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'prism'
2
4
 
3
5
  require_relative '../interfaces/adapter_interface'
@@ -6,77 +8,72 @@ require_relative '../proxies/return_proxy'
6
8
  require_relative '../error_types'
7
9
 
8
10
  module LowType
9
- class Status < Integer; end
10
- class Headers < Hash; end
11
-
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
- class SinatraAdapter < AdapterInterface
14
- def initialize(klass:, parser:, file_path:)
15
- @klass = klass
16
- @parser = parser
17
- @file_path = file_path
18
- end
11
+ module Adapter
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
+ class Sinatra < AdapterInterface
14
+ def initialize(klass:, parser:, file_path:)
15
+ @klass = klass
16
+ @parser = parser
17
+ @file_path = file_path
18
+ end
19
19
 
20
- def process
21
- method_calls = @parser.method_calls(method_names: [:get, :post, :patch, :put, :delete, :options, :query])
20
+ def process # rubocop:disable Metrics/AbcSize
21
+ method_calls = @parser.method_calls(method_names: %i[get post patch put delete options query])
22
22
 
23
- # Type check return values.
24
- method_calls.each do |method_call|
25
- arguments_node = method_call.compact_child_nodes.first
26
- next unless arguments_node.is_a?(Prism::ArgumentsNode)
23
+ # Type check return values.
24
+ method_calls.each do |method_call|
25
+ arguments_node = method_call.compact_child_nodes.first
26
+ next unless arguments_node.is_a?(Prism::ArgumentsNode)
27
27
 
28
- pattern = arguments_node.arguments.first.content
28
+ pattern = arguments_node.arguments.first.content
29
29
 
30
- line = method_call.respond_to?(:start_line) ? method_call.start_line : nil
31
- file = FileProxy.new(path: @file_path, line:, scope: "#{@klass}##{method_call.name}")
32
- params = [ParamProxy.new(type_expression: nil, name: :route, type: :req, position: 0, file:)]
33
- return_proxy = return_proxy(method_node: method_call, pattern:, file:)
34
- next unless return_proxy
30
+ line = Parser.line_number(node: method_call)
31
+ file = FileProxy.new(path: @file_path, line:, scope: "#{@klass}##{method_call.name}")
32
+ next unless (return_proxy = return_proxy(method_node: method_call, pattern:, file:))
35
33
 
36
- route = "#{method_call.name.upcase} #{pattern}"
37
- @klass.low_methods[route] = MethodProxy.new(name: method_call.name, params:, return_proxy:)
34
+ route = "#{method_call.name.upcase} #{pattern}"
35
+ params = [ParamProxy.new(type_expression: nil, name: :route, type: :req, position: 0, file:)]
36
+ @klass.low_methods[route] = MethodProxy.new(name: method_call.name, params:, return_proxy:)
37
+ end
38
38
  end
39
- end
40
39
 
41
- def redefine
42
- Module.new do
43
- def invoke(&block)
44
- res = catch(:halt, &block)
45
-
46
- raise AllowedTypeError, 'Did you mean "Response.finish"?' if res.to_s == 'Response'
47
-
48
- route = "#{request.request_method} #{request.path}"
49
- if (res && (method_proxy = self.class.low_methods[route]) && (proxy = method_proxy.return_proxy))
50
- proxy.type_expression.types.each do |type|
51
- proxy.type_expression.validate!(value: res, proxy:)
52
- end
53
- end
54
-
55
- res = [res] if (Integer === res) || (String === res)
56
- if (Array === res) && (Integer === res.first)
57
- res = res.dup
58
- status(res.shift)
59
- body(res.pop)
60
- headers(*res)
61
- elsif res.respond_to? :each
62
- body res
63
- end
64
-
65
- nil # avoid double setting the same response tuple twice
66
- end
40
+ def return_proxy(method_node:, pattern:, file:)
41
+ return_type = Parser.return_type(method_node:)
42
+ return nil if return_type.nil?
43
+
44
+ # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
45
+ expression = eval(return_type.slice).call # rubocop:disable Security/Eval
46
+ expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
47
+
48
+ ReturnProxy.new(type_expression: expression, name: "#{method_node.name.upcase} #{pattern}", file:)
67
49
  end
68
50
  end
69
51
 
70
- private
52
+ module Methods
53
+ def invoke(&block)
54
+ res = catch(:halt, &block)
55
+
56
+ low_validate!(value: res) if res
71
57
 
72
- def return_proxy(method_node:, pattern:, file:)
73
- return_type = Parser.return_type(method_node:)
74
- return nil if return_type.nil?
58
+ res = [res] if res.is_a?(Integer) || res.is_a?(String)
59
+ if res.is_a?(Array) && res.first.is_a?(Integer)
60
+ res = res.dup
61
+ status(res.shift)
62
+ body(res.pop)
63
+ headers(*res)
64
+ elsif res.respond_to? :each
65
+ body res
66
+ end
75
67
 
76
- expression = eval(return_type.slice).call
77
- expression = TypeExpression.new(type: expression) unless TypeExpression === expression
68
+ nil # avoid double setting the same response tuple twice
69
+ end
78
70
 
79
- ReturnProxy.new(type_expression: expression, name: "#{method_node.name.upcase} #{pattern}", file:)
71
+ def low_validate!(value:)
72
+ route = "#{request.request_method} #{request.path}"
73
+ if (method_proxy = self.class.low_methods[route]) && (proxy = method_proxy.return_proxy)
74
+ proxy.type_expression.validate!(value:, proxy:)
75
+ end
76
+ end
80
77
  end
81
78
  end
82
79
  end
data/lib/basic_types.rb CHANGED
@@ -1,6 +1,11 @@
1
- module LowType
2
- class Boolean; end # TrueClass or FalseClass
1
+ # frozen_string_literal: true
3
2
 
3
+ module LowType
4
+ # TrueClass or FalseClass
5
+ class Boolean; end
6
+ class Tuple < Array; end
7
+ class Status < Integer; end
8
+ class Headers < Hash; end
4
9
  class HTML < String; end
5
10
  class JSON < String; end
6
11
  class XML < String; end
data/lib/error_types.rb CHANGED
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module LowType
2
- class ArgumentTypeError < TypeError; end;
3
- class LocalTypeError < TypeError; end;
4
- class ReturnTypeError < TypeError; end;
5
- class AllowedTypeError < TypeError; end;
4
+ class ArgumentTypeError < TypeError; end
5
+ class LocalTypeError < TypeError; end
6
+ class ReturnTypeError < TypeError; end
7
+ class AllowedTypeError < TypeError; end
6
8
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module LowType
2
4
  class AdapterInterface
3
5
  def process
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module LowType
2
4
  class ErrorInterface
3
5
  attr_reader :file
data/lib/local_types.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'proxies/file_proxy'
2
4
  require_relative 'proxies/local_proxy'
3
5
  require_relative 'type_expression'
@@ -7,11 +9,9 @@ module LocalTypes
7
9
  class AssignmentError < StandardError; end
8
10
 
9
11
  def type(type_expression)
10
- referenced_object = type_expression.default_value
12
+ referenced_object = type_expression.default_value.dup
11
13
 
12
- if !LowType.value?(referenced_object)
13
- raise AssignmentError, "Single-instance objects like #{referenced_object} are not supported"
14
- end
14
+ raise AssignmentError, "Single-instance objects like #{referenced_object} are not supported" unless LowType.value?(referenced_object)
15
15
 
16
16
  last_caller = caller_locations(1, 1).first
17
17
  file = LowType::FileProxy.new(path: last_caller.path, line: last_caller.lineno, scope: 'local type')
@@ -20,36 +20,23 @@ module LocalTypes
20
20
 
21
21
  type_expression.validate!(value: referenced_object, proxy: local_proxy)
22
22
 
23
- def referenced_object.with_type=(value)
24
- local_proxy = self.instance_variable_get('@local_proxy')
25
- type_expression = local_proxy.type_expression
26
- type_expression.validate!(value:, proxy: local_proxy)
27
-
28
- # We can't reassign self in Ruby so we reassign instance variables instead.
29
- value.instance_variables.each do |variable|
30
- self.instance_variable_set(variable, value.instance_variable_get(variable))
31
- end
32
-
33
- self
34
- end
23
+ define_with_type(referenced_object:)
35
24
 
36
25
  return referenced_object.value if referenced_object.is_a?(ValueExpression)
37
26
 
38
27
  referenced_object
39
28
  end
40
- alias_method :low_type, :type
29
+ alias low_type type
41
30
 
42
31
  def value(type)
43
32
  LowType.value(type:)
44
33
  end
45
- alias_method :low_value, :value
34
+ alias low_value value
46
35
 
47
36
  # Scoped to the class that includes LowTypes module.
48
37
  class Array < ::Array
49
38
  def self.[](*types)
50
- if types.all? { |type| LowType.type?(type) }
51
- return LowType::TypeExpression.new(type: [*types])
52
- end
39
+ return LowType::TypeExpression.new(type: [*types]) if types.all? { |type| LowType.type?(type) }
53
40
 
54
41
  super
55
42
  end
@@ -59,7 +46,25 @@ module LocalTypes
59
46
  class Hash < ::Hash
60
47
  def self.[](type)
61
48
  return LowType::TypeExpression.new(type:) if LowType.type?(type)
49
+
62
50
  super
63
51
  end
64
52
  end
53
+
54
+ private
55
+
56
+ def define_with_type(referenced_object:)
57
+ def referenced_object.with_type=(value)
58
+ local_proxy = instance_variable_get('@local_proxy')
59
+ type_expression = local_proxy.type_expression
60
+ type_expression.validate!(value:, proxy: local_proxy)
61
+
62
+ # We can't reassign self in Ruby so we reassign instance variables instead.
63
+ value.instance_variables.each do |variable|
64
+ instance_variable_set(variable, value.instance_variable_get(variable))
65
+ end
66
+
67
+ self
68
+ end
69
+ end
65
70
  end
data/lib/low_type.rb CHANGED
@@ -6,29 +6,14 @@ require_relative 'redefiner'
6
6
  require_relative 'type_expression'
7
7
  require_relative 'value_expression'
8
8
 
9
+ # Include this module into your class to define and check types.
9
10
  module LowType
10
11
  # We do as much as possible on class load rather than on instantiation to be thread-safe and efficient.
11
- def self.included(klass)
12
-
13
- # Array[] class method returns a type expression only for the duration of this "included" hook.
12
+ def self.included(klass) # rubocop:disable Metrics/AbcSize
13
+ # Array[] and Hash[] class method returns a type expression only for the duration of this "included" hook.
14
14
  array_class_method = Array.method('[]').unbind
15
- Array.define_singleton_method('[]') do |*types|
16
- TypeExpression.new(type: [*types])
17
- end
18
-
19
- # Hash[] class method returns a type expression only for the duration of this "included" hook.
20
15
  hash_class_method = Hash.method('[]').unbind
21
- Hash.define_singleton_method('[]') do |type|
22
- # Support Pry which uses Hash[].
23
- unless LowType.type?(type)
24
- Hash.define_singleton_method('[]', hash_class_method)
25
- result = Hash[type]
26
- Hash.method('[]').unbind
27
- return result
28
- end
29
-
30
- TypeExpression.new(type:)
31
- end
16
+ LowType.redefine(hash_class_method:)
32
17
 
33
18
  class << klass
34
19
  def low_methods
@@ -37,15 +22,15 @@ module LowType
37
22
  end
38
23
 
39
24
  file_path = LowType.file_path(klass:)
40
- parser = Parser.new(file_path:)
25
+ parser = LowType::Parser.new(file_path:)
41
26
  private_start_line = parser.private_start_line
42
27
 
43
- klass.prepend LowType::Redefiner.redefine_methods(method_nodes: parser.instance_methods, klass:, private_start_line:, file_path:)
44
- klass.singleton_class.prepend LowType::Redefiner.redefine_methods(method_nodes: parser.class_methods, klass:, private_start_line:, file_path:)
28
+ klass.prepend LowType::Redefiner.redefine(method_nodes: parser.instance_methods, klass:, private_start_line:, file_path:)
29
+ klass.singleton_class.prepend LowType::Redefiner.redefine(method_nodes: parser.class_methods, klass:, private_start_line:, file_path:)
45
30
 
46
- if (adapter = AdapterLoader.load(klass:, parser:, file_path:))
31
+ if (adapter = Adapter::Loader.load(klass:, parser:, file_path:))
47
32
  adapter.process
48
- klass.prepend adapter.redefine
33
+ klass.prepend Adapter::Methods
49
34
  end
50
35
  ensure
51
36
  Array.define_singleton_method('[]', array_class_method)
@@ -63,12 +48,12 @@ module LowType
63
48
  def configure
64
49
  yield(config)
65
50
 
66
- if config.local_types
67
- require_relative 'local_types'
68
- include LocalTypes
69
- end
51
+ return unless config.local_types
52
+
53
+ require_relative 'local_types'
54
+ include LocalTypes
70
55
  end
71
-
56
+
72
57
  # Internal API.
73
58
 
74
59
  def file_path(klass:)
@@ -78,9 +63,21 @@ module LowType
78
63
  caller.find { |callee| callee.end_with?("<class:#{class_name}>'") }.split(':').first
79
64
  end
80
65
 
81
- # TODO: Unit test this.
66
+ # TODO: Unit test.
82
67
  def type?(type)
83
- type.respond_to?(:new) || type == Integer || type == Symbol || (type.is_a?(::Hash) && type.keys.first.respond_to?(:new) && type.values.first.respond_to?(:new))
68
+ LowType.basic_type?(type:) || LowType.complex_type?(type:)
69
+ end
70
+
71
+ def basic_type?(type:)
72
+ type.respond_to?(:new) || type == Integer || type == Symbol
73
+ end
74
+
75
+ def complex_type?(type:)
76
+ !basic_type?(type:) && LowType.typed_hash?(type:)
77
+ end
78
+
79
+ def typed_hash?(type:)
80
+ type.is_a?(::Hash) && LowType.basic_type?(type: type.keys.first) && LowType.basic_type?(type: type.values.first)
84
81
  end
85
82
 
86
83
  def value?(expression)
@@ -90,5 +87,24 @@ module LowType
90
87
  def value(type:)
91
88
  TypeExpression.new(default_value: ValueExpression.new(value: type))
92
89
  end
90
+
91
+ # TODO: Unit test.
92
+ def redefine(hash_class_method:)
93
+ Array.define_singleton_method('[]') do |*types|
94
+ TypeExpression.new(type: [*types])
95
+ end
96
+
97
+ Hash.define_singleton_method('[]') do |type|
98
+ # Support Pry which uses Hash[].
99
+ unless LowType.type?(type)
100
+ Hash.define_singleton_method('[]', hash_class_method)
101
+ result = Hash[type]
102
+ Hash.method('[]').unbind
103
+ return result
104
+ end
105
+
106
+ TypeExpression.new(type:)
107
+ end
108
+ end
93
109
  end
94
110
  end
data/lib/parser.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'prism'
2
4
 
3
5
  module LowType
@@ -25,23 +27,29 @@ module LowType
25
27
  block_visitor.method_calls
26
28
  end
27
29
 
28
- # Only a lambda defined immediately after a method's parameters/block is considered a return type expression.
29
- def self.return_type(method_node:)
30
- # Method statements.
31
- statements_node = method_node.compact_child_nodes.find { |node| node.is_a?(Prism::StatementsNode) }
30
+ class << self
31
+ # Only a lambda defined immediately after a method's parameters/block is considered a return type expression.
32
+ def return_type(method_node:)
33
+ # Method statements.
34
+ statements_node = method_node.compact_child_nodes.find { |node| node.is_a?(Prism::StatementsNode) }
32
35
 
33
- # Block statements.
34
- if statements_node.nil?
35
- block_node = method_node.compact_child_nodes.find { |node| node.is_a?(Prism::BlockNode) }
36
- statements_node = block_node.compact_child_nodes.find { |node| node.is_a?(Prism::StatementsNode) } if block_node
37
- end
36
+ # Block statements.
37
+ if statements_node.nil?
38
+ block_node = method_node.compact_child_nodes.find { |node| node.is_a?(Prism::BlockNode) }
39
+ statements_node = block_node.compact_child_nodes.find { |node| node.is_a?(Prism::StatementsNode) } if block_node
40
+ end
38
41
 
39
- return nil if statements_node.nil? # Sometimes developers define methods without code inside them.
42
+ return nil if statements_node.nil? # Sometimes developers define methods without code inside them.
40
43
 
41
- node = statements_node.body.first
42
- return node if node.is_a?(Prism::LambdaNode)
44
+ node = statements_node.body.first
45
+ return node if node.is_a?(Prism::LambdaNode)
43
46
 
44
- nil
47
+ nil
48
+ end
49
+
50
+ def line_number(node:)
51
+ node.respond_to?(:start_line) ? node.start_line : nil
52
+ end
45
53
  end
46
54
  end
47
55
 
@@ -73,7 +81,7 @@ module LowType
73
81
  private
74
82
 
75
83
  def class_method?(node)
76
- return true if node.is_a?(::Prism::DefNode) && node.receiver.class == Prism::SelfNode # self.method_name
84
+ return true if node.is_a?(::Prism::DefNode) && node.receiver.instance_of?(Prism::SelfNode) # self.method_name
77
85
  return true if node.is_a?(::Prism::SingletonClassNode) # class << self
78
86
 
79
87
  if (parent_node = @parent_map[node])
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module LowType
2
4
  class FileProxy
3
5
  attr_reader :path, :line, :scope
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../interfaces/error_interface'
2
4
  require_relative '../error_types'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module LowType
2
4
  class MethodProxy
3
5
  attr_reader :name, :params, :return_proxy
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../interfaces/error_interface'
2
4
  require_relative '../error_types'
3
5
 
@@ -5,7 +7,7 @@ module LowType
5
7
  class ParamProxy < ErrorInterface
6
8
  attr_reader :type_expression, :name, :type, :position
7
9
 
8
- def initialize(type_expression:, name:, type:, position: nil, file:)
10
+ def initialize(type_expression:, name:, type:, file:, position: nil)
9
11
  @type_expression = type_expression
10
12
  @name = name
11
13
  @type = type
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../interfaces/error_interface'
2
4
  require_relative '../error_types'
3
5
 
data/lib/redefiner.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'proxies/file_proxy'
2
4
  require_relative 'proxies/method_proxy'
3
5
  require_relative 'proxies/param_proxy'
@@ -6,13 +8,14 @@ require_relative 'parser'
6
8
  require_relative 'type_expression'
7
9
 
8
10
  module LowType
11
+ # Redefine methods to have their arguments and return values type checked.
9
12
  class Redefiner
10
13
  class << self
11
- def redefine_methods(method_nodes:, klass:, private_start_line:, file_path:)
14
+ def redefine(method_nodes:, klass:, private_start_line:, file_path:) # rubocop:disable Metrics
12
15
  Module.new do
13
16
  method_nodes.each do |method_node|
14
17
  name = method_node.name
15
- line = method_node.respond_to?(:start_line) ? method_node.start_line : nil
18
+ line = Parser.line_number(node: method_node)
16
19
  file = FileProxy.new(path: file_path, line:, scope: "#{klass}##{method_node.name}")
17
20
  params = Redefiner.params_with_type_expressions(method_node:, file:)
18
21
  return_proxy = Redefiner.return_proxy(method_node:, file:)
@@ -23,7 +26,9 @@ module LowType
23
26
  klass.low_methods[name].params.each do |param_proxy|
24
27
  # Get argument value or default value.
25
28
  value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
26
- value = param_proxy.type_expression.default_value if value.nil? && param_proxy.type_expression.default_value != :LOW_TYPE_UNDEFINED
29
+ if value.nil? && param_proxy.type_expression.default_value != :LOW_TYPE_UNDEFINED
30
+ value = param_proxy.type_expression.default_value
31
+ end
27
32
  # Validate argument type.
28
33
  param_proxy.type_expression.validate!(value:, proxy: param_proxy)
29
34
  # Handle value(type) special case.
@@ -41,45 +46,42 @@ module LowType
41
46
  super(*args, **kwargs)
42
47
  end
43
48
 
44
- if private_start_line && method_node.start_line > private_start_line
45
- private name
46
- end
49
+ private name if private_start_line && method_node.start_line > private_start_line
47
50
  end
48
51
  end
49
52
  end
50
53
 
51
- def params_with_type_expressions(method_node:, file:)
54
+ def params_with_type_expressions(method_node:, file:) # rubocop:disable Metrics/MethodLength
52
55
  return [] if method_node.parameters.nil?
53
56
 
54
57
  params = method_node.parameters.slice
55
- proxy_method = eval("-> (#{params}) {}")
58
+ # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
59
+ proxy_method = eval("-> (#{params}) {}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
56
60
  required_args, required_kwargs = required_args(proxy_method:)
57
61
 
58
- typed_method = eval(
59
- <<~RUBY
60
- -> (#{params}) {
61
- param_proxies = []
62
+ # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
63
+ typed_method = <<~RUBY
64
+ -> (#{params}) {
65
+ param_proxies = []
62
66
 
63
- proxy_method.parameters.each_with_index do |param, position|
64
- type, name = param
65
- position = nil unless [:opt, :req, :rest].include?(type)
67
+ proxy_method.parameters.each_with_index do |param, position|
68
+ type, name = param
69
+ position = nil unless [:opt, :req, :rest].include?(type)
70
+ expression = binding.local_variable_get(name)
66
71
 
67
- expression = binding.local_variable_get(name)
68
-
69
- if expression.class == TypeExpression
70
- param_proxies << ParamProxy.new(type_expression: expression, name:, type:, position:, file:)
71
- elsif ::LowType.type?(expression)
72
- param_proxies << ParamProxy.new(type_expression: TypeExpression.new(type: expression), name:, type:, position:, file:)
73
- end
72
+ if expression.is_a?(TypeExpression)
73
+ param_proxies << ParamProxy.new(type_expression: expression, name:, type:, position:, file:)
74
+ elsif ::LowType.type?(expression)
75
+ param_proxies << ParamProxy.new(type_expression: TypeExpression.new(type: expression), name:, type:, position:, file:)
74
76
  end
77
+ end
75
78
 
76
- param_proxies
77
- }
78
- RUBY
79
- )
79
+ param_proxies
80
+ }
81
+ RUBY
80
82
 
81
- # Called with only required args present (as nil) and optional args omitted, to evaluate type expressions (which are stored as default values).
82
- typed_method.call(*required_args, **required_kwargs)
83
+ # Called with only required args (as nil) and optional args omitted, to evaluate type expressions (stored as default values).
84
+ eval(typed_method, binding, __FILE__, __LINE__).call(*required_args, **required_kwargs) # rubocop:disable Security/Eval
83
85
 
84
86
  # TODO: Write spec for this.
85
87
  rescue ArgumentError => e
@@ -90,7 +92,8 @@ module LowType
90
92
  return_type = Parser.return_type(method_node:)
91
93
  return nil if return_type.nil?
92
94
 
93
- expression = eval(return_type.slice).call
95
+ # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
96
+ expression = eval(return_type.slice).call # rubocop:disable Security/Eval
94
97
  expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
95
98
 
96
99
  ReturnProxy.new(type_expression: expression, name: method_node.name, file:)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'proxies/param_proxy'
2
4
 
3
5
  module LowType
@@ -5,8 +7,9 @@ module LowType
5
7
  file_path = File.expand_path(__FILE__)
6
8
  adapter_paths = Dir.chdir(root_path) { Dir.glob('adapters/*') }.map { |path| File.join(root_path, path) }
7
9
 
8
- HIDDEN_PATHS = [file_path, *adapter_paths, File.join(root_path, 'redefiner.rb')]
10
+ HIDDEN_PATHS = [file_path, *adapter_paths, File.join(root_path, 'redefiner.rb')].freeze
9
11
 
12
+ # Represent types and default values as a series of chainable expressions.
10
13
  class TypeExpression
11
14
  attr_reader :types, :default_value
12
15
 
@@ -18,8 +21,8 @@ module LowType
18
21
  end
19
22
 
20
23
  def |(expression)
21
- if expression.class == ::LowType::TypeExpression
22
- @types = @types + expression.types
24
+ if expression.instance_of?(::LowType::TypeExpression)
25
+ @types += expression.types
23
26
  @default_value = expression.default_value
24
27
  elsif ::LowType.value?(expression)
25
28
  @default_value = expression
@@ -34,20 +37,17 @@ module LowType
34
37
  @default_value == :LOW_TYPE_UNDEFINED
35
38
  end
36
39
 
37
- def validate!(value:, proxy:)
40
+ def validate!(value:, proxy:) # rubocop:disable Metrics
38
41
  if value.nil?
39
42
  return true if @default_value.nil?
40
43
  raise proxy.error_type, proxy.error_message(value:) if required?
41
44
  end
42
45
 
43
46
  @types.each do |type|
44
- return true if LowType.type?(type) && type <= value.class # Example: HTML is a subclass of String and should pass as a String.
45
- return true if ::Array === type && ::Array === value && array_types_match_values?(types: type, values: value)
46
-
47
- # TODO: Shallow validation of hash could be made deeper with user config.
48
- if type.class == ::Hash && value.class == ::Hash && type.keys[0] == value.keys[0].class && type.values[0] == value.values[0].class
49
- return true
50
- end
47
+ # Example: HTML is a subclass of String and should pass as a String.
48
+ return true if LowType.basic_type?(type:) && type <= value.class
49
+ return true if type.is_a?(::Array) && value.is_a?(::Array) && array_types_match_values?(types: type, values: value)
50
+ return true if type.is_a?(::Hash) && value.is_a?(::Hash) && hash_types_match_values?(type:, value:)
51
51
  end
52
52
 
53
53
  raise proxy.error_type, proxy.error_message(value:)
@@ -57,7 +57,7 @@ module LowType
57
57
 
58
58
  def valid_types
59
59
  types = @types.map { |type| type.inspect.to_s }
60
- types = types + ['nil'] if @default_value.nil?
60
+ types += ['nil'] if @default_value.nil?
61
61
  types.join(' | ')
62
62
  end
63
63
 
@@ -67,7 +67,8 @@ module LowType
67
67
  # [T, T, T]
68
68
  if types.length > 1
69
69
  types.each_with_index do |type, index|
70
- return false unless type <= values[index].class # Example: HTML is a subclass of String and should pass as a String.
70
+ # Example: HTML is a subclass of String and should pass as a String.
71
+ return false unless type <= values[index].class
71
72
  end
72
73
  # [T]
73
74
  elsif types.length == 1
@@ -78,6 +79,11 @@ module LowType
78
79
  true
79
80
  end
80
81
 
82
+ def hash_types_match_values?(type:, value:)
83
+ # TODO: Shallow validation of hash could be made deeper with user config.
84
+ type.keys[0] == value.keys[0].class && type.values[0] == value.values[0].class
85
+ end
86
+
81
87
  def backtrace_with_proxy(proxy:, backtrace:)
82
88
  # Remove LowType defined method file paths from the backtrace.
83
89
  filtered_backtrace = backtrace.reject { |line| HIDDEN_PATHS.find { |file_path| line.include?(file_path) } }
@@ -92,21 +98,21 @@ module LowType
92
98
  end
93
99
  end
94
100
 
101
+ # For "Type | [type_expression/type/value]" situations, redirecting to or generating a type expression from types.
102
+ # "|" is not defined on Object class and this is the most compute-efficient way to achieve our goal (world peace).
103
+ # "|" is overridable by any child object. While we could def/undef this method, this approach is actually lighter.
104
+ # "|" bitwise operator on Integer is not defined when the receiver is an Integer class, so we are not in conflict.
95
105
  class Object
96
106
  class << self
97
- # For "Type | [type_expression/type/value]" situations, redirecting to or generating a type expression from types.
98
- # "|" is not defined on Object class and this is the most compute-efficient way to achieve our goal (world peace).
99
- # "|" is overridable by any child object. While we could def/undef this method, this approach is actually lighter.
100
- # "|" bitwise operator on Integer is not defined when the receiver is an Integer class, so we are not in conflict.
101
107
  def |(expression)
102
- if expression.class == ::LowType::TypeExpression
108
+ if expression.instance_of?(::LowType::TypeExpression)
103
109
  # We pass our type into their type expression.
104
110
  expression | self
105
111
  expression
106
112
  else
107
113
  # We turn our type into a type expression and pass in their [type_expression/type/value].
108
114
  type_expression = ::LowType::TypeExpression.new(type: self)
109
- type_expression | expression
115
+ type_expression | expression
110
116
  end
111
117
  end
112
118
  end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A value expression converts a type to a value in the eyes of LowType.
1
4
  class ValueExpression
2
5
  attr_reader :value
3
6
 
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LowType
4
- VERSION = '0.8.10'
4
+ VERSION = '0.8.11'
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: 0.8.10
4
+ version: 0.8.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi