solargraph 0.56.2 → 0.57.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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/linting.yml +125 -0
  3. data/.github/workflows/plugins.yml +148 -6
  4. data/.github/workflows/rspec.yml +39 -4
  5. data/.github/workflows/typecheck.yml +5 -2
  6. data/.gitignore +5 -0
  7. data/.overcommit.yml +72 -0
  8. data/.rspec +1 -0
  9. data/.rubocop.yml +66 -0
  10. data/.rubocop_todo.yml +2627 -0
  11. data/.yardopts +1 -0
  12. data/CHANGELOG.md +42 -0
  13. data/README.md +8 -4
  14. data/Rakefile +125 -13
  15. data/lib/solargraph/api_map/cache.rb +3 -2
  16. data/lib/solargraph/api_map/constants.rb +218 -0
  17. data/lib/solargraph/api_map/index.rb +20 -26
  18. data/lib/solargraph/api_map/source_to_yard.rb +10 -4
  19. data/lib/solargraph/api_map/store.rb +126 -18
  20. data/lib/solargraph/api_map.rb +212 -234
  21. data/lib/solargraph/bench.rb +1 -0
  22. data/lib/solargraph/complex_type/type_methods.rb +1 -0
  23. data/lib/solargraph/complex_type/unique_type.rb +7 -7
  24. data/lib/solargraph/complex_type.rb +5 -1
  25. data/lib/solargraph/convention/active_support_concern.rb +111 -0
  26. data/lib/solargraph/convention/base.rb +17 -0
  27. data/lib/solargraph/convention/data_definition/data_assignment_node.rb +1 -0
  28. data/lib/solargraph/convention/data_definition/data_definition_node.rb +3 -1
  29. data/lib/solargraph/convention/data_definition.rb +2 -1
  30. data/lib/solargraph/convention/gemspec.rb +1 -1
  31. data/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +1 -0
  32. data/lib/solargraph/convention/struct_definition/struct_definition_node.rb +3 -1
  33. data/lib/solargraph/convention/struct_definition.rb +36 -13
  34. data/lib/solargraph/convention.rb +31 -2
  35. data/lib/solargraph/diagnostics/rubocop.rb +6 -1
  36. data/lib/solargraph/diagnostics/rubocop_helpers.rb +1 -1
  37. data/lib/solargraph/doc_map.rb +40 -12
  38. data/lib/solargraph/environ.rb +9 -2
  39. data/lib/solargraph/gem_pins.rb +17 -11
  40. data/lib/solargraph/language_server/host/dispatch.rb +2 -0
  41. data/lib/solargraph/language_server/host/message_worker.rb +3 -0
  42. data/lib/solargraph/language_server/host.rb +2 -1
  43. data/lib/solargraph/language_server/message/base.rb +2 -1
  44. data/lib/solargraph/language_server/message/extended/check_gem_version.rb +1 -1
  45. data/lib/solargraph/language_server/message/text_document/definition.rb +2 -0
  46. data/lib/solargraph/language_server/message/text_document/formatting.rb +16 -2
  47. data/lib/solargraph/language_server/message/text_document/type_definition.rb +1 -0
  48. data/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb +2 -0
  49. data/lib/solargraph/language_server/progress.rb +8 -0
  50. data/lib/solargraph/language_server/request.rb +1 -0
  51. data/lib/solargraph/library.rb +8 -15
  52. data/lib/solargraph/location.rb +2 -0
  53. data/lib/solargraph/logging.rb +11 -2
  54. data/lib/solargraph/page.rb +4 -0
  55. data/lib/solargraph/parser/comment_ripper.rb +8 -1
  56. data/lib/solargraph/parser/flow_sensitive_typing.rb +32 -4
  57. data/lib/solargraph/parser/node_methods.rb +2 -2
  58. data/lib/solargraph/parser/node_processor/base.rb +1 -1
  59. data/lib/solargraph/parser/node_processor.rb +6 -2
  60. data/lib/solargraph/parser/parser_gem/class_methods.rb +1 -1
  61. data/lib/solargraph/parser/parser_gem/flawed_builder.rb +1 -0
  62. data/lib/solargraph/parser/parser_gem/node_chainer.rb +3 -1
  63. data/lib/solargraph/parser/parser_gem/node_methods.rb +4 -2
  64. data/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +3 -2
  65. data/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +2 -0
  66. data/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb +3 -0
  67. data/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +35 -14
  68. data/lib/solargraph/parser/region.rb +3 -0
  69. data/lib/solargraph/parser/snippet.rb +2 -0
  70. data/lib/solargraph/pin/base.rb +50 -8
  71. data/lib/solargraph/pin/base_variable.rb +1 -2
  72. data/lib/solargraph/pin/callable.rb +9 -0
  73. data/lib/solargraph/pin/closure.rb +2 -0
  74. data/lib/solargraph/pin/common.rb +6 -2
  75. data/lib/solargraph/pin/constant.rb +2 -0
  76. data/lib/solargraph/pin/delegated_method.rb +1 -0
  77. data/lib/solargraph/pin/local_variable.rb +4 -1
  78. data/lib/solargraph/pin/method.rb +8 -5
  79. data/lib/solargraph/pin/method_alias.rb +3 -0
  80. data/lib/solargraph/pin/parameter.rb +18 -8
  81. data/lib/solargraph/pin/proxy_type.rb +1 -0
  82. data/lib/solargraph/pin/reference/override.rb +15 -1
  83. data/lib/solargraph/pin/reference/superclass.rb +5 -0
  84. data/lib/solargraph/pin/reference.rb +26 -0
  85. data/lib/solargraph/pin/search.rb +3 -1
  86. data/lib/solargraph/pin/signature.rb +2 -0
  87. data/lib/solargraph/pin/symbol.rb +5 -0
  88. data/lib/solargraph/pin_cache.rb +64 -4
  89. data/lib/solargraph/position.rb +2 -0
  90. data/lib/solargraph/range.rb +1 -0
  91. data/lib/solargraph/rbs_map/conversions.rb +7 -5
  92. data/lib/solargraph/rbs_map/core_map.rb +3 -0
  93. data/lib/solargraph/rbs_map.rb +15 -2
  94. data/lib/solargraph/shell.rb +3 -0
  95. data/lib/solargraph/source/chain/link.rb +10 -1
  96. data/lib/solargraph/source/chain.rb +9 -2
  97. data/lib/solargraph/source/change.rb +2 -2
  98. data/lib/solargraph/source/cursor.rb +2 -3
  99. data/lib/solargraph/source/source_chainer.rb +1 -1
  100. data/lib/solargraph/source.rb +5 -2
  101. data/lib/solargraph/source_map/clip.rb +1 -1
  102. data/lib/solargraph/source_map/data.rb +4 -0
  103. data/lib/solargraph/source_map/mapper.rb +4 -2
  104. data/lib/solargraph/source_map.rb +21 -14
  105. data/lib/solargraph/type_checker/param_def.rb +2 -0
  106. data/lib/solargraph/type_checker/rules.rb +8 -0
  107. data/lib/solargraph/type_checker.rb +173 -120
  108. data/lib/solargraph/version.rb +1 -1
  109. data/lib/solargraph/workspace/config.rb +0 -2
  110. data/lib/solargraph/workspace/require_paths.rb +98 -0
  111. data/lib/solargraph/workspace.rb +16 -48
  112. data/lib/solargraph/yard_map/mapper/to_method.rb +2 -2
  113. data/lib/solargraph/yardoc.rb +16 -3
  114. data/lib/solargraph.rb +2 -0
  115. data/rbs/fills/tuple.rbs +2 -3
  116. data/sig/shims/parser/3.2.0.1/builders/default.rbs +195 -0
  117. data/sig/shims/thor/1.2.0.1/.rbs_meta.yaml +9 -0
  118. data/sig/shims/thor/1.2.0.1/manifest.yaml +7 -0
  119. data/sig/shims/thor/1.2.0.1/thor.rbs +17 -0
  120. data/solargraph.gemspec +14 -4
  121. metadata +123 -9
  122. data/lib/.rubocop.yml +0 -22
