diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b5984f3cb..c7ad72cb4 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: 3.4 bundler-cache: true - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -54,7 +54,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: 3.4 bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -83,7 +83,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: 3.4 bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -105,30 +105,64 @@ jobs: - name: Ensure specs still run run: bundle exec rake spec - # run_solargraph_rspec_specs: - # # check out solargraph-rspec as well as this project, and point the former to use the latter as a local gem - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v3 - # - name: clone https://github.com/lekemula/solargraph-rspec/ - # run: | - # cd .. - # git clone https://github.com/lekemula/solargraph-rspec.git - # cd solargraph-rspec - # - name: Set up Ruby - # uses: ruby/setup-ruby@v1 - # with: - # ruby-version: '3.0' - # bundler-cache: false - # - name: Install gems - # run: | - # cd ../solargraph-rspec - # echo "gem 'solargraph', path: '../solargraph'" >> Gemfile - # bundle install - # - name: Run specs - # run: | - # cd ../solargraph-rspec - # bundle exec rake spec + run_solargraph_rspec_specs: + # check out solargraph-rspec as well as this project, and point the former to use the latter as a local gem + runs-on: ubuntu-latest + env: + SOLARGRAPH_CACHE: ${{ github.workspace }}/../solargraph-rspec/vendor/solargraph/cache + BUNDLE_PATH: ${{ github.workspace }}/../solargraph-rspec/vendor/bundle + steps: + - uses: actions/checkout@v3 + - name: clone https://github.com/lekemula/solargraph-rspec/ + run: | + cd .. + # git clone https://github.com/lekemula/solargraph-rspec.git + + # pending https://github.com/lekemula/solargraph-rspec/pull/30 + git clone https://github.com/apiology/solargraph-rspec.git + cd solargraph-rspec + git checkout reset_closures + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1' + rubygems: latest + bundler-cache: false + - name: Install gems + run: | + set -x + + cd ../solargraph-rspec + echo "gem 'solargraph', path: '../solargraph'" >> Gemfile + bundle config path ${{ env.BUNDLE_PATH }} + bundle install --jobs 4 --retry 3 + bundle exec appraisal install + # @todo some kind of appraisal/bundle conflict? + # https://github.com/castwide/solargraph/actions/runs/19038710934/job/54369767122?pr=1116 + # /home/runner/work/solargraph/solargraph-rspec/vendor/bundle/ruby/3.1.0/gems/bundler-2.6.9/lib/bundler/runtime.rb:317:in + # `check_for_activated_spec!': You have already activated date + # 3.5.0, but your Gemfile requires date 3.4.1. Prepending + # `bundle exec` to your command may solve + # this. (Gem::LoadError) + bundle exec appraisal update date + # For some reason on ruby 3.1 it defaults to an old version: 1.3.2 + # https://github.com/lekemula/solargraph-rspec/actions/runs/17814581205/job/50645370316?pr=22 + # We update manually to the latest + bundle exec appraisal update rspec-rails + - name: Configure .solargraph.yml + run: | + cd ../solargraph-rspec + cp .solargraph.yml.example .solargraph.yml + - name: Solargraph generate RSpec gems YARD and RBS pins + run: | + cd ../solargraph-rspec + bundle exec appraisal rbs collection update + rspec_gems=$(bundle exec appraisal ruby -r './lib/solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1) + bundle exec appraisal solargraph gems $rspec_gems + - name: Run specs + run: | + cd ../solargraph-rspec + bundle exec appraisal rspec --format progress run_solargraph_rails_specs: # check out solargraph-rails as well as this project, and point the former to use the latter as a local gem diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ecc3d9771..2743ca84f 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -29,12 +29,24 @@ jobs: rbs-version: '3.9.4' - ruby-version: '3.0' rbs-version: '4.0.0.dev.4' + # Missing require in 'rbs collection update' - hopefully + # fixed in next RBS release + - ruby-version: 'head' + rbs-version: '4.0.0.dev.4' + - ruby-version: 'head' + rbs-version: '3.9.4' + - ruby-version: 'head' + rbs-version: '3.6.1' steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} + # see https://github.com/castwide/solargraph/actions/runs/19391419903/job/55485410493?pr=1119 + # + # match version in Gemfile.lock and use same version below + bundler: 2.5.23 bundler-cache: false - name: Set rbs version run: echo "gem 'rbs', '${{ matrix.rbs-version }}'" >> .Gemfile @@ -46,8 +58,13 @@ jobs: run: echo "gem 'tsort'" >> .Gemfile - name: Install gems run: | - bundle install + bundle _2.5.23_ install bundle update rbs # use latest available for this Ruby version + bundle list + bundle exec solargraph pin 'Bundler::Dsl#source' + - name: Update types + run: | + bundle exec rbs collection update - name: Run tests run: bundle exec rake spec undercover: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 0ae8a3d8a..f40977acf 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -18,7 +18,7 @@ permissions: jobs: solargraph_typed: - name: Solargraph / typed + name: Solargraph / strong runs-on: ubuntu-latest @@ -36,4 +36,4 @@ jobs: - name: Install gem types run: bundle exec rbs collection install - name: Typecheck self - run: SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level typed + run: SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strong diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 83339e756..c5a5ddf9d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -34,7 +34,6 @@ Gemspec/OrderedDependencies: # Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' @@ -664,14 +663,6 @@ RSpec/LetBeforeExamples: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: EnforcedStyle. -# SupportedStyles: have_received, receive -RSpec/MessageSpies: - Exclude: - - 'spec/doc_map_spec.rb' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - RSpec/MissingExampleGroupArgument: Exclude: - 'spec/diagnostics/rubocop_helpers_spec.rb' @@ -732,10 +723,6 @@ RSpec/ScatteredLet: Exclude: - 'spec/complex_type_spec.rb' -RSpec/StubbedMock: - Exclude: - - 'spec/language_server/host/message_worker_spec.rb' - # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Enabled: false diff --git a/Rakefile b/Rakefile index d731fc786..c83d9ab6b 100755 --- a/Rakefile +++ b/Rakefile @@ -9,7 +9,7 @@ task :console do end desc "Run the type checker" -task typecheck: [:typecheck_typed] +task typecheck: [:typecheck_strong] desc "Run the type checker at typed level - return code issues provable without annotations being correct" task :typecheck_typed do diff --git a/bin/solargraph b/bin/solargraph index d85561700..248dc42fd 100755 --- a/bin/solargraph +++ b/bin/solargraph @@ -1,5 +1,8 @@ #!/usr/bin/env ruby +# turn off warning diagnostics from Ruby +$VERBOSE=nil + require 'solargraph' Solargraph::Shell.start(ARGV) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 71855d04a..cc3031ea5 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -39,6 +39,7 @@ def initialize pins: [] # @param other [Object] def eql?(other) self.class == other.class && + # @sg-ignore Flow sensitive typing needs to handle self.class == other.class equality_fields == other.equality_fields end @@ -240,13 +241,6 @@ def keyword_pins store.pins_by_class(Pin::Keyword) end - # An array of namespace names defined in the ApiMap. - # - # @return [Set] - def namespaces - store.namespaces - end - # True if the namespace exists. # # @param name [String] The namespace to match @@ -537,7 +531,7 @@ def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, # @deprecated Use #get_path_pins instead. # # @param path [String] The path to find - # @return [Enumerable] + # @return [Array] def get_path_suggestions path return [] if path.nil? resolve_method_aliases store.get_path_pins(path) @@ -546,7 +540,7 @@ def get_path_suggestions path # Get an array of pins that match the specified path. # # @param path [String] - # @return [Enumerable] + # @return [Array] def get_path_pins path get_path_suggestions(path) end @@ -665,7 +659,7 @@ def super_and_sub?(sup, sub) # # @return [Boolean] def type_include?(host_ns, module_ns) - store.get_includes(host_ns).map { |inc_tag| inc_tag.parametrized_tag.name }.include?(module_ns) + store.get_includes(host_ns).map { |inc_tag| inc_tag.type.name }.include?(module_ns) end # @param pins [Enumerable] @@ -774,17 +768,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false if scope == :instance store.get_includes(fqns).reverse.each do |ref| - const = get_constants('', *ref.closure.gates).find { |pin| pin.path.end_with? ref.name } - if const.is_a?(Pin::Namespace) - result.concat inner_get_methods(const.path, scope, visibility, deep, skip, true) - elsif const.is_a?(Pin::Constant) - type = const.infer(self) - result.concat inner_get_methods(type.namespace, scope, visibility, deep, skip, true) if type.defined? - else - referenced_tag = ref.parametrized_tag - next unless referenced_tag.defined? - result.concat inner_get_methods_from_reference(referenced_tag.to_s, namespace_pin, rooted_type, scope, visibility, deep, skip, true) - end + in_tag = dereference(ref) + result.concat inner_get_methods_from_reference(in_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true) end rooted_sc_tag = qualify_superclass(rooted_tag) unless rooted_sc_tag.nil? @@ -793,7 +778,7 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false else logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}, #{skip}) - looking for get_extends() from #{fqns}" } store.get_extends(fqns).reverse.each do |em| - fqem = store.constants.dereference(em) + fqem = dereference(em) result.concat inner_get_methods(fqem, :instance, visibility, deep, skip, true) unless fqem.nil? end rooted_sc_tag = qualify_superclass(rooted_tag) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 0df8d83ce..8dcaf1945 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -12,14 +12,23 @@ def initialize store # Resolve a name to a fully qualified namespace or constant. # - # `Constants#resolve` is similar to `Constants#qualify`` in that its - # purpose is to find fully qualified (absolute) namespaces, except - # `#resolve`` is only concerned with real namespaces. It disregards - # parametrized types and special types like literals, self, and Boolean. + # `Constants#resolve` finds fully qualified (absolute) + # namespaces based on relative names and the open gates + # (namespaces) provided. Names must be runtime-visible (erased) + # non-literal types, non-duck, non-signature types - e.g., + # TrueClass, NilClass, Integer and Hash instead of true, nil, + # 96, or Hash{String => Symbol} # - # @param name [String] - # @param gates [Array, String>] - # @return [String, nil] + # Note: You may want to be using #qualify. Notably, #resolve: + # - does not handle anything with type parameters + # - will not gracefully handle nil, self and Boolean + # - will return a constant name instead of following its assignment + # + # @param name [String] Namespace which may relative and not be rooted. + # @param gates [Array, String>] Namespaces to search while resolving the name + # + # @return [String, nil] fully qualified namespace (i.e., is + # absolute, but will not start with ::) def resolve(name, *gates) return store.get_path_pins(name[2..]).first&.path if name.start_with?('::') @@ -39,39 +48,49 @@ def resolve(name, *gates) # @param pin [Pin::Reference] # @return [String, nil] def dereference pin - resolve(pin.name, pin.reference_gates) + qualify_type(pin.type, *pin.reference_gates)&.tag end # Collect a list of all constants defined in the specified gates. # # @param gates [Array, String>] - # @return [Array] + # @return [Array] def collect(*gates) flat = gates.flatten cached_collect[flat] || collect_and_cache(flat) end - # Determine a fully qualified namespace for a given name referenced - # from the specified open gates. This method will search in each gate - # until it finds a match for the name. + # Determine a fully qualified namespace for a given tag + # referenced from the specified open gates. This method will + # search in each gate until it finds a match for the name. # - # @param name [String, nil] The namespace to match + # @param tag [String, nil] The type to match # @param gates [Array] # @return [String, nil] fully qualified tag - def qualify name, *gates - return name if ['Boolean', 'self', nil].include?(name) + def qualify tag, *gates + type = ComplexType.try_parse(tag) + qualify_type(type, *gates)&.tag + end + + # @param type [ComplexType, nil] The type to match + # @param gates [Array] + # + # @return [ComplexType, nil] A new rooted ComplexType + def qualify_type type, *gates + return nil if type.nil? + return type if type.selfy? || type.literal? || type.tag == 'nil' || type.interface? || + type.tag == 'Boolean' gates.push '' unless gates.include?('') - fqns = resolve(name, gates) + fqns = resolve(type.rooted_namespace, *gates) return unless fqns pin = store.get_path_pins(fqns).first if pin.is_a?(Pin::Constant) const = Solargraph::Parser::NodeMethods.unpack_name(pin.assignment) return unless const - resolve(const, pin.gates) - else - fqns + fqns = resolve(const, *pin.gates) end + type.recreate(new_name: fqns, make_rooted: true) end # @return [void] @@ -126,7 +145,7 @@ def complex_resolve name, gates, internal resolved = simple_resolve(name, gate, internal) return [resolved, gates[(idx + 1)..]] if resolved store.get_ancestor_references(gate).each do |ref| - return ref.name.sub(/^::/, '') if ref.name.end_with?("::#{name}") + return ref.name.sub(/^::/, '') if ref.name.end_with?("::#{name}") && ref.name.start_with?('::') mixin = resolve(ref.name, ref.reference_gates) next unless mixin @@ -155,7 +174,7 @@ def simple_resolve name, gate, internal end # @param gates [Array] - # @return [Array] + # @return [Array] def collect_and_cache gates skip = Set.new cached_collect[gates] = gates.flat_map do |gate| @@ -168,7 +187,7 @@ def cached_resolve @cached_resolve ||= {} end - # @return [Hash{Array => Array}] + # @return [Hash{Array => Array}] def cached_collect @cached_collect ||= {} end @@ -213,7 +232,7 @@ def inner_qualify name, root, skip return fqns if store.namespace_exists?(fqns) incs = store.get_includes(roots.join('::')) incs.each do |inc| - foundinc = inner_qualify(name, inc.parametrized_tag.to_s, skip) + foundinc = inner_qualify(name, inc.type.to_s, skip) possibles.push foundinc unless foundinc.nil? end roots.pop @@ -221,7 +240,7 @@ def inner_qualify name, root, skip if possibles.empty? incs = store.get_includes('') incs.each do |inc| - foundinc = inner_qualify(name, inc.parametrized_tag.to_s, skip) + foundinc = inner_qualify(name, inc.type.to_s, skip) possibles.push foundinc unless foundinc.nil? end end @@ -233,7 +252,7 @@ def inner_qualify name, root, skip # @param fqns [String] # @param visibility [Array] # @param skip [Set] - # @return [Array] + # @return [Array] def inner_get_constants fqns, visibility, skip return [] if fqns.nil? || skip.include?(fqns) skip.add fqns diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index 35d86446a..944f02c79 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -17,16 +17,22 @@ def pins # @return [Hash{String => Array}] def namespace_hash + # @param h [String] + # @param k [Array] @namespace_hash ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def pin_class_hash + # @param h [String] + # @param k [Array] @pin_class_hash ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def path_pin_hash + # @param h [String] + # @param k [Array] @path_pin_hash ||= Hash.new { |h, k| h[k] = [] } end @@ -42,26 +48,36 @@ def pins_by_class klass # @return [Hash{String => Array}] def include_references + # @param h [String] + # @param k [Array] @include_references ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def include_reference_pins + # @param h [String] + # @param k [Array] @include_reference_pins ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def extend_references + # @param h [String] + # @param k [Array] @extend_references ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def prepend_references + # @param h [String] + # @param k [Array] @prepend_references ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def superclass_references + # @param h [String] + # @param k [Array] @superclass_references ||= Hash.new { |h, k| h[k] = [] } end @@ -99,12 +115,18 @@ def catalog new_pins @pin_select_cache = {} pins.concat new_pins set = new_pins.to_set + # @param k [String] + # @param v [Set] set.classify(&:class) - .map { |k, v| pin_class_hash[k].concat v.to_a } + .map { |k, v| pin_class_hash[k].concat v.to_a } + # @param k [String] + # @param v [Set] set.classify(&:namespace) - .map { |k, v| namespace_hash[k].concat v.to_a } + .map { |k, v| namespace_hash[k].concat v.to_a } + # @param k [String] + # @param v [Set] set.classify(&:path) - .map { |k, v| path_pin_hash[k].concat v.to_a } + .map { |k, v| path_pin_hash[k].concat v.to_a } @namespaces = path_pin_hash.keys.compact.to_set map_references Pin::Reference::Include, include_references map_references Pin::Reference::Prepend, prepend_references @@ -114,10 +136,13 @@ def catalog new_pins self end - # @param klass [Class] - # @param hash [Hash{String => Array}] + # @generic T + # @param klass [Class>] + # @param hash [Hash{String => generic}] + # # @return [void] def map_references klass, hash + # @param pin [generic] pins_by_class(klass).each do |pin| hash[pin.namespace].push pin end @@ -125,6 +150,7 @@ def map_references klass, hash # @return [void] def map_overrides + # @param ovr [Pin::Reference::Override] pins_by_class(Pin::Reference::Override).each do |ovr| logger.debug { "ApiMap::Index#map_overrides: Looking at override #{ovr} for #{ovr.name}" } pins = path_pin_hash[ovr.name] diff --git a/lib/solargraph/api_map/source_to_yard.rb b/lib/solargraph/api_map/source_to_yard.rb index ccbed3eb6..55452582b 100644 --- a/lib/solargraph/api_map/source_to_yard.rb +++ b/lib/solargraph/api_map/source_to_yard.rb @@ -32,11 +32,13 @@ def rake_yard store next end if pin.type == :class + # @param obj [YARD::CodeObjects::RootObject] code_object_map[pin.path] ||= YARD::CodeObjects::ClassObject.new(root_code_object, pin.path) { |obj| next if pin.location.nil? || pin.location.filename.nil? obj.add_file(pin.location.filename, pin.location.range.start.line, !pin.comments.empty?) } else + # @param obj [YARD::CodeObjects::RootObject] code_object_map[pin.path] ||= YARD::CodeObjects::ModuleObject.new(root_code_object, pin.path) { |obj| next if pin.location.nil? || pin.location.filename.nil? obj.add_file(pin.location.filename, pin.location.range.start.line, !pin.comments.empty?) @@ -46,13 +48,13 @@ def rake_yard store store.get_includes(pin.path).each do |ref| include_object = code_object_at(pin.path, YARD::CodeObjects::ClassObject) unless include_object.nil? || include_object.nil? - include_object.instance_mixins.push code_object_map[ref.parametrized_tag.to_s] + include_object.instance_mixins.push code_object_map[ref.type.to_s] end end store.get_extends(pin.path).each do |ref| extend_object = code_object_at(pin.path, YARD::CodeObjects::ClassObject) next unless extend_object - code_object = code_object_map[ref.parametrized_tag.to_s] + code_object = code_object_map[ref.type.to_s] next unless code_object extend_object.class_mixins.push code_object # @todo add spec showing why this next line is necessary @@ -65,6 +67,7 @@ def rake_yard store next end + # @param obj [YARD::CodeObjects::RootObject] code_object_map[pin.path] ||= YARD::CodeObjects::MethodObject.new(code_object_at(pin.namespace, YARD::CodeObjects::NamespaceObject), pin.name, pin.scope) { |obj| next if pin.location.nil? || pin.location.filename.nil? obj.add_file pin.location.filename, pin.location.range.start.line diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index c41e19c09..bc829ba5a 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -17,7 +17,7 @@ def pins index.pins end - # @param pinsets [Array>] + # @param pinsets [Array>] # - pinsets[0] = core Ruby pins # - pinsets[1] = documentation/gem pins # - pinsets[2] = convention pins @@ -60,6 +60,7 @@ def inspect # @return [Enumerable] def get_constants fqns, visibility = [:public] namespace_children(fqns).select { |pin| + # @sg-ignore flow-sensitive typing not smart enough to handle this case !pin.name.empty? && (pin.is_a?(Pin::Namespace) || pin.is_a?(Pin::Constant)) && visibility.include?(pin.visibility) } end @@ -70,6 +71,7 @@ def get_constants fqns, visibility = [:public] # @return [Enumerable] def get_methods fqns, scope: :instance, visibility: [:public] all_pins = namespace_children(fqns).select do |pin| + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 pin.is_a?(Pin::Method) && pin.scope == scope && visibility.include?(pin.visibility) end GemPins.combine_method_pins_by_path(all_pins) @@ -97,7 +99,7 @@ def qualify_superclass fq_sub_tag return unless ref res = constants.dereference(ref) return unless res - res + type.substring + res end # @param fqns [String] @@ -151,11 +153,6 @@ def namespace_exists?(fqns) fqns_pins(fqns).any? end - # @return [Set] - def namespaces - index.namespaces - end - # @return [Enumerable] def namespace_pins pins_by_class(Solargraph::Pin::Namespace) @@ -245,7 +242,8 @@ def get_ancestors(fqns) # Add includes, prepends, and extends [get_includes(current), get_prepends(current), get_extends(current)].each do |refs| next if refs.nil? - refs.map(&:parametrized_tag).map(&:to_s).each do |ref| + # @param ref [String] + refs.map(&:type).map(&:to_s).each do |ref| next if ref.nil? || ref.empty? || visited.include?(ref) ancestors << ref queue << ref @@ -258,7 +256,7 @@ def get_ancestors(fqns) # @param fqns [String] # - # @return [Array] + # @return [Array] def get_ancestor_references(fqns) (get_prepends(fqns) + get_includes(fqns) + [get_superclass(fqns)]).compact end @@ -275,7 +273,7 @@ def index @indexes.last end - # @param pinsets [Array>] + # @param pinsets [Array>] # # @return [void] def catalog pinsets @@ -296,6 +294,9 @@ def catalog pinsets # @return [Hash{::Array(String, String) => ::Array}] def fqns_pins_map + # @param h [Hash{::Array(String, String) => ::Array}] + # @param base [String] + # @param name [String] @fqns_pins_map ||= Hash.new do |h, (base, name)| value = namespace_children(base).select { |pin| pin.name == name && pin.is_a?(Pin::Namespace) } h[[base, name]] = value diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 669a66900..1b9f96dfa 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -105,6 +105,21 @@ def can_assign?(api_map, atype) any? { |ut| ut.can_assign?(api_map, atype) } end + # @param new_name [String, nil] + # @param make_rooted [Boolean, nil] + # @param new_key_types [Array, nil] + # @param rooted [Boolean, nil] + # @param new_subtypes [Array, nil] + # @return [self] + def recreate(new_name: nil, make_rooted: nil, new_key_types: nil, new_subtypes: nil) + ComplexType.new(map do |ut| + ut.recreate(new_name: new_name, + make_rooted: make_rooted, + new_key_types: new_key_types, + new_subtypes: new_subtypes) + end) + end + # @return [Integer] def length @items.length @@ -324,6 +339,7 @@ def parse *strings, partial: false paren_stack = 0 base = String.new subtype_string = String.new + # @param char [String] type_string&.each_char do |char| if char == '=' #raise ComplexTypeError, "Invalid = in type #{type_string}" unless curly_stack > 0 diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index d8d4fc7d7..a0e99cdee 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -43,6 +43,10 @@ def rooted_tag @rooted_tag ||= rooted_name + rooted_substring end + def interface? + name.start_with?('_') + end + # @return [Boolean] def duck_type? @duck_type ||= name.start_with?('#') @@ -190,6 +194,7 @@ def scope # @param other [Object] def == other return false unless self.class == other.class + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 tag == other.tag end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 05a585dcf..e490bff35 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -138,11 +138,17 @@ def determine_non_literal_name def eql?(other) self.class == other.class && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @name == other.name && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @key_types == other.key_types && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @subtypes == other.subtypes && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @rooted == other.rooted? && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @all_params == other.all_params && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @parameters_type == other.parameters_type end diff --git a/lib/solargraph/convention/active_support_concern.rb b/lib/solargraph/convention/active_support_concern.rb index 74c9ce765..ed1fba175 100644 --- a/lib/solargraph/convention/active_support_concern.rb +++ b/lib/solargraph/convention/active_support_concern.rb @@ -80,7 +80,7 @@ def process_include include_tag "ActiveSupportConcern#object(#{fqns}, #{scope}, #{visibility}, #{deep}) - " \ "Handling class include include_tag=#{include_tag}" end - module_extends = api_map.get_extends(rooted_include_tag).map(&:parametrized_tag).map(&:to_s) + module_extends = api_map.get_extends(rooted_include_tag).map(&:type).map(&:to_s) logger.debug do "ActiveSupportConcern#object(#{fqns}, #{scope}, #{visibility}, #{deep}) - " \ "found module extends of #{rooted_include_tag}: #{module_extends}" diff --git a/lib/solargraph/diagnostics/rubocop_helpers.rb b/lib/solargraph/diagnostics/rubocop_helpers.rb index f6f4c82c8..fc458956e 100644 --- a/lib/solargraph/diagnostics/rubocop_helpers.rb +++ b/lib/solargraph/diagnostics/rubocop_helpers.rb @@ -20,9 +20,11 @@ def require_rubocop(version = nil) gem_lib_path = File.join(gem_path, 'lib') $LOAD_PATH.unshift(gem_lib_path) unless $LOAD_PATH.include?(gem_lib_path) rescue Gem::MissingSpecVersionError => e + # @type [Array] + specs = e.specs raise InvalidRubocopVersionError, "could not find '#{e.name}' (#{e.requirement}) - "\ - "did find: [#{e.specs.map { |s| s.version.version }.join(', ')}]" + "did find: [#{specs.map { |s| s.version.version }.join(', ')}]" end require 'rubocop' end @@ -36,6 +38,7 @@ def generate_options filename, code args = ['-f', 'j', '--force-exclusion', filename] base_options = RuboCop::Options.new options, paths = base_options.parse(args) + # @sg-ignore options[:stdin] = code [options, paths] end diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 5966717f4..e45ff0b65 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -128,7 +128,7 @@ def unresolved_requires @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys end - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version + # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version def self.all_yard_gems_in_memory @yard_gems_in_memory ||= {} end @@ -359,13 +359,16 @@ def change_gemspec_version gemspec, version # @return [Array] def fetch_dependencies gemspec # @param spec [Gem::Dependency] + # @param deps [Set] only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps| Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}" dep = Gem.loaded_specs[spec.name] # @todo is next line necessary? + # @sg-ignore Unresolved call to requirement on Gem::Dependency dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement) deps.merge fetch_dependencies(dep) if deps.add?(dep) rescue Gem::MissingSpecError + # @sg-ignore Unresolved call to requirement on Gem::Dependency Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for #{gemspec.name} not found in RubyGems." end.to_a end diff --git a/lib/solargraph/equality.rb b/lib/solargraph/equality.rb index 0667efacd..f8c50ff31 100644 --- a/lib/solargraph/equality.rb +++ b/lib/solargraph/equality.rb @@ -12,6 +12,7 @@ module Equality # @return [Boolean] def eql?(other) self.class.eql?(other.class) && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 equality_fields.eql?(other.equality_fields) end diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index a193a8a39..1c4330389 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -27,6 +27,8 @@ def self.combine_method_pins_by_path(pins) def self.combine_method_pins(*pins) # @type [Pin::Method, nil] combined_pin = nil + # @param memo [Pin::Method, nil] + # @param pin [Pin::Method] out = pins.reduce(combined_pin) do |memo, pin| next pin if memo.nil? if memo == pin && memo.source != :combined @@ -63,6 +65,7 @@ def self.combine(yard_pins, rbs_pins) next yard_pin unless rbs_pin && yard_pin.class == Pin::Method unless rbs_pin + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 logger.debug { "GemPins.combine: No rbs pin for #{yard_pin.path} - using YARD's '#{yard_pin.inspect} (return_type=#{yard_pin.return_type}; signatures=#{yard_pin.signatures})" } next yard_pin end diff --git a/lib/solargraph/language_server/host.rb b/lib/solargraph/language_server/host.rb index 53da20175..b228bdba6 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -504,6 +504,7 @@ def locate_pins params name: 'new', scope: :class, location: pin.location, + # @sg-ignore Unresolved call to parameters on Solargraph::Pin::Base parameters: pin.parameters, return_type: ComplexType.try_parse(params['data']['path']), comments: pin.comments, diff --git a/lib/solargraph/language_server/message/text_document/formatting.rb b/lib/solargraph/language_server/message/text_document/formatting.rb index 821de7ffc..d67a0b414 100644 --- a/lib/solargraph/language_server/message/text_document/formatting.rb +++ b/lib/solargraph/language_server/message/text_document/formatting.rb @@ -18,6 +18,7 @@ def process require_rubocop(config['version']) options, paths = ::RuboCop::Options.new.parse(args) + # @sg-ignore Unresolved call to []= options[:stdin] = original # Ensure only one instance of RuboCop::Runner is running at @@ -28,6 +29,7 @@ def process ::RuboCop::Runner.new(options, ::RuboCop::ConfigStore.new).run(paths) end end + # @sg-ignore Unresolved call to []= result = options[:stdin] log_corrections(corrections) diff --git a/lib/solargraph/language_server/progress.rb b/lib/solargraph/language_server/progress.rb index 10900a37e..98b155714 100644 --- a/lib/solargraph/language_server/progress.rb +++ b/lib/solargraph/language_server/progress.rb @@ -134,7 +134,7 @@ def keep_alive host end end - # @return [Mutex] + # @return [Thread::Mutex] def mutex @mutex ||= Mutex.new end diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index bdd579976..5c7851201 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -522,7 +522,7 @@ def find_external_requires source_map @external_requires = nil end - # @return [Mutex] + # @return [Thread::Mutex] def mutex @mutex ||= Mutex.new end diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index 713b4fef1..df92668bf 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -71,6 +71,7 @@ def self.from_node(node) # @param other [BasicObject] def == other return false unless other.is_a?(Location) + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 filename == other.filename and range == other.range end diff --git a/lib/solargraph/parser/node_methods.rb b/lib/solargraph/parser/node_methods.rb deleted file mode 100644 index f33a924c1..000000000 --- a/lib/solargraph/parser/node_methods.rb +++ /dev/null @@ -1,97 +0,0 @@ -module Solargraph - module Parser - module NodeMethods - module_function - - # @abstract - # @param node [Parser::AST::Node] - # @return [String] - def unpack_name node - raise NotImplementedError - end - - # @abstract - # @todo Temporarily here for testing. Move to Solargraph::Parser. - # @param node [Parser::AST::Node] - # @return [Array] - def call_nodes_from node - raise NotImplementedError - end - - # Find all the nodes within the provided node that potentially return a - # value. - # - # The node parameter typically represents a method's logic, e.g., the - # second child (after the :args node) of a :def node. A simple one-line - # method would typically return itself, while a node with conditions - # would return the resulting node from each conditional branch. Nodes - # that follow a :return node are assumed to be unreachable. Nil values - # are converted to nil node types. - # - # @abstract - # @param node [Parser::AST::Node] - # @return [Array] - def returns_from_method_body node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # - # @return [Array] - def const_nodes_from node - raise NotImplementedError - end - - # @abstract - # @param cursor [Solargraph::Source::Cursor] - # @return [Parser::AST::Node, nil] - def find_recipient_node cursor - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Array] low-level value nodes in - # value position. Does not include explicit return - # statements - def value_position_nodes_only(node) - raise NotImplementedError - end - - # @abstract - # @param nodes [Enumerable] - def any_splatted_call?(nodes) - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [void] - def process node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Hash{Symbol => Source::Chain}] - def convert_hash node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Position] - def get_node_start_position(node) - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Position] - def get_node_end_position(node) - raise NotImplementedError - end - end - end -end diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index 02f790c00..b77c4cd47 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -119,6 +119,7 @@ def convert_hash node result end + # @sg-ignore Wrong argument type for AST::Node.new: type expected AST::_ToSym, received :nil NIL_NODE = ::Parser::AST::Node.new(:nil) # @param node [Parser::AST::Node] diff --git a/lib/solargraph/parser/parser_gem/node_processors/and_node.rb b/lib/solargraph/parser/parser_gem/node_processors/and_node.rb index d3485af7c..a761ae38c 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/and_node.rb @@ -11,6 +11,7 @@ def process process_children position = get_node_start_position(node) + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 enclosing_breakable_pin = pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location.range.contain?(position)}.last FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_and(node) end diff --git a/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb index 0e4d7b26a..a4359af9d 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb @@ -13,8 +13,10 @@ def process operator = node.children[1] argument = node.children[2] if target.type == :send + # @sg-ignore Need a downcast here process_send_target(target, operator, argument) elsif target.type.to_s.end_with?('vasgn') + # @sg-ignore Need a downcast here process_vasgn_target(target, operator, argument) else Solargraph.assert_or_log(:opasgn_unknown_target, diff --git a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb index 645baf00f..861d6b157 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb @@ -36,15 +36,12 @@ def process process_autoload elsif method_name == :private_constant process_private_constant - # @sg-ignore elsif method_name == :alias_method && node.children[2] && node.children[2] && node.children[2].type == :sym && node.children[3] && node.children[3].type == :sym process_alias_method - # @sg-ignore elsif method_name == :private_class_method && node.children[2].is_a?(AST::Node) # Processing a private class can potentially handle children on its own return if process_private_class_method end - # @sg-ignore elsif method_name == :require && node.children[0].to_s == '(const nil :Bundler)' pins.push Pin::Reference::Require.new(Solargraph::Location.new(region.filename, Solargraph::Range.from_to(0, 0, 0, 0)), 'bundler/require', source: :parser) end @@ -231,6 +228,7 @@ def process_module_function closure: cm, name: ivar.name, comments: ivar.comments, + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 assignment: ivar.assignment, source: :parser ) @@ -239,6 +237,7 @@ def process_module_function closure: mm, name: ivar.name, comments: ivar.comments, + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 assignment: ivar.assignment, source: :parser ) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 511c7deb7..57d083453 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -259,6 +259,7 @@ def rbs_location? def assert_same_macros(other) return unless self.source == :yardoc && other.source == :yardoc assert_same_count(other, :macros) + # @param [YARD::Tags::MacroDirective] assert_same_array_content(other, :macros) { |macro| macro.tag.name } end @@ -466,6 +467,7 @@ def nearly? other # @param other [Object] def == other return false unless nearly? other + # @sg-ignore Should add more explicit type check on other comments == other.comments && location == other.location end diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 764c1fb39..ab7bd3961 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -12,8 +12,9 @@ class BaseVariable < Base attr_accessor :mass_assignment # @param return_type [ComplexType, nil] + # @param mass_assignment [::Array(Parser::AST::Node, Integer), nil] # @param assignment [Parser::AST::Node, nil] - def initialize assignment: nil, return_type: nil, **splat + def initialize assignment: nil, return_type: nil, mass_assignment: nil, **splat super(**splat) @assignment = assignment # @type [nil, ::Array(Parser::AST::Node, Integer)] @@ -22,12 +23,12 @@ def initialize assignment: nil, return_type: nil, **splat end def combine_with(other, attrs={}) - attrs.merge({ + new_attrs = attrs.merge({ assignment: assert_same(other, :assignment), mass_assignment: assert_same(other, :mass_assignment), return_type: combine_return_type(other), }) - super(other, attrs) + super(other, new_attrs) end def completion_item_kind @@ -104,6 +105,7 @@ def probe api_map # @param other [Object] def == other return false unless super + # @sg-ignore Should add type check on other assignment == other.assignment end diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 227bc0873..0c6ecd258 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -54,6 +54,7 @@ def typify_parameters(api_map) locals = clip.locals - [self] meths = chain.define(api_map, closure, locals) # @todo Convert logic to use signatures + # @param meth [Pin::Method] meths.each do |meth| next if meth.block.nil? diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index 207c2619b..edbc3f941 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -67,6 +67,8 @@ def generics # @return [Array] def choose_parameters(other) raise "Trying to combine two pins with different arities - \nself =#{inspect}, \nother=#{other.inspect}, \n\n self.arity=#{self.arity}, \nother.arity=#{other.arity}" if other.arity != arity + # @param param [Pin::Parameter] + # @param other_param [Pin::Parameter] parameters.zip(other.parameters).map do |param, other_param| if param.nil? && other_param.block? other_param diff --git a/lib/solargraph/pin/delegated_method.rb b/lib/solargraph/pin/delegated_method.rb index 9483fb058..bcf5b5912 100644 --- a/lib/solargraph/pin/delegated_method.rb +++ b/lib/solargraph/pin/delegated_method.rb @@ -51,7 +51,6 @@ def type_location %i[typify realize infer probe].each do |method| # @param api_map [ApiMap] define_method(method) do |api_map| - # @sg-ignore Unresolved call to resolve_method resolve_method(api_map) # @sg-ignore Need to set context correctly in define_method blocks @resolved_method ? @resolved_method.send(method, api_map) : super(api_map) diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 9eae6cc6f..cb2dda140 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -26,9 +26,6 @@ def combine_with(other, attrs={}) assignment: assert_same(other, :assignment), presence_certain: assert_same(other, :presence_certain?), }.merge(attrs) - # @sg-ignore Wrong argument type for - # Solargraph::Pin::Base#assert_same: other expected - # Solargraph::Pin::Base, received self new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence) super(other, new_attrs) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 011f096f6..86bf1cd09 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -34,14 +34,18 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil @anon_splat = anon_splat end + # @param signature_pins [Array] # @return [Array] def combine_all_signature_pins(*signature_pins) + # @type [Hash{Array => Array}] by_arity = {} signature_pins.each do |signature_pin| by_arity[signature_pin.arity] ||= [] by_arity[signature_pin.arity] << signature_pin end by_arity.transform_values! do |same_arity_pins| + # @param memo [Pin::Signature, nil] + # @param signature [Pin::Signature] same_arity_pins.reduce(nil) do |memo, signature| next signature if memo.nil? memo.combine_with(signature) @@ -88,6 +92,7 @@ def combine_with(other, attrs = {}) end new_attrs = { visibility: combine_visibility(other), + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 explicit: explicit? || other.explicit?, block: combine_blocks(other), node: choose_node(other, :node), @@ -376,11 +381,14 @@ def attribute? @attribute end - # @parm other [Method] + # @parm other [self] def nearly? other super && + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 parameters == other.parameters && + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 scope == other.scope && + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 visibility == other.visibility end @@ -392,9 +400,12 @@ def probe api_map def overloads # Ignore overload tags with nil parameters. If it's not an array, the # tag's source is likely malformed. + + # @param tag [YARD::Tags::OverloadTag] @overloads ||= docstring.tags(:overload).select(&:parameters).map do |tag| Pin::Signature.new( generics: generics, + # @param src [Array(String, String)] parameters: tag.parameters.map do |src| name, decl = parse_overload_param(src.first) Pin::Parameter.new( @@ -507,6 +518,7 @@ def clean_param name # # @return [ComplexType] def param_type_from_name(tag, name) + # @param t [YARD::Tags::Tag] param = tag.tags(:param).select { |t| t.name == name }.first return ComplexType::UNDEFINED unless param ComplexType.try_parse(*param.types) @@ -522,8 +534,12 @@ def generate_complex_type # @param api_map [ApiMap] # @return [ComplexType, nil] def see_reference api_map + # This should actually be an intersection type + # @param ref [YARD::Tags::Tag, Solargraph::Yard::Tags::RefTag] docstring.ref_tags.each do |ref| + # @sg-ignore ref should actually be an intersection type next unless ref.tag_name == 'return' && ref.owner + # @sg-ignore ref should actually be an intersection type result = resolve_reference(ref.owner.to_s, api_map) return result unless result.nil? end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 947513689..91c205921 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -232,8 +232,12 @@ def typify_method_param api_map # @param skip [::Array] # @return [::Array] def see_reference heredoc, api_map, skip = [] + # This should actually be an intersection type + # @param ref [YARD::Tags::Tag, Solargraph::Yard::Tags::RefTag] heredoc.ref_tags.each do |ref| + # @sg-ignore ref should actually be an intersection type next unless ref.tag_name == 'param' && ref.owner + # @sg-ignore ref should actually be an intersection type result = resolve_reference(ref.owner.to_s, api_map, skip) return result unless result.nil? end diff --git a/lib/solargraph/pin/reference.rb b/lib/solargraph/pin/reference.rb index d678ab7b7..d456fbbf8 100644 --- a/lib/solargraph/pin/reference.rb +++ b/lib/solargraph/pin/reference.rb @@ -18,18 +18,9 @@ def initialize generic_values: [], **splat @generic_values = generic_values end - # @return [String] - def parameter_tag - @parameter_tag ||= if generic_values&.any? - "<#{generic_values.join(', ')}>" - else - '' - end - end - # @return [ComplexType] - def parametrized_tag - @parametrized_tag ||= ComplexType.try_parse( + def type + @type ||= ComplexType.try_parse( name + if generic_values&.length&.> 0 "<#{generic_values.join(', ')}>" diff --git a/lib/solargraph/pin/search.rb b/lib/solargraph/pin/search.rb index f92978a35..0f9883b65 100644 --- a/lib/solargraph/pin/search.rb +++ b/lib/solargraph/pin/search.rb @@ -42,6 +42,9 @@ def do_query Result.new(match, pin) if match > 0.7 end .compact + # @param a [self] + # @param b [self] + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 .sort { |a, b| b.match <=> a.match } .map(&:pin) end diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index 2faa0a99b..74606f142 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -112,6 +112,7 @@ def self.normalize object def == other return false unless other.is_a?(Position) + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 line == other.line and character == other.character end end diff --git a/lib/solargraph/range.rb b/lib/solargraph/range.rb index 7a9bc0e30..86452d646 100644 --- a/lib/solargraph/range.rb +++ b/lib/solargraph/range.rb @@ -27,9 +27,12 @@ def initialize start, ending # @param other [BasicObject] def <=>(other) return nil unless other.is_a?(Range) + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 if start == other.start + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 ending <=> other.ending else + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 start <=> other.start end end @@ -98,6 +101,7 @@ def self.from_expr expr def == other return false unless other.is_a?(Range) + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 start == other.start && ending == other.ending end diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 3e777f726..54bca0f73 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -65,6 +65,7 @@ def convert_decl_to_pin decl, closure # STDERR.puts "Skipping interface #{decl.name.relative!}" interface_decl_to_pin decl, closure when RBS::AST::Declarations::TypeAlias + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 type_aliases[decl.name.to_s] = decl when RBS::AST::Declarations::Module module_decl_to_pin decl @@ -426,6 +427,7 @@ def method_def_to_pin decl, closure, context # @param pin [Pin::Method] # @return [void] def method_def_to_sigs decl, pin + # @param overload [RBS::AST::Members::MethodDefinition::Overload] decl.overloads.map do |overload| type_location = location_decl_to_pin_location(overload.method_type.location) generics = overload.method_type.type_params.map(&:name).map(&:to_s) @@ -466,12 +468,16 @@ def parts_of_function type, pin parameters = [] arg_num = -1 type.type.required_positionals.each do |param| + # @sg-ignore RBS generic type understanding issue name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" + # @sg-ignore RBS generic type understanding issue parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, source: :rbs, type_location: type_location) end type.type.optional_positionals.each do |param| + # @sg-ignore RBS generic type understanding issue name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :optarg, name: name, closure: pin, + # @sg-ignore RBS generic type understanding issue return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, type_location: type_location, source: :rbs) @@ -489,18 +495,23 @@ def parts_of_function type, pin return_type: rest_positional_type,) end type.type.trailing_positionals.each do |param| + # @sg-ignore RBS generic type understanding issue name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, source: :rbs, type_location: type_location) end type.type.required_keywords.each do |orig, param| + # @sg-ignore RBS generic type understanding issue name = orig ? orig.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :kwarg, name: name, closure: pin, + # @sg-ignore RBS generic type understanding issue return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, source: :rbs, type_location: type_location) end type.type.optional_keywords.each do |orig, param| + # @sg-ignore RBS generic type understanding issue name = orig ? orig.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :kwoptarg, name: name, closure: pin, + # @sg-ignore RBS generic type understanding issue return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, type_location: type_location, source: :rbs) @@ -792,7 +803,9 @@ def other_type_to_tag type # @param namespace [Pin::Namespace] # @return [void] def add_mixins decl, namespace + # @param mixin [RBS::AST::Members::Include, RBS::AST::Members::Members::Extend, RBS::AST::Members::Members::Prepend] decl.each_mixin do |mixin| + # @todo are we handling prepend correctly? klass = mixin.is_a?(RBS::AST::Members::Include) ? Pin::Reference::Include : Pin::Reference::Extend type = build_type(mixin.name, mixin.args) generic_values = type.all_params.map(&:to_s) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index a005f600b..4db5fe388 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -79,6 +79,7 @@ def config(directory = '.') conf['extensions'].push m end end + # @param file [File] File.open(File.join(directory, '.solargraph.yml'), 'w') do |file| file.puts conf.to_yaml end @@ -241,6 +242,63 @@ def list puts "#{workspace.filenames.length} files total." end + desc 'pin [PATH]', 'Describe a pin', hide: true + option :rbs, type: :boolean, desc: 'Output the pin as RBS', default: false + option :typify, type: :boolean, desc: 'Output the calculated return type of the pin from annotations', default: false + option :references, type: :boolean, desc: 'Show references', default: false + option :probe, type: :boolean, desc: 'Output the calculated return type of the pin from annotations and inference', default: false + option :stack, type: :boolean, desc: 'Show entire stack of a method pin by including definitions in superclasses', default: false + # @param path [String] The path to the method pin, e.g. 'Class#method' or 'Class.method' + # @return [void] + def pin path + api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) + is_method = path.include?('#') || path.include?('.') + if is_method && options[:stack] + scope, ns, meth = if path.include? '#' + [:instance, *path.split('#', 2)] + else + [:class, *path.split('.', 2)] + end + + # @sg-ignore Wrong argument type for + # Solargraph::ApiMap#get_method_stack: rooted_tag + # expected String, received Array + pins = api_map.get_method_stack(ns, meth, scope: scope) + else + pins = api_map.get_path_pins path + end + # @type [Hash{Symbol => Pin::Base}] + references = {} + pin = pins.first + case pin + when nil + $stderr.puts "Pin not found for path '#{path}'" + exit 1 + when Pin::Namespace + if options[:references] + superclass_tag = api_map.qualify_superclass(pin.return_type.tag) + superclass_pin = api_map.get_path_pins(superclass_tag).first if superclass_tag + references[:superclass] = superclass_pin if superclass_pin + end + end + + pins.each do |pin| + if options[:typify] || options[:probe] + type = ComplexType::UNDEFINED + type = pin.typify(api_map) if options[:typify] + type = pin.probe(api_map) if options[:probe] && type.undefined? + print_type(type) + next + end + + print_pin(pin) + end + references.each do |key, refpin| + puts "\n# #{key.to_s.capitalize}:\n\n" + print_pin(refpin) + end + end + private # @param pin [Solargraph::Pin::Base] @@ -267,5 +325,25 @@ def do_cache gemspec, api_map # typecheck doesn't complain on the below line api_map.cache_gem(gemspec, rebuild: options.rebuild, out: $stdout) end + + # @param type [ComplexType] + # @return [void] + def print_type(type) + if options[:rbs] + puts type.to_rbs + else + puts type.rooted_tag + end + end + + # @param pin [Solargraph::Pin::Base] + # @return [void] + def print_pin(pin) + if options[:rbs] + puts pin.to_rbs + else + puts pin.inspect + end + end end end diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index c08d04878..f7a03b552 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -264,6 +264,8 @@ def infer_from_definitions pins, context, api_map, locals ComplexType::UNDEFINED elsif types.length > 1 # Move nil to the end by convention + + # @param a [ComplexType::UniqueType] sorted = types.flat_map(&:items).sort { |a, _| a.tag == 'nil' ? 1 : 0 } ComplexType.new(sorted.uniq) else diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 24d10656d..194771c1f 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -266,6 +266,7 @@ def yield_pins api_map, name_pin method_pin = find_method_pin(name_pin) return [] unless method_pin + # @param signature_pin [Pin::Signature] method_pin.signatures.map(&:block).compact.map do |signature_pin| return_type = signature_pin.return_type.qualify(api_map, *name_pin.gates) signature_pin.proxy(return_type) diff --git a/lib/solargraph/source/chain/if.rb b/lib/solargraph/source/chain/if.rb index c14d00ddf..3a7fa0ca9 100644 --- a/lib/solargraph/source/chain/if.rb +++ b/lib/solargraph/source/chain/if.rb @@ -8,7 +8,7 @@ def word '' end - # @param links [::Array] + # @param links [::Array] def initialize links @links = links end diff --git a/lib/solargraph/source/chain/link.rb b/lib/solargraph/source/chain/link.rb index bcd9eb196..344f7affd 100644 --- a/lib/solargraph/source/chain/link.rb +++ b/lib/solargraph/source/chain/link.rb @@ -38,7 +38,7 @@ def constant? # @param api_map [ApiMap] # @param name_pin [Pin::Base] - # @param locals [::Enumerable] + # @param locals [::Array] # @return [::Array] def resolve api_map, name_pin, locals [] diff --git a/lib/solargraph/source/chain/or.rb b/lib/solargraph/source/chain/or.rb index 1e3a70f40..9264d4107 100644 --- a/lib/solargraph/source/chain/or.rb +++ b/lib/solargraph/source/chain/or.rb @@ -8,7 +8,7 @@ def word '' end - # @param links [::Array] + # @param links [::Array] def initialize links @links = links end diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index d7b6fb4fc..15b747760 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -41,11 +41,13 @@ def initialize source # solargraph-rails is known to use this method to get the document symbols. It should probably be removed. @document_symbols = nil self.convention_pins = conventions_environ.pins + # @type [Hash{Class => Array}] @pin_select_cache = {} end # @generic T # @param klass [Class>] + # # @return [Array>] def pins_by_class klass @pin_select_cache[klass] ||= pin_class_hash.select { |key, _| key <= klass }.values.flatten @@ -171,10 +173,10 @@ def map source private - # @return [Hash{Class => Array}] # @return [Array] attr_writer :convention_pins + # @return [Hash{Class => Array}] def pin_class_hash @pin_class_hash ||= pins.to_set.classify(&:class).transform_values(&:to_a) end diff --git a/lib/solargraph/source_map/mapper.rb b/lib/solargraph/source_map/mapper.rb index 18fdf1f88..871f3ed5f 100644 --- a/lib/solargraph/source_map/mapper.rb +++ b/lib/solargraph/source_map/mapper.rb @@ -70,7 +70,9 @@ def closure_at(position) # @param comment [String] # @return [void] def process_comment source_position, comment_position, comment - # @sg-ignore Wrong argument type for String#=~: object expected String::_MatchAgainst, received Regexp + # @sg-ignore Wrong argument type for String#=~: object + # expected String::_MatchAgainst, received + # Regexp return unless comment.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP cmnt = remove_inline_comment_hashes(comment) parse = Solargraph::Source.parse_docstring(cmnt) @@ -245,7 +247,9 @@ def remove_inline_comment_hashes comment # @return [void] def process_comment_directives - # @sg-ignore Wrong argument type for String#=~: object expected String::_MatchAgainst, received Regexp + # @sg-ignore Wrong argument type for String#=~: object + # expected String::_MatchAgainst, received + # Regexp return unless @code.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP code_lines = @code.lines @source.associated_comments.each do |line, comments| diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 4600767b5..34a1e129f 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -195,7 +195,7 @@ def variable_type_tag_problems if pin.return_type.defined? declared = pin.typify(api_map) next if declared.duck_type? - if declared.defined? + if declared.defined? && pin.assignment if rules.validate_tags? inferred = pin.probe(api_map) if inferred.undefined? @@ -216,7 +216,7 @@ def variable_type_tag_problems elsif !pin.is_a?(Pin::Parameter) && !resolved_constant?(pin) result.push Problem.new(pin.location, "Unresolved type #{pin.return_type} for variable #{pin.name}", pin: pin) end - else + elsif pin.assignment inferred = pin.probe(api_map) if inferred.undefined? && declared_externally?(pin) ignored_pins.push pin @@ -420,6 +420,7 @@ def signature_argument_problems_for location, locals, closure_pin, params, argum # @todo Some level (strong, I guess) should require the param here else argtype = argchain.infer(api_map, closure_pin, locals) + argtype = argtype.self_to_type(closure_pin.context) if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype) errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") return errors @@ -459,8 +460,11 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi # @todo Some level (strong, I guess) should require the param here else ptype = data[:qualified] + ptype = ptype.self_to_type(pin.context) unless ptype.undefined? - argtype = argchain.infer(api_map, closure_pin, locals) + # @sg-ignore https://github.com/castwide/solargraph/pull/1127 + argtype = argchain.infer(api_map, closure_pin, locals).self_to_type(closure_pin.context) + # @sg-ignore Unresolved call to defined? if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") end @@ -486,7 +490,9 @@ def kwrestarg_problems_for(api_map, closure_pin, locals, location, pin, params, kwargs.each_pair do |pname, argchain| next unless params.key?(pname.to_s) ptype = params[pname.to_s][:qualified] + ptype = ptype.self_to_type(pin.context) argtype = argchain.infer(api_map, closure_pin, locals) + argtype = argtype.self_to_type(closure_pin.context) if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}") end @@ -604,7 +610,8 @@ def external? pin # @param pin [Pin::BaseVariable] def declared_externally? pin - return true if pin.assignment.nil? + raise "No assignment found" if pin.assignment.nil? + chain = Solargraph::Parser.chain(pin.assignment, filename) rng = Solargraph::Range.from_node(pin.assignment) closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) diff --git a/lib/solargraph/workspace/config.rb b/lib/solargraph/workspace/config.rb index d1e6c27b5..3e30e5d74 100644 --- a/lib/solargraph/workspace/config.rb +++ b/lib/solargraph/workspace/config.rb @@ -63,6 +63,7 @@ def calculated # namespace. It's typically used to identify available DSLs. # # @return [Array] + # @sg-ignore Need to validate config def domains raw_data['domains'] end @@ -70,6 +71,7 @@ def domains # An array of required paths to add to the workspace. # # @return [Array] + # @sg-ignore Need to validate config def required raw_data['require'] end @@ -83,6 +85,7 @@ def require_paths # An array of reporters to use for diagnostics. # + # @sg-ignore Need to validate config # @return [Array] def reporters raw_data['reporters'] @@ -90,6 +93,7 @@ def reporters # A hash of options supported by the formatter # + # @sg-ignore Need to validate config # @return [Hash] def formatter raw_data['formatter'] @@ -97,6 +101,7 @@ def formatter # An array of plugins to require. # + # @sg-ignore Need to validate config # @return [Array] def plugins raw_data['plugins'] @@ -104,6 +109,7 @@ def plugins # The maximum number of files to parse from the workspace. # + # @sg-ignore Need to validate config # @return [Integer] def max_files raw_data['max_files'] diff --git a/lib/solargraph/workspace/require_paths.rb b/lib/solargraph/workspace/require_paths.rb index 67adae9e6..c8eea161b 100644 --- a/lib/solargraph/workspace/require_paths.rb +++ b/lib/solargraph/workspace/require_paths.rb @@ -76,7 +76,6 @@ def require_path_from_gemspec_file gemspec_file_path "spec = eval(File.read('#{gemspec_file_path}'), TOPLEVEL_BINDING, '#{gemspec_file_path}'); " \ 'return unless Gem::Specification === spec; ' \ 'puts({name: spec.name, paths: spec.require_paths}.to_json)'] - # @sg-ignore Unresolved call to capture3 on Module o, e, s = Open3.capture3(*cmd) if s.success? begin diff --git a/lib/solargraph/yard_map/mapper/to_method.rb b/lib/solargraph/yard_map/mapper/to_method.rb index d8e3b8b43..0838b9f4f 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -11,7 +11,7 @@ module ToMethod ["Rails::Engine", :class, "find_root_with_flag"] => :public } - # @param code_object [YARD::CodeObjects::Base] + # @param code_object [YARD::CodeObjects::MethodObject] # @param name [String, nil] # @param scope [Symbol, nil] # @param visibility [Symbol, nil] @@ -85,6 +85,7 @@ def get_parameters code_object, location, comments, pin # HACK: Skip `nil` and `self` parameters that are sometimes emitted # for methods defined in C # See https://github.com/castwide/solargraph/issues/345 + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 code_object.parameters.select { |a| a[0] && a[0] != 'self' }.map do |a| Solargraph::Pin::Parameter.new( location: location, diff --git a/lib/solargraph/yard_map/to_method.rb b/lib/solargraph/yard_map/to_method.rb index 3ecb7ac26..010db89a5 100644 --- a/lib/solargraph/yard_map/to_method.rb +++ b/lib/solargraph/yard_map/to_method.rb @@ -15,6 +15,7 @@ def get_parameters code_object, location, comments # HACK: Skip `nil` and `self` parameters that are sometimes emitted # for methods defined in C # See https://github.com/castwide/solargraph/issues/345 + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 code_object.parameters.select { |a| a[0] && a[0] != 'self' }.map do |a| Solargraph::Pin::Parameter.new( location: location, @@ -57,7 +58,7 @@ def arg_type a include Helpers - # @param code_object [YARD::CodeObjects::Base] + # @param code_object [YARD::CodeObjects::MethodObject] # @param name [String, nil] # @param scope [Symbol, nil] # @param visibility [Symbol, nil] diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 2d709f778..09bcd4586 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -23,7 +23,7 @@ def cache(yard_plugins, gemspec) yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" } Solargraph.logger.debug { "Running: #{cmd}" } # @todo set these up to run in parallel - stdout_and_stderr_str, status = Open3.capture2e(cmd, chdir: gemspec.gem_dir) + stdout_and_stderr_str, status = Open3.capture2e(current_bundle_env_tweaks, cmd, chdir: gemspec.gem_dir) unless status.success? Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } Solargraph.logger.info stdout_and_stderr_str @@ -57,5 +57,22 @@ def load!(gemspec) YARD::Registry.load! PinCache.yardoc_path gemspec YARD::Registry.all end + + # If the BUNDLE_GEMFILE environment variable is set, we need to + # make sure it's an absolute path, as we'll be changing + # directories. + # + # 'bundle exec' sets an absolute path here, but at least the + # overcommit gem does not, breaking on-the-fly documention with a + # spawned yardoc command from our current bundle + # + # @return [Hash{String => String}] a hash of environment variables to override + def current_bundle_env_tweaks + tweaks = {} + if ENV['BUNDLE_GEMFILE'] && !ENV['BUNDLE_GEMFILE'].empty? + tweaks['BUNDLE_GEMFILE'] = File.expand_path(ENV['BUNDLE_GEMFILE']) + end + tweaks + end end end diff --git a/solargraph.gemspec b/solargraph.gemspec index 49265f9c6..0414f27d8 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -2,6 +2,7 @@ $LOAD_PATH.unshift File.dirname(__FILE__) + '/lib' require 'solargraph/version' require 'date' +# @param s [Gem::Specification] Gem::Specification.new do |s| s.name = 'solargraph' s.version = Solargraph::VERSION diff --git a/spec/api_map/constants_spec.rb b/spec/api_map/constants_spec.rb index c0460e79a..833a928cf 100644 --- a/spec/api_map/constants_spec.rb +++ b/spec/api_map/constants_spec.rb @@ -20,6 +20,31 @@ module Quuz expect(resolved).to eq('Foo::Bar') end + it 'resolves constants in includes' do + code = %( + module A + module Parser + module C + # @return [String] + def baz; "abc"; end + end + + B = C + end + + class Foo + include Parser::B + + # @return [String] + def bar + baz + end + end + end) + checker = Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) + expect(checker.problems.map(&:message)).to be_empty + end + it 'resolves straightforward mixins' do source_map = Solargraph::SourceMap.load_string(%( module Bar diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 1315f6c90..e82332161 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -42,8 +42,9 @@ it 'does not warn for redundant requires' do # Requiring 'set' is unnecessary because it's already included in core. It # might make sense to log redundant requires, but a warning is overkill. - expect(Solargraph.logger).not_to receive(:warn).with(/path set/) + allow(Solargraph.logger).to receive(:warn).and_call_original Solargraph::DocMap.new(['set'], []) + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) end it 'ignores nil requires' do diff --git a/spec/language_server/host/diagnoser_spec.rb b/spec/language_server/host/diagnoser_spec.rb index d59a843f1..69ee0b866 100644 --- a/spec/language_server/host/diagnoser_spec.rb +++ b/spec/language_server/host/diagnoser_spec.rb @@ -3,7 +3,8 @@ host = double(Solargraph::LanguageServer::Host, options: { 'diagnostics' => true }, synchronizing?: false) diagnoser = Solargraph::LanguageServer::Host::Diagnoser.new(host) diagnoser.schedule 'file.rb' - expect(host).to receive(:diagnose).with('file.rb') + allow(host).to receive(:diagnose) diagnoser.tick + expect(host).to have_received(:diagnose).with('file.rb') end end diff --git a/spec/language_server/host/message_worker_spec.rb b/spec/language_server/host/message_worker_spec.rb index b9ce2a41f..5e5bef481 100644 --- a/spec/language_server/host/message_worker_spec.rb +++ b/spec/language_server/host/message_worker_spec.rb @@ -2,11 +2,12 @@ it "handle requests on queue" do host = double(Solargraph::LanguageServer::Host) message = {'method' => '$/example'} - expect(host).to receive(:receive).with(message).and_return(nil) + allow(host).to receive(:receive).with(message).and_return(nil) worker = Solargraph::LanguageServer::Host::MessageWorker.new(host) worker.queue(message) expect(worker.messages).to eq [message] worker.tick + expect(host).to have_received(:receive).with(message) end end diff --git a/spec/pin/parameter_spec.rb b/spec/pin/parameter_spec.rb index 082ec54c6..14c39f3fe 100644 --- a/spec/pin/parameter_spec.rb +++ b/spec/pin/parameter_spec.rb @@ -473,5 +473,33 @@ def self.foo bar: 'bar' type = pin.probe(api_map) expect(type.simple_tags).to eq('String') end + + it 'handles a relative type name case' do + source = Solargraph::Source.load_string(%( + module A + module B + class Method + end + end + end + + module A + module B + class C < B::Method + # @param alt [Method] + # @return [B::Method, nil] + def resolve_method alt + alt + end + end + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map(source) + + clip = api_map.clip_at('test.rb', [14, 16]) + expect(clip.infer.rooted_tags).to eq('::A::B::Method') + end end end diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index 91f84b4c7..b9dc6b327 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -1,7 +1,11 @@ +# frozen_string_literal: true + require 'tmpdir' require 'open3' describe Solargraph::Shell do + let(:shell) { described_class.new } + let(:temp_dir) { Dir.mktmpdir } before do @@ -41,4 +45,114 @@ def bundle_exec(*cmd) expect(output).to include('Clearing pin cache in') end end + + # @type cmd [Array] + # @return [String] + def bundle_exec(*cmd) + # run the command in the temporary directory with bundle exec + Bundler.with_unbundled_env do + output, status = Open3.capture2e("bundle exec #{cmd.join(' ')}") + expect(status.success?).to be(true), "Command failed: #{output}" + output + end + end + + describe 'pin' do + let(:api_map) { instance_double(Solargraph::ApiMap) } + let(:to_s_pin) { instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) } + + before do + allow(Solargraph::Pin::Method).to receive(:===).with(to_s_pin).and_return(true) + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + allow(api_map).to receive(:get_path_pins).with('String#to_s').and_return([to_s_pin]) + end + + context 'with no options' do + it 'prints a pin' do + allow(to_s_pin).to receive(:inspect).and_return('pin inspect result') + + out = capture_both { shell.pin('String#to_s') } + + expect(out).to eq("pin inspect result\n") + end + end + + context 'with --rbs option' do + it 'prints a pin with RBS type' do + allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') + + out = capture_both do + shell.options = { rbs: true } + shell.pin('String#to_s') + end + expect(out).to eq("pin RBS result\n") + end + end + + context 'with --stack option' do + it 'prints a pin using stack results' do + allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') + + allow(api_map).to receive(:get_method_stack).and_return([to_s_pin]) + capture_both do + shell.options = { stack: true } + shell.pin('String#to_s') + end + expect(api_map).to have_received(:get_method_stack).with('String', 'to_s', scope: :instance) + end + + it 'prints a static pin using stack results' do + # allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') + string_new_pin = instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) + + allow(api_map).to receive(:get_method_stack).with('String', 'new', scope: :class).and_return([string_new_pin]) + allow(Solargraph::Pin::Method).to receive(:===).with(string_new_pin).and_return(true) + allow(api_map).to receive(:get_path_pins).with('String.new').and_return([string_new_pin]) + capture_both do + shell.options = { stack: true } + shell.pin('String.new') + end + expect(api_map).to have_received(:get_method_stack).with('String', 'new', scope: :class) + end + end + + context 'with --typify option' do + it 'prints a pin with typify type' do + allow(to_s_pin).to receive(:typify).and_return(Solargraph::ComplexType.parse('::String')) + + out = capture_both do + shell.options = { typify: true } + shell.pin('String#to_s') + end + expect(out).to eq("::String\n") + end + end + + context 'with --typify --rbs options' do + it 'prints a pin with typify type' do + allow(to_s_pin).to receive(:typify).and_return(Solargraph::ComplexType.parse('::String')) + + out = capture_both do + shell.options = { typify: true, rbs: true } + shell.pin('String#to_s') + end + expect(out).to eq("::String\n") + end + end + + context 'with no pin' do + it 'prints error' do + allow(api_map).to receive(:get_path_pins).with('Not#found').and_return([]) + allow(Solargraph::Pin::Method).to receive(:===).with(nil).and_return(false) + + out = capture_both do + shell.options = {} + shell.pin('Not#found') + rescue SystemExit + # Ignore the SystemExit raised by the shell when no pin is found + end + expect(out).to include("Pin not found for path 'Not#found'") + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 00cc6c8c3..59d107aa3 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -43,3 +43,29 @@ def with_env_var(name, value) ENV[name] = old_value # Restore the old value end end + +def capture_stdout &block + original_stdout = $stdout + $stdout = StringIO.new + begin + block.call + $stdout.string + ensure + $stdout = original_stdout + end +end + +def capture_both &block + original_stdout = $stdout + original_stderr = $stderr + stringio = StringIO.new + $stdout = stringio + $stderr = stringio + begin + block.call + ensure + $stdout = original_stdout + $stderr = original_stderr + end + stringio.string +end diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 970435dc3..659b0f22e 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -4,6 +4,21 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end + it 'provides nil checking on calls from parameters without assignments' do + pending('https://github.com/castwide/solargraph/pull/1127') + + checker = type_checker(%( + # @param baz [String, nil] + # + # @return [String] + def quux(baz) + baz.upcase # ERROR: Unresolved call to upcase on String, nil + end + )) + expect(checker.problems.map(&:message)).to eq(['#quux return type could not be inferred', + 'Unresolved call to upcase on String, nil']) + end + it 'does not complain on array dereference' do checker = type_checker(%( # @param idx [Integer, nil] an index @@ -97,6 +112,29 @@ def bar expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') end + it 'understands complex use of other' do + checker = type_checker(%( + class A + # @param other [self] + # + # @return [void] + def foo other; end + + # @param other [self] + # + # @return [void] + def bar(other); end + end + + class B < A + def bar(other) + foo(other) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'calls out type issues even when keyword issues are there' do pending('fixes to arg vs param checking algorithm') diff --git a/spec/yardoc_spec.rb b/spec/yardoc_spec.rb new file mode 100644 index 000000000..34dcad45c --- /dev/null +++ b/spec/yardoc_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'tmpdir' +require 'open3' + +describe Solargraph::Yardoc do + let(:gem_yardoc_path) do + Solargraph::PinCache.yardoc_path gemspec + end + + before do + FileUtils.mkdir_p(gem_yardoc_path) + end + + describe '#cache' do + let(:api_map) { Solargraph::ApiMap.new } + let(:doc_map) { api_map.doc_map } + let(:gemspec) { Gem::Specification.find_by_path('rubocop') } + let(:output) { '' } + + before do + allow(Solargraph.logger).to receive(:warn) + allow(Solargraph.logger).to receive(:info) + FileUtils.rm_rf(gem_yardoc_path) + end + + context 'when given a relative BUNDLE_GEMFILE path' do + around do |example| + # turn absolute BUNDLE_GEMFILE path into relative + existing_gemfile = ENV.fetch('BUNDLE_GEMFILE', nil) + current_dir = Dir.pwd + # remove prefix current_dir from path + ENV['BUNDLE_GEMFILE'] = existing_gemfile.sub("#{current_dir}/", '') + raise 'could not figure out relative path' if Pathname.new(ENV.fetch('BUNDLE_GEMFILE', nil)).absolute? + example.run + ENV['BUNDLE_GEMFILE'] = existing_gemfile + end + + it 'sends Open3 an absolute path' do + called_with = nil + allow(Open3).to receive(:capture2e) do |*args| + called_with = args + ['output', instance_double(Process::Status, success?: true)] + end + + described_class.cache([], gemspec) + + expect(called_with[0]['BUNDLE_GEMFILE']).to start_with('/') + end + end + end +end