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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8dae7f03ed51a1acea72d53deec50af0722e9c57ac8baab72297420bae36f4b
4
- data.tar.gz: d7de0927fb139ac7d2b934c01f464224bb30fe2fbba6ca8b62a789955561e602
3
+ metadata.gz: 31fd9fee4aba001196fa20d6aaf51428121c4512ec85093bd3c68a1103b0ba83
4
+ data.tar.gz: 6ddc9528e3f5b0b6ac3fb841bf620874ab3ef08f0cd7acb0d6e0f6aebe3d20d7
5
5
  SHA512:
6
- metadata.gz: f4a91869b8c041091cd2dcadfb5ff931883041f7d57319b6cfb60b8ef7c1607924d81b93fc1a936724a3d577e904ed1d76c5bb03c89d6bf872e6446f0490f52e
7
- data.tar.gz: d42d49968af57b0122e76449d01a5ed118bb4394d28fe42a6a9ec9095f6900ec02940e237e75fe3c1c4417b307c0b85497193a492b721e3c139902a15d92be7b
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
- line = FileParser.line_number(node: method_call)
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 'proxies/return_proxy'
4
- require_relative 'queries/type_query'
5
- require_relative 'type_expression'
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 InstanceTypes
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, line: last_caller.lineno, scope: "#{self}##{name}")
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, line: last_caller.lineno, scope: "#{self}##{name}")
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 LocalTypes
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, line: last_caller.lineno, scope: 'local type')
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 local_types redefiner].map { |path| File.join(root_path, "#{path}.rb") }
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,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../type_expression'
4
- require_relative '../value_expression'
3
+ require_relative '../expressions/type_expression'
4
+ require_relative '../expressions/value_expression'
5
5
 
6
6
  module LowType
7
7
  class ExpressionFactory
@@ -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 '../type_expression'
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
- # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
15
- 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
+
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.line}:in '#{file.scope}'"
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 'syntax/syntax'
5
- require_relative 'types/complex_types'
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 'instance_types'
9
- require_relative 'local_types'
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 InstanceTypes
28
- klass.include LocalTypes
29
- klass.prepend Redefiner.redefine(method_nodes: parser.instance_methods, klass:, line_numbers:, file_path:)
30
- klass.singleton_class.prepend Redefiner.redefine(method_nodes: parser.class_methods, klass:, line_numbers:, file_path:)
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
@@ -2,12 +2,18 @@
2
2
 
3
3
  module LowType
4
4
  class FileProxy
5
- attr_reader :path, :line, :scope
5
+ attr_reader :path, :scope
6
+ attr_accessor :start_line, :end_line
6
7
 
7
- def initialize(path:, line:, scope:)
8
+ def initialize(path:, scope:, start_line:, end_line: nil)
8
9
  @path = path
9
- @line = line
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
@@ -20,7 +20,7 @@ module LowType
20
20
  end
21
21
 
22
22
  def error_message(value:)
23
- "Invalid variable type #{output(value:)} in '#{@name.class}' on line #{@file.line}. Valid types: '#{@type_expression.valid_types}'"
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
@@ -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
- attr_reader :name, :params, :return_proxy
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
@@ -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
@@ -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, :line_numbers
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
- @line_numbers = method_visitor.line_numbers
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, :line_numbers
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
- @line_numbers = { class_start: 0, class_end: root_node.respond_to?(:end_line) ? root_node.end_line : nil }
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 << node
70
+ @class_methods[node.name] = node
71
71
  else
72
- @instance_methods << node
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
- start_line = node.name == :private && node.respond_to?(:start_line) && node.start_line || nil
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
- if start_line && class_start && class_end && start_line > class_start && start_line < class_end
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
- @line_numbers = { class_start: node.class_keyword_loc.start_line, class_end: node.end_keyword_loc.end_line }
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../type_expression'
3
+ require_relative '../expressions/type_expression'
4
4
 
5
5
  module LowType
6
6
  # TODO: Unit test.
@@ -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.1'
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.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