@@ -38,15 +38,20 @@ module Solargraph
38
38
  @source_map ||= api_map.source_map(filename)
39
39
  end
40
40
 
41
+ # @return [Source]
42
+ def source
43
+ @source_map.source
44
+ end
45
+
41
46
  # @return [Array<Problem>]
42
47
  def problems
43
48
  @problems ||= begin
44
- without_ignored(
45
- method_tag_problems
46
- .concat variable_type_tag_problems
47
- .concat const_problems
48
- .concat call_problems
49
- )
49
+ all = method_tag_problems
50
+ .concat(variable_type_tag_problems)
51
+ .concat(const_problems)
52
+ .concat(call_problems)
53
+ unignored = without_ignored(all)
54
+ unignored.concat(unneeded_sgignore_problems)
50
55
  end
51
56
  end
52
57
 
@@ -140,7 +145,7 @@ module Solargraph
140
145
 
141
146
  # @param pin [Pin::Base]
142
147
  def virtual_pin? pin
143
- pin.location && source_map.source.comment_at?(pin.location.range.ending)
148
+ pin.location && source.comment_at?(pin.location.range.ending)
144
149
  end
145
150
 
146
151
  # @param pin [Pin::Method]
@@ -231,7 +236,7 @@ module Solargraph
231
236
  def const_problems
