low_type 1.0.7 → 1.1.0
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/factories/type_factory.rb +17 -0
- data/lib/interfaces/error_interface.rb +30 -0
- data/lib/low_type.rb +2 -2
- data/lib/proxies/local_proxy.rb +3 -1
- data/lib/proxies/param_proxy.rb +3 -1
- data/lib/proxies/return_proxy.rb +3 -1
- data/lib/queries/file_parser.rb +1 -1
- data/lib/queries/file_query.rb +5 -3
- data/lib/queries/type_query.rb +17 -7
- data/lib/redefiner.rb +3 -5
- data/lib/syntax/syntax.rb +4 -2
- data/lib/syntax/union_types.rb +2 -0
- data/lib/type_expression.rb +62 -30
- data/lib/types/complex_types.rb +11 -8
- data/lib/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a175f78281d51fe16720ca8231b1790506d2eea0d96660c52ff78f1717689836
|
|
4
|
+
data.tar.gz: f8615b0754c2a5e30e99e37922311d7d9436a37025f3a1a76334d4b14da21e7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04d69b82160152e0c04cc114e196c68fc5122dc79fa3b622ebafa21106a01ab8d44d7a483df497f2cc51c8280c2038bbf114b680505a2c9e35b73b9cbef2cd04
|
|
7
|
+
data.tar.gz: f2baaeeeaaec312a7306c12094e3b4cd661a935e3b6072b8824df5451fbd88538ffcacc48ba6beedc06e75ec2d685f015227289c529f051cadd246de127aa033
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LowType
|
|
4
|
+
class TypeFactory
|
|
5
|
+
class << self
|
|
6
|
+
def complex_type(parent_type)
|
|
7
|
+
Class.new(parent_type) do
|
|
8
|
+
def self.match?(value:)
|
|
9
|
+
return true if value.instance_of?(self.class) || value.instance_of?(superclass)
|
|
10
|
+
|
|
11
|
+
false
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -4,6 +4,12 @@ module LowType
|
|
|
4
4
|
class ErrorInterface
|
|
5
5
|
attr_reader :file
|
|
6
6
|
|
|
7
|
+
def initialize
|
|
8
|
+
@file = nil
|
|
9
|
+
@output_mode = LowType.config.output_mode
|
|
10
|
+
@output_size = LowType.config.output_size
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
def error_type
|
|
8
14
|
raise NotImplementedError
|
|
9
15
|
end
|
|
@@ -11,5 +17,29 @@ module LowType
|
|
|
11
17
|
def error_message(value:)
|
|
12
18
|
raise NotImplementedError
|
|
13
19
|
end
|
|
20
|
+
|
|
21
|
+
def output(value:)
|
|
22
|
+
case @output_mode
|
|
23
|
+
when :type
|
|
24
|
+
# TODO: Show full type structure in error output instead of just the type of the supertype.
|
|
25
|
+
value.class
|
|
26
|
+
when :value
|
|
27
|
+
value.inspect[0...@output_size]
|
|
28
|
+
else
|
|
29
|
+
'REDACTED'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def backtrace(backtrace:, hidden_paths:)
|
|
34
|
+
# Remove LowType defined method file paths from the backtrace.
|
|
35
|
+
filtered_backtrace = backtrace.reject { |line| hidden_paths.find { |file_path| line.include?(file_path) } }
|
|
36
|
+
|
|
37
|
+
# Add the proxied file to the backtrace.
|
|
38
|
+
proxy_file_backtrace = "#{file.path}:#{file.line}:in '#{file.scope}'"
|
|
39
|
+
from_prefix = filtered_backtrace.first.match(/\s+from /)
|
|
40
|
+
proxy_file_backtrace = "#{from_prefix}#{proxy_file_backtrace}" if from_prefix
|
|
41
|
+
|
|
42
|
+
[proxy_file_backtrace, *filtered_backtrace]
|
|
43
|
+
end
|
|
14
44
|
end
|
|
15
45
|
end
|
data/lib/low_type.rb
CHANGED
|
@@ -37,8 +37,8 @@ module LowType
|
|
|
37
37
|
|
|
38
38
|
class << self
|
|
39
39
|
def config
|
|
40
|
-
config = Struct.new(:
|
|
41
|
-
@config ||= config.new(
|
|
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)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def configure
|
data/lib/proxies/local_proxy.rb
CHANGED
|
@@ -8,6 +8,8 @@ module LowType
|
|
|
8
8
|
attr_reader :type_expression, :name
|
|
9
9
|
|
|
10
10
|
def initialize(type_expression:, name:, file:)
|
|
11
|
+
super()
|
|
12
|
+
|
|
11
13
|
@type_expression = type_expression
|
|
12
14
|
@name = name
|
|
13
15
|
@file = file
|
|
@@ -18,7 +20,7 @@ module LowType
|
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def error_message(value:)
|
|
21
|
-
"Invalid variable type #{value
|
|
23
|
+
"Invalid variable type #{output(value:)} in '#{@name.class}' on line #{@file.line}. Valid types: '#{@type_expression.valid_types}'"
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
end
|
data/lib/proxies/param_proxy.rb
CHANGED
|
@@ -8,6 +8,8 @@ module LowType
|
|
|
8
8
|
attr_reader :type_expression, :name, :type, :position
|
|
9
9
|
|
|
10
10
|
def initialize(type_expression:, name:, type:, file:, position: nil)
|
|
11
|
+
super()
|
|
12
|
+
|
|
11
13
|
@type_expression = type_expression
|
|
12
14
|
@name = name
|
|
13
15
|
@type = type
|
|
@@ -20,7 +22,7 @@ module LowType
|
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def error_message(value:)
|
|
23
|
-
"Invalid argument type '#{value
|
|
25
|
+
"Invalid argument type '#{output(value:)}' for parameter '#{@name}'. Valid types: '#{@type_expression.valid_types}'"
|
|
24
26
|
end
|
|
25
27
|
end
|
|
26
28
|
end
|
data/lib/proxies/return_proxy.rb
CHANGED
|
@@ -8,6 +8,8 @@ module LowType
|
|
|
8
8
|
attr_reader :type_expression, :name
|
|
9
9
|
|
|
10
10
|
def initialize(type_expression:, name:, file:)
|
|
11
|
+
super()
|
|
12
|
+
|
|
11
13
|
@type_expression = type_expression
|
|
12
14
|
@name = name
|
|
13
15
|
@file = file
|
|
@@ -18,7 +20,7 @@ module LowType
|
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def error_message(value:)
|
|
21
|
-
"Invalid return type '#{value
|
|
23
|
+
"Invalid return type '#{output(value:)}' for method '#{@name}'. Valid types: '#{@type_expression.valid_types}'"
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
end
|
data/lib/queries/file_parser.rb
CHANGED
|
@@ -62,7 +62,7 @@ module LowType
|
|
|
62
62
|
|
|
63
63
|
@instance_methods = []
|
|
64
64
|
@class_methods = []
|
|
65
|
-
@line_numbers = { class_start: 0, class_end: root_node.respond_to?(:end_line) ? root_node.end_line : nil}
|
|
65
|
+
@line_numbers = { class_start: 0, class_end: root_node.respond_to?(:end_line) ? root_node.end_line : nil }
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def visit_def_node(node)
|
data/lib/queries/file_query.rb
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module LowType
|
|
2
4
|
class FileQuery
|
|
3
5
|
class << self
|
|
4
6
|
def file_path(klass:)
|
|
5
|
-
includer_line =
|
|
7
|
+
includer_line = line_from_class(klass:) || line_from_include || ''
|
|
6
8
|
includer_line.split(':').first || ''
|
|
7
9
|
end
|
|
8
10
|
|
|
9
11
|
private
|
|
10
12
|
|
|
11
|
-
def
|
|
13
|
+
def line_from_class(klass:)
|
|
12
14
|
class_name = klass.to_s.split(':').last # Also remove the module namespaces from the class.
|
|
13
15
|
caller.find { |callee| callee.end_with?("<class:#{class_name}>'") }
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
def
|
|
18
|
+
def line_from_include
|
|
17
19
|
caller.find { |callee| callee.end_with?("include'") }
|
|
18
20
|
end
|
|
19
21
|
end
|
data/lib/queries/type_query.rb
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../type_expression'
|
|
4
|
+
|
|
1
5
|
module LowType
|
|
2
6
|
# TODO: Unit test.
|
|
3
7
|
class TypeQuery
|
|
@@ -6,20 +10,26 @@ module LowType
|
|
|
6
10
|
basic_type?(type:) || complex_type?(type:)
|
|
7
11
|
end
|
|
8
12
|
|
|
9
|
-
def
|
|
10
|
-
type
|
|
13
|
+
def value?(value)
|
|
14
|
+
!basic_type?(type: value) && !complex_type?(type: value)
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def complex_type?(type:)
|
|
14
|
-
|
|
18
|
+
LowType::COMPLEX_TYPES.include?(type) || typed_array?(type:) || typed_hash?(type:)
|
|
15
19
|
end
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def basic_type?(type:)
|
|
24
|
+
type.instance_of?(Class)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def typed_array?(type:)
|
|
28
|
+
type.is_a?(Array) && (basic_type?(type: type.first) || type.first.is_a?(TypeExpression))
|
|
19
29
|
end
|
|
20
30
|
|
|
21
|
-
def
|
|
22
|
-
|
|
31
|
+
def typed_hash?(type:)
|
|
32
|
+
type.is_a?(Hash) && basic_type?(type: type.keys.first) && basic_type?(type: type.values.first)
|
|
23
33
|
end
|
|
24
34
|
end
|
|
25
35
|
end
|
data/lib/redefiner.rb
CHANGED
|
@@ -5,8 +5,8 @@ require_relative 'factories/proxy_factory'
|
|
|
5
5
|
require_relative 'proxies/file_proxy'
|
|
6
6
|
require_relative 'proxies/method_proxy'
|
|
7
7
|
require_relative 'proxies/param_proxy'
|
|
8
|
-
require_relative 'syntax/syntax'
|
|
9
8
|
require_relative 'queries/type_query'
|
|
9
|
+
require_relative 'syntax/syntax'
|
|
10
10
|
require_relative 'type_expression'
|
|
11
11
|
require_relative 'value_expression'
|
|
12
12
|
|
|
@@ -45,9 +45,7 @@ module LowType
|
|
|
45
45
|
method_start = method_node.respond_to?(:start_line) ? method_node.start_line : nil
|
|
46
46
|
method_end = method_node.respond_to?(:end_line) ? method_node.end_line : nil
|
|
47
47
|
|
|
48
|
-
if method_start && method_end && class_end
|
|
49
|
-
next unless method_start > class_start && method_end <= class_end
|
|
50
|
-
end
|
|
48
|
+
next if method_start && method_end && class_end && !(method_start > class_start && method_end <= class_end)
|
|
51
49
|
|
|
52
50
|
name = method_node.name
|
|
53
51
|
|
|
@@ -108,7 +106,7 @@ module LowType
|
|
|
108
106
|
}
|
|
109
107
|
RUBY
|
|
110
108
|
|
|
111
|
-
# Called with only required args (as nil) and optional args omitted, to evaluate type expressions (
|
|
109
|
+
# Called with only required args (as nil) and optional args omitted, to evaluate type expressions (from default values).
|
|
112
110
|
eval(typed_method, binding, __FILE__, __LINE__).call(*required_args, **required_kwargs) # rubocop:disable Security/Eval
|
|
113
111
|
|
|
114
112
|
# TODO: Write spec for this.
|
data/lib/syntax/syntax.rb
CHANGED
|
@@ -6,14 +6,16 @@ module LowType
|
|
|
6
6
|
module Syntax
|
|
7
7
|
refine Array.singleton_class do
|
|
8
8
|
def [](*types)
|
|
9
|
-
return
|
|
9
|
+
return TypeExpression.new(type: [*types]) if types.all? { |type| TypeQuery.type?(type) }
|
|
10
|
+
|
|
10
11
|
super
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
refine Hash.singleton_class do
|
|
15
16
|
def [](type)
|
|
16
|
-
return
|
|
17
|
+
return TypeExpression.new(type:) if TypeQuery.type?(type)
|
|
18
|
+
|
|
17
19
|
super
|
|
18
20
|
end
|
|
19
21
|
end
|
data/lib/syntax/union_types.rb
CHANGED
data/lib/type_expression.rb
CHANGED
|
@@ -5,10 +5,10 @@ require_relative 'queries/type_query'
|
|
|
5
5
|
|
|
6
6
|
module LowType
|
|
7
7
|
root_path = File.expand_path(__dir__)
|
|
8
|
-
file_path = File.expand_path(__FILE__)
|
|
9
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") }
|
|
10
10
|
|
|
11
|
-
HIDDEN_PATHS = [
|
|
11
|
+
HIDDEN_PATHS = [File.expand_path(__FILE__), *adapter_paths, *module_paths].freeze
|
|
12
12
|
|
|
13
13
|
# Represent types and default values as a series of chainable expressions.
|
|
14
14
|
class TypeExpression
|
|
@@ -19,6 +19,8 @@ module LowType
|
|
|
19
19
|
@types = []
|
|
20
20
|
@types << type unless type.nil?
|
|
21
21
|
@default_value = default_value
|
|
22
|
+
# TODO: Override per type expression with a config expression.
|
|
23
|
+
@deep_type_check = LowType.config.deep_type_check
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def |(expression)
|
|
@@ -45,64 +47,94 @@ module LowType
|
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
@types.each do |type|
|
|
48
|
-
|
|
49
|
-
return true if
|
|
50
|
-
return true if type.is_a?(
|
|
51
|
-
return true if type.is_a?(::Hash) && value.is_a?(::Hash) && hash_types_match_values?(type:, value:)
|
|
50
|
+
return true if type_matches_value?(type:, value:, proxy:)
|
|
51
|
+
return true if type.is_a?(Array) && value.is_a?(Array) && array_types_match_values?(types: type, values: value, proxy:)
|
|
52
|
+
return true if type.is_a?(Hash) && value.is_a?(Hash) && hash_types_match_values?(types: type, values: value)
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
raise proxy.error_type, proxy.error_message(value:)
|
|
55
56
|
rescue proxy.error_type => e
|
|
56
|
-
raise proxy.error_type, e.message,
|
|
57
|
+
raise proxy.error_type, e.message, proxy.backtrace(backtrace: e.backtrace, hidden_paths: HIDDEN_PATHS)
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
def valid_types
|
|
60
61
|
types = @types.map do |type|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"[#{type.map { |subtype| subtype.to_s.delete_prefix('LowType::') }.join(', ')}]"
|
|
62
|
+
if type.is_a?(Array)
|
|
63
|
+
"[#{type.map { |subtype| valid_subtype(subtype:) }.join(', ')}]"
|
|
64
64
|
else
|
|
65
65
|
type.inspect.to_s.delete_prefix('LowType::')
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
types
|
|
69
|
+
types << 'nil' if @default_value.nil?
|
|
70
70
|
types.join(' | ')
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
private
|
|
74
74
|
|
|
75
|
-
def
|
|
76
|
-
|
|
75
|
+
def valid_subtype(subtype:)
|
|
76
|
+
if subtype.is_a?(TypeExpression)
|
|
77
|
+
types = subtype.types
|
|
78
|
+
types << 'nil' if subtype.default_value.nil?
|
|
79
|
+
types.join(' | ')
|
|
80
|
+
else
|
|
81
|
+
subtype.to_s.delete_prefix('LowType::')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def array_types_match_values?(types:, values:, proxy:)
|
|
86
|
+
# [X, Y, Z] An arbitrary amount of elements are arbitrary types in an arbitrary order.
|
|
77
87
|
if types.length > 1
|
|
78
|
-
types
|
|
79
|
-
|
|
80
|
-
return false unless type <= values[index].class
|
|
81
|
-
end
|
|
82
|
-
# [T]
|
|
88
|
+
return multiple_types_match_values?(types:, values:, proxy:)
|
|
89
|
+
# [T] All elements are the same type.
|
|
83
90
|
elsif types.length == 1
|
|
84
|
-
return
|
|
91
|
+
return single_type_matches_values?(type: types.first, values:, proxy:)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# [] Misconfigured empty Array[] type.
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def multiple_types_match_values?(types:, values:, proxy:)
|
|
99
|
+
types.each_with_index do |type, index|
|
|
100
|
+
return false unless type_matches_value?(type:, value: values[index], proxy:)
|
|
85
101
|
end
|
|
86
|
-
# TODO: Deep type check (all elements for [T]).
|
|
87
102
|
|
|
88
103
|
true
|
|
89
104
|
end
|
|
90
105
|
|
|
91
|
-
def
|
|
106
|
+
def single_type_matches_values?(type:, values:, proxy:)
|
|
107
|
+
# [V, ...] Type check all elements.
|
|
108
|
+
if deep_type_check?
|
|
109
|
+
return false if values.any? { |value| !type_matches_value?(type:, value:, proxy:) }
|
|
110
|
+
# [V] Type check the first element.
|
|
111
|
+
else
|
|
112
|
+
return false unless type_matches_value?(type:, value: values.first, proxy:)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def hash_types_match_values?(types:, values:)
|
|
92
119
|
# TODO: Shallow validation of hash could be made deeper with user config.
|
|
93
|
-
|
|
120
|
+
types.keys[0] == values.keys[0].class && types.values[0] == values.values[0].class
|
|
94
121
|
end
|
|
95
122
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
123
|
+
def type_matches_value?(type:, value:, proxy:)
|
|
124
|
+
if type.instance_of?(Class)
|
|
125
|
+
return type.match?(value:) if LowType::TypeQuery.complex_type?(type:)
|
|
126
|
+
|
|
127
|
+
return type == value.class
|
|
128
|
+
elsif type.instance_of?(::LowType::TypeExpression)
|
|
129
|
+
type.validate!(value:, proxy:)
|
|
130
|
+
return true
|
|
131
|
+
end
|
|
99
132
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
from_prefix = filtered_backtrace.first.match(/\s+from /)
|
|
103
|
-
proxy_file_backtrace = "#{from_prefix}#{proxy_file_backtrace}" if from_prefix
|
|
133
|
+
false
|
|
134
|
+
end
|
|
104
135
|
|
|
105
|
-
|
|
136
|
+
def deep_type_check?
|
|
137
|
+
@deep_type_check || LowType.config.deep_type_check || false
|
|
106
138
|
end
|
|
107
139
|
end
|
|
108
140
|
end
|
data/lib/types/complex_types.rb
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../factories/type_factory'
|
|
4
|
+
|
|
3
5
|
module LowType
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
COMPLEX_TYPES = [
|
|
7
|
+
Boolean = TypeFactory.complex_type(Object),
|
|
8
|
+
Headers = TypeFactory.complex_type(Hash),
|
|
9
|
+
HTML = TypeFactory.complex_type(String),
|
|
10
|
+
JSON = TypeFactory.complex_type(String),
|
|
11
|
+
Status = TypeFactory.complex_type(Integer),
|
|
12
|
+
Tuple = TypeFactory.complex_type(Array),
|
|
13
|
+
XML = TypeFactory.complex_type(String)
|
|
14
|
+
].freeze
|
|
12
15
|
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.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
@@ -21,6 +21,7 @@ files:
|
|
|
21
21
|
- lib/adapters/sinatra_adapter.rb
|
|
22
22
|
- lib/factories/expression_factory.rb
|
|
23
23
|
- lib/factories/proxy_factory.rb
|
|
24
|
+
- lib/factories/type_factory.rb
|
|
24
25
|
- lib/instance_types.rb
|
|
25
26
|
- lib/interfaces/adapter_interface.rb
|
|
26
27
|
- lib/interfaces/error_interface.rb
|