docscribe 1.6.0 → 1.6.2
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/README.md +76 -193
- data/exe/docscribe-client +26 -7
- data/lib/docscribe/cli/config_builder.rb +37 -2
- data/lib/docscribe/cli/coverage.rb +5 -5
- data/lib/docscribe/cli/formatters/json.rb +74 -29
- data/lib/docscribe/cli/formatters/sarif.rb +20 -3
- data/lib/docscribe/cli/options.rb +17 -2
- data/lib/docscribe/cli/rbs_gen.rb +4 -4
- data/lib/docscribe/cli/run.rb +107 -24
- data/lib/docscribe/cli/update_types.rb +61 -17
- data/lib/docscribe/cli.rb +19 -13
- data/lib/docscribe/config/defaults.rb +1 -0
- data/lib/docscribe/config/rbs.rb +22 -1
- data/lib/docscribe/config/template.rb +3 -0
- data/lib/docscribe/config/validation.rb +19 -0
- data/lib/docscribe/config.rb +1 -0
- data/lib/docscribe/infer/behavior.rb +13 -13
- data/lib/docscribe/infer/params.rb +2 -2
- data/lib/docscribe/infer/raises.rb +5 -6
- data/lib/docscribe/infer/returns.rb +1612 -151
- data/lib/docscribe/infer.rb +7 -7
- data/lib/docscribe/inline_rewriter/doc_builder.rb +485 -102
- data/lib/docscribe/inline_rewriter.rb +263 -97
- data/lib/docscribe/plugin/registry.rb +1 -0
- data/lib/docscribe/server/base.rb +255 -0
- data/lib/docscribe/server/client.rb +95 -0
- data/lib/docscribe/server/daemon.rb +678 -0
- data/lib/docscribe/server/protocol.rb +50 -0
- data/lib/docscribe/server.rb +4 -835
- data/lib/docscribe/types/primitive.rb +160 -0
- data/lib/docscribe/types/sorbet/base_provider.rb +33 -1
- data/lib/docscribe/types/yard/formatter.rb +35 -6
- data/lib/docscribe/types/yard/parser.rb +25 -20
- data/lib/docscribe/types/yard/validator.rb +131 -0
- data/lib/docscribe/validator/generic_compatibility.rb +698 -0
- data/lib/docscribe/validator/type_mismatch_validator.rb +287 -0
- data/lib/docscribe/version.rb +1 -1
- metadata +12 -3
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../types/primitive'
|
|
4
|
+
require 'docscribe/types/yard/validator'
|
|
5
|
+
|
|
6
|
+
module Docscribe
|
|
7
|
+
module Validator
|
|
8
|
+
# Service object answering `generic_compatible?` without hardcoding alias names.
|
|
9
|
+
#
|
|
10
|
+
# Dynamically decides if two type strings are compatible via generics/aliases.
|
|
11
|
+
# Uses hash dispatch to avoid long if/else chains and to allow easy extension.
|
|
12
|
+
module GenericCompatibility
|
|
13
|
+
# Check registry: name => checker method symbol
|
|
14
|
+
CHECKS = {
|
|
15
|
+
generic_base: :generic_base_compatible?,
|
|
16
|
+
short_name: :short_name_compatible?,
|
|
17
|
+
alias_hash: :alias_hash_compatible?,
|
|
18
|
+
tuple_array: :tuple_array_compatible?,
|
|
19
|
+
optional_nil: :optional_nil_compatible?,
|
|
20
|
+
union_containment: :union_containment?,
|
|
21
|
+
optional_suffix: :optional_suffix_compatible?,
|
|
22
|
+
generic_inner_alias: :generic_inner_alias_compatible?,
|
|
23
|
+
bare_alias: :bare_alias_compatible?,
|
|
24
|
+
union_optional: :union_vs_optional_compatible?,
|
|
25
|
+
object_compatible: :object_compatible?,
|
|
26
|
+
fallback_union: :fallback_union_check?
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# Whether two type strings are generic compatible (any checker true).
|
|
31
|
+
#
|
|
32
|
+
# @param [String, nil] yard_type YARD type string
|
|
33
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
34
|
+
# @param [String] fallback_type fallback type for union checks (default "Object")
|
|
35
|
+
# @param [String, Symbol, nil] method_name method name for void compatibility (e.g., "initialize")
|
|
36
|
+
# @return [Boolean] true if any compatibility checker matches
|
|
37
|
+
def compatible?(yard_type, expected_type, fallback_type: 'Object', method_name: nil)
|
|
38
|
+
return true if void_compatible?(yard_type, expected_type, fallback_type, method_name: method_name)
|
|
39
|
+
|
|
40
|
+
return true if union_parts_compatible?(yard_type, expected_type, fallback_type, method_name)
|
|
41
|
+
|
|
42
|
+
CHECKS.any? do |name, checker|
|
|
43
|
+
if name == :fallback_union
|
|
44
|
+
send(checker, yard_type, expected_type, fallback_type)
|
|
45
|
+
else
|
|
46
|
+
send(checker, yard_type, expected_type)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param [String, nil] yard_type
|
|
52
|
+
# @param [String, nil] expected_type
|
|
53
|
+
# @param [String] fallback_type
|
|
54
|
+
# @param [String, Symbol, nil] _method_name
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def union_parts_compatible?(yard_type, expected_type, fallback_type, _method_name)
|
|
57
|
+
yt = yard_type.to_s
|
|
58
|
+
et = expected_type.to_s
|
|
59
|
+
return false unless yt.include?(',') || et.include?(',')
|
|
60
|
+
|
|
61
|
+
parts_yard = union_parts(yt)
|
|
62
|
+
parts_expected = union_parts(et)
|
|
63
|
+
|
|
64
|
+
return true if single_vs_union?(parts_yard, parts_expected, fallback_type)
|
|
65
|
+
return true if single_vs_union?(parts_expected, parts_yard, fallback_type)
|
|
66
|
+
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @param [String] str
|
|
71
|
+
# @return [Array<String>]
|
|
72
|
+
def union_parts(str)
|
|
73
|
+
str.include?(',') ? split_top_level_commas_local(str).map { |p| normalize(p) } : [normalize(str)]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param [Array<String>] single_parts
|
|
77
|
+
# @param [Array<String>] union_parts
|
|
78
|
+
# @param [String] fallback_type
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
def single_vs_union?(single_parts, union_parts, fallback_type)
|
|
81
|
+
return false unless single_parts.size == 1 && union_parts.size > 1
|
|
82
|
+
|
|
83
|
+
single = single_parts.first
|
|
84
|
+
union_parts.any? { |part| part_compatible_with_single?(single, part, fallback_type) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param [String] single
|
|
88
|
+
# @param [String] part
|
|
89
|
+
# @param [String] fallback_type
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def part_compatible_with_single?(single, part, fallback_type)
|
|
92
|
+
CHECKS.any? do |name, checker|
|
|
93
|
+
next if %i[union_containment fallback_union].include?(name)
|
|
94
|
+
|
|
95
|
+
compatible = name == :fallback_union ? send(checker, single, part, fallback_type) : send(checker, single, part)
|
|
96
|
+
return true if compatible || single == part
|
|
97
|
+
end
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Whether void YARD type is compatible with expected type.
|
|
102
|
+
#
|
|
103
|
+
# Handles fallback unions and dynamic `initialize`/`setup` compatibility
|
|
104
|
+
# where `void` is treated as compatible with `Hash`, `self`, or `Boolean`
|
|
105
|
+
# for initializers per Ruby idiom.
|
|
106
|
+
#
|
|
107
|
+
# @param [String, nil] yard_type YARD type string
|
|
108
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
109
|
+
# @param [String] fallback_type fallback type for union checks
|
|
110
|
+
# @param [String, Symbol, nil] method_name method name for dynamic check
|
|
111
|
+
# @return [Boolean] true if void compatibility holds
|
|
112
|
+
def void_compatible?(yard_type, expected_type, fallback_type = 'Object', method_name: nil)
|
|
113
|
+
return false unless normalize(yard_type) == 'void'
|
|
114
|
+
return true if void_fallback_or_nil?(expected_type, fallback_type)
|
|
115
|
+
return true if void_initialize_compatible?(expected_type, method_name)
|
|
116
|
+
return true if void_predicate_compatible?(expected_type, method_name)
|
|
117
|
+
|
|
118
|
+
false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @param [String, nil] expected_type
|
|
122
|
+
# @param [String] fallback_type
|
|
123
|
+
# @return [Boolean]
|
|
124
|
+
def void_fallback_or_nil?(expected_type, fallback_type)
|
|
125
|
+
fallback_union?(expected_type, fallback_type) || %w[nil void].include?(normalize(expected_type))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @param [String, nil] expected_type
|
|
129
|
+
# @param [String, Symbol, nil] method_name
|
|
130
|
+
# @return [Boolean]
|
|
131
|
+
def void_initialize_compatible?(expected_type, method_name)
|
|
132
|
+
return false unless method_name.to_s =~ /initialize|setup/
|
|
133
|
+
|
|
134
|
+
norm = normalize(expected_type).delete_suffix('?').strip
|
|
135
|
+
norm == 'Hash' || norm.start_with?('Hash<') || norm.start_with?('Hash[') || %w[self Boolean].include?(norm)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @param [String, nil] expected_type
|
|
139
|
+
# @param [String, Symbol, nil] method_name
|
|
140
|
+
# @return [Boolean]
|
|
141
|
+
def void_predicate_compatible?(expected_type, method_name)
|
|
142
|
+
return false unless method_name.to_s.end_with?('?')
|
|
143
|
+
|
|
144
|
+
normalize(expected_type).delete_suffix('?').strip == 'Boolean'
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Whether either type is a fallback-only union for the given fallback type.
|
|
148
|
+
#
|
|
149
|
+
# Delegates to {#fallback_union?} for both yard_type and expected_type.
|
|
150
|
+
#
|
|
151
|
+
# @param [String, nil] yard_type YARD type string
|
|
152
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
153
|
+
# @param [String] fallback_type fallback type to match (e.g., "Object")
|
|
154
|
+
# @return [Boolean] true if either type contains only fallback parts
|
|
155
|
+
def fallback_union_check?(yard_type, expected_type, fallback_type)
|
|
156
|
+
fallback_union?(yard_type, fallback_type) || fallback_union?(expected_type, fallback_type)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Whether a type string contains only the fallback type (comma-separated, ignoring trailing `?`).
|
|
160
|
+
#
|
|
161
|
+
# @param [String, nil] type_str type string to test, may be comma-separated union
|
|
162
|
+
# @param [String] fallback fallback type name (e.g., "Object")
|
|
163
|
+
# @return [Boolean] true if every comma-separated part equals the normalized fallback
|
|
164
|
+
def fallback_union?(type_str, fallback)
|
|
165
|
+
return false if type_str.nil? || type_str.strip.empty?
|
|
166
|
+
|
|
167
|
+
fallback_norm = normalize(fallback)
|
|
168
|
+
parts = type_str.to_s.split(',').map { |part| normalize(part.strip.delete_suffix('?').strip) }
|
|
169
|
+
parts.all? { |part| part == fallback_norm || part.empty? }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Whether generic base matches: Hash vs Hash<Symbol,String> or Array vs Array<String>.
|
|
173
|
+
#
|
|
174
|
+
# True when normalized types equal or one is bare base of the other's generic.
|
|
175
|
+
#
|
|
176
|
+
# @param [String, nil] yard_type YARD type string
|
|
177
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
178
|
+
# @return [Boolean] true if generic base matches (bare vs generic or identical)
|
|
179
|
+
def generic_base_compatible?(yard_type, expected_type)
|
|
180
|
+
norm_yard = normalize(yard_type)
|
|
181
|
+
norm_expected = normalize(expected_type)
|
|
182
|
+
return true if norm_yard == norm_expected
|
|
183
|
+
|
|
184
|
+
yard_generic = norm_expected !~ /[<\[]/ && (norm_yard.start_with?("#{norm_expected}<") || norm_yard.start_with?("#{norm_expected}["))
|
|
185
|
+
expected_generic = norm_yard !~ /[<\[]/ && (norm_expected.start_with?("#{norm_yard}<") || norm_expected.start_with?("#{norm_yard}["))
|
|
186
|
+
yard_generic || expected_generic
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Whether short names equal: Docscribe::Config vs Config, Parser::Source::Range vs Range.
|
|
190
|
+
#
|
|
191
|
+
# Ignores generic brackets and checks namespace-elided compatibility via {#short_compatible?}.
|
|
192
|
+
#
|
|
193
|
+
# @param [String, nil] yard_type YARD type string
|
|
194
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
195
|
+
# @return [Boolean] true if short names match with namespace variation
|
|
196
|
+
def short_name_compatible?(yard_type, expected_type)
|
|
197
|
+
norm_yard = normalize(yard_type)
|
|
198
|
+
norm_expected = normalize(expected_type)
|
|
199
|
+
return false if generic_string?(norm_yard) || generic_string?(norm_expected)
|
|
200
|
+
|
|
201
|
+
short_yard = short_name(norm_yard)
|
|
202
|
+
short_expected = short_name(norm_expected)
|
|
203
|
+
short_compatible?(short_yard, short_expected, norm_yard, norm_expected)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Short name without namespace or generic args.
|
|
207
|
+
#
|
|
208
|
+
# Strips module prefix and generic suffix. E.g., "Docscribe::Config<String>" => "Config".
|
|
209
|
+
#
|
|
210
|
+
# @param [String] type_str raw type string, may be nil
|
|
211
|
+
# @return [String] short name (last namespace segment without < or [)
|
|
212
|
+
def short_name(type_str)
|
|
213
|
+
normalize(type_str).split('::').last.to_s.split('<').first.split('[').first.strip
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Whether a normalized type string contains generic brackets.
|
|
217
|
+
#
|
|
218
|
+
# @param [String] normalized normalized type string (after {#normalize})
|
|
219
|
+
# @return [Boolean] true if string includes "<" or "[" indicating generic
|
|
220
|
+
def generic_string?(normalized)
|
|
221
|
+
normalized.include?('<') || normalized.include?('[')
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Whether short names are compatible given full normalized forms.
|
|
225
|
+
#
|
|
226
|
+
# Allows Docscribe::Config vs Config and cross-checks short vs full forms.
|
|
227
|
+
#
|
|
228
|
+
# @param [String] short_yard short name derived from YARD type
|
|
229
|
+
# @param [String] short_expected short name derived from expected type
|
|
230
|
+
# @param [String] norm_yard full normalized YARD type
|
|
231
|
+
# @param [String] norm_expected full normalized expected type
|
|
232
|
+
# @return [Boolean] true if short names align via namespace elision
|
|
233
|
+
def short_compatible?(short_yard, short_expected, norm_yard, norm_expected)
|
|
234
|
+
return true if short_yard == short_expected && short_yard != norm_yard && short_expected != norm_expected
|
|
235
|
+
return true if short_yard == norm_expected
|
|
236
|
+
return true if short_expected == norm_yard
|
|
237
|
+
|
|
238
|
+
false
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Whether alias (lowercase after ::) vs Hash/Array/Range is compatible.
|
|
242
|
+
#
|
|
243
|
+
# Checks if one side is a namespaced alias starting with lowercase and the other is Hash, Array, or Range.
|
|
244
|
+
#
|
|
245
|
+
# @param [String, nil] yard_type YARD type string
|
|
246
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
247
|
+
# @return [Boolean] true if alias vs Hash/Array/Range pair detected
|
|
248
|
+
def alias_hash_compatible?(yard_type, expected_type)
|
|
249
|
+
norm_yard = normalize(yard_type)
|
|
250
|
+
norm_expected = normalize(expected_type)
|
|
251
|
+
[[norm_yard, norm_expected], [norm_expected, norm_yard]].any? do |alias_type, hash_type|
|
|
252
|
+
alias_hash_pair?(alias_type, hash_type)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# @param [String] alias_type potential alias side
|
|
257
|
+
# @param [String] hash_type potential Hash/Array side
|
|
258
|
+
# @return [Boolean]
|
|
259
|
+
def alias_hash_pair?(alias_type, hash_type)
|
|
260
|
+
base = base_name(hash_type)
|
|
261
|
+
return false unless %w[Hash Array Range].include?(base) ||
|
|
262
|
+
hash_type.start_with?('Hash') ||
|
|
263
|
+
hash_type.start_with?('Array') ||
|
|
264
|
+
hash_type == 'Range'
|
|
265
|
+
|
|
266
|
+
short_alias = alias_type.split('::').last.to_s
|
|
267
|
+
return false if %w[node Node].include?(short_alias)
|
|
268
|
+
|
|
269
|
+
short_alias =~ /\A[a-z]/ && alias_type.include?('::')
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Base name before generic or paren.
|
|
273
|
+
#
|
|
274
|
+
# Strips "<", "[", "(" suffixes. E.g., "Hash<Symbol,String>" => "Hash".
|
|
275
|
+
#
|
|
276
|
+
# @param [String] type_str raw type string, may be nil
|
|
277
|
+
# @return [String] base type name without generic arguments
|
|
278
|
+
def base_name(type_str)
|
|
279
|
+
normalize(type_str).split('<').first.split('[').first.split('(').first.strip
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Whether tuple "(String, Integer)" vs Array is compatible.
|
|
283
|
+
#
|
|
284
|
+
# True when one side is bare "Array" and the other is parenthesized tuple.
|
|
285
|
+
#
|
|
286
|
+
# @param [String, nil] yard_type YARD type string
|
|
287
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
288
|
+
# @return [Boolean] true if tuple vs Array pair
|
|
289
|
+
def tuple_array_compatible?(yard_type, expected_type)
|
|
290
|
+
norm_yard = normalize(yard_type)
|
|
291
|
+
norm_expected = normalize(expected_type)
|
|
292
|
+
(norm_expected == 'Array' && norm_yard =~ /\A\(.*\)\z/) || (norm_yard == 'Array' && norm_expected =~ /\A\(.*\)\z/)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Whether optional vs nil pair is compatible.
|
|
296
|
+
#
|
|
297
|
+
# Delegates to {#question_nil_pair?}, {#comma_nil_pair?}, and {#node_nil_pair?}.
|
|
298
|
+
#
|
|
299
|
+
# @param [String, nil] yard_type YARD type string
|
|
300
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
301
|
+
# @return [Boolean] true if any nil-optional pairing matches
|
|
302
|
+
def optional_nil_compatible?(yard_type, expected_type)
|
|
303
|
+
norm_yard = normalize(yard_type)
|
|
304
|
+
norm_expected = normalize(expected_type)
|
|
305
|
+
return true if question_nil_pair?(norm_yard, norm_expected)
|
|
306
|
+
return true if comma_nil_pair?(norm_yard, norm_expected)
|
|
307
|
+
|
|
308
|
+
node_nil_pair?(norm_yard, norm_expected)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Whether one side is "nil" and the other uses trailing "?" optional syntax.
|
|
312
|
+
#
|
|
313
|
+
# E.g., "nil" vs "String?" is considered compatible.
|
|
314
|
+
#
|
|
315
|
+
# @param [String] norm_yard normalized YARD type
|
|
316
|
+
# @param [String] norm_expected normalized expected type
|
|
317
|
+
# @return [Boolean] true if nil vs "?" optional pair
|
|
318
|
+
def question_nil_pair?(norm_yard, norm_expected)
|
|
319
|
+
(norm_yard == 'nil' && norm_expected.end_with?('?')) || (norm_expected == 'nil' && norm_yard.end_with?('?'))
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Whether one side is "nil" and the other contains ", nil" union.
|
|
323
|
+
#
|
|
324
|
+
# E.g., "nil" vs "String, nil" is considered compatible.
|
|
325
|
+
#
|
|
326
|
+
# @param [String] norm_yard normalized YARD type
|
|
327
|
+
# @param [String] norm_expected normalized expected type
|
|
328
|
+
# @return [Boolean] true if nil vs comma-nil union pair
|
|
329
|
+
def comma_nil_pair?(norm_yard, norm_expected)
|
|
330
|
+
(norm_yard.include?(', nil') && norm_expected == 'nil') || (norm_expected.include?(', nil') && norm_yard == 'nil')
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Whether Parser::AST::Node vs nil is considered compatible.
|
|
334
|
+
#
|
|
335
|
+
# Special-case for AST nodes where nil represents absent node.
|
|
336
|
+
#
|
|
337
|
+
# @param [String] norm_yard normalized YARD type
|
|
338
|
+
# @param [String] norm_expected normalized expected type
|
|
339
|
+
# @return [Boolean] true if Parser::AST::Node vs nil pair
|
|
340
|
+
def node_nil_pair?(norm_yard, norm_expected)
|
|
341
|
+
(norm_yard == 'Parser::AST::Node' && norm_expected == 'nil') || (norm_expected == 'Parser::AST::Node' && norm_yard == 'nil')
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Whether one type is contained in the other's comma-separated union.
|
|
345
|
+
#
|
|
346
|
+
# Checks both directions after normalization (e.g., "String" in "String, Integer").
|
|
347
|
+
#
|
|
348
|
+
# @param [String, nil] yard_type YARD type string
|
|
349
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
350
|
+
# @return [Boolean] true if one type appears in the other's union parts
|
|
351
|
+
def union_containment?(yard_type, expected_type)
|
|
352
|
+
return false if yard_type.nil? || expected_type.nil?
|
|
353
|
+
|
|
354
|
+
norm_yard = normalize(yard_type)
|
|
355
|
+
expected_type.split(',').any? { |part| normalize(part) == norm_yard } ||
|
|
356
|
+
yard_type.split(',').any? { |part| normalize(part) == normalize(expected_type) }
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Whether types match after stripping trailing "?".
|
|
360
|
+
#
|
|
361
|
+
# E.g., "String" vs "String?" considered compatible.
|
|
362
|
+
#
|
|
363
|
+
# @param [String, nil] yard_type YARD type string
|
|
364
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
365
|
+
# @return [Boolean] true if types equal ignoring optional "?" suffix
|
|
366
|
+
def optional_suffix_compatible?(yard_type, expected_type)
|
|
367
|
+
normalize(yard_type).delete_suffix('?') == normalize(expected_type).delete_suffix('?')
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Whether Array/Hash generic inners contain alias tokens.
|
|
371
|
+
#
|
|
372
|
+
# True when both are Array/Hash generics and either inner contains an alias (lowercase or "::").
|
|
373
|
+
# E.g., "Array<my_alias>" vs "Array<String>".
|
|
374
|
+
#
|
|
375
|
+
# @param [String, nil] yard_type YARD type string
|
|
376
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
377
|
+
# @return [Boolean] true if generic inners alias-compatible
|
|
378
|
+
def generic_inner_alias_compatible?(yard_type, expected_type)
|
|
379
|
+
norm_yard = normalize(yard_type)
|
|
380
|
+
norm_expected = normalize(expected_type)
|
|
381
|
+
return false unless generic_pair?(norm_yard, norm_expected)
|
|
382
|
+
|
|
383
|
+
inner_yard = extract_inner(norm_yard)
|
|
384
|
+
inner_expected = extract_inner(norm_expected)
|
|
385
|
+
return false unless inner_yard && inner_expected
|
|
386
|
+
|
|
387
|
+
inner_has_alias?(inner_yard) || inner_has_alias?(inner_expected)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Whether both normalized types are Array or Hash generics.
|
|
391
|
+
#
|
|
392
|
+
# @param [String] norm_yard normalized YARD type
|
|
393
|
+
# @param [String] norm_expected normalized expected type
|
|
394
|
+
# @return [Boolean] true if both match Array/Hash generic pattern
|
|
395
|
+
def generic_pair?(norm_yard, norm_expected)
|
|
396
|
+
norm_yard =~ /\A(?:Array|Hash)[<\[]/ && norm_expected =~ /\A(?:Array|Hash)[<\[]/
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Extracts inner generic arguments from normalized Array/Hash type.
|
|
400
|
+
#
|
|
401
|
+
# E.g., "Array<String>" => "String", "Hash<Symbol, String>" => "Symbol, String".
|
|
402
|
+
#
|
|
403
|
+
# @param [String] normalized normalized generic type string
|
|
404
|
+
# @return [String, nil] inner content or nil if not generic
|
|
405
|
+
def extract_inner(normalized)
|
|
406
|
+
normalized[/\A(?:Array|Hash)[<\[](.*)[>\]]\z/, 1]
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Whether any comma-separated part of generic inner is an alias token.
|
|
410
|
+
#
|
|
411
|
+
# @param [String] inner inner generic string (comma-separated)
|
|
412
|
+
# @return [Boolean] true if any part satisfies {#alias_token?}
|
|
413
|
+
def inner_has_alias?(inner)
|
|
414
|
+
inner.split(',').any? { |part| alias_token?(part.strip) }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Whether token looks like an alias (dynamic, not hardcoded Elem/U).
|
|
418
|
+
#
|
|
419
|
+
# Any token that is not a known Ruby/YARD primitive is considered alias.
|
|
420
|
+
# E.g., `Elem`, `U`, `my_alias`, `Foo::Bar`, `change` vs `String`, `Integer`, `Hash`.
|
|
421
|
+
#
|
|
422
|
+
# @param [String] token single type token (trimmed inner part, may include ?)
|
|
423
|
+
# @return [Boolean] true if token is alias (unknown primitive or namespaced/lowercase)
|
|
424
|
+
def alias_token?(token) # rubocop:disable SortedMethodsByCall/Waterfall
|
|
425
|
+
base = token.split('<').first.split('[').first.strip.delete_suffix('?').strip
|
|
426
|
+
return false if Docscribe::Types::Primitive.primitive?(base)
|
|
427
|
+
return true if base =~ /\A[a-z]/ || base.include?('::')
|
|
428
|
+
return true if base =~ /\A[A-Z]\z/
|
|
429
|
+
|
|
430
|
+
!!(base =~ /\A[A-Z][A-Za-z0-9_]*\z/)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Whether one side is a bare alias (e.g., V, U, T, Elem) vs concrete type.
|
|
434
|
+
#
|
|
435
|
+
# Bare alias means single token without generic brackets or commas that
|
|
436
|
+
# satisfies {#alias_token?}. E.g., "V" vs "Array<String>" => true via alias.
|
|
437
|
+
# Handles single capital letter A-Z explicitly for RBS type params.
|
|
438
|
+
#
|
|
439
|
+
# @param [String, nil] yard_type YARD type string
|
|
440
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
441
|
+
# @return [Boolean] true if either side is bare alias
|
|
442
|
+
def bare_alias_compatible?(yard_type, expected_type)
|
|
443
|
+
norm_yard = normalize(yard_type)
|
|
444
|
+
norm_expected = normalize(expected_type)
|
|
445
|
+
return false if norm_yard.empty? || norm_expected.empty?
|
|
446
|
+
|
|
447
|
+
bare_single_cap?(norm_yard) || bare_single_cap?(norm_expected)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Whether normalized string is a bare single-capital alias (V, U, T, K, Elem? no, single A-Z only).
|
|
451
|
+
#
|
|
452
|
+
# Single capital letters are RBS type parameters (generic placeholders) that should
|
|
453
|
+
# be compatible with concrete types like Array<String> or String. Multi-letter aliases
|
|
454
|
+
# like Config are not considered bare aliases (handled via other checks).
|
|
455
|
+
#
|
|
456
|
+
# @param [String] normalized normalized type string
|
|
457
|
+
# @return [Boolean] true if bare single capital
|
|
458
|
+
def bare_single_cap?(normalized)
|
|
459
|
+
return false if normalized.include?('<') || normalized.include?('[') || normalized.include?(',')
|
|
460
|
+
|
|
461
|
+
stripped = normalized.strip.delete_suffix('?').strip
|
|
462
|
+
stripped =~ /\A[A-Z]\z/ && alias_token?(stripped)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Whether normalized string is a bare alias token (no generics, no union).
|
|
466
|
+
#
|
|
467
|
+
# @param [String] normalized normalized type string
|
|
468
|
+
# @return [Boolean] true if bare alias
|
|
469
|
+
def bare_alias?(normalized)
|
|
470
|
+
return false if normalized.include?('<') || normalized.include?('[') || normalized.include?(',')
|
|
471
|
+
|
|
472
|
+
alias_token?(normalized.strip)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Whether union `?, nil` forms are compatible: `Object?` vs `Object, nil` vs `Object|nil`.
|
|
476
|
+
#
|
|
477
|
+
# Handles all three forms `SomeType? == SomeType|nil == SomeType, nil` plus
|
|
478
|
+
# bare `SomeType` vs optional equivalence via pipe/comma normalization.
|
|
479
|
+
#
|
|
480
|
+
# @param [String, nil] yard_type YARD type string
|
|
481
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
482
|
+
# @return [Boolean] true if one is `Type?` and other is `Type, nil` / `Type|nil`
|
|
483
|
+
def union_vs_optional_compatible?(yard_type, expected_type)
|
|
484
|
+
norm_yard = normalize(yard_type).gsub('|', ',')
|
|
485
|
+
norm_expected = normalize(expected_type).gsub('|', ',')
|
|
486
|
+
return true if question_vs_comma_nil?(norm_yard, norm_expected)
|
|
487
|
+
return true if suffix_vs_union_first?(yard_type, expected_type)
|
|
488
|
+
return true if pipe_aware_suffix_vs_union_first?(yard_type, expected_type)
|
|
489
|
+
|
|
490
|
+
optional_forms_equal?(yard_type, expected_type)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Pipe-aware variant of {#suffix_vs_union_first?} normalizing `|` to `,`.
|
|
494
|
+
#
|
|
495
|
+
# @param [String, nil] yard_type YARD type string
|
|
496
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
497
|
+
# @return [Boolean] true if suffix vs union matches after pipe normalization
|
|
498
|
+
def pipe_aware_suffix_vs_union_first?(yard_type, expected_type)
|
|
499
|
+
# Normalize both by converting pipe to comma before comparison
|
|
500
|
+
y = yard_type.to_s.gsub('|', ',')
|
|
501
|
+
e = expected_type.to_s.gsub('|', ',')
|
|
502
|
+
left_match = normalize(y).delete_suffix('?') ==
|
|
503
|
+
normalize(split_top_level_commas_local(e).first || '')
|
|
504
|
+
right_match = normalize(e).delete_suffix('?') ==
|
|
505
|
+
normalize(split_top_level_commas_local(y).first || '')
|
|
506
|
+
left_match || right_match
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Whether optional forms `T?`, `T, nil`, `T|nil` are equivalent (same canonical parts).
|
|
510
|
+
#
|
|
511
|
+
# Canonicalizes `T?` => `[T, nil]`, `T, nil` / `T|nil` => `[T, nil]`, `T` => `[T]`.
|
|
512
|
+
# Generic-aware split keeps `Hash<String, Integer>, nil` intact.
|
|
513
|
+
#
|
|
514
|
+
# @param [String, nil] first first type string
|
|
515
|
+
# @param [String, nil] second second type string
|
|
516
|
+
# @return [Boolean] true if canonical optional parts equal and non-empty
|
|
517
|
+
def optional_forms_equal?(first, second)
|
|
518
|
+
pa = optional_canonical_parts(first)
|
|
519
|
+
pb = optional_canonical_parts(second)
|
|
520
|
+
!pa.empty? && pa == pb
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Whether Object supertype compatibility holds: e.g., String vs Object, String, nil vs Object, nil.
|
|
524
|
+
#
|
|
525
|
+
# If expected is Object (or Object, nil, Object? etc) and yard is a concrete type
|
|
526
|
+
# (String, Array<String>, etc) with same nil presence, then yard is considered
|
|
527
|
+
# compatible with expected since Object is supertype of all. Handles
|
|
528
|
+
# receiver_or_and_type: String, nil vs Object, nil dynamically.
|
|
529
|
+
#
|
|
530
|
+
# @param [String, nil] yard_type YARD type string
|
|
531
|
+
# @param [String, nil] expected_type inferred/RBS type string
|
|
532
|
+
# @return [Boolean] true if Object supertype compatibility holds
|
|
533
|
+
def object_compatible?(yard_type, expected_type)
|
|
534
|
+
yard_canonical = optional_canonical_parts(yard_type)
|
|
535
|
+
exp_canonical = optional_canonical_parts(expected_type)
|
|
536
|
+
return false if yard_canonical.empty? || exp_canonical.empty?
|
|
537
|
+
|
|
538
|
+
yard_without_nil = canonical_without_nil(yard_canonical)
|
|
539
|
+
exp_without_nil = canonical_without_nil(exp_canonical)
|
|
540
|
+
return false unless object_supertype_pair?(yard_without_nil, exp_without_nil)
|
|
541
|
+
|
|
542
|
+
yard_canonical.include?('nil') == exp_canonical.include?('nil')
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# @param [Array<String>] yard_without_nil
|
|
546
|
+
# @param [Array<String>] exp_without_nil
|
|
547
|
+
# @return [Boolean]
|
|
548
|
+
def object_supertype_pair?(yard_without_nil, exp_without_nil)
|
|
549
|
+
(exp_without_nil == ['Object'] && yard_without_nil != ['Object'] && !yard_without_nil.empty?) ||
|
|
550
|
+
(yard_without_nil == ['Object'] && exp_without_nil != ['Object'] && !exp_without_nil.empty?)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# @param [Array<String>] canonical
|
|
554
|
+
# @return [Array<String>]
|
|
555
|
+
def canonical_without_nil(canonical)
|
|
556
|
+
canonical.reject { |p| p == 'nil' }
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# Canonical optional parts for `T?` / `T, nil` / `T|nil` / `T` forms.
|
|
560
|
+
#
|
|
561
|
+
# @param [String, nil] str raw type string
|
|
562
|
+
# @return [Array<String>] sorted unique normalized parts
|
|
563
|
+
def optional_canonical_parts(str)
|
|
564
|
+
return [] if str.nil? || empty_str?(str)
|
|
565
|
+
|
|
566
|
+
s = normalized_union_str(str)
|
|
567
|
+
return single_optional_parts(s) if single_optional_form?(s)
|
|
568
|
+
|
|
569
|
+
canonicalize_parts(split_top_level_commas_local(s))
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# @param [String, nil] str
|
|
573
|
+
# @return [Boolean]
|
|
574
|
+
def empty_str?(str)
|
|
575
|
+
str.to_s.strip.empty?
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# @param [String, nil] str
|
|
579
|
+
# @return [String]
|
|
580
|
+
def normalized_union_str(str)
|
|
581
|
+
str.to_s.gsub('|', ',').strip
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# @param [String] str
|
|
585
|
+
# @return [Boolean]
|
|
586
|
+
def single_optional_form?(str)
|
|
587
|
+
str.end_with?('?') && !str.include?(',')
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# @param [Array<String>] parts
|
|
591
|
+
# @return [Array<String>]
|
|
592
|
+
def canonicalize_parts(parts)
|
|
593
|
+
expand_optional_parts(parts).map { |part| normalize(part) }.reject(&:empty?).uniq.sort
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# @param [String] str
|
|
597
|
+
# @return [Array<String>]
|
|
598
|
+
def single_optional_parts(str)
|
|
599
|
+
base = normalize(str.delete_suffix('?').strip)
|
|
600
|
+
[base, 'nil'].sort
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# @param [Array<String>] parts
|
|
604
|
+
# @return [Array<String>]
|
|
605
|
+
def expand_optional_parts(parts)
|
|
606
|
+
parts.flat_map do |part|
|
|
607
|
+
stripped = part.strip
|
|
608
|
+
if stripped.end_with?('?')
|
|
609
|
+
[normalize(stripped.delete_suffix('?').strip), 'nil']
|
|
610
|
+
else
|
|
611
|
+
[normalize(stripped)]
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# Split by top-level commas outside `< > [ ] ( )` (generic-aware).
|
|
617
|
+
#
|
|
618
|
+
# Mirrors `Docscribe::Infer::Returns.split_top_level_commas` but local to avoid cross-dep.
|
|
619
|
+
#
|
|
620
|
+
# @param [String] str type string to split
|
|
621
|
+
# @return [Array<String>] parts split on top-level commas
|
|
622
|
+
def split_top_level_commas_local(str)
|
|
623
|
+
state = { parts: [], cur: +'', da: 0, db: 0, dp: 0 } #: Hash[Symbol, untyped]
|
|
624
|
+
str.each_char { |chr| handle_split_char(chr, state) }
|
|
625
|
+
state[:parts] << state[:cur] unless state[:cur].empty?
|
|
626
|
+
state[:parts]
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# @param [String] chr
|
|
630
|
+
# @param [Hash<Symbol, Object>] state
|
|
631
|
+
# @return [void]
|
|
632
|
+
def handle_split_char(chr, state)
|
|
633
|
+
case chr
|
|
634
|
+
when '<', '>', '[', ']', '(', ')'
|
|
635
|
+
update_split_depth(chr, state)
|
|
636
|
+
when ','
|
|
637
|
+
handle_split_comma(state)
|
|
638
|
+
else
|
|
639
|
+
state[:cur] << chr
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# @param [String] chr
|
|
644
|
+
# @param [Hash<Symbol, Object>] state
|
|
645
|
+
# @return [void]
|
|
646
|
+
def update_split_depth(chr, state)
|
|
647
|
+
deltas = { '<' => [:da, 1], '>' => [:da, -1], '[' => [:db, 1], ']' => [:db, -1], '(' => [:dp, 1], ')' => [:dp, -1] }
|
|
648
|
+
key, delta = deltas[chr]
|
|
649
|
+
state[key] += delta if key
|
|
650
|
+
state[:cur] << chr
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# @param [Hash<Symbol, Object>] state
|
|
654
|
+
# @return [void]
|
|
655
|
+
def handle_split_comma(state)
|
|
656
|
+
if state[:da].zero? && state[:db].zero? && state[:dp].zero?
|
|
657
|
+
state[:parts] << state[:cur]
|
|
658
|
+
state[:cur] = +''
|
|
659
|
+
else
|
|
660
|
+
state[:cur] << ','
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
# Method documentation.
|
|
665
|
+
#
|
|
666
|
+
# @param [String] norm_yard Param documentation.
|
|
667
|
+
# @param [String] norm_expected Param documentation.
|
|
668
|
+
# @return [Boolean]
|
|
669
|
+
def question_vs_comma_nil?(norm_yard, norm_expected)
|
|
670
|
+
norm_yard.delete(' ') == "#{norm_expected.delete(' ').delete_suffix('?')},nil" ||
|
|
671
|
+
norm_expected.delete(' ') == "#{norm_yard.delete(' ').delete_suffix('?')},nil"
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Method documentation.
|
|
675
|
+
#
|
|
676
|
+
# @param [String, nil] yard_type Param documentation.
|
|
677
|
+
# @param [String, nil] expected_type Param documentation.
|
|
678
|
+
# @return [Boolean]
|
|
679
|
+
def suffix_vs_union_first?(yard_type, expected_type)
|
|
680
|
+
normalize(yard_type).delete_suffix('?') == normalize(expected_type).split(',').first&.strip ||
|
|
681
|
+
normalize(expected_type).delete_suffix('?') == normalize(yard_type).split(',').first&.strip
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# Normalizes type string for comparison.
|
|
685
|
+
#
|
|
686
|
+
# Strips, squeezes spaces, converts "["/"]" to "<"/">", replaces "untyped"/"FALLBACK_TYPE" with "Object".
|
|
687
|
+
#
|
|
688
|
+
# @param [String, nil] type_str raw type string, may be nil
|
|
689
|
+
# @return [String] normalized type string
|
|
690
|
+
def normalize(type_str)
|
|
691
|
+
s = type_str.to_s
|
|
692
|
+
s = s.sub(/#.*\z/m, '').strip unless s.lstrip.start_with?('#')
|
|
693
|
+
s.strip.squeeze(' ').gsub('[', '<').gsub(']', '>').gsub(/\buntyped\b/, 'Object').gsub(/\bFALLBACK_TYPE\b/, 'Object')
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
end
|