232
237
  return [] unless rules.validate_consts?
233
238
  result = []
234
- Solargraph::Parser::NodeMethods.const_nodes_from(source_map.source.node).each do |const|
239
+ Solargraph::Parser::NodeMethods.const_nodes_from(source.node).each do |const|
235
240
  rng = Solargraph::Range.from_node(const)
236
241
  chain = Solargraph::Parser.chain(const, filename)
237
242
  block_pin = source_map.locate_block_pin(rng.start.line, rng.start.column)
@@ -249,7 +254,7 @@ module Solargraph
249
254
  # @return [Array<Problem>]
250
255
  def call_problems
251
256
  result = []
252
- Solargraph::Parser::NodeMethods.call_nodes_from(source_map.source.node).each do |call|
257
+ Solargraph::Parser::NodeMethods.call_nodes_from(source.node).each do |call|
253
258
  rng = Solargraph::Range.from_node(call)
254
259
  next if @marked_ranges.any? { |d| d.contain?(rng.start) }
255
260
  chain = Solargraph::Parser.chain(call, filename)
@@ -272,7 +277,11 @@ module Solargraph
272
277
  # @todo remove the internal_or_core? check at a higher-than-strict level
273
278
  if !found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found))
274
279
  unless closest.generic? || ignored_pins.include?(found)
275
- result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}")
280
+ if closest.defined?
281
+ result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}")
282
+ else
283
+ result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}")
284
+ end
276
285
  @marked_ranges.push rng
277
286
  end
278
287
  end
@@ -284,127 +293,136 @@ module Solargraph
284
293
 
285
294
  # @param chain [Solargraph::Source::Chain]
286
295
  # @param api_map [Solargraph::ApiMap]
287
- # @param block_pin [Solargraph::Pin::Base]
296
+ # @param closure_pin [Solargraph::Pin::Closure]
288
297
  # @param locals [Array<Solargraph::Pin::Base>]
289
298
  # @param location [Solargraph::Location]
290
299
  # @return [Array<Problem>]
291
- def argument_problems_for chain, api_map, block_pin, locals, location
300
+ def argument_problems_for chain, api_map, closure_pin, locals, location
292
301
  result = []
293
302
  base = chain
