sord 3.0.1 → 5.0.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/.github/workflows/ruby.yml +22 -0
- data/CHANGELOG.md +43 -0
- data/README.md +6 -0
- data/exe/sord +10 -4
- data/lib/sord/generator.rb +146 -37
- data/lib/sord/logging.rb +22 -21
- data/lib/sord/parlour_plugin.rb +21 -5
- data/lib/sord/resolver.rb +69 -8
- data/lib/sord/type_converter.rb +102 -24
- data/lib/sord/version.rb +1 -1
- data/rbi/sord.rbi +147 -40
- data/sord.gemspec +1 -0
- metadata +18 -3
data/lib/sord/parlour_plugin.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# typed: true
|
2
2
|
require 'parlour'
|
3
|
+
require 'yard/tags/library'
|
3
4
|
|
4
5
|
module Sord
|
5
6
|
class ParlourPlugin < Parlour::Plugin
|
@@ -42,17 +43,21 @@ module Sord
|
|
42
43
|
Sord::Logging.error('No output format given; please specify --rbi or --rbs')
|
43
44
|
exit 1
|
44
45
|
end
|
45
|
-
|
46
|
+
|
46
47
|
if (options[:rbi] && options[:rbs])
|
47
48
|
Sord::Logging.error('You cannot specify both --rbi and --rbs; please use only one')
|
48
49
|
exit 1
|
49
50
|
end
|
50
|
-
|
51
|
+
|
51
52
|
if options[:regenerate]
|
52
53
|
begin
|
53
54
|
Sord::Logging.info('Running YARD...')
|
54
55
|
Sord::ParlourPlugin.with_clean_env do
|
55
|
-
|
56
|
+
tag_param = ''
|
57
|
+
options[:tags]&.each do |tag|
|
58
|
+
tag_param += "--tag #{tag} "
|
59
|
+
end
|
60
|
+
system("bundle exec yard #{tag_param} --no-output")
|
56
61
|
end
|
57
62
|
rescue Errno::ENOENT
|
58
63
|
Sord::Logging.error('The YARD tool could not be found on your PATH.')
|
@@ -63,15 +68,26 @@ module Sord
|
|
63
68
|
end
|
64
69
|
|
65
70
|
options[:mode] = \
|
66
|
-
if options[:rbi] then :rbi elsif options[:rbs] then :rbs end
|
71
|
+
if options[:rbi] then :rbi elsif options[:rbs] then :rbs end
|
67
72
|
options[:parlour] = @parlour
|
68
73
|
options[:root] = root
|
69
74
|
|
75
|
+
add_custom_tags
|
76
|
+
|
70
77
|
Sord::Generator.new(options).run
|
71
78
|
|
72
79
|
true
|
73
80
|
end
|
74
81
|
|
82
|
+
def add_custom_tags
|
83
|
+
return if options[:tags].empty?
|
84
|
+
|
85
|
+
options[:tags].each do |tag|
|
86
|
+
name, description = tag.split(':')
|
87
|
+
YARD::Tags::Library.define_tag(description, name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
75
91
|
def self.with_clean_env &block
|
76
92
|
meth = if Bundler.respond_to?(:with_unbundled_env)
|
77
93
|
:with_unbundled_env
|
@@ -81,4 +97,4 @@ module Sord
|
|
81
97
|
Bundler.send meth, &block
|
82
98
|
end
|
83
99
|
end
|
84
|
-
end
|
100
|
+
end
|
data/lib/sord/resolver.rb
CHANGED
@@ -1,30 +1,91 @@
|
|
1
1
|
# typed: false
|
2
2
|
require 'stringio'
|
3
|
+
require 'rbs'
|
4
|
+
require 'rbs/cli'
|
3
5
|
|
4
6
|
module Sord
|
5
7
|
module Resolver
|
6
8
|
# @return [void]
|
7
9
|
def self.prepare
|
10
|
+
return @names_to_paths if @names_to_paths
|
11
|
+
|
12
|
+
gem_objects = {}
|
13
|
+
load_gem_objects(gem_objects)
|
14
|
+
|
8
15
|
# Construct a hash of class names to full paths
|
9
|
-
|
16
|
+
@names_to_paths = YARD::Registry.all(:class)
|
10
17
|
.group_by(&:name)
|
11
|
-
.map { |k, v| [k.to_s, v.map(&:path)] }
|
18
|
+
.map { |k, v| [k.to_s, v.map(&:path).to_set] }
|
12
19
|
.to_h
|
13
|
-
.merge(builtin_classes.map { |x| [x, [x]] }.to_h)
|
14
|
-
|
20
|
+
.merge(builtin_classes.map { |x| [x, Set.new([x])] }.to_h) { |_k, a, b| a.union(b) }
|
21
|
+
.merge(gem_objects) { |_k, a, b| a.union(b) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.load_gem_objects(hash)
|
25
|
+
all_decls = []
|
26
|
+
begin
|
27
|
+
RBS::CLI::LibraryOptions.new.loader.load(env: all_decls)
|
28
|
+
rescue RBS::Collection::Config::CollectionNotAvailable
|
29
|
+
Sord::Logging.warn("Could not load RBS collection - run rbs collection install for dependencies")
|
30
|
+
end
|
31
|
+
add_rbs_objects_to_paths(all_decls, hash)
|
32
|
+
|
33
|
+
gem_paths = Bundler.load.specs.map(&:full_gem_path)
|
34
|
+
gem_paths.each do |path|
|
35
|
+
if File.exists?("#{path}/rbi")
|
36
|
+
Dir["#{path}/rbi/**/*.rbi"].each do |sigfile|
|
37
|
+
tree = Parlour::TypeLoader.load_file(sigfile)
|
38
|
+
add_rbi_objects_to_paths(tree.children, hash)
|
39
|
+
end
|
15
40
|
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.add_rbs_objects_to_paths(all_decls, names_to_paths, path=[])
|
45
|
+
klasses = [
|
46
|
+
RBS::AST::Declarations::Module,
|
47
|
+
RBS::AST::Declarations::Class,
|
48
|
+
RBS::AST::Declarations::Constant
|
49
|
+
]
|
50
|
+
all_decls.each do |decl|
|
51
|
+
next unless klasses.include?(decl.class)
|
52
|
+
name = decl.name.to_s
|
53
|
+
new_path = path + [name]
|
54
|
+
names_to_paths[name] ||= Set.new
|
55
|
+
names_to_paths[name] << new_path.join('::')
|
56
|
+
add_rbs_objects_to_paths(decl.members, names_to_paths, new_path) if decl.respond_to?(:members)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.add_rbi_objects_to_paths(nodes, names_to_paths, path=[])
|
61
|
+
klasses = [
|
62
|
+
Parlour::RbiGenerator::Constant,
|
63
|
+
Parlour::RbiGenerator::ModuleNamespace,
|
64
|
+
Parlour::RbiGenerator::ClassNamespace
|
65
|
+
]
|
66
|
+
nodes.each do |node|
|
67
|
+
next unless klasses.include?(node.class)
|
68
|
+
new_path = path + [node.name]
|
69
|
+
names_to_paths[node.name] ||= Set.new
|
70
|
+
names_to_paths[node.name] << new_path.join('::')
|
71
|
+
add_rbi_objects_to_paths(node.children, names_to_paths, new_path) if node.respond_to?(:children)
|
72
|
+
end
|
16
73
|
end
|
17
74
|
|
18
75
|
# @return [void]
|
19
76
|
def self.clear
|
20
|
-
|
77
|
+
@names_to_paths = nil
|
21
78
|
end
|
22
79
|
|
23
80
|
# @param [String] name
|
24
81
|
# @return [Array<String>]
|
25
82
|
def self.paths_for(name)
|
26
83
|
prepare
|
27
|
-
|
84
|
+
|
85
|
+
# If the name starts with ::, then we've been given an explicit path from root - just use that
|
86
|
+
return [name] if name.start_with?('::')
|
87
|
+
|
88
|
+
(@names_to_paths[name.split('::').last] || [])
|
28
89
|
.select { |x| x.end_with?(name) }
|
29
90
|
end
|
30
91
|
|
@@ -39,9 +100,9 @@ module Sord
|
|
39
100
|
# This prints some deprecation warnings, so suppress them
|
40
101
|
prev_stderr = $stderr
|
41
102
|
$stderr = StringIO.new
|
42
|
-
|
103
|
+
|
43
104
|
major = RUBY_VERSION.split('.').first.to_i
|
44
|
-
sorted_set_removed = major >= 3
|
105
|
+
sorted_set_removed = major >= 3
|
45
106
|
|
46
107
|
Object.constants
|
47
108
|
.reject { |x| sorted_set_removed && x == :SortedSet }
|
data/lib/sord/type_converter.rb
CHANGED
@@ -21,10 +21,15 @@ module Sord
|
|
21
21
|
GENERIC_TYPE_REGEX =
|
22
22
|
/(#{SIMPLE_TYPE_REGEX})\s*[<{]\s*(.*)\s*[>}]/
|
23
23
|
|
24
|
+
# Matches valid method names.
|
25
|
+
# From: https://stackoverflow.com/a/4379197/2626000
|
26
|
+
METHOD_NAME_REGEX =
|
27
|
+
/(?:[a-z_]\w*[?!=]?|\[\]=?|<<|>>|\*\*|[!~+\*\/%&^|-]|[<>]=?|<=>|={2,3}|![=~]|=~)/i
|
28
|
+
|
24
29
|
# Match duck types which require the object implement one or more methods,
|
25
30
|
# like '#foo', '#foo & #bar', '#foo&#bar&#baz', and '#foo&#bar&#baz&#foo_bar'.
|
26
31
|
DUCK_TYPE_REGEX =
|
27
|
-
|
32
|
+
/^\##{METHOD_NAME_REGEX}(?:\s*\&\s*\##{METHOD_NAME_REGEX})*$/
|
28
33
|
|
29
34
|
# A regular expression which matches ordered lists in the format of
|
30
35
|
# either "Array(String, Symbol)" or "(String, Symbol)".
|
@@ -90,17 +95,35 @@ module Sord
|
|
90
95
|
result
|
91
96
|
end
|
92
97
|
|
98
|
+
# Configuration for how the type converter should work in particular cases.
|
99
|
+
class Configuration
|
100
|
+
def initialize(replace_errors_with_untyped:, replace_unresolved_with_untyped:, output_language:)
|
101
|
+
@output_language = output_language
|
102
|
+
@replace_errors_with_untyped = replace_errors_with_untyped
|
103
|
+
@replace_unresolved_with_untyped = replace_unresolved_with_untyped
|
104
|
+
end
|
105
|
+
|
106
|
+
# The language which the generated types will be converted to - one of
|
107
|
+
# `:rbi` or `:rbs`.
|
108
|
+
attr_accessor :output_language
|
109
|
+
|
110
|
+
# @return [Boolean] If true, T.untyped is used instead of SORD_ERROR_
|
111
|
+
# constants for unknown types.
|
112
|
+
attr_accessor :replace_errors_with_untyped
|
113
|
+
|
114
|
+
# @param [Boolean] replace_unresolved_with_untyped If true, T.untyped is
|
115
|
+
# used when Sord is unable to resolve a constant.
|
116
|
+
attr_accessor :replace_unresolved_with_untyped
|
117
|
+
end
|
118
|
+
|
93
119
|
# Converts a YARD type into a Parlour type.
|
94
120
|
# @param [Boolean, Array, String] yard The YARD type.
|
95
121
|
# @param [YARD::CodeObjects::Base] item The CodeObject which the YARD type
|
96
122
|
# is associated with. This is used for logging and can be nil, but this
|
97
123
|
# will lead to less informative log messages.
|
98
|
-
# @param [
|
99
|
-
# instead of SORD_ERROR_ constants for unknown types.
|
100
|
-
# @param [Boolean] replace_unresolved_with_untyped If true, T.untyped is used
|
101
|
-
# when Sord is unable to resolve a constant.
|
124
|
+
# @param [Configuration] config The generation configuration.
|
102
125
|
# @return [Parlour::Types::Type]
|
103
|
-
def self.yard_to_parlour(yard, item
|
126
|
+
def self.yard_to_parlour(yard, item, config)
|
104
127
|
case yard
|
105
128
|
when nil # Type not specified
|
106
129
|
Parlour::Types::Untyped.new
|
@@ -113,7 +136,7 @@ module Sord
|
|
113
136
|
# selection of any of the types
|
114
137
|
types = yard
|
115
138
|
.reject { |x| x == 'nil' }
|
116
|
-
.map { |x| yard_to_parlour(x, item,
|
139
|
+
.map { |x| yard_to_parlour(x, item, config) }
|
117
140
|
.uniq(&:hash)
|
118
141
|
result = types.length == 1 \
|
119
142
|
? types.first
|
@@ -142,7 +165,7 @@ module Sord
|
|
142
165
|
unless yard == new_path
|
143
166
|
Parlour::Types::Raw.new(new_path)
|
144
167
|
else
|
145
|
-
if replace_unresolved_with_untyped
|
168
|
+
if config.replace_unresolved_with_untyped
|
146
169
|
Logging.warn("#{yard} wasn't able to be resolved to a constant in this project, replaced with untyped", item)
|
147
170
|
Parlour::Types::Untyped.new
|
148
171
|
else
|
@@ -154,33 +177,43 @@ module Sord
|
|
154
177
|
Parlour::Types::Raw.new(yard)
|
155
178
|
end
|
156
179
|
when DUCK_TYPE_REGEX
|
157
|
-
|
158
|
-
|
180
|
+
if config.output_language == :rbs && (type = duck_type_to_rbs_type(yard))
|
181
|
+
Logging.duck("#{yard} looks like a duck type with an equivalent RBS interface, replacing with #{type.generate_rbs}", item)
|
182
|
+
type
|
183
|
+
else
|
184
|
+
Logging.duck("#{yard} looks like a duck type, replacing with untyped", item)
|
185
|
+
Parlour::Types::Untyped.new
|
186
|
+
end
|
159
187
|
when /^#{GENERIC_TYPE_REGEX}$/
|
160
188
|
generic_type = $1
|
161
189
|
type_parameters = $2
|
162
190
|
|
191
|
+
# If we don't do this, `const_defined?` will resolve "::Array" as the actual Ruby `Array`
|
192
|
+
# type, not `Parlour::Types::Array`!
|
193
|
+
relative_generic_type = generic_type.start_with?('::') \
|
194
|
+
? generic_type[2..-1] : generic_type
|
195
|
+
|
163
196
|
parameters = split_type_parameters(type_parameters)
|
164
|
-
.map { |x| yard_to_parlour(x, item,
|
165
|
-
if SINGLE_ARG_GENERIC_TYPES.include?(
|
166
|
-
Parlour::Types.const_get(
|
167
|
-
elsif
|
197
|
+
.map { |x| yard_to_parlour(x, item, config) }
|
198
|
+
if SINGLE_ARG_GENERIC_TYPES.include?(relative_generic_type) && parameters.length > 1
|
199
|
+
Parlour::Types.const_get(relative_generic_type).new(Parlour::Types::Union.new(parameters))
|
200
|
+
elsif relative_generic_type == 'Class' && parameters.length == 1
|
168
201
|
Parlour::Types::Class.new(parameters.first)
|
169
|
-
elsif
|
202
|
+
elsif relative_generic_type == 'Hash'
|
170
203
|
if parameters.length == 2
|
171
204
|
Parlour::Types::Hash.new(*parameters)
|
172
205
|
else
|
173
|
-
handle_sord_error(parameters.map(&:describe).join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, replace_errors_with_untyped)
|
206
|
+
handle_sord_error(parameters.map(&:describe).join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, config.replace_errors_with_untyped)
|
174
207
|
end
|
175
208
|
else
|
176
|
-
if Parlour::Types.
|
209
|
+
if Parlour::Types.constants.include?(relative_generic_type.to_sym)
|
177
210
|
# This generic is built in to parlour, but sord doesn't
|
178
211
|
# explicitly know about it.
|
179
|
-
Parlour::Types.const_get(
|
212
|
+
Parlour::Types.const_get(relative_generic_type).new(*parameters)
|
180
213
|
else
|
181
214
|
# This is a user defined generic
|
182
215
|
Parlour::Types::Generic.new(
|
183
|
-
yard_to_parlour(generic_type),
|
216
|
+
yard_to_parlour(generic_type, nil, config),
|
184
217
|
parameters
|
185
218
|
)
|
186
219
|
end
|
@@ -190,22 +223,22 @@ module Sord
|
|
190
223
|
when ORDERED_LIST_REGEX
|
191
224
|
type_parameters = $1
|
192
225
|
parameters = split_type_parameters(type_parameters)
|
193
|
-
.map { |x| yard_to_parlour(x, item,
|
226
|
+
.map { |x| yard_to_parlour(x, item, config) }
|
194
227
|
Parlour::Types::Tuple.new(parameters)
|
195
228
|
when SHORTHAND_HASH_SYNTAX
|
196
229
|
type_parameters = $1
|
197
230
|
parameters = split_type_parameters(type_parameters)
|
198
|
-
.map { |x| yard_to_parlour(x, item,
|
231
|
+
.map { |x| yard_to_parlour(x, item, config) }
|
199
232
|
# Return a warning about an invalid hash when it has more or less than two elements.
|
200
233
|
if parameters.length == 2
|
201
234
|
Parlour::Types::Hash.new(*parameters)
|
202
235
|
else
|
203
|
-
handle_sord_error(parameters.map(&:describe).join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, replace_errors_with_untyped)
|
236
|
+
handle_sord_error(parameters.map(&:describe).join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, config.replace_errors_with_untyped)
|
204
237
|
end
|
205
238
|
when SHORTHAND_ARRAY_SYNTAX
|
206
239
|
type_parameters = $1
|
207
240
|
parameters = split_type_parameters(type_parameters)
|
208
|
-
.map { |x| yard_to_parlour(x, item,
|
241
|
+
.map { |x| yard_to_parlour(x, item, config) }
|
209
242
|
parameters.one? \
|
210
243
|
? Parlour::Types::Array.new(parameters.first)
|
211
244
|
: Parlour::Types::Array.new(Parlour::Types::Union.new(parameters))
|
@@ -215,7 +248,7 @@ module Sord
|
|
215
248
|
return Parlour::Types::Raw.new(from_yaml.class.to_s) \
|
216
249
|
if [Symbol, Float, Integer].include?(from_yaml.class)
|
217
250
|
|
218
|
-
return handle_sord_error(yard.to_s, "#{yard.inspect} does not appear to be a type", item, replace_errors_with_untyped)
|
251
|
+
return handle_sord_error(yard.to_s, "#{yard.inspect} does not appear to be a type", item, config.replace_errors_with_untyped)
|
219
252
|
end
|
220
253
|
end
|
221
254
|
|
@@ -233,5 +266,50 @@ module Sord
|
|
233
266
|
? Parlour::Types::Untyped.new
|
234
267
|
: Parlour::Types::Raw.new("SORD_ERROR_#{name.gsub(/[^0-9A-Za-z_]/i, '')}")
|
235
268
|
end
|
269
|
+
|
270
|
+
# Taken from: https://github.com/ruby/rbs/blob/master/core/builtin.rbs
|
271
|
+
# When the latest commit was: 6c847d1
|
272
|
+
#
|
273
|
+
# Interfaces which use generic arguments have those arguments as `untyped`, since I'm not aware
|
274
|
+
# of any standard way that these are specified.
|
275
|
+
DUCK_TYPES_TO_RBS_TYPE_NAMES = {
|
276
|
+
# Concrete
|
277
|
+
"#to_i" => "_ToI",
|
278
|
+
"#to_int" => "_ToInt",
|
279
|
+
"#to_r" => "_ToR",
|
280
|
+
"#to_s" => "_ToS",
|
281
|
+
"#to_str" => "_ToStr",
|
282
|
+
"#to_proc" => "_ToProc",
|
283
|
+
"#to_path" => "_ToPath",
|
284
|
+
"#read" => "_Reader",
|
285
|
+
"#readpartial" => "_ReaderPartial",
|
286
|
+
"#write" => "_Writer",
|
287
|
+
"#rewind" => "_Rewindable",
|
288
|
+
"#to_io" => "_ToIO",
|
289
|
+
"#exception" => "_Exception",
|
290
|
+
|
291
|
+
# Generic - these will be put in a `Types::Raw`, so writing RBS syntax is a little devious,
|
292
|
+
# but by their nature we know they'll only be used in an RBS file, so it's probably fine
|
293
|
+
"#to_hash" => "_ToHash[untyped, untyped]",
|
294
|
+
"#each" => "_Each[untyped]",
|
295
|
+
}
|
296
|
+
|
297
|
+
# Given a YARD duck type string, attempts to convert it to one of a list of pre-defined RBS
|
298
|
+
# built-in interfaces.
|
299
|
+
#
|
300
|
+
# For example, the common duck type `#to_s` has a built-in RBS equivalent `_ToS`.
|
301
|
+
#
|
302
|
+
# If no such interface exists, returns `nil`.
|
303
|
+
#
|
304
|
+
# @param [String] type
|
305
|
+
# @return [Parlour::Types::Type, nil]
|
306
|
+
def self.duck_type_to_rbs_type(type)
|
307
|
+
type_name = DUCK_TYPES_TO_RBS_TYPE_NAMES[type]
|
308
|
+
if !type_name.nil?
|
309
|
+
Parlour::Types::Raw.new(type_name)
|
310
|
+
else
|
311
|
+
nil
|
312
|
+
end
|
313
|
+
end
|
236
314
|
end
|
237
315
|
end
|
data/lib/sord/version.rb
CHANGED