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.
@@ -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
- system('bundle exec yard')
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
- @@names_to_paths ||= YARD::Registry.all(:class)
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) do |k, a, b|
14
- a | b
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
- @@names_to_paths = nil
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
- (@@names_to_paths[name.split('::').last] || [])
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 }
@@ -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
- /^\#[a-zA-Z_][\w]*(?:[a-zA-Z_][\w=]*)*(?:( ?\& ?\#)*[a-zA-Z_][\w=]*)*$/
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 [Boolean] replace_errors_with_untyped If true, T.untyped is used
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 = nil, replace_errors_with_untyped = false, replace_unresolved_with_untyped = false)
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, replace_errors_with_untyped, replace_unresolved_with_untyped) }
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
- Logging.duck("#{yard} looks like a duck type, replacing with untyped", item)
158
- Parlour::Types::Untyped.new
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, replace_errors_with_untyped, replace_unresolved_with_untyped) }
165
- if SINGLE_ARG_GENERIC_TYPES.include?(generic_type) && parameters.length > 1
166
- Parlour::Types.const_get(generic_type).new(Parlour::Types::Union.new(parameters))
167
- elsif generic_type == 'Class' && parameters.length == 1
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 generic_type == 'Hash'
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.const_defined?(generic_type)
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(generic_type).new(*parameters)
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, replace_errors_with_untyped, replace_unresolved_with_untyped) }
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, replace_errors_with_untyped, replace_unresolved_with_untyped) }
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, replace_errors_with_untyped, replace_unresolved_with_untyped) }
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
@@ -1,4 +1,4 @@
1
1
  # typed: strong
2
2
  module Sord
3
- VERSION = '3.0.1'
3
+ VERSION = '5.0.0'
4
4
  end