294
- until base.links.length == 1 && base.undefined?
295
- last_base_link = base.links.last
296
- break unless last_base_link.is_a?(Solargraph::Source::Chain::Call)
297
-
298
- arguments = last_base_link.arguments
299
-
300
- pins = base.define(api_map, block_pin, locals)
301
-
302
- first_pin = pins.first
303
- if first_pin.is_a?(Pin::DelegatedMethod) && !first_pin.resolvable?(api_map)
304
- # Do nothing, as we can't find the actual method implementation
305
- elsif first_pin.is_a?(Pin::Method)
306
- # @type [Pin::Method]
307
- pin = first_pin
308
- ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper)
309
- arity_problems_for(pin, fake_args_for(block_pin), location)
310
- elsif pin.path == 'Class#new'
311
- fqns = if base.links.one?
312
- block_pin.namespace
303
+ # @type last_base_link [Solargraph::Source::Chain::Call]
304
+ last_base_link = base.links.last
305
+ return [] unless last_base_link.is_a?(Solargraph::Source::Chain::Call)
306
+
307
+ arguments = last_base_link.arguments
308
+
309
+ pins = base.define(api_map, closure_pin, locals)
310
+
311
+ first_pin = pins.first
312
+ unresolvable = first_pin.is_a?(Pin::DelegatedMethod) && !first_pin.resolvable?(api_map)
313
+ if !unresolvable && first_pin.is_a?(Pin::Method)
314
+ # @type [Pin::Method]
315
+ pin = first_pin
316
+ ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper)
317
+ arity_problems_for(pin, fake_args_for(closure_pin), location)
318
+ elsif pin.path == 'Class#new'
319
+ fqns = if base.links.one?
320
+ closure_pin.namespace
321
+ else
322
+ base.base.infer(api_map, closure_pin, locals).namespace
323
+ end
324
+ init = api_map.get_method_stack(fqns, 'initialize').first
325
+
326
+ init ? arity_problems_for(init, arguments, location) : []
327
+ else
328
+ arity_problems_for(pin, arguments, location)
329
+ end
330
+ return ap unless ap.empty?
331
+ return [] if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper)
332
+
333
+ params = first_param_hash(pins)
334
+
335
+ all_errors = []
336
+ pin.signatures.sort { |sig| sig.parameters.length }.each do |sig|
337
+ signature_errors = signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin
338
+ if signature_errors.empty?
339
+ # we found a signature that works - meaning errors from
340
+ # other signatures don't matter.
341
+ return []
342
+ end
343
+ all_errors.concat signature_errors
344
+ end
345
+ result.concat all_errors
346
+ end
347
+ result
348
+ end
349
+
350
+ # @param location [Location]
351
+ # @param locals [Array<Pin::LocalVariable>]
352
+ # @param closure_pin [Pin::Closure]
353
+ # @param params [Hash{String => Hash{Symbol => String, Solargraph::ComplexType}}]
354
+ # @param arguments [Array<Source::Chain>]
355
+ # @param sig [Pin::Signature]
356
+ # @param pin [Pin::Method]
357
+ # @param pins [Array<Pin::Method>]
358
+ #
359
+ # @return [Array<Problem>]
360
+ def signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin
361
+ errors = []
362
+ # @todo add logic mapping up restarg parameters with
363
+ # arguments (including restarg arguments). Use tuples
364
+ # when possible, and when not, ensure provably
365
+ # incorrect situations are detected.
366
+ sig.parameters.each_with_index do |par, idx|
367
+ return errors if par.decl == :restarg # bail out and assume the rest is valid pending better arg processing
368
+ argchain = arguments[idx]
369
+ if argchain.nil?
370
+ if par.decl == :arg
371
+ final_arg = arguments.last
372
+ if final_arg && final_arg.node.type == :splat
373
+ argchain = final_arg
374
+ return errors
313
375
  else
314
- base.base.infer(api_map, block_pin, locals).namespace
376
+ errors.push Problem.new(location, "Not enough arguments to #{pin.path}")
315
377
  end
316
- init = api_map.get_method_stack(fqns, 'initialize').first
317
- init ? arity_problems_for(init, arguments, location) : []
318
378
  else
319
- arity_problems_for(pin, arguments, location)
320
- end
321
- unless ap.empty?
322
- result.concat ap
323
- break
379
+ final_arg = arguments.last
380
+ argchain = final_arg if final_arg && [:kwsplat, :hash].include?(final_arg.node.type)
324
381
  end
