docscribe 1.6.1 → 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 +46 -15
- data/lib/docscribe/server/client.rb +20 -11
- data/lib/docscribe/server/daemon.rb +200 -23
- data/lib/docscribe/server/protocol.rb +4 -4
- 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 +8 -3
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# NOTE: no top-level `require 'rbs'` here on purpose — the rbs gem is
|
|
4
|
+
# optional (excluded on old rubies via BUNDLE_WITHOUT). It is required
|
|
5
|
+
# lazily in `.load_rbs_core`, which falls back to a hardcoded list when
|
|
6
|
+
# the gem is unavailable.
|
|
7
|
+
#
|
|
8
|
+
# `set` is required eagerly: it is a default gem on all supported rubies
|
|
9
|
+
# and `load_core_primitives` cannot run without it.
|
|
10
|
+
require 'set'
|
|
11
|
+
|
|
12
|
+
module Docscribe
|
|
13
|
+
module Types
|
|
14
|
+
# Dynamic primitive type detection via RBS core + YARD.
|
|
15
|
+
#
|
|
16
|
+
# Replaces hardcoded %w[String Integer ...] lists in
|
|
17
|
+
# Returns and GenericCompatibility with RBS environment inspection.
|
|
18
|
+
module Primitive
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Whether token matches alias pattern (lowercase after :: or capitalized not in primitives)
|
|
22
|
+
#
|
|
23
|
+
# @note module_function: defines #alias_pattern? (visibility: private)
|
|
24
|
+
# @param [String] token
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def alias_pattern?(token)
|
|
27
|
+
base = normalized_base(token)
|
|
28
|
+
return true if base =~ /\A[a-z]/ || base.include?('::')
|
|
29
|
+
return true if base =~ /\A[A-Z]\z/
|
|
30
|
+
|
|
31
|
+
!!(base =~ /\A[A-Z][A-Za-z0-9_]*\z/ && !core_primitives.include?(base))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether a type string is an alias (contains alias token inside generic)
|
|
35
|
+
#
|
|
36
|
+
# @note module_function: defines #alias_type? (visibility: private)
|
|
37
|
+
# @param [String] type_str
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def alias_type?(type_str)
|
|
40
|
+
# Check if any comma-separated inner is alias
|
|
41
|
+
# For "Array<Elem>" or "MyAlias" etc
|
|
42
|
+
type_str.split(',').any? { |part| alias_token?(part.strip) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Whether token is an alias/generic placeholder (Elem, U, ParamTag, MyAlias)
|
|
46
|
+
# vs primitive. Inverse of primitive?.
|
|
47
|
+
#
|
|
48
|
+
# @note module_function: defines #alias_token? (visibility: private)
|
|
49
|
+
# @param [String] token single type token
|
|
50
|
+
# @return [Boolean] true if alias
|
|
51
|
+
def alias_token?(token)
|
|
52
|
+
base = normalized_base(token)
|
|
53
|
+
return false if primitive?(base)
|
|
54
|
+
return true if base =~ /\A[a-z]/ || base.include?('::')
|
|
55
|
+
return true if base =~ /\A[A-Z]\z/
|
|
56
|
+
|
|
57
|
+
!!(base =~ /\A[A-Z][A-Za-z0-9_]*\z/ && !core_primitives.include?(base))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Whether token is a primitive type (String, Integer, etc) vs alias (Elem, U, ParamTag).
|
|
61
|
+
#
|
|
62
|
+
# Uses RBS core class declarations + YARD pseudo types (Boolean, void, untyped)
|
|
63
|
+
# to avoid hardcoding the full list. Falls back to a minimal list if RBS unavailable.
|
|
64
|
+
#
|
|
65
|
+
# @note module_function: defines #primitive? (visibility: private)
|
|
66
|
+
# @param [String] token type token (e.g., "String", "Elem", "ParamTag", "untyped")
|
|
67
|
+
# @return [Boolean] true if primitive, false if alias/generic placeholder
|
|
68
|
+
def primitive?(token)
|
|
69
|
+
base = normalized_base(token)
|
|
70
|
+
return false if base.empty?
|
|
71
|
+
return true if %w[untyped void nil].include?(base)
|
|
72
|
+
# YARD pseudo types that are not real RBS classes but considered primitives
|
|
73
|
+
return true if %w[Boolean void untyped nil].include?(base)
|
|
74
|
+
|
|
75
|
+
core_primitives.include?(base)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @note module_function: defines #normalized_base (visibility: private)
|
|
79
|
+
# @param [String] token
|
|
80
|
+
# @return [String]
|
|
81
|
+
def normalized_base(token)
|
|
82
|
+
token.split('<').first.split('[').first.strip.delete_suffix('?').strip
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# All core primitive class/module names from RBS environment (String, Array, etc)
|
|
86
|
+
# plus YARD primitives. Computed once and cached.
|
|
87
|
+
#
|
|
88
|
+
# @note module_function: defines #core_primitives (visibility: private)
|
|
89
|
+
# @return [Array<String>] primitive names
|
|
90
|
+
def core_primitives
|
|
91
|
+
@core_primitives ||= load_core_primitives
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Load core primitives from RBS environment. Falls back to minimal hardcoded list
|
|
95
|
+
# if RBS not available (e.g., in test env without rbs gem).
|
|
96
|
+
#
|
|
97
|
+
# @note module_function: defines #load_core_primitives (visibility: private)
|
|
98
|
+
# @return [Array<String>]
|
|
99
|
+
def load_core_primitives
|
|
100
|
+
primitives = Set.new
|
|
101
|
+
load_yard_primitives(primitives)
|
|
102
|
+
load_rbs_core(primitives)
|
|
103
|
+
merge_primitives(primitives)
|
|
104
|
+
primitives.to_a
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @note module_function: defines #load_yard_primitives (visibility: private)
|
|
108
|
+
# @param [Set<String>] primitives
|
|
109
|
+
# @return [Set<String>]
|
|
110
|
+
def load_yard_primitives(primitives)
|
|
111
|
+
primitives.merge(%w[Boolean void untyped nil true false])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @note module_function: defines #load_rbs_core (visibility: private)
|
|
115
|
+
# @param [Set<String>] primitives
|
|
116
|
+
# @raise [LoadError]
|
|
117
|
+
# @raise [StandardError]
|
|
118
|
+
# @return [Set<String>]
|
|
119
|
+
# @return [Set<String>] if LoadError, StandardError
|
|
120
|
+
def load_rbs_core(primitives)
|
|
121
|
+
require 'rbs'
|
|
122
|
+
loader = RBS::EnvironmentLoader.new
|
|
123
|
+
env = RBS::Environment.new
|
|
124
|
+
loader.load(env: env)
|
|
125
|
+
populate_class_decls(env, primitives)
|
|
126
|
+
populate_interface_decls(env, primitives)
|
|
127
|
+
rescue LoadError, StandardError
|
|
128
|
+
primitives.merge(%w[String Integer Float Numeric Symbol Array Hash Range Regexp Proc Method NilClass TrueClass FalseClass BasicObject Kernel Object Class Module IO File Dir Time Date Enumerator
|
|
129
|
+
Set Enumerable])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @note module_function: defines #populate_class_decls (visibility: private)
|
|
133
|
+
# @param [RBS::Environment] env
|
|
134
|
+
# @param [Set<String>] primitives
|
|
135
|
+
# @return [void]
|
|
136
|
+
def populate_class_decls(env, primitives)
|
|
137
|
+
return unless env.respond_to?(:class_decls)
|
|
138
|
+
|
|
139
|
+
env.class_decls.each_key { |k| primitives.merge([k.to_s.split('::').last, k.to_s]) }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @note module_function: defines #populate_interface_decls (visibility: private)
|
|
143
|
+
# @param [RBS::Environment] env
|
|
144
|
+
# @param [Set<String>] primitives
|
|
145
|
+
# @return [void]
|
|
146
|
+
def populate_interface_decls(env, primitives)
|
|
147
|
+
return unless env.respond_to?(:interface_decls)
|
|
148
|
+
|
|
149
|
+
env.interface_decls.each_key { |k| primitives << k.to_s.split('::').last }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @note module_function: defines #merge_primitives (visibility: private)
|
|
153
|
+
# @param [Set<String>] primitives
|
|
154
|
+
# @return [Set<String>]
|
|
155
|
+
def merge_primitives(primitives)
|
|
156
|
+
primitives.merge(%w[String Integer Float Numeric Symbol Array Hash Range Regexp Proc Method NilClass TrueClass FalseClass BasicObject Kernel Object])
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -59,7 +59,7 @@ module Docscribe
|
|
|
59
59
|
return unless defined?(RubyVM::AbstractSyntaxTree)
|
|
60
60
|
|
|
61
61
|
parser = ::RBS::Prototype::RBI.new
|
|
62
|
-
|
|
62
|
+
parse(parser, source)
|
|
63
63
|
index_decls(parser.decls)
|
|
64
64
|
rescue LoadError
|
|
65
65
|
nil
|
|
@@ -68,6 +68,38 @@ module Docscribe
|
|
|
68
68
|
nil
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Parse the source with the RBS RBI prototype, suppressing the parser's
|
|
72
|
+
# "Unexpected type_node" STDERR noise unless DOCSCRIBE_RBS_DEBUG=1.
|
|
73
|
+
#
|
|
74
|
+
# @private
|
|
75
|
+
# @param [::RBS::Prototype::RBI] parser RBI parser instance
|
|
76
|
+
# @param [String] source source text to parse
|
|
77
|
+
# @return [void]
|
|
78
|
+
def parse(parser, source)
|
|
79
|
+
if ENV['DOCSCRIBE_RBS_DEBUG'] == '1'
|
|
80
|
+
parser.parse(source)
|
|
81
|
+
else
|
|
82
|
+
suppress_rbs_noise { parser.parse(source) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Run the block with STDERR discarded. RBS's RBI prototype prints
|
|
87
|
+
# "Unexpected type_node" directly to STDERR for constructs it doesn't
|
|
88
|
+
# model (e.g. Herb-typed RBIs); the fallback to `Any` is already handled
|
|
89
|
+
# by the parser, so the noise is safe to drop. `IO#reopen` needs a real
|
|
90
|
+
# IO or path, so redirect to File::NULL rather than a StringIO.
|
|
91
|
+
#
|
|
92
|
+
# @private
|
|
93
|
+
# @return [T] the block's return value
|
|
94
|
+
def suppress_rbs_noise
|
|
95
|
+
original_stderr = $stderr.dup
|
|
96
|
+
$stderr.reopen(File::NULL, 'w')
|
|
97
|
+
yield
|
|
98
|
+
ensure
|
|
99
|
+
$stderr.reopen(original_stderr)
|
|
100
|
+
original_stderr.close
|
|
101
|
+
end
|
|
102
|
+
|
|
71
103
|
# Index parsed declarations into the provider lookup table.
|
|
72
104
|
#
|
|
73
105
|
# @private
|
|
@@ -10,24 +10,53 @@ module Docscribe
|
|
|
10
10
|
class << self
|
|
11
11
|
# @param [Docscribe::Types::Yard::node?] node
|
|
12
12
|
# @return [String]
|
|
13
|
-
def to_rbs(node)
|
|
13
|
+
def to_rbs(node)
|
|
14
14
|
return 'untyped' if node.nil?
|
|
15
15
|
|
|
16
|
+
rbs_for_node(node) || 'untyped'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# @private
|
|
22
|
+
# @param [Docscribe::Types::Yard::node] node
|
|
23
|
+
# @return [String, nil]
|
|
24
|
+
def rbs_for_node(node)
|
|
25
|
+
simple_type(node) || composite_type(node) || collection_type(node)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @private
|
|
29
|
+
# @param [Docscribe::Types::Yard::node] node
|
|
30
|
+
# @return [String, nil]
|
|
31
|
+
def simple_type(node)
|
|
16
32
|
case node
|
|
17
33
|
when Named then format_named(node)
|
|
18
|
-
when
|
|
34
|
+
when Literal then format_literal(node)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @private
|
|
39
|
+
# @param [Docscribe::Types::Yard::node] node
|
|
40
|
+
# @return [String, nil]
|
|
41
|
+
def composite_type(node)
|
|
42
|
+
case node
|
|
19
43
|
when Union then format_union(node)
|
|
20
44
|
when Intersection then format_intersection(node)
|
|
21
45
|
when Optional then format_optional(node)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @private
|
|
50
|
+
# @param [Docscribe::Types::Yard::node] node
|
|
51
|
+
# @return [String, nil]
|
|
52
|
+
def collection_type(node)
|
|
53
|
+
case node
|
|
54
|
+
when Generic then format_generic(node)
|
|
22
55
|
when Tuple then format_tuple(node)
|
|
23
56
|
when HashMap then format_hash_map(node)
|
|
24
|
-
when Literal then format_literal(node)
|
|
25
|
-
else 'untyped'
|
|
26
57
|
end
|
|
27
58
|
end
|
|
28
59
|
|
|
29
|
-
private
|
|
30
|
-
|
|
31
60
|
# @private
|
|
32
61
|
# @param [Docscribe::Types::Yard::Named] node
|
|
33
62
|
# @return [String]
|
|
@@ -40,7 +40,7 @@ module Docscribe
|
|
|
40
40
|
def parse_union
|
|
41
41
|
types = [parse_intersection]
|
|
42
42
|
skip_space
|
|
43
|
-
while
|
|
43
|
+
while peek == ','
|
|
44
44
|
@i += 1
|
|
45
45
|
skip_space
|
|
46
46
|
types << parse_intersection
|
|
@@ -54,7 +54,7 @@ module Docscribe
|
|
|
54
54
|
def parse_intersection
|
|
55
55
|
types = [parse_optional]
|
|
56
56
|
skip_space
|
|
57
|
-
while
|
|
57
|
+
while peek == '&'
|
|
58
58
|
@i += 1
|
|
59
59
|
skip_space
|
|
60
60
|
types << parse_optional
|
|
@@ -68,7 +68,7 @@ module Docscribe
|
|
|
68
68
|
def parse_optional
|
|
69
69
|
type = parse_primary
|
|
70
70
|
skip_space
|
|
71
|
-
if
|
|
71
|
+
if peek == '?'
|
|
72
72
|
@i += 1
|
|
73
73
|
Optional.new(type: type)
|
|
74
74
|
else
|
|
@@ -96,9 +96,9 @@ module Docscribe
|
|
|
96
96
|
return Literal.new(value: name) if literal?(name)
|
|
97
97
|
|
|
98
98
|
skip_space
|
|
99
|
-
if
|
|
99
|
+
if peek == '<'
|
|
100
100
|
parse_generic(Named.new(name: name))
|
|
101
|
-
elsif
|
|
101
|
+
elsif peek == '{'
|
|
102
102
|
parse_named_hash_map
|
|
103
103
|
else
|
|
104
104
|
Named.new(name: name)
|
|
@@ -111,7 +111,7 @@ module Docscribe
|
|
|
111
111
|
def parse_generic(base)
|
|
112
112
|
@i += 1
|
|
113
113
|
args = parse_generic_args
|
|
114
|
-
@i += 1 if
|
|
114
|
+
@i += 1 if peek == '>'
|
|
115
115
|
Generic.new(base: base.name, args: args)
|
|
116
116
|
end
|
|
117
117
|
|
|
@@ -120,7 +120,7 @@ module Docscribe
|
|
|
120
120
|
def parse_generic_arg
|
|
121
121
|
types = [parse_intersection]
|
|
122
122
|
skip_space
|
|
123
|
-
while
|
|
123
|
+
while peek == '|'
|
|
124
124
|
@i += 1
|
|
125
125
|
skip_space
|
|
126
126
|
types << parse_intersection
|
|
@@ -134,10 +134,10 @@ module Docscribe
|
|
|
134
134
|
def parse_generic_args
|
|
135
135
|
args = [] #: Array[untyped]
|
|
136
136
|
skip_space
|
|
137
|
-
while
|
|
137
|
+
while peek && peek != '>'
|
|
138
138
|
args << parse_generic_arg
|
|
139
139
|
skip_space
|
|
140
|
-
next unless
|
|
140
|
+
next unless peek == ','
|
|
141
141
|
|
|
142
142
|
@i += 1
|
|
143
143
|
skip_space
|
|
@@ -150,11 +150,11 @@ module Docscribe
|
|
|
150
150
|
def parse_tuple
|
|
151
151
|
@i += 1
|
|
152
152
|
types = [] #: Array[untyped]
|
|
153
|
-
while
|
|
153
|
+
while peek && peek != ')'
|
|
154
154
|
types << parse_tuple_element
|
|
155
|
-
@i += 1 and skip_space if
|
|
155
|
+
@i += 1 and skip_space if peek == ','
|
|
156
156
|
end
|
|
157
|
-
@i += 1 if
|
|
157
|
+
@i += 1 if peek == ')'
|
|
158
158
|
Tuple.new(types: types)
|
|
159
159
|
end
|
|
160
160
|
|
|
@@ -163,7 +163,7 @@ module Docscribe
|
|
|
163
163
|
def parse_tuple_element
|
|
164
164
|
type = parse_intersection
|
|
165
165
|
skip_space
|
|
166
|
-
if
|
|
166
|
+
if peek == '?'
|
|
167
167
|
@i += 1
|
|
168
168
|
Optional.new(type: type)
|
|
169
169
|
else
|
|
@@ -176,9 +176,9 @@ module Docscribe
|
|
|
176
176
|
def parse_hash_map
|
|
177
177
|
@i += 1
|
|
178
178
|
key = parse_union
|
|
179
|
-
@i += 2 if @s[@i
|
|
179
|
+
@i += 2 if @s[@i, 2] == '=>'
|
|
180
180
|
value = parse_union
|
|
181
|
-
@i += 1 if
|
|
181
|
+
@i += 1 if peek == '}'
|
|
182
182
|
HashMap.new(key_type: key, value_type: value)
|
|
183
183
|
end
|
|
184
184
|
|
|
@@ -192,7 +192,7 @@ module Docscribe
|
|
|
192
192
|
# @return [Docscribe::Types::Yard::Duck]
|
|
193
193
|
def parse_duck_type
|
|
194
194
|
methods = [] #: Array[String]
|
|
195
|
-
while
|
|
195
|
+
while peek == '#'
|
|
196
196
|
@i += 1
|
|
197
197
|
name = scan_name
|
|
198
198
|
methods << name
|
|
@@ -205,7 +205,12 @@ module Docscribe
|
|
|
205
205
|
# @return [String]
|
|
206
206
|
def scan_name
|
|
207
207
|
start = @i
|
|
208
|
-
|
|
208
|
+
loop do
|
|
209
|
+
c = peek
|
|
210
|
+
break unless c && name_char?(c)
|
|
211
|
+
|
|
212
|
+
@i += 1
|
|
213
|
+
end
|
|
209
214
|
@s[start...@i]
|
|
210
215
|
end
|
|
211
216
|
|
|
@@ -226,13 +231,13 @@ module Docscribe
|
|
|
226
231
|
# @private
|
|
227
232
|
# @return [void]
|
|
228
233
|
def skip_space
|
|
229
|
-
@i += 1 while
|
|
234
|
+
@i += 1 while peek&.match?(/\s/)
|
|
230
235
|
end
|
|
231
236
|
|
|
232
237
|
# @private
|
|
233
238
|
# @return [String?]
|
|
234
|
-
def peek
|
|
235
|
-
@
|
|
239
|
+
def peek #: String?
|
|
240
|
+
@s[@i]
|
|
236
241
|
end
|
|
237
242
|
end
|
|
238
243
|
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'parser'
|
|
4
|
+
|
|
5
|
+
module Docscribe
|
|
6
|
+
module Types
|
|
7
|
+
module Yard
|
|
8
|
+
# Validates YARD type strings for syntax errors.
|
|
9
|
+
#
|
|
10
|
+
# This validator is intentionally lightweight and does not perform
|
|
11
|
+
# semantic type-existence checks (e.g. `Symbol2l` vs `Symbol`).
|
|
12
|
+
# Semantic checks are handled by `Docscribe::Validator::TypeMismatchValidator`
|
|
13
|
+
# which compares a YARD tag against the inferred / RBS type.
|
|
14
|
+
module Validator
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Check if a YARD type string is syntactically valid.
|
|
18
|
+
#
|
|
19
|
+
# Criteria:
|
|
20
|
+
# - non-empty after strip
|
|
21
|
+
# - brackets are balanced (`<>`, `()`, `{}`, `[]`)
|
|
22
|
+
# - no `,,`, `<>`, `,]` artefacts
|
|
23
|
+
# - `Yard::Parser` consumes the entire string (catches `Sym bol` leftover)
|
|
24
|
+
#
|
|
25
|
+
# @note module_function: defines #valid? (visibility: private)
|
|
26
|
+
# @param [String, nil] type_str the YARD type string to validate (e.g. "String", "Array<String>", "Sym bol")
|
|
27
|
+
# @raise [StandardError]
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
# @return [Boolean] if StandardError
|
|
30
|
+
def valid?(type_str)
|
|
31
|
+
syntax_valid?(type_str)
|
|
32
|
+
rescue StandardError
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Strict syntax validation with balanced brackets and full consumption.
|
|
37
|
+
#
|
|
38
|
+
# @note module_function: defines #syntax_valid? (visibility: private)
|
|
39
|
+
# @param [String, nil] type_str
|
|
40
|
+
# @raise [StandardError]
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
# @return [Boolean] if StandardError
|
|
43
|
+
def syntax_valid?(type_str)
|
|
44
|
+
return false if blank_type?(type_str)
|
|
45
|
+
return false unless balanced_brackets?(type_str)
|
|
46
|
+
return false if artefact_type?(type_str)
|
|
47
|
+
|
|
48
|
+
fully_consumed?(type_str)
|
|
49
|
+
rescue StandardError
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @note module_function: defines #blank_type? (visibility: private)
|
|
54
|
+
# @param [String, nil] type_str
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def blank_type?(type_str)
|
|
57
|
+
type_str.nil? || type_str.strip.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @note module_function: defines #artefact_type? (visibility: private)
|
|
61
|
+
# @param [String, nil] type_str
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def artefact_type?(type_str)
|
|
64
|
+
return false if type_str.nil?
|
|
65
|
+
|
|
66
|
+
type_str.include?(',,') || type_str.include?('<>') || type_str.include?(',]')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check balanced brackets for `<>`, `()`, `{}`, `[]`.
|
|
70
|
+
#
|
|
71
|
+
# @note module_function: defines #balanced_brackets? (visibility: private)
|
|
72
|
+
# @param [String, nil] type_str
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def balanced_brackets?(type_str)
|
|
75
|
+
return false if type_str.nil?
|
|
76
|
+
|
|
77
|
+
stack = [] #: Array[String]
|
|
78
|
+
pairs = { '>' => '<', ')' => '(', '}' => '{', ']' => '[' }
|
|
79
|
+
opens = pairs.values
|
|
80
|
+
|
|
81
|
+
type_str.each_char do |ch|
|
|
82
|
+
return false unless process_bracket_char?(ch, stack, pairs, opens)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
stack.empty?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @note module_function: defines #process_bracket_char? (visibility: private)
|
|
89
|
+
# @param [String] char
|
|
90
|
+
# @param [Array<String>] stack
|
|
91
|
+
# @param [Hash<String, String>] pairs
|
|
92
|
+
# @param [Array<String>] opens
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def process_bracket_char?(char, stack, pairs, opens)
|
|
95
|
+
if opens.include?(char)
|
|
96
|
+
stack << char
|
|
97
|
+
elsif pairs.key?(char)
|
|
98
|
+
return false if stack.empty? || stack.pop != pairs[char]
|
|
99
|
+
end
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Whether `Yard::Parser` consumes the entire string.
|
|
104
|
+
#
|
|
105
|
+
# Catches cases like `Sym bol` where parser would return `Sym` and
|
|
106
|
+
# leave ` bol` unconsumed.
|
|
107
|
+
#
|
|
108
|
+
# @note module_function: defines #fully_consumed? (visibility: private)
|
|
109
|
+
# @param [String, nil] type_str
|
|
110
|
+
# @raise [StandardError]
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
# @return [Boolean] if StandardError
|
|
113
|
+
def fully_consumed?(type_str)
|
|
114
|
+
return false if type_str.nil?
|
|
115
|
+
|
|
116
|
+
stripped = type_str.strip
|
|
117
|
+
parser = Parser.new(stripped)
|
|
118
|
+
node = parser.parse
|
|
119
|
+
return false unless node
|
|
120
|
+
|
|
121
|
+
idx = parser.instance_variable_get(:@i)
|
|
122
|
+
# `Parser#parse` calls `skip_space` before and after `parse_union`,
|
|
123
|
+
# so `idx` should be at the end if fully consumed.
|
|
124
|
+
idx == stripped.length
|
|
125
|
+
rescue StandardError
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|