325
- break if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper)
326
-
327
- params = first_param_hash(pins)
328
-
329
- all_errors = []
330
- pin.signatures.sort { |sig| sig.parameters.length }.each do |sig|
331
- errors = []
332
- sig.parameters.each_with_index do |par, idx|
333
- # @todo add logic mapping up restarg parameters with
334
- # arguments (including restarg arguments). Use tuples
335
- # when possible, and when not, ensure provably
336
- # incorrect situations are detected.
337
- break if par.decl == :restarg # bail out pending better arg processing
338
- argchain = arguments[idx]
339
- if argchain.nil?
340
- if par.decl == :arg
341
- final_arg = arguments.last
342
- if final_arg && final_arg.node.type == :splat
343
- argchain = final_arg
344
- next # don't try to apply the type of the splat - unlikely to be specific enough
345
- else
346
- errors.push Problem.new(location, "Not enough arguments to #{pin.path}")
347
- next
348
- end
349
- else
350
- final_arg = arguments.last
351
- argchain = final_arg if final_arg && [:kwsplat, :hash].include?(final_arg.node.type)
352
- end
353
- end
354
- if argchain
355
- if par.decl != :arg
356
- errors.concat kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, params, idx
357
- next
358
- else
359
- if argchain.node.type == :splat && argchain == arguments.last
360
- final_arg = argchain
361
- end
362
- if (final_arg && final_arg.node.type == :splat)
363
- # The final argument given has been seen and was a
364
- # splat, which doesn't give us useful types or
365
- # arities against positional parameters, so let's
366
- # continue on in case there are any required
367
- # kwargs we should warn about
368
- next
369
- end
370
-
371
- if argchain.node.type == :splat && par != sig.parameters.last
372
- # we have been given a splat and there are more
373
- # arguments to come.
374
-
375
- # @todo Improve this so that we can skip past the
376
- # rest of the positional parameters here but still
377
- # process the kwargs
378
- break
379
- end
380
- ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED
381
- ptype = ptype.self_to_type(par.context)
382
- if ptype.nil?
383
- # @todo Some level (strong, I guess) should require the param here
384
- else
385
- argtype = argchain.infer(api_map, block_pin, locals)
386
- if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype)
387
- errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")
388
- next
389
- end
390
- end
391
- end
392
- elsif par.decl == :kwarg
393
- errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}")
394
- next
395
- end
382
+ end
383
+ if argchain
384
+ if par.decl != :arg
385
+ errors.concat kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx
386
+ next
387
+ else
388
+ if argchain.node.type == :splat && argchain == arguments.last
389
+ final_arg = argchain
390
+ end
391
+ if (final_arg && final_arg.node.type == :splat)
392
+ # The final argument given has been seen and was a
393
+ # splat, which doesn't give us useful types or
394
+ # arities against positional parameters, so let's
395
+ # continue on in case there are any required
396
+ # kwargs we should warn about
397
+ next
396
398
  end
397
- if errors.empty?
398
- all_errors.clear
399
- break
399
+ if argchain.node.type == :splat && par != sig.parameters.last
400
+ # we have been given a splat and there are more
401
+ # arguments to come.
402
+
403
+ # @todo Improve this so that we can skip past the
404
+ # rest of the positional parameters here but still
405
+ # process the kwargs
406
+ return errors
407
+ end
408
+ ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED
409
+ ptype = ptype.self_to_type(par.context)
410
+ if ptype.nil?
411
+ # @todo Some level (strong, I guess) should require the param here
412
+ else
413
+ argtype = argchain.infer(api_map, closure_pin, locals)
414
+ if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype)
415
+ errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")
416
+ return errors
417
+ end
400
418
  end
401
- all_errors.concat errors
402
419
  end
403
- result.concat all_errors
420
+ elsif par.decl == :kwarg
421
+ errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}")
422
+ next
404
423
  end
405
- base = base.base
406
424
  end
407
- result
425
+ errors
408
426
  end
409
427
 
410
428
  # @param sig [Pin::Signature]
@@ -646,7 +664,6 @@ module Solargraph
646
664
  # @param parameters [Enumerable<Pin::Parameter>]
647
665
  # @todo need to use generic types in method to choose correct
648
666
  # signature and generate Integer as return type
649
- # @sg-ignore
650
667
  # @return [Integer]
651
668
  def required_param_count(parameters)
652
669
  parameters.sum { |param| %i[arg kwarg].include?(param.decl) ? 1 : 0 }
@@ -687,12 +704,48 @@ module Solargraph
687
704
  args
688
705
  end
689
706
 
707
+ # @return [Set<Integer>]
708
+ def sg_ignore_lines_processed
709
+ @sg_ignore_lines_processed ||= Set.new
710
+ end
711
+
712
+ # @return [Set<Integer>]
713
+ def all_sg_ignore_lines
714
+ source.associated_comments.select do |_line, text|
715
+ text.include?('@sg-ignore')
716
+ end.keys.to_set
717
+ end
718
+
719
+ # @return [Array<Integer>]
720
+ def unprocessed_sg_ignore_lines
721
+ (all_sg_ignore_lines - sg_ignore_lines_processed).to_a.sort
722
+ end
723
+
724
+ # @return [Array<Problem>]
725
+ def unneeded_sgignore_problems
726
+ return [] unless rules.validate_sg_ignores?
727
+
728
+ unprocessed_sg_ignore_lines.map do |line|
729
+ Problem.new(
730
+ Location.new(filename, Range.from_to(line, 0, line, 0)),
731
+ 'Unneeded @sg-ignore comment'
732
+ )
733
+ end
734
+ end
735
+
690
736
  # @param problems [Array<Problem>]
691
737
  # @return [Array<Problem>]
692
738
  def without_ignored problems
693
739
  problems.reject do |problem|
694
- node = source_map.source.node_at(problem.location.range.start.line, problem.location.range.start.column)
695
- node && source_map.source.comments_for(node)&.include?('@sg-ignore')
740
+ node = source.node_at(problem.location.range.start.line, problem.location.range.start.column)
741
+ ignored = node && source.comments_for(node)&.include?('@sg-ignore')
742
+ unless !ignored || all_sg_ignore_lines.include?(problem.location.range.start.line)
743
+ # :nocov:
744
+ Solargraph.assert_or_log(:sg_ignore) { "@sg-ignore accounting issue - node is #{node}" }
745
+ # :nocov:
746
+ end
747
+ sg_ignore_lines_processed.add problem.location.range.start.line if ignored
748
+ ignored
696
749
  end
697
750
  end
698
751
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- VERSION = '0.56.2'
4
+ VERSION = '0.57.0'
5
5
  end
@@ -90,7 +90,6 @@ module Solargraph
90
90
 
91
91
  # A hash of options supported by the formatter
92
92
  #
93
- # @sg-ignore pending https://github.com/castwide/solargraph/pull/905
94
93
  # @return [Hash]
95
94
  def formatter
96
95
  raw_data['formatter']
@@ -105,7 +104,6 @@ module Solargraph
105
104
 
106
105
  # The maximum number of files to parse from the workspace.
107
106
  #
108
- # @sg-ignore pending https://github.com/castwide/solargraph/pull/905
109
107
  # @return [Integer]
110
108
  def max_files
111
109
  raw_data['max_files']
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Solargraph
6
+ # A workspace consists of the files in a project's directory and the
7
+ # project's configuration. It provides a Source for each file to be used
8
+ # in an associated Library or ApiMap.
9
+ #
10
+ class Workspace
11
+ # Manages determining which gemspecs are available in a workspace
12
+ class RequirePaths
13
+ attr_reader :directory, :config
14
+
15
+ # @param directory [String, nil]
16
+ # @param config [Config, nil]
17
+ def initialize directory, config
18
+ @directory = directory
19
+ @config = config
20
+ end
21
+
22
+ # Generate require paths from gemspecs if they exist or assume the default
23
+ # lib directory.
24
+ #
25
+ # @return [Array<String>]
26
+ def generate
27
+ result = require_paths_from_gemspec_files
28
+ return configured_require_paths if result.empty?
29
+ result.concat(config.require_paths.map { |p| File.join(directory, p) }) if config
30
+ result
31
+ end
32
+
33
+ private
34
+
35
+ # @return [Array<String>]
36
+ def require_paths_from_gemspec_files
37
+ results = []
38
+ gemspec_file_paths.each do |gemspec_file_path|
39
+ results.concat require_path_from_gemspec_file(gemspec_file_path)
40
+ end
41
+ results
42
+ end
43
+
44
+ # Get an array of all gemspec files in the workspace.
45
+ #
46
+ # @return [Array<String>]
47
+ def gemspec_file_paths
48
+ return [] if directory.nil?
49
+ @gemspec_file_paths ||= Dir[File.join(directory, '**/*.gemspec')].select do |gs|
50
+ config.nil? || config.allow?(gs)
51
+ end
52
+ end
53
+
54
+ # Get additional require paths defined in the configuration.
55
+ #
56
+ # @return [Array<String>]
57
+ def configured_require_paths
58
+ return ['lib'] unless directory
59
+ return [File.join(directory, 'lib')] if !config || config.require_paths.empty?
60
+ config.require_paths.map { |p| File.join(directory, p) }
61
+ end
62
+
63
+ # Generate require paths from gemspecs if they exist or assume the default
64
+ # lib directory.
65
+ #
66
+ # @param gemspec_file_path [String]
67
+ # @return [Array<String>]
68
+ def require_path_from_gemspec_file gemspec_file_path
69
+ base = File.dirname(gemspec_file_path)
70
+ # HACK: Evaluating gemspec files violates the goal of not running
71
+ # workspace code, but this is how Gem::Specification.load does it
72
+ # anyway.
73
+ cmd = ['ruby', '-e',
74
+ "require 'rubygems'; " \
75
+ "require 'json'; " \
76
+ "spec = eval(File.read('#{gemspec_file_path}'), TOPLEVEL_BINDING, '#{gemspec_file_path}'); " \
77
+ 'return unless Gem::Specification === spec; ' \
78
+ 'puts({name: spec.name, paths: spec.require_paths}.to_json)']
79
+ # @sg-ignore Unresolved call to capture3 on Module<Open3>
80
+ o, e, s = Open3.capture3(*cmd)
81
+ if s.success?
82
+ begin
83
+ hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {}
84
+ return [] if hash.empty?
85
+ hash['paths'].map { |path| File.join(base, path) }
86
+ rescue StandardError => e
87
+ Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}"
88
+ []
89
+ end
90
+ else
91
+ Solargraph.logger.warn "Error reading #{gemspec_file_path}"
92
+ Solargraph.logger.warn e
93
+ []
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -10,15 +10,11 @@ module Solargraph
10
10
  #
11
11
  class Workspace
12
12
  autoload :Config, 'solargraph/workspace/config'
13
+ autoload :RequirePaths, 'solargraph/workspace/require_paths'
13
14
 
14
15
  # @return [String]
15
16
  attr_reader :directory
16
17
 
17
- # The require paths associated with the workspace.
18
- #
19
- # @return [Array<String>]
20
- attr_reader :require_paths
21
-
22
18
  # @return [Array<String>]
23
19
  attr_reader :gemnames
24
20
  alias source_gems gemnames
@@ -32,10 +28,17 @@ module Solargraph
32
28
  @server = server
33
29
  load_sources
34
30
  @gemnames = []
35
- @require_paths = generate_require_paths
36
31
  require_plugins
37
32
  end
38
33
 
34
+ # The require paths associated with the workspace.
35
+ #
36
+ # @return [Array<String>]
37
+ def require_paths
38
+ # @todo are the semantics of '*' the same as '', meaning 'don't send back any require paths'?
39
+ @require_paths ||= RequirePaths.new(directory_or_nil, config).generate
40
+ end
41
+
39
42
  # @return [Solargraph::Workspace::Config]
40
43
  def config
41
44
  @config ||= Solargraph::Workspace::Config.new(directory)
@@ -133,6 +136,7 @@ module Solargraph
133
136
  @gem_rbs_collection ||= read_rbs_collection_path
134
137
  end
135
138
 
139
+ # @return [String, nil]
136
140
  def rbs_collection_config_path
137
141
  @rbs_collection_config_path ||= begin
138
142
  unless directory.empty? || directory == '*'
@@ -155,6 +159,12 @@ module Solargraph
155
159
  server['commandPath'] || 'solargraph'
156
160
  end
157
161
 
162
+ # @return [String, nil]
163
+ def directory_or_nil
164
+ return nil if directory.empty? || directory == '*'
165
+ directory
166
+ end
167
+
158
168
  # True if the workspace has a root Gemfile.
159
169
  #
160
170
  # @todo Handle projects with custom Bundler/Gemfile setups (see DocMap#gemspecs_required_from_bundler)
@@ -192,48 +202,6 @@ module Solargraph
192
202
  end
193
203
  end
194
204
 
195
- # Generate require paths from gemspecs if they exist or assume the default
196
- # lib directory.
197
- #
198
- # @return [Array<String>]
199
- def generate_require_paths
200
- return configured_require_paths unless gemspec?
201
- result = []
202
- gemspecs.each do |file|
203
- base = File.dirname(file)
204
- # HACK: Evaluating gemspec files violates the goal of not running
205
- # workspace code, but this is how Gem::Specification.load does it
206
- # anyway.
207
- cmd = ['ruby', '-e', "require 'rubygems'; require 'json'; spec = eval(File.read('#{file}'), TOPLEVEL_BINDING, '#{file}'); return unless Gem::Specification === spec; puts({name: spec.name, paths: spec.require_paths}.to_json)"]
208
- o, e, s = Open3.capture3(*cmd)
209
- if s.success?
210
- begin
211
- hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {}
212
- next if hash.empty?
213
- @gemnames.push hash['name']
214
- result.concat(hash['paths'].map { |path| File.join(base, path) })
215
- rescue StandardError => e
216
- Solargraph.logger.warn "Error reading #{file}: [#{e.class}] #{e.message}"
217
- end
218
- else
219
- Solargraph.logger.warn "Error reading #{file}"
220
- Solargraph.logger.warn e
221
- end
222
- end
223
- result.concat(config.require_paths.map { |p| File.join(directory, p) })
224
- result.push File.join(directory, 'lib') if result.empty?
225
- result
226
- end
227
-
228
- # Get additional require paths defined in the configuration.
229
- #
230
- # @return [Array<String>]
231
- def configured_require_paths
232
- return ['lib'] if directory.empty?
233
- return [File.join(directory, 'lib')] if config.require_paths.empty?
234
- config.require_paths.map { |p| File.join(directory, p) }
235
- end
236
-
237
205
  # @return [void]
238
206
  def require_plugins
239
207
  config.plugins.each do |plugin|
@@ -27,8 +27,8 @@ module Solargraph
27
27
  final_scope = scope || code_object.scope
28
28
  override_key = [closure.path, final_scope, name]
29
29
  final_visibility = VISIBILITY_OVERRIDE[override_key]
30
- final_visibility ||= VISIBILITY_OVERRIDE[override_key[0..-2]]
31
- final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name)
30
+ final_visibility ||= VISIBILITY_OVERRIDE[[closure.path, final_scope]]
31
+ final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name.to_sym)
32
32
  final_visibility ||= visibility
33
33
  final_visibility ||= :private if code_object.module_function? && final_scope == :instance
34
34
  final_visibility ||= :public if code_object.module_function? && final_scope == :class
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'open3'
4
+
3
5
  module Solargraph
4
6
  # Methods for caching and loading YARD documentation for gems.
5
7
  #
@@ -9,15 +11,25 @@ module Solargraph
9
11
  # Build and cache a gem's yardoc and return the path. If the cache already
10
12
  # exists, do nothing and return the path.
11
13
  #
14
+ # @param yard_plugins [Array<String>] The names of YARD plugins to use.
12
15
  # @param gemspec [Gem::Specification]
13
16
  # @return [String] The path to the cached yardoc.
14
- def cache(gemspec)
17
+ def cache(yard_plugins, gemspec)
15
18
  path = PinCache.yardoc_path gemspec
16
19
  return path if cached?(gemspec)
17
20
 
18
21
  Solargraph.logger.info "Caching yardoc for #{gemspec.name} #{gemspec.version}"
19
- Dir.chdir gemspec.gem_dir do
20
- `yardoc --db #{path} --no-output --plugin solargraph`
22
+ cmd = "yardoc --db #{path} --no-output --plugin solargraph"
23
+ yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" }
24
+ Solargraph.logger.debug { "Running: #{cmd}" }
25
+ # @todo set these up to run in parallel
26
+ #
27
+ # @sg-ignore RBS gem doesn't reflect that Open3.* also include
28
+ # kwopts from Process.spawn()
29
+ stdout_and_stderr_str, status = Open3.capture2e(cmd, chdir: gemspec.gem_dir)
30
+ unless status.success?
31
+ Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" }
32
+ Solargraph.logger.info stdout_and_stderr_str
21
33
  end
22
34
  path
23
35
  end
@@ -32,6 +44,7 @@ module Solargraph
32
44
 
33
45
  # True if another process is currently building the yardoc cache.
34
46
  #
47
+ # @param gemspec [Gem::Specification]
35
48
  def processing?(gemspec)
36
49
  yardoc = File.join(PinCache.yardoc_path(gemspec), 'processing')
37
50
  File.exist?(yardoc)
data/lib/solargraph.rb CHANGED
@@ -53,6 +53,8 @@ module Solargraph
53
53
  dir = File.dirname(__FILE__)
54
54
  VIEWS_PATH = File.join(dir, 'solargraph', 'views')
55
55
 
56
+ CHDIR_MUTEX = Mutex.new
57
+
56
58
  # @param type [Symbol] Type of assert.
57
59
  def self.asserts_on?(type)
58
60
  if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty?