diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..92f925b17 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +# current git branch +SOLARGRAPH_FORCE_VERSION=0.0.1.dev-$(git rev-parse --abbrev-ref HEAD | tr -d '\n' | tr -d '/' | tr -d '-'| tr -d '_') +export SOLARGRAPH_FORCE_VERSION diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index b4ef26bfe..7a5ef4d05 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -8,10 +8,10 @@ name: Linting on: workflow_dispatch: {} pull_request: - branches: [ master ] + branches: ['*'] push: branches: - - 'main' + - 'master' tags: - 'v*' @@ -31,13 +31,12 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 - bundler: latest bundler-cache: true - cache-version: 2025-06-06 + cache-version: 2026-01-11 - name: Update to best available RBS run: | - bundle update rbs # use latest available for this Ruby version + bundle update --pre rbs # use latest available for this Ruby version - name: Restore cache of gem annotations id: dot-cache-restore diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index c7ad72cb4..3de7288eb 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -9,7 +9,7 @@ on: push: branches: [master] pull_request: - branches: [master] + branches: ['*'] permissions: contents: read @@ -23,7 +23,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4 + ruby-version: 3.4 # keep same as typecheck.yml bundler-cache: true - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -34,7 +34,7 @@ jobs: echo 'gem "solargraph-rails"' > .Gemfile echo 'gem "solargraph-rspec"' >> .Gemfile bundle install - bundle update rbs + bundle update --pre rbs - name: Configure to use plugins run: | bundle exec solargraph config @@ -43,7 +43,7 @@ jobs: - name: Install gem types run: bundle exec rbs collection update - name: Ensure typechecking still works - run: bundle exec solargraph typecheck --level typed + run: bundle exec solargraph typecheck --level strong - name: Ensure specs still run run: bundle exec rake spec rails: @@ -54,7 +54,9 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4 + ruby-version: 3.4 # keep same as typecheck.yml + # See https://github.com/castwide/solargraph/actions/runs/19000135777/job/54265647107?pr=1119 + rubygems: latest bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -64,7 +66,7 @@ jobs: run: | echo 'gem "solargraph-rails"' > .Gemfile bundle install - bundle update rbs + bundle update --pre rbs - name: Configure to use plugins run: | bundle exec solargraph config @@ -72,7 +74,7 @@ jobs: - name: Install gem types run: bundle exec rbs collection update - name: Ensure typechecking still works - run: bundle exec solargraph typecheck --level typed + run: bundle exec solargraph typecheck --level strong - name: Ensure specs still run run: bundle exec rake spec rspec: @@ -83,7 +85,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4 + ruby-version: 3.4 # keep same as typecheck.yml bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -93,7 +95,7 @@ jobs: run: | echo 'gem "solargraph-rspec"' >> .Gemfile bundle install - bundle update rbs + bundle update --pre rbs - name: Configure to use plugins run: | bundle exec solargraph config @@ -101,7 +103,7 @@ jobs: - name: Install gem types run: bundle exec rbs collection update - name: Ensure typechecking still works - run: bundle exec solargraph typecheck --level typed + run: bundle exec solargraph typecheck --level strong - name: Ensure specs still run run: bundle exec rake spec @@ -118,45 +120,48 @@ jobs: cd .. # git clone https://github.com/lekemula/solargraph-rspec.git - # pending https://github.com/lekemula/solargraph-rspec/pull/30 + # pending https://github.com/lekemula/solargraph-rspec/pull/31 git clone https://github.com/apiology/solargraph-rspec.git cd solargraph-rspec - git checkout reset_closures + git checkout test_solargraph_prereleases - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.1' + ruby-version: 3.4 rubygems: latest bundler-cache: false - name: Install gems run: | - set -x + 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 + 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 + - name: Solargraph generate RSpec gems YARD pins run: | cd ../solargraph-rspec - bundle exec appraisal rbs collection update + # solargraph-rspec's specs don't pass a workspace, so it + # doesn't know where to look for the RBS collection - let's + # not load one so that the solargraph gems command below works + 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 @@ -180,6 +185,8 @@ jobs: # solargraph-rails supports Ruby 3.0+ ruby-version: '3.0' bundler-cache: false + # https://github.com/apiology/solargraph/actions/runs/19400815835/job/55508092473?pr=17 + rubygems: latest bundler: latest env: MATRIX_RAILS_VERSION: "7.0" @@ -191,7 +198,7 @@ jobs: cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile bundle install - bundle update rbs + bundle update --pre rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR cd ${RAILS_DIR} diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index c82ade49b..174a1a6e3 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -11,7 +11,7 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: ['*'] permissions: contents: read @@ -21,31 +21,56 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0'] - rbs-version: ['3.6.1', '3.9.5', '4.0.0.dev.4'] + ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0', 'head'] + rbs-version: ['3.6.1', '3.8.1', '3.9.5', '3.10.0', '4.0.0.dev.5'] # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 exclude: + # only include the 3.0 variants we include later - ruby-version: '3.0' - rbs-version: '3.9.5' - - ruby-version: '3.0' - rbs-version: '4.0.0.dev.4' - # Missing require in 'rbs collection update' - hopefully - # fixed in next RBS release + # only include the 3.1 variants we include later + - ruby-version: '3.1' + # only include the 3.2 variants we include later + - ruby-version: '3.2' + # only include the 3.3 variants we include later + - ruby-version: '3.3' + # only include the 3.4 variants we include later + - ruby-version: '3.4' + # only include the 4.0 variants we include later - ruby-version: '4.0' + # Don't exclude 'head' - let's test all RBS versions we + # can there. + # + # + # Just exclude some odd-ball compatibility issues we can't + # work around: + # + # https://github.com/castwide/solargraph/actions/runs/20627923548/job/59241444380?pr=1102 + - ruby-version: 'head' + rbs-version: '3.6.1' + - ruby-version: 'head' + rbs-version: '3.8.1' + include: + - ruby-version: '3.0' rbs-version: '3.6.1' + - ruby-version: '3.1' + rbs-version: '3.6.1' + - ruby-version: '3.2' + rbs-version: '3.8.1' + - ruby-version: '3.3' + rbs-version: '3.9.5' + - ruby-version: '3.3' + rbs-version: '3.10.0' + - ruby-version: '3.4' + rbs-version: '4.0.0.dev.5' - ruby-version: '4.0' - rbs-version: '4.0.0.dev.4' + rbs-version: '4.0.0.dev.5' 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 + bundler-cache: true - name: Set rbs version run: echo "gem 'rbs', '${{ matrix.rbs-version }}'" >> .Gemfile # /home/runner/.rubies/ruby-head/lib/ruby/gems/3.5.0+2/gems/rbs-3.9.4/lib/rbs.rb:11: @@ -54,15 +79,11 @@ jobs: # starting from Ruby 3.6.0 - name: Work around legacy rbs deprecation on ruby > 3.4 run: echo "gem 'tsort'" >> .Gemfile - - name: Install gems + - name: Update gems run: | - 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 + run: bundle exec rbs collection update - name: Run tests run: bundle exec rake spec undercover: @@ -77,9 +98,9 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' - bundler-cache: false - - name: Install gems - run: bundle install + bundler-cache: true + - name: Update types + run: bundle exec rbs collection update - name: Run tests run: bundle exec rake spec - name: Check PR coverage diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index f40977acf..ddb3e6527 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -11,7 +11,7 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: ['*'] permissions: contents: read @@ -32,7 +32,7 @@ jobs: - name: Install gems run: | bundle install - bundle update rbs # use latest available for this Ruby version + bundle update --pre rbs # use latest available for this Ruby version - name: Install gem types run: bundle exec rbs collection install - name: Typecheck self diff --git a/.rubocop.yml b/.rubocop.yml index 51b022f51..96ec6c6b2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -21,6 +21,25 @@ AllCops: - "vendor/**/.*" TargetRubyVersion: 3.0 +Gemspec/RequiredRubyVersion: + Exclude: + - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' + - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' + - 'spec/fixtures/vendored/vendor/do_not_use.gemspec' + +Gemspec/DevelopmentDependencies: + EnforcedStyle: gemspec + Exclude: + - 'spec/fixtures/**/*' + +Lint/EmptyFile: + Exclude: + - 'spec/fixtures/vendored/vendor/do_not_use.gemspec' + +Naming/VariableName: + Exclude: + - 'spec/fixtures/unicode.rb' + # We don't use the spec/solargraph directory RSpec/SpecFilePathFormat: Enabled: false @@ -31,6 +50,14 @@ Style/MethodDefParentheses: Layout/EmptyLineAfterGuardClause: Enabled: false +Naming/AsciiIdentifiers: + Exclude: + - 'spec/fixtures/unicode.rb' + +Lint/EmptyClass: + Exclude: + - spec/fixtures/**/*.rb + Lint/UnusedMethodArgument: AllowUnusedKeywordArguments: true @@ -48,17 +75,17 @@ Style/ClassVars: # improve existing code! # Metrics/AbcSize: - Max: 65 + Max: 110 Metrics/MethodLength: - Max: 60 + Max: 70 Metrics/ClassLength: Max: 500 Metrics/CyclomaticComplexity: - Max: 23 + Max: 40 Metrics/PerceivedComplexity: - Max: 29 + Max: 40 RSpec/ExampleLength: - Max: 17 + Max: 310 plugins: - rubocop-rspec diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 962ac9bb6..eea514d1c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,30 +6,12 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# This cop supports safe autocorrection (--autocorrect). -Gemspec/AddRuntimeDependency: - Exclude: - - 'solargraph.gemspec' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Severity. Gemspec/DeprecatedAttributeAssignment: Exclude: - - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' -# Configuration parameters: EnforcedStyle, AllowedGems. -# SupportedStyles: Gemfile, gems.rb, gemspec -Gemspec/DevelopmentDependencies: - Exclude: - - 'solargraph.gemspec' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation. -Gemspec/OrderedDependencies: - Exclude: - - 'solargraph.gemspec' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Severity. Gemspec/RequireMFA: @@ -37,268 +19,18 @@ Gemspec/RequireMFA: - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' -# Configuration parameters: Severity. -Gemspec/RequiredRubyVersion: - Exclude: - - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' - - 'spec/fixtures/vendored/vendor/do_not_use.gemspec' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: with_first_argument, with_fixed_indentation -Layout/ArgumentAlignment: - Exclude: - - 'lib/solargraph/pin/callable.rb' - - 'spec/source/source_chainer_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleAlignWith. -# SupportedStylesAlignWith: either, start_of_block, start_of_line -Layout/BlockAlignment: - Exclude: - - 'spec/source_map/mapper_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -Layout/ClosingHeredocIndentation: - Exclude: - - 'spec/diagnostics/rubocop_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment. -Layout/CommentIndentation: - Exclude: - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/source_map/mapper.rb' - -# This cop supports safe autocorrection (--autocorrect). -Layout/ElseAlignment: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. -Layout/EmptyLineBetweenDefs: - Exclude: - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/language_server/message/initialize.rb' - - 'lib/solargraph/pin/delegated_method.rb' - -# This cop supports safe autocorrection (--autocorrect). -Layout/EmptyLines: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines -Layout/EmptyLinesAroundModuleBody: - Exclude: - - 'lib/solargraph/api_map/source_to_yard.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleAlignWith, Severity. # SupportedStylesAlignWith: keyword, variable, start_of_line Layout/EndAlignment: - Enabled: false - -# Configuration parameters: EnforcedStyle. -# SupportedStyles: native, lf, crlf -Layout/EndOfLine: - Exclude: - - 'Gemfile' - - 'Rakefile' - - 'lib/solargraph/source/encoding_fixes.rb' - - 'solargraph.gemspec' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. -Layout/ExtraSpacing: - Exclude: - - 'lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb' - - 'lib/solargraph/pin/closure.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/type_checker.rb' - - 'spec/spec_helper.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses -Layout/FirstArgumentIndentation: - Exclude: - - 'lib/solargraph/parser/parser_gem/node_processors/args_node.rb' - - 'spec/source/source_chainer_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: special_inside_parentheses, consistent, align_brackets -Layout/FirstArrayElementIndentation: - Exclude: - - 'lib/solargraph/source.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: special_inside_parentheses, consistent, align_braces -Layout/FirstHashElementIndentation: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. -# SupportedHashRocketStyles: key, separator, table -# SupportedColonStyles: key, separator, table -# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit -Layout/HashAlignment: - Exclude: - - 'lib/solargraph/workspace/config.rb' - -# This cop supports safe autocorrection (--autocorrect). -Layout/HeredocIndentation: Exclude: - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' + - 'lib/solargraph/shell.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Width, AllowedPatterns. Layout/IndentationWidth: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment, AllowRBSInlineAnnotation, AllowSteepAnnotation. -Layout/LeadingCommentSpace: - Exclude: - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source_map/clip.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/LineContinuationSpacing: - Exclude: - - 'lib/solargraph/diagnostics/rubocop_helpers.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: symmetrical, new_line, same_line -Layout/MultilineMethodCallBraceLayout: - Exclude: - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'spec/source/source_chainer_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented, indented_relative_to_receiver -Layout/MultilineMethodCallIndentation: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented -Layout/MultilineOperationIndentation: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/language_server/host/dispatch.rb' - - 'lib/solargraph/source.rb' - -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceAfterComma: - Exclude: - - 'spec/source/cursor_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceAroundEqualsInParameterDefault: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceAroundKeyword: - Exclude: - - 'spec/rbs_map/conversions_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals. -# SupportedStylesForExponentOperator: space, no_space -# SupportedStylesForRationalLiterals: space, no_space -Layout/SpaceAroundOperators: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceBeforeBlockBraces: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceBeforeComma: - Exclude: - - 'spec/source/cursor_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceInsideBlockBraces: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space, compact -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceInsideHashLiteralBraces: - Exclude: - - 'lib/solargraph/language_server/message/extended/search.rb' - - 'lib/solargraph/language_server/message/initialize.rb' - - 'lib/solargraph/workspace/config.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, compact, no_space -Layout/SpaceInsideParens: - Exclude: - - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/source_map.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowInHeredoc. -Layout/TrailingWhitespace: - Exclude: - - 'lib/solargraph/language_server/message/client/register_capability.rb' - - 'spec/api_map/config_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods, AllowedPatterns. -Lint/AmbiguousBlockAssociation: - Exclude: - - 'lib/solargraph/language_server/host.rb' - -# This cop supports safe autocorrection (--autocorrect). -Lint/AmbiguousOperator: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -Lint/AmbiguousOperatorPrecedence: - Exclude: - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/source.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: RequireParenthesesForMethodChains. -Lint/AmbiguousRange: - Enabled: false - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowSafeAssignment. -Lint/AssignmentInCondition: Exclude: - - 'lib/solargraph/library.rb' + - 'lib/solargraph/shell.rb' Lint/BinaryOperatorWithIdenticalOperands: Exclude: @@ -308,7 +40,6 @@ Lint/BinaryOperatorWithIdenticalOperands: Lint/BooleanSymbol: Exclude: - 'lib/solargraph/convention/struct_definition/struct_definition_node.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - 'lib/solargraph/source/chain/literal.rb' # Configuration parameters: AllowedMethods. @@ -320,144 +51,48 @@ Lint/ConstantDefinitionInBlock: # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch. Lint/DuplicateBranch: Exclude: - - 'lib/solargraph/complex_type/type_methods.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/pin/base.rb' - 'lib/solargraph/rbs_map/conversions.rb' -Lint/DuplicateMethods: - Enabled: false - -# Configuration parameters: AllowComments. -Lint/EmptyClass: - Enabled: false - -# Configuration parameters: AllowComments. -Lint/EmptyFile: - Exclude: - - 'spec/fixtures/vendored/vendor/do_not_use.gemspec' - # This cop supports unsafe autocorrection (--autocorrect-all). Lint/InterpolationCheck: Exclude: - - 'spec/complex_type_spec.rb' - - 'spec/parser/node_methods_spec.rb' - 'spec/source/chain_spec.rb' - 'spec/source/cursor_spec.rb' -# Configuration parameters: AllowedParentClasses. -Lint/MissingSuper: - Exclude: - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/chain/constant.rb' - - 'lib/solargraph/source/chain/if.rb' - - 'lib/solargraph/source/chain/literal.rb' - - 'lib/solargraph/source/chain/or.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Lint/NonAtomicFileOperation: - Exclude: - - 'spec/diagnostics/rubocop_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -Lint/ParenthesesAsGroupedExpression: - Exclude: - - 'lib/solargraph.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/source_map/clip_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -Lint/RedundantRequireStatement: - Exclude: - - 'spec/language_server/protocol_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowedMethods, InferNonNilReceiver, AdditionalNilMethods. -# AllowedMethods: instance_of?, kind_of?, is_a?, eql?, respond_to?, equal? -# AdditionalNilMethods: present?, blank?, try, try! -Lint/RedundantSafeNavigation: - Exclude: - - 'lib/solargraph/api_map/source_to_yard.rb' - - 'lib/solargraph/rbs_map.rb' - -# This cop supports safe autocorrection (--autocorrect). -Lint/RedundantStringCoercion: - Exclude: - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/pin/conversions.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - -# This cop supports safe autocorrection (--autocorrect). -Lint/RedundantWithIndex: - Exclude: - - 'lib/solargraph/language_server/message/completion_item/resolve.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: strict, consistent -Lint/SymbolConversion: - Exclude: - - 'lib/solargraph/pin/base.rb' - # Configuration parameters: AllowKeywordBlockArguments. Lint/UnderscorePrefixedVariableName: Exclude: - 'lib/solargraph/library.rb' -# Configuration parameters: Methods. -Lint/UnexpectedBlockArity: - Exclude: - - 'lib/solargraph/language_server/message/completion_item/resolve.rb' - - 'lib/solargraph/type_checker.rb' - -Lint/UnmodifiedReduceAccumulator: - Exclude: - - 'lib/solargraph/pin/method.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. -Lint/UnusedBlockArgument: - Exclude: - - 'lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb' - - 'lib/solargraph/logging.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. # NotImplementedExceptions: NotImplementedError Lint/UnusedMethodArgument: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. -Lint/UselessAccessModifier: - Exclude: - - 'lib/solargraph/api_map.rb' - # This cop supports safe autocorrection (--autocorrect). Lint/UselessAssignment: - Enabled: false - -Lint/UselessConstantScoping: Exclude: - - 'lib/solargraph/rbs_map/conversions.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Lint/UselessMethodDefinition: - Exclude: - - 'lib/solargraph/pin/signature.rb' + - 'lib/solargraph/pin/block.rb' + - 'spec/fixtures/long_squiggly_heredoc.rb' + - 'spec/fixtures/rubocop-unused-variable-error/app.rb' + - 'spec/fixtures/unicode.rb' # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max. Metrics/AbcSize: - Enabled: false + Exclude: + - 'lib/solargraph/api_map/source_to_yard.rb' + - 'lib/solargraph/parser/parser_gem/node_chainer.rb' + - 'lib/solargraph/source/source_chainer.rb' + - 'lib/solargraph/source_map/clip.rb' + - 'lib/solargraph/source_map/mapper.rb' # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. # AllowedMethods: refine Metrics/BlockLength: - Max: 56 + Max: 61 # Configuration parameters: CountBlocks, CountModifierForms. Metrics/BlockNesting: @@ -468,12 +103,14 @@ Metrics/ClassLength: Exclude: - 'lib/solargraph/api_map.rb' - 'lib/solargraph/language_server/host.rb' + - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/rbs_map/conversions.rb' - 'lib/solargraph/type_checker.rb' # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/CyclomaticComplexity: - Enabled: false + Exclude: + - 'lib/solargraph/type_checker.rb' # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: @@ -481,20 +118,22 @@ Metrics/MethodLength: # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 169 + Max: 167 # Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: Exclude: - 'lib/solargraph/api_map.rb' + - 'lib/solargraph/parser/node_processor.rb' - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/type_checker.rb' - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'lib/solargraph/yard_map/to_method.rb' # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/PerceivedComplexity: - Enabled: false + Exclude: + - 'lib/solargraph/parser/parser_gem/node_chainer.rb' + - 'lib/solargraph/type_checker.rb' Naming/AccessorMethodName: Exclude: @@ -502,27 +141,20 @@ Naming/AccessorMethodName: - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/language_server/message/base.rb' -# Configuration parameters: AsciiConstants. -Naming/AsciiIdentifiers: - Exclude: - - 'spec/fixtures/unicode.rb' - # Configuration parameters: ForbiddenDelimiters. # ForbiddenDelimiters: (?i-mx:(^|\s)(EO[A-Z]{1}|END)(\s|$)) Naming/HeredocDelimiterNaming: Exclude: - 'spec/yard_map/mapper/to_method_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyleForLeadingUnderscores. -# SupportedStylesForLeadingUnderscores: disallowed, required, optional -Naming/MemoizedInstanceVariableName: - Enabled: false - # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: - Enabled: false + Exclude: + - 'lib/solargraph/parser/parser_gem/node_chainer.rb' + - 'lib/solargraph/range.rb' + - 'lib/solargraph/source.rb' + - 'lib/solargraph/yard_map/mapper/to_method.rb' # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates. # AllowedMethods: call @@ -539,52 +171,19 @@ Naming/PredicatePrefix: Exclude: - 'spec/**/*' - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/parser/parser_gem/class_methods.rb' - 'lib/solargraph/pin_cache.rb' - 'lib/solargraph/workspace.rb' -# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns. -# SupportedStyles: snake_case, camelCase -Naming/VariableName: - Exclude: - - 'spec/fixtures/unicode.rb' - -RSpec/Be: - Exclude: - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - - 'spec/source/source_chainer_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -RSpec/BeEq: - Exclude: - - 'spec/complex_type_spec.rb' - - 'spec/pin/method_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: be, be_nil -RSpec/BeNil: - Exclude: - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/language_server/host_spec.rb' - RSpec/BeforeAfterAll: Exclude: - '**/spec/spec_helper.rb' - '**/spec/rails_helper.rb' - '**/spec/support/**/*.rb' - 'spec/api_map_spec.rb' - - 'spec/doc_map_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/protocol_spec.rb' -# Configuration parameters: Prefixes, AllowedPatterns. -# Prefixes: when, with, without -RSpec/ContextWording: - Enabled: false - # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: Exclude: @@ -593,80 +192,23 @@ RSpec/DescribeClass: - '**/spec/routing/**/*' - '**/spec/system/**/*' - '**/spec/views/**/*' - - 'spec/api_map_method_spec.rb' - 'spec/complex_type_spec.rb' - - 'spec/source_map/node_processor_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. -# SupportedStyles: described_class, explicit -RSpec/DescribedClass: - Enabled: false # This cop supports safe autocorrection (--autocorrect). -RSpec/EmptyLineAfterFinalLet: +RSpec/ExpectActual: Exclude: - - 'spec/workspace/config_spec.rb' - -# Configuration parameters: Max, CountAsOne. -RSpec/ExampleLength: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples. -# DisallowedExamples: works -RSpec/ExampleWording: - Exclude: - - 'spec/pin/base_spec.rb' - - 'spec/pin/method_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -RSpec/ExcessiveDocstringSpacing: - Exclude: - - 'spec/rbs_map/conversions_spec.rb' - - 'spec/source/chain/call_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -RSpec/ExpectActual: - Exclude: - - '**/spec/routing/**/*' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/source_map/mapper_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: implicit, each, example -RSpec/HookArgument: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: . -# SupportedStyles: is_expected, should -RSpec/ImplicitExpect: - EnforcedStyle: should + - '**/spec/routing/**/*' + - 'spec/rbs_map/stdlib_map_spec.rb' + - 'spec/source_map/mapper_spec.rb' # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -RSpec/LeadingSubject: - Exclude: - - 'spec/rbs_map/conversions_spec.rb' - RSpec/LeakyConstantDeclaration: Exclude: - 'spec/complex_type_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -RSpec/LetBeforeExamples: - Exclude: - - 'spec/complex_type_spec.rb' - -RSpec/MissingExampleGroupArgument: - Exclude: - - 'spec/diagnostics/rubocop_helpers_spec.rb' - RSpec/MultipleExpectations: Max: 14 @@ -674,59 +216,10 @@ RSpec/MultipleExpectations: RSpec/NestedGroups: Max: 4 -# Configuration parameters: AllowedPatterns. -# AllowedPatterns: ^expect_, ^assert_ -RSpec/NoExpectationExample: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: not_to, to_not -RSpec/NotToNot: - Exclude: - - 'spec/api_map_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - -RSpec/PendingWithoutReason: - Enabled: false - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Strict, EnforcedStyle, AllowedExplicitMatchers. -# SupportedStyles: inflected, explicit -RSpec/PredicateMatcher: - Exclude: - - 'spec/language_server/message/workspace/did_change_configuration_spec.rb' - - 'spec/source_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -RSpec/ReceiveMessages: - Exclude: - - 'spec/language_server/host_spec.rb' - RSpec/RemoveConst: Exclude: - 'spec/diagnostics/rubocop_helpers_spec.rb' -RSpec/RepeatedDescription: - Enabled: false - -RSpec/RepeatedExample: - Exclude: - - 'spec/api_map_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -RSpec/ScatteredLet: - Exclude: - - 'spec/complex_type_spec.rb' - -# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. -RSpec/VerifiedDoubles: - Enabled: false - Security/MarshalLoad: Exclude: - 'lib/solargraph/pin_cache.rb' @@ -735,23 +228,8 @@ Security/MarshalLoad: # Configuration parameters: EnforcedStyle, AllowModifiersOnSymbols, AllowModifiersOnAttrs, AllowModifiersOnAliasMethod. # SupportedStyles: inline, group Style/AccessModifierDeclarations: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: separated, grouped -Style/AccessorGrouping: Exclude: - - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/rbs_map.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: always, conditionals -Style/AndOr: - Enabled: false + - 'lib/solargraph/source/chain/literal.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowOnlyRestArgument, UseAnonymousForwarding, RedundantRestArgumentNames, RedundantKeywordRestArgumentNames, RedundantBlockArgumentNames. @@ -760,7 +238,6 @@ Style/AndOr: # RedundantBlockArgumentNames: blk, block, proc Style/ArgumentsForwarding: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/complex_type.rb' # This cop supports safe autocorrection (--autocorrect). @@ -770,49 +247,8 @@ Style/ArgumentsForwarding: # FunctionalMethods: let, let!, subject, watch # AllowedMethods: lambda, proc, it Style/BlockDelimiters: - Enabled: false - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: MinBranchesCount. -Style/CaseLikeIf: - Enabled: false - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules. -# SupportedStyles: nested, compact -# SupportedStylesForClasses: ~, nested, compact -# SupportedStylesForModules: ~, nested, compact -Style/ClassAndModuleChildren: - Enabled: false - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowedMethods, AllowedPatterns. -# AllowedMethods: ==, equal?, eql? -Style/ClassEqualityComparison: - Exclude: - - 'lib/solargraph/gem_pins.rb' - - 'lib/solargraph/pin/base.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowedReceivers. -Style/CollectionCompact: Exclude: - - 'lib/solargraph/pin/constant.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/ColonMethodCall: - Exclude: - - 'spec/type_checker_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/CombinableLoops: - Exclude: - - 'lib/solargraph/pin/parameter.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/ConcatArrayLiterals: - Exclude: - - 'lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb' + - 'spec/source/chain_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. @@ -820,214 +256,55 @@ Style/ConcatArrayLiterals: Style/ConditionalAssignment: Exclude: - 'lib/solargraph/api_map/source_to_yard.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/defs_node.rb' - - 'lib/solargraph/source/chain/call.rb' # Configuration parameters: AllowedConstants. Style/Documentation: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -Style/EmptyLambdaParameter: - Exclude: - - 'spec/rbs_map/core_map_spec.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: compact, expanded Style/EmptyMethod: Exclude: - - 'lib/solargraph/language_server/message/client/register_capability.rb' - - 'lib/solargraph/pin/base.rb' - 'spec/fixtures/formattable.rb' - 'spec/fixtures/rdoc-lib/lib/example.rb' - 'spec/fixtures/workspace-with-gemfile/lib/thing.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: trailing_conditional, ternary -Style/EmptyStringInsideInterpolation: - Exclude: - - 'lib/solargraph/library.rb' - - 'lib/solargraph/pin/documenting.rb' - - 'lib/solargraph/shell.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/ExpandPathArguments: - Exclude: - - 'solargraph.gemspec' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedVars, DefaultToNil. -Style/FetchEnvVar: - Exclude: - - 'spec/api_map/config_spec.rb' - - 'spec/spec_helper.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: left_coerce, right_coerce, single_coerce, fdiv -Style/FloatDivision: - Exclude: - - 'lib/solargraph/library.rb' - - 'lib/solargraph/pin/search.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never Style/FrozenStringLiteralComment: Enabled: false -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/GlobalStdStream: - Exclude: - - 'lib/solargraph/logging.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/shell.rb' - - 'spec/logging_spec.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: - Enabled: false - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowSplatArgument. -Style/HashConversion: Exclude: - - 'lib/solargraph/doc_map.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowedReceivers. -# AllowedReceivers: Thread.current -Style/HashEachMethods: - Exclude: - - 'lib/solargraph/library.rb' - - 'lib/solargraph/source.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. -# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys -# SupportedShorthandSyntax: always, never, either, consistent, either_consistent -Style/HashSyntax: - Exclude: - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/IdenticalConditionalBranches: - Exclude: - - 'lib/solargraph/library.rb' - - 'lib/solargraph/type_checker.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowIfModifier. -Style/IfInsideElse: - Enabled: false + - 'lib/solargraph/source_map/clip.rb' # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: call, braces -Style/LambdaCall: - Exclude: - - 'lib/solargraph/library.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/MapIntoArray: - Exclude: - - 'lib/solargraph/diagnostics/update_errors.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/type_checker/param_def.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/MapToHash: - Exclude: - - 'lib/solargraph/bench.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/MapToSet: - Exclude: - - 'lib/solargraph/library.rb' - - 'spec/source_map/clip_spec.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline Style/MethodDefParentheses: - Enabled: false + Exclude: + - 'spec/fixtures/rdoc-lib/lib/example.rb' Style/MultilineBlockChain: Exclude: - 'lib/solargraph/pin/search.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/MultilineIfModifier: - Exclude: - - 'lib/solargraph/pin/callable.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/MultilineTernaryOperator: - Exclude: - - 'lib/solargraph/language_server/host.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMethodComparison, ComparisonsThreshold. -Style/MultipleComparison: - Enabled: false - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: literals, strict -Style/MutableConstant: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: both, prefix, postfix -Style/NegatedIf: - Exclude: - - 'lib/solargraph/language_server/host/diagnoser.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/NegatedIfElseCondition: - Exclude: - - 'lib/solargraph/diagnostics/rubocop.rb' - - 'lib/solargraph/language_server/message/extended/document_gems.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/type_checker.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/NestedTernaryOperator: - Exclude: - - 'lib/solargraph/pin/conversions.rb' - - 'lib/solargraph/pin/method.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinBodyLength, AllowConsecutiveConditionals. -# SupportedStyles: skip_modifier_ifs, always -Style/Next: - Exclude: - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/signature.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/type_checker/checks.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Strict, AllowedNumbers, AllowedPatterns. -Style/NumericLiterals: - MinDigits: 6 - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. # SupportedStyles: predicate, comparison Style/NumericPredicate: - Enabled: false + Exclude: + - 'spec/**/*' + - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' + - 'lib/solargraph/language_server/message/extended/document_gems.rb' Style/OpenStructUse: Exclude: @@ -1038,242 +315,49 @@ Style/OpenStructUse: Style/OptionalBooleanParameter: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions. -Style/ParenthesesAroundCondition: - Exclude: - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/type_checker.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: short, verbose -Style/PreferredHashMethods: - Exclude: - - 'lib/solargraph/language_server/message.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Methods. -Style/RedundantArgument: - Exclude: - - 'lib/solargraph/source_map/mapper.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantAssignment: - Exclude: - - 'lib/solargraph/language_server/host/dispatch.rb' - - 'lib/solargraph/workspace/config.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantBegin: - Exclude: - - 'lib/solargraph/language_server/transport/data_reader.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/encoding_fixes.rb' - - 'lib/solargraph/workspace.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantException: - Exclude: - - 'spec/language_server/host_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantFreeze: - Exclude: - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/source_map/mapper.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/RedundantInterpolation: - Exclude: - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/source_map/mapper.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantParentheses: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpArgument: - Exclude: - - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/workspace/config.rb' - - 'spec/diagnostics/rubocop_helpers_spec.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/language_server/host_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpEscape: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMultipleReturnValues. -Style/RedundantReturn: - Exclude: - - 'lib/solargraph/complex_type/type_methods.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/source/chain/z_super.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantSelf: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowInnerSlashes. -# SupportedStyles: slashes, percent_r, mixed -Style/RegexpLiteral: - Exclude: - - 'lib/solargraph/language_server/uri_helpers.rb' - - 'lib/solargraph/workspace/config.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: implicit, explicit -Style/RescueStandardError: - Exclude: - - 'lib/solargraph/pin/base.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! Style/SafeNavigation: - Enabled: false + Exclude: + - 'lib/solargraph/pin/base.rb' # Configuration parameters: Max. Style/SafeNavigationChainLength: Exclude: - - 'lib/solargraph/doc_map.rb' + - 'lib/solargraph/workspace/gemspecs.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowModifier. -Style/SoleNestedConditional: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -Style/StderrPuts: Exclude: - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/shell.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Mode. -Style/StringConcatenation: - Enabled: false + - 'lib/solargraph/convention/struct_definition/struct_definition_node.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes Style/StringLiterals: - Enabled: false + Exclude: + - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' + - 'spec/source/chain_spec.rb' # This cop supports safe autocorrection (--autocorrect). Style/SuperArguments: Exclude: - - 'lib/solargraph/pin/base_variable.rb' - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/signature.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinSize. -# SupportedStyles: percent, brackets -Style/SymbolArray: - Enabled: false - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. -# AllowedMethods: define_method -Style/SymbolProc: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowSafeAssignment. -# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex -Style/TernaryParentheses: - Exclude: - - 'lib/solargraph/source_map/mapper.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInArguments: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma -Style/TrailingCommaInArrayLiteral: - Exclude: - - 'lib/solargraph/language_server/message/text_document/formatting.rb' - - 'lib/solargraph/rbs_map/core_fills.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma -Style/TrailingCommaInHashLiteral: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. -# AllowedMethods: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym -Style/TrivialAccessors: - Exclude: - - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - - 'lib/solargraph/pin/keyword.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/WhileUntilModifier: - Exclude: - - 'lib/solargraph/complex_type.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinSize, WordRegex. -# SupportedStyles: percent, brackets -Style/WordArray: - Enabled: false - -# This cop supports safe autocorrection (--autocorrect). -Style/YAMLFileRead: - Exclude: - - 'lib/solargraph/workspace/config.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/ZeroLengthPredicate: - Exclude: - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/source/chain/array.rb' - - 'spec/language_server/protocol_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: long, short -YARD/CollectionType: - Exclude: - - 'lib/solargraph/range.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStylePrototypeName. # SupportedStylesPrototypeName: before, after YARD/MismatchName: - Enabled: false + Exclude: + - 'lib/solargraph/pin/reference.rb' YARD/TagTypeSyntax: - Exclude: - - 'lib/solargraph/api_map/constants.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https Layout/LineLength: - Max: 244 + Max: 224 diff --git a/Gemfile b/Gemfile index 3a5ae2b84..fafce06ec 100755 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,9 @@ -source 'https://rubygems.org' - -gemspec name: 'solargraph' - -# Local gemfile for development tools, etc. -local_gemfile = File.expand_path(".Gemfile", __dir__) -instance_eval File.read local_gemfile if File.exist? local_gemfile +# frozen_string_literal: true + +source 'https://rubygems.org' + +gemspec name: 'solargraph' + +# Local gemfile for development tools, etc. +local_gemfile = File.expand_path('.Gemfile', __dir__) +instance_eval File.read local_gemfile if File.exist? local_gemfile diff --git a/README.md b/README.md index 7f344c712..3289358bd 100755 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ Plug-ins and extensions are available for the following editors: Solargraph's behavior can be controlled via optional [configuration](https://solargraph.org/guides/configuration) files. The highest priority file is a `.solargraph.yml` file at the root of the project. If not present, any global configuration at `~/.config/solargraph/config.yml` will apply. The path to the global configuration can be overridden with the `SOLARGRAPH_GLOBAL_CONFIG` environment variable. +Use `bundle exec solargraph config` to create a configuration file. + ### Plugins Solargraph supports [plugins](https://solargraph.org/guides/plugins) that implement their own Solargraph features, such as diagnostics reporters and conventions to provide LSP features and type-checking, e.g. for frameworks which use metaprogramming and/or DSLs. @@ -132,9 +134,7 @@ See [https://solargraph.org/guides](https://solargraph.org/guides) for more tips ### Development -To see more logging when typechecking or running specs, set the -`SOLARGRAPH_LOG` environment variable to `debug` or `info`. `warn` is -the default value. +To see more logging when typechecking or running specs, set the `SOLARGRAPH_LOG` environment variable to `debug` or `info`. `warn` is the default value. Code contributions are always appreciated. Feel free to fork the repo and submit pull requests. Check for open issues that could use help. Start new issues to discuss changes that have a major impact on the code or require large time commitments. diff --git a/Rakefile b/Rakefile index c83d9ab6b..398957b1a 100755 --- a/Rakefile +++ b/Rakefile @@ -1,137 +1,140 @@ -require 'rake' -require 'bundler/gem_tasks' -require 'fileutils' -require 'open3' - -desc "Open a Pry session preloaded with this library" -task :console do - sh "pry -I lib -r solargraph.rb" -end - -desc "Run the type checker" -task typecheck: [:typecheck_strong] - -desc "Run the type checker at typed level - return code issues provable without annotations being correct" -task :typecheck_typed do - sh "SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level typed" -end - -desc "Run the type checker at strict level - report issues using type annotations" -task :typecheck_strict do - sh "SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strict" -end - -desc "Run the type checker at strong level - enforce that type annotations exist" -task :typecheck_strong do - sh "SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strong" -end - -desc "Run the type checker at alpha level - run high-false-alarm checks" -task :typecheck_alpha do - sh "SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level alpha" -end - -desc "Run RSpec tests, starting with the ones that failed last time" -task spec: %i[spec_failed undercover_no_fail full_spec] do - undercover -end - -desc "Run all RSpec tests" -task :full_spec do - warn 'starting spec' - sh 'TEST_COVERAGE_COMMAND_NAME=full-new bundle exec rspec' # --profile' - warn 'ending spec' - # move coverage/full-new to coverage/full on success so that we - # always have the last successful run's 'coverage info - FileUtils.rm_rf('coverage/full') - FileUtils.mv('coverage/full-new', 'coverage/full') -end - -# @sg-ignore #undercover return type could not be inferred -# @return [Process::Status] -def undercover - simplecov_collate - cmd = 'bundle exec undercover ' \ - '--simplecov coverage/combined/coverage.json ' \ - '--exclude-files "Rakefile,spec/*,spec/**/*,lib/solargraph/version.rb" ' \ - '--compare origin/master' - output, status = Bundler.with_unbundled_env do - Open3.capture2e(cmd) - end - puts output - $stdout.flush - status -rescue StandardError => e - warn "hit error: #{e.message}" - warn "Backtrace:\n#{e.backtrace.join("\n")}" - warn "output: #{output}" - puts "Flushing" - $stdout.flush - raise -end - -desc "Check PR coverage" -task :undercover do - raise "Undercover failed" unless undercover.success? -end - -desc "Branch-focused fast-feedback quality/spec/coverage checks" -task test: %i[overcommit spec typecheck] do - # do these in order - Rake::Task['typecheck_strict'].invoke - Rake::Task['typecheck_strong'].invoke - Rake::Task['typecheck_alpha'].invoke -end - -desc "Re-run failed specs. Add --fail-fast in your .rspec-local file if desired." -task :spec_failed do - # allow user to check out any persistent failures while looking for - # more in the whole test suite - sh 'TEST_COVERAGE_COMMAND_NAME=next-failure bundle exec rspec --only-failures || true' -end - -desc "Run undercover and show output without failing the task if it fails" -task :undercover_no_fail do - undercover -rescue StandardError - puts "Undercover failed, but continuing with other tasks." -end - -# @return [void] -def simplecov_collate - require 'simplecov' - require 'simplecov-lcov' - require 'undercover/simplecov_formatter' - - SimpleCov.collate(Dir["coverage/{next-failure,full,ad-hoc}/.resultset.json"]) do - cname = 'combined' - command_name cname - new_dir = File.join('coverage', cname) - coverage_dir new_dir - - formatter \ - SimpleCov::Formatter::MultiFormatter - .new([ - SimpleCov::Formatter::HTMLFormatter, - SimpleCov::Formatter::Undercover, - SimpleCov::Formatter::LcovFormatter - ]) - SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true - end - puts "Simplecov collated results into coverage/combined/.resultset.json" -rescue StandardError => e - puts "Simplecov collate failed: #{e.message}" -ensure - $stdout.flush -end - -desc 'Add incremental coverage for rapid iteration with undercover' -task :simplecov_collate do - simplecov_collate -end - -desc "Show quality checks on this development branch so far, including any staged files" -task :overcommit do - # OVERCOMMIT_DEBUG=1 will show more detail - sh 'SOLARGRAPH_ASSERTS=on bundle exec overcommit --run --diff origin/master' -end +# frozen_string_literal: true + +require 'rake' +require 'bundler/gem_tasks' +require 'fileutils' +require 'open3' + +desc 'Open a Pry session preloaded with this library' +task :console do + sh 'pry -I lib -r solargraph.rb' +end + +desc 'Run the type checker' +task typecheck: [:typecheck_strong] + +desc 'Run the type checker at typed level - return code issues provable without annotations being correct' +task :typecheck_typed do + sh 'SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level typed' +end + +desc 'Run the type checker at strict level - report issues using type annotations' +task :typecheck_strict do + sh 'SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strict' +end + +desc 'Run the type checker at strong level - enforce that type annotations exist' +task :typecheck_strong do + sh 'SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strong' +end + +desc 'Run the type checker at alpha level - run high-false-alarm checks' +task :typecheck_alpha do + sh 'SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level alpha' +end + +desc 'Run RSpec tests, starting with the ones that failed last time' +task spec: %i[spec_failed undercover_no_fail full_spec] do + undercover +end + +desc 'Run all RSpec tests' +task :full_spec do + warn 'starting spec' + sh 'TEST_COVERAGE_COMMAND_NAME=full-new bundle exec rspec' # --profile' + warn 'ending spec' + # move coverage/full-new to coverage/full on success so that we + # always have the last successful run's 'coverage info + FileUtils.rm_rf('coverage/full') + FileUtils.mv('coverage/full-new', 'coverage/full') +end + +# @sg-ignore #undercover return type could not be inferred +# @return [Process::Status] +def undercover + simplecov_collate + cmd = 'bundle exec undercover ' \ + '--simplecov coverage/combined/coverage.json ' \ + '--exclude-files "Rakefile,spec/*,spec/**/*,lib/solargraph/version.rb" ' \ + '--compare origin/master' + output, status = Bundler.with_unbundled_env do + Open3.capture2e(cmd) + end + puts output + $stdout.flush + status +rescue StandardError => e + warn "hit error: #{e.message}" + # @sg-ignore Need to add nil check here + warn "Backtrace:\n#{e.backtrace.join("\n")}" + warn "output: #{output}" + puts 'Flushing' + $stdout.flush + raise +end + +desc 'Check PR coverage' +task :undercover do + raise 'Undercover failed' unless undercover.success? +end + +desc 'Branch-focused fast-feedback quality/spec/coverage checks' +task test: %i[overcommit spec typecheck] do + # do these in order + Rake::Task['typecheck_strict'].invoke + Rake::Task['typecheck_strong'].invoke + Rake::Task['typecheck_alpha'].invoke +end + +desc 'Re-run failed specs. Add --fail-fast in your .rspec-local file if desired.' +task :spec_failed do + # allow user to check out any persistent failures while looking for + # more in the whole test suite + sh 'TEST_COVERAGE_COMMAND_NAME=next-failure bundle exec rspec --only-failures || true' +end + +desc 'Run undercover and show output without failing the task if it fails' +task :undercover_no_fail do + undercover +rescue StandardError + puts 'Undercover failed, but continuing with other tasks.' +end + +# @return [void] +def simplecov_collate + require 'simplecov' + require 'simplecov-lcov' + require 'undercover/simplecov_formatter' + + SimpleCov.collate(Dir['coverage/{next-failure,full,ad-hoc}/.resultset.json']) do + cname = 'combined' + command_name cname + new_dir = File.join('coverage', cname) + coverage_dir new_dir + + formatter \ + SimpleCov::Formatter::MultiFormatter + .new([ + SimpleCov::Formatter::HTMLFormatter, + SimpleCov::Formatter::Undercover, + SimpleCov::Formatter::LcovFormatter + ]) + SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true + end + puts 'Simplecov collated results into coverage/combined/.resultset.json' +rescue StandardError => e + puts "Simplecov collate failed: #{e.message}" +ensure + $stdout.flush +end + +desc 'Add incremental coverage for rapid iteration with undercover' +task :simplecov_collate do + simplecov_collate +end + +desc 'Show quality checks on this development branch so far, including any staged files' +task :overcommit do + # OVERCOMMIT_DEBUG=1 will show more detail + sh 'SOLARGRAPH_ASSERTS=on bundle exec overcommit --run --diff origin/master' +end diff --git a/bin/solargraph b/bin/solargraph index 248dc42fd..cd5341820 100755 --- a/bin/solargraph +++ b/bin/solargraph @@ -1,7 +1,8 @@ #!/usr/bin/env ruby +# frozen_string_literal: true # turn off warning diagnostics from Ruby -$VERBOSE=nil +$VERBOSE = nil require 'solargraph' diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 038e7bccf..544a4e2e2 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -55,8 +55,8 @@ class InvalidRubocopVersionError < RuntimeError; end CHDIR_MUTEX = Mutex.new - # @param type [Symbol] Type of assert. - def self.asserts_on?(type) + def self.asserts_on? + # @sg-ignore Translate to something flow sensitive typing understands if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? false elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' @@ -71,8 +71,29 @@ def self.asserts_on?(type) # @param msg [String, nil] An optional message to log # @param block [Proc] A block that returns a message to log # @return [void] - def self.assert_or_log(type, msg = nil, &block) - raise (msg || block.call) if asserts_on?(type) && ![:combine_with_visibility].include?(type) + def self.assert_or_log type, msg = nil, &block + if asserts_on? + # @type [String, nil] + msg ||= block.call + + raise "No message given for #{type.inspect}" if msg.nil? + + # conditional aliases to handle compatibility corner cases + # @sg-ignore flow sensitive typing needs to handle 'raise if' + return if type == :alias_target_missing && msg.include?('highline/compatibility.rb') + # @sg-ignore flow sensitive typing needs to handle 'raise if' + return if type == :alias_target_missing && msg.include?('lib/json/add/date.rb') + # @todo :combine_with_visibility is not ready for prime time - + # lots of disagreements found in practice that heuristics need + # to be created for and/or debugging needs to resolve in pin + # generation. + # @todo :api_map_namespace_pin_stack triggers in a badly handled + # self type case - 'keeps track of self type in method + # parameters in subclass' in call_spec.rb + return if %i[api_map_namespace_pin_stack combine_with_visibility].include?(type) + + raise msg + end logger.info msg, &block end @@ -92,10 +113,10 @@ def self.logger # @return [generic] def self.with_clean_env &block meth = if Bundler.respond_to?(:with_original_env) - :with_original_env - else - :with_clean_env - end + :with_original_env + else + :with_clean_env + end Bundler.send meth, &block end end diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 6c0da316e..88e82a692 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -24,12 +24,26 @@ class ApiMap attr_reader :missing_docs # @param pins [Array] - def initialize pins: [] + # @param loose_unions [Boolean] if true, a potential type can be + # inferred if ANY of the UniqueTypes in the base chain's + # ComplexType match it. If false, every single UniqueTypes in + # the base must be ALL able to independently provide this + # type. The former is useful during completion, but the + # latter is best for typechecking at higher levels. + # + def initialize pins: [], loose_unions: true @source_map_hash = {} @cache = Cache.new + @loose_unions = loose_unions index pins end + # @param out [StringIO, IO, nil] output stream for logging + # @return [void] + def self.reset_core out: nil + @@core_map = RbsMap::CoreMap.new + end + # # This is a mutable object, which is cached in the Chain class - # if you add any fields which change the results of calls (not @@ -37,21 +51,24 @@ def initialize pins: [] # # @param other [Object] - def eql?(other) + def eql? other self.class == other.class && - # @sg-ignore Flow sensitive typing needs to handle self.class == other.class + # @sg-ignore flow sensitive typing needs to handle self.class == other.class equality_fields == other.equality_fields end # @param other [Object] - def ==(other) - self.eql?(other) + def == other + eql?(other) end + # @return [Integer] def hash equality_fields.hash end + attr_reader :loose_unions + def to_s self.class.to_s end @@ -98,11 +115,11 @@ def catalog bench end unresolved_requires = (bench.external_requires + conventions_environ.requires + bench.workspace.config.required).to_a.compact.uniq recreate_docmap = @unresolved_requires != unresolved_requires || - @doc_map&.uncached_yard_gemspecs&.any? || - @doc_map&.uncached_rbs_collection_gemspecs&.any? || - @doc_map&.rbs_collection_path != bench.workspace.rbs_collection_path + workspace.rbs_collection_path != bench.workspace.rbs_collection_path || + @doc_map.any_uncached? + if recreate_docmap - @doc_map = DocMap.new(unresolved_requires, [], bench.workspace) # @todo Implement gem preferences + @doc_map = DocMap.new(unresolved_requires, bench.workspace, out: nil) # @todo Implement gem preferences @unresolved_requires = @doc_map.unresolved_requires end @cache.clear if store.update(@@core_map.pins, @doc_map.pins, conventions_environ.pins, iced_pins, live_pins) @@ -110,31 +127,14 @@ def catalog bench self end - # @todo need to model type def statement in chains as a symbol so - # that this overload of 'protected' will typecheck @sg-ignore - # @sg-ignore - protected def equality_fields - [self.class, @source_map_hash, conventions_environ, @doc_map, @unresolved_requires] - end - # @return [DocMap] def doc_map - @doc_map ||= DocMap.new([], []) + @doc_map ||= DocMap.new([], Workspace.new('.')) end # @return [::Array] def uncached_gemspecs - @doc_map&.uncached_gemspecs || [] - end - - # @return [::Array] - def uncached_rbs_collection_gemspecs - @doc_map.uncached_rbs_collection_gemspecs - end - - # @return [::Array] - def uncached_yard_gemspecs - @doc_map.uncached_yard_gemspecs + doc_map.uncached_gemspecs || [] end # @return [Enumerable] @@ -142,9 +142,10 @@ def core_pins @@core_map.pins end - # @param name [String] + # @param name [String, nil] # @return [YARD::Tags::MacroDirective, nil] def named_macro name + # @sg-ignore Need to add nil check here store.named_macros[name] end @@ -180,10 +181,11 @@ def clip_at filename, position # Create an ApiMap with a workspace in the specified directory. # # @param directory [String] + # @param loose_unions [Boolean] See #initialize # # @return [ApiMap] - def self.load directory - api_map = new + def self.load directory, loose_unions: true + api_map = new(loose_unions: loose_unions) workspace = Solargraph::Workspace.new(directory) # api_map.catalog Bench.new(workspace: workspace) library = Library.new(workspace) @@ -192,18 +194,19 @@ def self.load directory api_map end - # @param out [IO, nil] + # @param out [StringIO, IO, nil] + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached # @return [void] - def cache_all!(out) - @doc_map.cache_all!(out) + def cache_all_for_doc_map! out: $stderr, rebuild: false + doc_map.cache_doc_map_gems!(out, rebuild: rebuild) end # @param gemspec [Gem::Specification] # @param rebuild [Boolean] - # @param out [IO, nil] + # @param out [StringIO, IO, nil] # @return [void] - def cache_gem(gemspec, rebuild: false, out: nil) - @doc_map.cache(gemspec, rebuild: rebuild, out: out) + def cache_gem gemspec, rebuild: false, out: nil + doc_map.cache(gemspec, rebuild: rebuild, out: out) end class << self @@ -215,18 +218,19 @@ class << self # # # @param directory [String] - # @param out [IO] The output stream for messages + # @param out [IO, StringIO, nil] The output stream for messages + # @param loose_unions [Boolean] See #initialize # # @return [ApiMap] - def self.load_with_cache directory, out - api_map = load(directory) + def self.load_with_cache directory, out = $stderr, loose_unions: true + api_map = load(directory, loose_unions: loose_unions) if api_map.uncached_gemspecs.empty? logger.info { "All gems cached for #{directory}" } return api_map end - api_map.cache_all!(out) - load(directory) + api_map.cache_all_for_doc_map!(out: out) + load(directory, loose_unions: loose_unions) end # @return [Array] @@ -306,19 +310,19 @@ def resolve name, *gates # # @param pin [Pin::Reference] # @return [String, nil] - def dereference(pin) + def dereference pin store.constants.dereference(pin) end # @param fqns [String] # @return [Array] - def get_extends(fqns) + def get_extends fqns store.get_extends(fqns) end # @param fqns [String] # @return [Array] - def get_includes(fqns) + def get_includes fqns store.get_includes(fqns) end @@ -328,29 +332,47 @@ def get_includes(fqns) # @param namespace [String] A fully qualified namespace # @param scope [Symbol] :instance or :class # @return [Array] - def get_instance_variable_pins(namespace, scope = :instance) + def get_instance_variable_pins namespace, scope = :instance result = [] - used = [namespace] + [namespace] result.concat store.get_instance_variables(namespace, scope) sc_fqns = namespace while (sc = store.get_superclass(sc_fqns)) + # @sg-ignore flow sensitive typing needs to handle "if foo = bar" sc_fqns = store.constants.dereference(sc) result.concat store.get_instance_variables(sc_fqns, scope) end result end - # @sg-ignore Missing @return tag for Solargraph::ApiMap#visible_pins - # @see Solargraph::Parser::FlowSensitiveTyping#visible_pins - def visible_pins(*args, **kwargs, &blk) - Solargraph::Parser::FlowSensitiveTyping.visible_pins(*args, **kwargs, &blk) + # Find a variable pin by name and where it is used. + # + # Resolves our most specific view of this variable's type by + # preferring pins created by flow-sensitive typing when we have + # them based on the Closure and Location. + # + # @param candidates [Array] + # @param name [String] + # @param closure [Pin::Closure] + # @param location [Location] + # + # @return [Pin::BaseVariable, nil] + def var_at_location candidates, name, closure, location + with_correct_name = candidates.select { |pin| pin.name == name } + vars_at_location = with_correct_name.reject do |pin| + # visible_at? excludes the starting position, but we want to + # include it for this purpose + !pin.visible_at?(closure, location) && !pin.starts_at?(location) + end + + vars_at_location.inject(&:combine_with) end # Get an array of class variable pins for a namespace. # # @param namespace [String] A fully qualified namespace # @return [Enumerable] - def get_class_variable_pins(namespace) + def get_class_variable_pins namespace prefer_non_nil_variables(store.get_class_variables(namespace)) end @@ -417,7 +439,7 @@ def get_methods rooted_tag, scope: :instance, visibility: [:public], deep: true comments: init_pin.comments, closure: init_pin.closure, source: init_pin.source, - type_location: init_pin.type_location, + type_location: init_pin.type_location ) new_pin.parameters = init_pin.parameters.map do |init_param| param = init_param.clone @@ -476,7 +498,7 @@ def get_complex_type_methods complex_type, context = '', internal = false result = Set.new complex_type.each do |type| if type.duck_type? - result.add Pin::DuckMethod.new(name: type.to_s[1..-1], source: :api_map) + result.add Pin::DuckMethod.new(name: type.to_s[1..], source: :api_map) result.merge get_methods('Object') else unless type.nil? || type.name == 'void' @@ -507,12 +529,14 @@ def get_complex_type_methods complex_type, context = '', internal = false # @param preserve_generics [Boolean] True to preserve any # unresolved generic parameters, false to erase them # @return [Array] - def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], preserve_generics: false + def get_method_stack rooted_tag, name, scope: :instance, visibility: %i[private protected public], + preserve_generics: false rooted_type = ComplexType.parse(rooted_tag) fqns = rooted_type.namespace namespace_pin = store.get_path_pins(fqns).first methods = if namespace_pin.is_a?(Pin::Constant) - type = namespace_pin.infer(self) + type = namespace_pin.typify(self) + type = namespace_pin.probe(self) unless type.defined? if type.defined? namespace_pin = store.get_path_pins(type.namespace).first get_methods(type.namespace, scope: scope, visibility: visibility).select { |p| p.name == name } @@ -589,6 +613,7 @@ def locate_pins location # @param cursor [Source::Cursor] # @return [SourceMap::Clip] def clip cursor + # @sg-ignore Need to add nil check here raise FileNotFoundError, "ApiMap did not catalog #{cursor.filename}" unless source_map_hash.key?(cursor.filename) SourceMap::Clip.new(self, cursor) @@ -630,18 +655,22 @@ def bundled? filename # @param sup [String] The superclass # @param sub [String] The subclass # @return [Boolean] - def super_and_sub?(sup, sub) + def super_and_sub? sup, sub sup = ComplexType.try_parse(sup) sub = ComplexType.try_parse(sub) # @todo If two literals are different values of the same type, it would # make more sense for super_and_sub? to return true, but there are a # few callers that currently expect this to be false. + # @sg-ignore flow-sensitive typing should be able to handle redefinition return false if sup.literal? && sub.literal? && sup.to_s != sub.to_s + # @sg-ignore flow sensitive typing should be able to handle redefinition sup = sup.simplify_literals.to_s + # @sg-ignore flow sensitive typing should be able to handle redefinition sub = sub.simplify_literals.to_s return true if sup == sub sc_fqns = sub while (sc = store.get_superclass(sc_fqns)) + # @sg-ignore flow sensitive typing needs to handle "if foo = bar" sc_new = store.constants.dereference(sc) # Cyclical inheritance is invalid return false if sc_new == sc_fqns @@ -658,24 +687,32 @@ def super_and_sub?(sup, sub) # @param module_ns [String] The module namespace (no type parameters) # # @return [Boolean] - def type_include?(host_ns, module_ns) + def type_include? host_ns, module_ns store.get_includes(host_ns).map { |inc_tag| inc_tag.type.name }.include?(module_ns) end # @param pins [Enumerable] # @param visibility [Enumerable] # @return [Array] - def resolve_method_aliases pins, visibility = [:public, :private, :protected] + def resolve_method_aliases pins, visibility = %i[public private protected] with_resolved_aliases = pins.map do |pin| next pin unless pin.is_a?(Pin::MethodAlias) resolved = resolve_method_alias(pin) + # @sg-ignore Need to add nil check here next nil if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility) resolved end.compact - logger.debug { "ApiMap#resolve_method_aliases(pins=#{pins.map(&:name)}, visibility=#{visibility}) => #{with_resolved_aliases.map(&:name)}" } + logger.debug do + "ApiMap#resolve_method_aliases(pins=#{pins.map(&:name)}, visibility=#{visibility}) => #{with_resolved_aliases.map(&:name)}" + end GemPins.combine_method_pins_by_path(with_resolved_aliases) end + # @return [Workspace] + def workspace + doc_map.workspace + end + # @param fq_reference_tag [String] A fully qualified whose method should be pulled in # @param namespace_pin [Pin::Base] Namespace pin for the rooted_type # parameter - used to pull generics information @@ -687,7 +724,7 @@ def resolve_method_aliases pins, visibility = [:public, :private, :protected] # @param skip [Set] # @param no_core [Boolean] Skip core classes if true # @return [Array] - def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scope, visibility, deep, skip, no_core) + def inner_get_methods_from_reference fq_reference_tag, namespace_pin, type, scope, visibility, deep, skip, no_core logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) starting" } # Ensure the types returned by the methods in the referenced @@ -743,7 +780,7 @@ def store def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false rooted_type = ComplexType.parse(rooted_tag).force_rooted fqns = rooted_type.namespace - fqns_generic_params = rooted_type.all_params + rooted_type.all_params namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first return [] if no_core && fqns =~ /^(Object|BasicObject|Class|Module)$/ reqstr = "#{fqns}|#{scope}|#{visibility.sort}|#{deep}" @@ -754,7 +791,9 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false # ensure we start out with any immediate methods in this # namespace so we roughly match the same ordering of get_methods # and obey the 'deep' instruction - direct_convention_methods, convention_methods_by_reference = environ.pins.partition { |p| p.namespace == rooted_tag } + direct_convention_methods, convention_methods_by_reference = environ.pins.partition do |p| + p.namespace == rooted_tag + end result.concat direct_convention_methods if deep && scope == :instance @@ -766,8 +805,10 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false # Store#get_methods doesn't know about full tags, just # namespaces; resolving the generics in the method pins is this # class' responsibility - methods = store.get_methods(fqns, scope: scope, visibility: visibility).sort{ |a, b| a.name <=> b.name } - logger.info { "ApiMap#inner_get_methods(rooted_tag=#{rooted_tag.inspect}, scope=#{scope.inspect}, visibility=#{visibility.inspect}, deep=#{deep.inspect}, skip=#{skip.inspect}, fqns=#{fqns}) - added from store: #{methods}" } + methods = store.get_methods(fqns, scope: scope, visibility: visibility).sort { |a, b| a.name <=> b.name } + logger.info do + "ApiMap#inner_get_methods(rooted_tag=#{rooted_tag.inspect}, scope=#{scope.inspect}, visibility=#{visibility.inspect}, deep=#{deep.inspect}, skip=#{skip.inspect}, fqns=#{fqns}) - added from store: #{methods}" + end result.concat methods if deep result.concat convention_methods_by_reference @@ -775,21 +816,27 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false if scope == :instance store.get_includes(fqns).reverse.each do |ref| in_tag = dereference(ref) - result.concat inner_get_methods_from_reference(in_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true) + # @sg-ignore Need to add nil check here + 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? - result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, no_core) + result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, + visibility, true, skip, no_core) end else - logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}, #{skip}) - looking for get_extends() from #{fqns}" } + logger.info do + "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}, #{skip}) - looking for get_extends() from #{fqns}" + end store.get_extends(fqns).reverse.each do |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) unless rooted_sc_tag.nil? - result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, true) + result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, + visibility, true, skip, true) end unless no_core || fqns.empty? type = get_namespace_type(fqns) @@ -817,7 +864,7 @@ def path_macros def get_namespace_type fqns return nil if fqns.nil? # @type [Pin::Namespace, nil] - pin = store.get_path_pins(fqns).select{|p| p.is_a?(Pin::Namespace)}.first + pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first return nil if pin.nil? pin.type end @@ -841,18 +888,21 @@ def prefer_non_nil_variables pins include Logging - private - # @param alias_pin [Pin::MethodAlias] # @return [Pin::Method, nil] - def resolve_method_alias(alias_pin) + def resolve_method_alias alias_pin ancestors = store.get_ancestors(alias_pin.full_context.reduce_class_type.tag) + # @type [Pin::Method, nil] original = nil # Search each ancestor for the original method ancestors.each do |ancestor_fqns| next if ancestor_fqns.nil? - ancestor_method_path = "#{ancestor_fqns}#{alias_pin.scope == :instance ? '#' : '.'}#{alias_pin.original}" + ancestor_method_path = if alias_pin.original == 'new' && alias_pin.scope == :class + "#{ancestor_fqns}#initialize" + else + "#{ancestor_fqns}#{alias_pin.scope == :instance ? '#' : '.'}#{alias_pin.original}" + end # Search for the original method in the ancestor original = store.get_path_pins(ancestor_method_path).find do |candidate_pin| @@ -864,21 +914,29 @@ def resolve_method_alias(alias_pin) break resolved if resolved end - candidate_pin.is_a?(Pin::Method) && candidate_pin.scope == alias_pin.scope + candidate_pin.is_a?(Pin::Method) end break if original end + if original.nil? + # :nocov: + Solargraph.assert_or_log(:alias_target_missing) do + "Rejecting alias - target is missing while looking for #{alias_pin.full_context.tag} #{alias_pin.original} in #{alias_pin.scope} scope = #{alias_pin.inspect}" + end + return nil + # :nocov: + end # @sg-ignore ignore `received nil` for original - create_resolved_alias_pin(alias_pin, original) if original + create_resolved_alias_pin(alias_pin, original) end # Fast path for creating resolved alias pins without individual method stack lookups # @param alias_pin [Pin::MethodAlias] The alias pin to resolve # @param original [Pin::Method] The original method pin that was already found # @return [Pin::Method] The resolved method pin - def create_resolved_alias_pin(alias_pin, original) + def create_resolved_alias_pin alias_pin, original # Build the resolved method pin directly (same logic as resolve_method_alias but without lookup) args = { location: alias_pin.location, @@ -894,7 +952,7 @@ def create_resolved_alias_pin(alias_pin, original) return_type: original.return_type, source: :resolve_method_alias } - resolved_pin = Pin::Method.new **args + resolved_pin = Pin::Method.new(**args) # Clone signatures and parameters resolved_pin.signatures.each do |sig| @@ -916,7 +974,7 @@ def create_resolved_alias_pin(alias_pin, original) # @param rooted_type [ComplexType] # @param pins [Enumerable] # @return [Array] - def erase_generics(namespace_pin, rooted_type, pins) + def erase_generics namespace_pin, rooted_type, pins return pins unless should_erase_generics_when_done?(namespace_pin, rooted_type) logger.debug("Erasing generics on namespace_pin=#{namespace_pin} / rooted_type=#{rooted_type}") @@ -927,19 +985,29 @@ def erase_generics(namespace_pin, rooted_type, pins) # @param namespace_pin [Pin::Namespace] # @param rooted_type [ComplexType] - def should_erase_generics_when_done?(namespace_pin, rooted_type) + def should_erase_generics_when_done? namespace_pin, rooted_type has_generics?(namespace_pin) && !can_resolve_generics?(namespace_pin, rooted_type) end # @param namespace_pin [Pin::Namespace, Pin::Constant] - def has_generics?(namespace_pin) + def has_generics? namespace_pin namespace_pin.is_a?(Pin::Namespace) && !namespace_pin.generics.empty? end # @param namespace_pin [Pin::Namespace] # @param rooted_type [ComplexType] - def can_resolve_generics?(namespace_pin, rooted_type) + def can_resolve_generics? namespace_pin, rooted_type has_generics?(namespace_pin) && !rooted_type.all_params.empty? end + + protected + + # @todo need to model type def statement in chains as a symbol so + # that this overload of 'protected' will typecheck @sg-ignore + # @sg-ignore + def equality_fields + [self.class, @source_map_hash, conventions_environ, @doc_map, @unresolved_requires, @missing_docs, + @loose_unions] + end end end diff --git a/lib/solargraph/api_map/cache.rb b/lib/solargraph/api_map/cache.rb index 0052d91ea..c69d223b4 100644 --- a/lib/solargraph/api_map/cache.rb +++ b/lib/solargraph/api_map/cache.rb @@ -8,7 +8,7 @@ def initialize @methods = {} # @type [Hash{String, Array => Array}] @constants = {} - # @type [Hash{String => String}] + # @type [Hash{String => String, nil}] @qualified_namespaces = {} # @type [Hash{String => Pin::Method}] @receiver_definitions = {} @@ -61,14 +61,14 @@ def set_constants namespace, contexts, value # @param name [String] # @param context [String] - # @return [String] + # @return [String, nil] def get_qualified_namespace name, context @qualified_namespaces["#{name}|#{context}"] end # @param name [String] # @param context [String] - # @param value [String] + # @param value [String, nil] # @return [void] def set_qualified_namespace name, context, value @qualified_namespaces["#{name}|#{context}"] = value diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 8dcaf1945..aeda4352c 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -27,9 +27,11 @@ def initialize store # @param name [String] Namespace which may relative and not be rooted. # @param gates [Array, String>] Namespaces to search while resolving the name # + # @sg-ignore flow sensitive typing needs to eliminate literal from union with return if foo == :bar # @return [String, nil] fully qualified namespace (i.e., is # absolute, but will not start with ::) def resolve(name, *gates) + # @sg-ignore Need to add nil check here return store.get_path_pins(name[2..]).first&.path if name.start_with?('::') flat = gates.flatten @@ -86,6 +88,7 @@ def qualify_type type, *gates return unless fqns pin = store.get_path_pins(fqns).first if pin.is_a?(Pin::Constant) + # @sg-ignore Need to add nil check here const = Solargraph::Parser::NodeMethods.unpack_name(pin.assignment) return unless const fqns = resolve(const, *pin.gates) @@ -105,6 +108,7 @@ def clear # @param name [String] # @param gates [Array] + # @sg-ignore flow sensitive typing should be able to handle redefinition # @return [String, nil] def resolve_and_cache name, gates cached_resolve[[name, gates]] = :in_process @@ -125,6 +129,7 @@ def resolve_uncached name, gates if resolved base = [resolved] else + # @sg-ignore flow sensitive typing needs better handling of ||= on lvars return resolve(name, first) unless first.empty? end end @@ -138,7 +143,7 @@ def resolve_uncached name, gates # @param name [String] # @param gates [Array] # @param internal [Boolean] True if the name is not the last in the namespace - # @return [Array(Object, Array)] + # @return [Array(String, Array), Array(nil, Array), String] def complex_resolve name, gates, internal resolved = nil gates.each.with_index do |gate, idx| @@ -165,6 +170,7 @@ def simple_resolve name, gate, internal here = "#{gate}::#{name}".sub(/^::/, '').sub(/::$/, '') pin = store.get_path_pins(here).first if pin.is_a?(Pin::Constant) && internal + # @sg-ignore Need to add nil check here const = Solargraph::Parser::NodeMethods.unpack_name(pin.assignment) return unless const resolve(const, pin.gates) @@ -197,13 +203,14 @@ def cached_collect # will start the search in the specified context until it finds a # match for the namespace. # - # @param namespace [String, nil] The namespace to + # @param namespace [String] The namespace to # match # @param context_namespace [String] The context namespace in which the # tag was referenced; start from here to resolve the name # @return [String, nil] fully qualified namespace def qualify_namespace namespace, context_namespace = '' if namespace.start_with?('::') + # @sg-ignore Need to add nil check here inner_qualify(namespace[2..], '', Set.new) else inner_qualify(namespace, context_namespace, Set.new) @@ -249,7 +256,7 @@ def inner_qualify name, root, skip end end - # @param fqns [String] + # @param fqns [String, nil] # @param visibility [Array] # @param skip [Set] # @return [Array] @@ -259,17 +266,20 @@ def inner_get_constants fqns, visibility, skip result = [] store.get_prepends(fqns).each do |pre| + # @sg-ignore Need to add nil check here pre_fqns = resolve(pre.name, pre.closure.gates - skip.to_a) result.concat inner_get_constants(pre_fqns, [:public], skip) end result.concat(store.get_constants(fqns, visibility).sort { |a, b| a.name <=> b.name }) store.get_includes(fqns).each do |pin| + # @sg-ignore Need to add nil check here inc_fqns = resolve(pin.name, pin.closure.gates - skip.to_a) result.concat inner_get_constants(inc_fqns, [:public], skip) end sc_ref = store.get_superclass(fqns) if sc_ref fqsc = dereference(sc_ref) + # @sg-ignore Need to add nil check here result.concat inner_get_constants(fqsc, [:public], skip) unless %w[Object BasicObject].include?(fqsc) end result diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index 944f02c79..df3e6984e 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -40,13 +40,13 @@ def path_pin_hash # @param klass [Class>] # @return [Set>] def pins_by_class klass - # @type [Set] + # @type [Set>] s = Set.new # @sg-ignore need to support destructured args in blocks @pin_select_cache[klass] ||= pin_class_hash.each_with_object(s) { |(key, o), n| n.merge(o) if key <= klass } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def include_references # @param h [String] # @param k [Array] @@ -60,21 +60,21 @@ def include_reference_pins @include_reference_pins ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @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}] + # @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}] + # @return [Hash{String => Array}] def superclass_references # @param h [String] # @param k [Array] @@ -118,15 +118,15 @@ def catalog new_pins # @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 @@ -138,7 +138,7 @@ def catalog new_pins # @generic T # @param klass [Class>] - # @param hash [Hash{String => generic}] + # @param hash [Hash{String => Array>}] # # @return [void] def map_references klass, hash @@ -150,26 +150,32 @@ def map_references klass, hash # @return [void] def map_overrides + # @todo should complain when type for 'ovr' is not provided # @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] logger.debug { "ApiMap::Index#map_overrides: pins for path=#{ovr.name}: #{pins}" } pins.each do |pin| - new_pin = if pin.path.end_with?('#initialize') - path_pin_hash[pin.path.sub(/#initialize/, '.new')].first - end + new_pin = (path_pin_hash[pin.path.sub('#initialize', '.new')].first if pin.path.end_with?('#initialize')) (ovr.tags.map(&:tag_name) + ovr.delete).uniq.each do |tag| + # @sg-ignore Wrong argument type for + # YARD::Docstring#delete_tags: name expected String, + # received String, Symbol - delete_tags is ok with a + # _ToS, but we should fix anyway pin.docstring.delete_tags tag - new_pin.docstring.delete_tags tag if new_pin + new_pin&.docstring&.delete_tags tag end ovr.tags.each do |tag| pin.docstring.add_tag(tag) redefine_return_type pin, tag - if new_pin - new_pin.docstring.add_tag(tag) - redefine_return_type new_pin, tag - end + pin.reset_generated! + + next unless new_pin + + new_pin.docstring.add_tag(tag) + redefine_return_type new_pin, tag + new_pin.reset_generated! end end end @@ -186,7 +192,6 @@ def redefine_return_type pin, tag pin.signatures.each do |sig| sig.instance_variable_set(:@return_type, ComplexType.try_parse(tag.type)) end - pin.reset_generated! end end end diff --git a/lib/solargraph/api_map/source_to_yard.rb b/lib/solargraph/api_map/source_to_yard.rb index 55452582b..a121a348b 100644 --- a/lib/solargraph/api_map/source_to_yard.rb +++ b/lib/solargraph/api_map/source_to_yard.rb @@ -3,16 +3,18 @@ module Solargraph class ApiMap module SourceToYard - # Get the YARD CodeObject at the specified path. # + # @sg-ignore Declared return type generic, nil does not match + # inferred type ::YARD::CodeObjects::Base, nil for + # Solargraph::ApiMap::SourceToYard#code_object_at # @generic T # @param path [String] # @param klass [Class>] # @return [generic, nil] def code_object_at path, klass = YARD::CodeObjects::Base obj = code_object_map[path] - obj if obj&.is_a?(klass) + obj if obj.is_a?(klass) end # @return [Array] @@ -33,16 +35,20 @@ def rake_yard store 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| + code_object_map[pin.path] ||= YARD::CodeObjects::ClassObject.new(root_code_object, pin.path) do |obj| + # @sg-ignore flow sensitive typing needs to handle attrs next if pin.location.nil? || pin.location.filename.nil? + # @sg-ignore flow sensitive typing needs to handle attrs obj.add_file(pin.location.filename, pin.location.range.start.line, !pin.comments.empty?) - } + end else # @param obj [YARD::CodeObjects::RootObject] - code_object_map[pin.path] ||= YARD::CodeObjects::ModuleObject.new(root_code_object, pin.path) { |obj| + code_object_map[pin.path] ||= YARD::CodeObjects::ModuleObject.new(root_code_object, pin.path) do |obj| + # @sg-ignore flow sensitive typing needs to handle attrs next if pin.location.nil? || pin.location.filename.nil? + # @sg-ignore flow sensitive typing needs to handle attrs obj.add_file(pin.location.filename, pin.location.range.start.line, !pin.comments.empty?) - } + end end code_object_map[pin.path].docstring = pin.docstring store.get_includes(pin.path).each do |ref| @@ -57,7 +63,6 @@ def rake_yard store 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 extend_object.instance_mixins.push code_object end end @@ -67,14 +72,22 @@ def rake_yard store next end + # @sg-ignore Need to add nil check here # @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| + code_object_map[pin.path] ||= YARD::CodeObjects::MethodObject.new( + code_object_at(pin.namespace, YARD::CodeObjects::NamespaceObject), pin.name, pin.scope + ) do |obj| + # @sg-ignore flow sensitive typing needs to handle attrs next if pin.location.nil? || pin.location.filename.nil? + # @sg-ignore flow sensitive typing needs to handle attrs obj.add_file pin.location.filename, pin.location.range.start.line - } + end method_object = code_object_at(pin.path, YARD::CodeObjects::MethodObject) + # @sg-ignore Need to add nil check here method_object.docstring = pin.docstring + # @sg-ignore Need to add nil check here method_object.visibility = pin.visibility || :public + # @sg-ignore Need to add nil check here method_object.parameters = pin.parameters.map do |p| [p.full_name, p.asgn_code] end diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index bc829ba5a..29b58bb1a 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -32,15 +32,16 @@ def update *pinsets # @todo Fix this map @fqns_pins_map = nil - return catalog(pinsets) if changed == 0 + return catalog(pinsets) if changed.zero? + # @sg-ignore Need to add nil check here pinsets[changed..].each_with_index do |pins, idx| @pinsets[changed + idx] = pins @indexes[changed + idx] = if pins.empty? - @indexes[changed + idx - 1] - else - @indexes[changed + idx - 1].merge(pins) - end + @indexes[changed + idx - 1] + else + @indexes[changed + idx - 1].merge(pins) + end end constants.clear cached_qualify_superclass.clear @@ -59,10 +60,10 @@ def inspect # @param visibility [Array] # @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 + namespace_children(fqns).select do |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 end # @param fqns [String] @@ -71,17 +72,18 @@ 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) end - BOOLEAN_SUPERCLASS_PIN = Pin::Reference::Superclass.new(name: 'Boolean', closure: Pin::ROOT_PIN, source: :solargraph) - OBJECT_SUPERCLASS_PIN = Pin::Reference::Superclass.new(name: 'Object', closure: Pin::ROOT_PIN, source: :solargraph) + BOOLEAN_SUPERCLASS_PIN = Pin::Reference::Superclass.new(name: 'Boolean', closure: Pin::ROOT_PIN, + source: :solargraph) + OBJECT_SUPERCLASS_PIN = Pin::Reference::Superclass.new(name: 'Object', closure: Pin::ROOT_PIN, + source: :solargraph) - # @param fqns [String] - # @return [Pin::Reference::Superclass] + # @param fqns [String, nil] + # @return [Pin::Reference::Superclass, nil] def get_superclass fqns return nil if fqns.nil? || fqns.empty? return BOOLEAN_SUPERCLASS_PIN if %w[TrueClass FalseClass].include?(fqns) @@ -126,20 +128,20 @@ def get_path_pins path index.path_pin_hash[path] end - # @param fqns [String] + # @param fqns [String, nil] # @param scope [Symbol] :class or :instance # @return [Enumerable] - def get_instance_variables(fqns, scope = :instance) - all_instance_variables.select { |pin| + def get_instance_variables fqns, scope = :instance + all_instance_variables.select do |pin| pin.binder.namespace == fqns && pin.binder.scope == scope - } + end end # @param fqns [String] # # @return [Enumerable] - def get_class_variables(fqns) - namespace_children(fqns).select { |pin| pin.is_a?(Pin::ClassVariable)} + def get_class_variables fqns + namespace_children(fqns).select { |pin| pin.is_a?(Pin::ClassVariable) } end # @return [Enumerable] @@ -149,7 +151,7 @@ def get_symbols # @param fqns [String] # @return [Boolean] - def namespace_exists?(fqns) + def namespace_exists? fqns fqns_pins(fqns).any? end @@ -165,7 +167,7 @@ def method_pins # @param fqns [String] # @return [Array] - def domains(fqns) + def domains fqns result = [] fqns_pins(fqns).each do |nspin| result.concat nspin.domains @@ -178,7 +180,7 @@ def named_macros @named_macros ||= begin result = {} pins.each do |pin| - pin.macros.select{|m| m.tag.tag_name == 'macro' && !m.tag.text.empty? }.each do |macro| + pin.macros.select { |m| m.tag.tag_name == 'macro' && !m.tag.text.empty? }.each do |macro| next if macro.tag.name.nil? || macro.tag.name.empty? result[macro.tag.name] = macro end @@ -199,7 +201,7 @@ def pins_by_class klass index.pins_by_class klass end - # @param fqns [String] + # @param fqns [String, nil] # @return [Array] def fqns_pins fqns return [] if fqns.nil? @@ -217,7 +219,7 @@ def fqns_pins fqns # Get all ancestors (superclasses, includes, prepends, extends) for a namespace # @param fqns [String] The fully qualified namespace # @return [Array] Array of ancestor namespaces including the original - def get_ancestors(fqns) + def get_ancestors fqns return [] if fqns.nil? || fqns.empty? ancestors = [fqns] @@ -244,8 +246,11 @@ def get_ancestors(fqns) next if refs.nil? # @param ref [String] refs.map(&:type).map(&:to_s).each do |ref| + # @sg-ignore flow sensitive typing should be able to handle redefinition next if ref.nil? || ref.empty? || visited.include?(ref) + # @sg-ignore flow sensitive typing should be able to handle redefinition ancestors << ref + # @sg-ignore flow sensitive typing should be able to handle redefinition queue << ref end end @@ -257,7 +262,7 @@ def get_ancestors(fqns) # @param fqns [String] # # @return [Array] - def get_ancestor_references(fqns) + def get_ancestor_references fqns (get_prepends(fqns) + get_includes(fqns) + [get_superclass(fqns)]).compact end @@ -275,7 +280,7 @@ def index # @param pinsets [Array>] # - # @return [void] + # @return [true] def catalog pinsets @pinsets = pinsets # @type [Array] @@ -308,7 +313,7 @@ def symbols index.pins_by_class(Pin::Symbol) end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def superclass_references index.superclass_references end @@ -347,7 +352,7 @@ def all_instance_variables # @param fqns [String] # @return [Pin::Reference::Superclass, nil] - def try_special_superclasses(fqns) + def try_special_superclasses fqns return OBJECT_SUPERCLASS_PIN if fqns == 'Boolean' return OBJECT_SUPERCLASS_PIN if !%w[BasicObject Object].include?(fqns) && namespace_exists?(fqns) diff --git a/lib/solargraph/bench.rb b/lib/solargraph/bench.rb index e6180c933..dda2bbc88 100644 --- a/lib/solargraph/bench.rb +++ b/lib/solargraph/bench.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true - module Solargraph # A container of source maps and workspace data to be cataloged in an ApiMap. # @@ -30,11 +29,11 @@ def initialize source_maps: [], workspace: Workspace.new, live_map: nil, externa .to_set end + # @sg-ignore flow sensitive typing needs better handling of ||= on lvars # @return [Hash{String => SourceMap}] def source_map_hash # @todo Work around #to_h bug in current Ruby head (3.5) with #map#to_h - @source_map_hash ||= source_maps.map { |s| [s.filename, s] } - .to_h + @source_map_hash ||= source_maps.to_h { |s| [s.filename, s] } end # @return [Set] diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 1b9f96dfa..c2dcf469b 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -4,11 +4,12 @@ module Solargraph # A container for type data based on YARD type tags. # class ComplexType - GENERIC_TAG_NAME = 'generic'.freeze + GENERIC_TAG_NAME = 'generic' # @!parse # include TypeMethods include Equality + autoload :Conformance, 'solargraph/complex_type/conformance' autoload :TypeMethods, 'solargraph/complex_type/type_methods' autoload :UniqueType, 'solargraph/complex_type/unique_type' @@ -18,25 +19,24 @@ def initialize types = [UniqueType::UNDEFINED] # @type [Array] items = types.flat_map(&:items).uniq(&:to_s) if items.any? { |i| i.name == 'false' } && items.any? { |i| i.name == 'true' } - items.delete_if { |i| i.name == 'false' || i.name == 'true' } - items.unshift(ComplexType::BOOLEAN) + items.delete_if { |i| %w[false true].include?(i.name) } + items.unshift(UniqueType::BOOLEAN) end + # @type [Array] items = [UniqueType::UNDEFINED] if items.any?(&:undefined?) + # @todo shouldn't need this cast - if statement above adds an 'Array' type + # @type [Array] @items = items end - # @sg-ignore Fix "Not enough arguments to Module#protected" - protected def equality_fields - [self.class, items] - end - # @param api_map [ApiMap] - # @param context [String] + # @param gates [Array] + # # @return [ComplexType] def qualify api_map, *gates red = reduce_object types = red.items.map do |t| - next t if ['nil', 'void', 'undefined'].include?(t.name) + next t if %w[nil void undefined].include?(t.name) next t if ['::Boolean'].include?(t.rooted_name) t.qualify api_map, *gates end @@ -44,13 +44,16 @@ def qualify api_map, *gates end # @param generics_to_resolve [Enumerable]] - # @param context_type [UniqueType, nil] + # @param context_type [ComplexType, ComplexType::UniqueType, nil] # @param resolved_generic_values [Hash{String => ComplexType}] Added to as types are encountered or resolved # @return [self] def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {} return self unless generic? - ComplexType.new(@items.map { |i| i.resolve_generics_from_context(generics_to_resolve, context_type, resolved_generic_values: resolved_generic_values) }) + ComplexType.new(@items.map do |i| + i.resolve_generics_from_context(generics_to_resolve, context_type, + resolved_generic_values: resolved_generic_values) + end) end # @return [UniqueType] @@ -65,7 +68,7 @@ def to_rbs (@items.length > 1 ? ')' : '')) end - # @param dst [ComplexType] + # @param dst [ComplexType, ComplexType::UniqueType] # @return [ComplexType] def self_to_type dst object_type_dst = dst.reduce_class_type @@ -76,15 +79,19 @@ def self_to_type dst end # @yieldparam [UniqueType] + # @yieldreturn [UniqueType] # @return [Array] + # @sg-ignore Declared return type + # ::Array<::Solargraph::ComplexType::UniqueType> does not match + # inferred type ::Array<::Proc> for Solargraph::ComplexType#map def map &block - @items.map &block + @items.map(&block) end # @yieldparam [UniqueType] # @return [Enumerable] def each &block - @items.each &block + @items.each(&block) end # @yieldparam [UniqueType] @@ -95,23 +102,17 @@ def each_unique_type &block return enum_for(__method__) unless block_given? @items.each do |item| - item.each_unique_type &block + item.each_unique_type(&block) end end - # @param atype [ComplexType] type which may be assigned to this type - # @param api_map [ApiMap] The ApiMap that performs qualification - 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 make_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) + 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, @@ -132,13 +133,13 @@ def to_a # @param index [Integer] # @return [UniqueType] - def [](index) + def [] index @items[index] end # @return [Array] def select &block - @items.select &block + @items.select(&block) end # @return [String] @@ -153,7 +154,9 @@ def namespaces end # @param name [Symbol] + # # @return [Object, nil] + # @param [Array] args def method_missing name, *args, &block return if @items.first.nil? return @items.first.send(name, *args, &block) if respond_to_missing?(name) @@ -162,7 +165,7 @@ def method_missing name, *args, &block # @param name [Symbol] # @param include_private [Boolean] - def respond_to_missing?(name, include_private = false) + def respond_to_missing? name, include_private = false TypeMethods.public_instance_methods.include?(name) || super end @@ -194,6 +197,65 @@ def desc rooted_tags end + # @param api_map [ApiMap] + # @param expected [ComplexType, ComplexType::UniqueType] + # @param situation [:method_call, :return_type, :assignment] + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic, :allow_unmatched_interface>] + # + # allow_subtype_skew: if not provided, check if any subtypes of + # the expected type match the inferred type + # + # allow_reverse_match: check if any subtypes + # of the expected type match the inferred type + # + # allow_empty_params: allow a general inferred type without + # parameters to conform to a more specific expected type + # + # allow_any_match: any unique type matched in the inferred + # qualifies as a match + # + # allow_undefined: treat undefined as a wildcard that matches + # anything + # + # @param variance [:invariant, :covariant, :contravariant] + # @return [Boolean] + def conforms_to? api_map, expected, + situation, + rules = [], + variance: erased_variance(situation) + expected = expected.downcast_to_literal_if_possible + inferred = downcast_to_literal_if_possible + + return duck_types_match?(api_map, expected, inferred) if expected.duck_type? + + if rules.include? :allow_any_match + inferred.any? do |inf| + inf.conforms_to?(api_map, expected, situation, rules, + variance: variance) + end + else + inferred.all? do |inf| + inf.conforms_to?(api_map, expected, situation, rules, + variance: variance) + end + end + end + + # @param api_map [ApiMap] + # @param expected [ComplexType, UniqueType] + # @param inferred [ComplexType, UniqueType] + # @return [Boolean] + def duck_types_match? api_map, expected, inferred + raise ArgumentError, 'Expected type must be duck type' unless expected.duck_type? + expected.each do |exp| + next unless exp.duck_type? + quack = exp.to_s[1..] + # @sg-ignore Need to add nil check here + return false if api_map.get_method_stack(inferred.namespace, quack, scope: inferred.scope).empty? + end + true + end + # @return [String] def rooted_tags map(&:rooted_tag).join(', ') @@ -201,14 +263,14 @@ def rooted_tags # @yieldparam [UniqueType] def all? &block - @items.all? &block + @items.all?(&block) end # @yieldparam [UniqueType] # @yieldreturn [Boolean] # @return [Boolean] def any? &block - @items.compact.any? &block + @items.compact.any?(&block) end def selfy? @@ -228,8 +290,10 @@ def simplify_literals # @yieldparam t [UniqueType] # @yieldreturn [UniqueType] # @return [ComplexType] - def transform(new_name = nil, &transform_type) - raise "Please remove leading :: and set rooted with recreate() instead - #{new_name}" if new_name&.start_with?('::') + def transform new_name = nil, &transform_type + if new_name&.start_with?('::') + raise "Please remove leading :: and set rooted with recreate() instead - #{new_name}" + end ComplexType.new(map { |ut| ut.transform(new_name, &transform_type) }) end @@ -252,6 +316,13 @@ def nullable? @items.any?(&:nil_type?) end + # @return [ComplexType] + def without_nil + new_items = @items.reject(&:nil_type?) + return ComplexType::UNDEFINED if new_items.empty? + ComplexType.new(new_items) + end + # @return [Array] def all_params @items.first.all_params || [] @@ -260,7 +331,7 @@ def all_params # @return [ComplexType] def reduce_class_type new_items = items.flat_map do |type| - next type unless ['Module', 'Class'].include?(type.name) + next type unless %w[Module Class].include?(type.name) next type if type.all_params.empty? type.all_params @@ -274,6 +345,13 @@ def all_rooted? all?(&:all_rooted?) end + # @param other [ComplexType, UniqueType] + def erased_version_of? other + return false if items.length != 1 || other.items.length != 1 + + @items.first.erased_version_of?(other.items.first) + end + # every top-level type has resolved to be fully qualified; see # #all_rooted? to check their subtypes as well def rooted? @@ -282,12 +360,46 @@ def rooted? attr_reader :items - def rooted? - @items.all?(&:rooted?) + # @param exclude_types [ComplexType, nil] + # @param api_map [ApiMap] + # @return [ComplexType, self] + def exclude exclude_types, api_map + return self if exclude_types.nil? + + types = items - exclude_types.items + types = [ComplexType::UniqueType::UNDEFINED] if types.empty? + ComplexType.new(types) + end + + # @see https://en.wikipedia.org/wiki/Intersection_type + # + # @param intersection_type [ComplexType, ComplexType::UniqueType, nil] + # @param api_map [ApiMap] + # @return [self, ComplexType::UniqueType] + def intersect_with intersection_type, api_map + return self if intersection_type.nil? + return intersection_type if undefined? + types = [] + # try to find common types via conformance + items.each do |ut| + intersection_type.each do |int_type| + if int_type.conforms_to?(api_map, ut, :assignment) + types << int_type + elsif ut.conforms_to?(api_map, int_type, :assignment) + types << ut + end + end + end + types = [ComplexType::UniqueType::UNDEFINED] if types.empty? + ComplexType.new(types) end protected + def equality_fields + [self.class, items] + end + # @return [ComplexType] def reduce_object new_items = items.flat_map do |ut| @@ -307,30 +419,29 @@ class << self # @example # ComplexType.parse 'String', 'Foo', 'nil' #=> [String, Foo, nil] # - # @note - # The `partial` parameter is used to indicate that the method is - # receiving a string that will be used inside another ComplexType. - # It returns arrays of ComplexTypes instead of a single cohesive one. - # Consumers should not need to use this parameter; it should only be - # used internally. - # + # @param partial [Boolean] if true, method is receiving a string + # that will be used inside another ComplexType. It returns + # arrays of ComplexTypes instead of a single cohesive one. + # Consumers should not need to use this parameter; it should + # only be used internally. # @param strings [Array] The type definitions to parse # @return [ComplexType] # # @overload parse(*strings, partial: false) # # @todo Need ability to use a literal true as a type below # # @param partial [Boolean] True if the string is part of a another type # # @return [Array] - # @todo To be able to select the right signature above, + # @sg-ignore To be able to select the right signature above, # Chain::Call needs to know the decl type (:arg, :optarg, # :kwarg, etc) of the arguments given, instead of just having # an array of Chains as the arguments. def parse *strings, partial: false - # @type [Hash{Array => ComplexType}] + # @type [Hash{Array => ComplexType, Array}] @cache ||= {} unless partial cached = @cache[strings] return cached unless cached.nil? end + # @types [Array] types = [] key_types = nil strings.each do |type_string| @@ -342,15 +453,16 @@ def parse *strings, partial: false # @param char [String] type_string&.each_char do |char| if char == '=' - #raise ComplexTypeError, "Invalid = in type #{type_string}" unless curly_stack > 0 + # raise ComplexTypeError, "Invalid = in type #{type_string}" unless curly_stack > 0 elsif char == '<' point_stack += 1 elsif char == '>' - if subtype_string.end_with?('=') && curly_stack > 0 + if subtype_string.end_with?('=') && curly_stack.positive? subtype_string += char elsif base.end_with?('=') - raise ComplexTypeError, "Invalid hash thing" unless key_types.nil? + raise ComplexTypeError, 'Invalid hash thing' unless key_types.nil? # types.push ComplexType.new([UniqueType.new(base[0..-2].strip)]) + # @sg-ignore Need to add nil check here types.push UniqueType.parse(base[0..-2].strip, subtype_string) # @todo this should either expand key_type's type # automatically or complain about not being @@ -361,7 +473,7 @@ def parse *strings, partial: false subtype_string.clear next else - raise ComplexTypeError, "Invalid close in type #{type_string}" if point_stack == 0 + raise ComplexTypeError, "Invalid close in type #{type_string}" if point_stack.zero? point_stack -= 1 subtype_string += char end @@ -371,34 +483,37 @@ def parse *strings, partial: false elsif char == '}' curly_stack -= 1 subtype_string += char - raise ComplexTypeError, "Invalid close in type #{type_string}" if curly_stack < 0 + raise ComplexTypeError, "Invalid close in type #{type_string}" if curly_stack.negative? next elsif char == '(' paren_stack += 1 elsif char == ')' paren_stack -= 1 subtype_string += char - raise ComplexTypeError, "Invalid close in type #{type_string}" if paren_stack < 0 + raise ComplexTypeError, "Invalid close in type #{type_string}" if paren_stack.negative? next - elsif char == ',' && point_stack == 0 && curly_stack == 0 && paren_stack == 0 + elsif char == ',' && point_stack.zero? && curly_stack.zero? && paren_stack.zero? # types.push ComplexType.new([UniqueType.new(base.strip, subtype_string.strip)]) types.push UniqueType.parse(base.strip, subtype_string.strip) base.clear subtype_string.clear next end - if point_stack == 0 && curly_stack == 0 && paren_stack == 0 + if point_stack.zero? && curly_stack.zero? && paren_stack.zero? base.concat char else subtype_string.concat char end end - raise ComplexTypeError, "Unclosed subtype in #{type_string}" if point_stack != 0 || curly_stack != 0 || paren_stack != 0 + if point_stack != 0 || curly_stack != 0 || paren_stack != 0 + raise ComplexTypeError, + "Unclosed subtype in #{type_string}" + end # types.push ComplexType.new([UniqueType.new(base, subtype_string)]) types.push UniqueType.parse(base.strip, subtype_string.strip) end unless key_types.nil? - raise ComplexTypeError, "Invalid use of key/value parameters" unless partial + raise ComplexTypeError, 'Invalid use of key/value parameters' unless partial return key_types if types.empty? return [key_types, types] end @@ -410,7 +525,7 @@ def parse *strings, partial: false # @param strings [Array] # @return [ComplexType] def try_parse *strings - parse *strings + parse(*strings) rescue ComplexTypeError => e Solargraph.logger.info "Error parsing complex type `#{strings.join(', ')}`: #{e.message}" ComplexType::UNDEFINED @@ -435,9 +550,7 @@ def try_parse *strings # @param dst [String] # @return [String] def reduce_class dst - while dst =~ /^(Class|Module)\<(.*?)\>$/ - dst = dst.sub(/^(Class|Module)\$/, '') - end + dst = dst.sub(/^(Class|Module)$/, '') while dst =~ /^(Class|Module)<(.*?)>$/ dst end end diff --git a/lib/solargraph/complex_type/conformance.rb b/lib/solargraph/complex_type/conformance.rb new file mode 100644 index 000000000..c2a48b255 --- /dev/null +++ b/lib/solargraph/complex_type/conformance.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +module Solargraph + class ComplexType + # Checks whether a type can be used in a given situation + class Conformance + # @param api_map [ApiMap] + # @param inferred [ComplexType::UniqueType] + # @param expected [ComplexType::UniqueType] + # @param situation [:method_call, :return_type] + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, + # :allow_any_match, :allow_undefined, :allow_unresolved_generic, + # :allow_unmatched_interface>] + # @param variance [:invariant, :covariant, :contravariant] + def initialize api_map, inferred, expected, + situation = :method_call, rules = [], + variance: inferred.erased_variance(situation) + @api_map = api_map + @inferred = inferred + @expected = expected + @situation = situation + @rules = rules + @variance = variance + # :nocov: + unless expected.is_a?(UniqueType) + # @sg-ignore This should never happen and the typechecker is angry about it + raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" + end + # :nocov: + return if inferred.is_a?(UniqueType) + # :nocov: + # @sg-ignore This should never happen and the typechecker is angry about it + raise "Inferred type must be a UniqueType, got #{inferred.class} in #{inferred.inspect}" + # :nocov: + end + + def conforms_to_unique_type? + unless expected.is_a?(UniqueType) + # :nocov: + raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" + # :nocov: + end + + return true if ignore_interface? + return true if conforms_via_reverse_match? + + downcast_inferred = inferred.downcast_to_literal_if_possible + downcast_expected = expected.downcast_to_literal_if_possible + if (downcast_inferred.name != inferred.name) || (downcast_expected.name != expected.name) + return with_new_types(downcast_inferred, downcast_expected).conforms_to_unique_type? + end + + if rules.include?(:allow_subtype_skew) && !expected.all_params.empty? + # parameters are not considered in this case + return with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type? + end + + return with_new_types(inferred.erase_parameters, expected).conforms_to_unique_type? if only_inferred_parameters? + + return conforms_via_stripped_expected_parameters? if can_strip_expected_parameters? + + return true if inferred == expected + + return false unless erased_type_conforms? + + return true if inferred.all_params.empty? && rules.include?(:allow_empty_params) + + # at this point we know the erased type is fine - time to look at parameters + + # there's an implicit 'any' on the expectation parameters + # if there are none specified + return true if expected.all_params.empty? + + return false unless key_types_conform? + + subtypes_conform? + end + + private + + def only_inferred_parameters? + !expected.parameters? && inferred.parameters? + end + + def conforms_via_stripped_expected_parameters? + with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type? + end + + def ignore_interface? + (expected.any?(&:interface?) && rules.include?(:allow_unmatched_interface)) || + (inferred.interface? && rules.include?(:allow_unmatched_interface)) + end + + def can_strip_expected_parameters? + expected.parameters? && !inferred.parameters? && rules.include?(:allow_empty_params) + end + + def conforms_via_reverse_match? + return false unless rules.include? :allow_reverse_match + + expected.conforms_to?(api_map, inferred, situation, + rules - [:allow_reverse_match], + variance: variance) + end + + def erased_type_conforms? + case variance + when :invariant + return false unless inferred.name == expected.name + when :covariant + # covariant: we can pass in a more specific type + # we contain the expected mix-in, or we have a more specific type + return false unless api_map.type_include?(inferred.name, expected.name) || + api_map.super_and_sub?(expected.name, inferred.name) || + inferred.name == expected.name + when :contravariant + # contravariant: we can pass in a more general type + # we contain the expected mix-in, or we have a more general type + return false unless api_map.type_include?(inferred.name, expected.name) || + api_map.super_and_sub?(inferred.name, expected.name) || + inferred.name == expected.name + else + # :nocov: + raise "Unknown variance: #{variance.inspect}" + # :nocov: + end + true + end + + def key_types_conform? + return true if expected.key_types.empty? + + return false if inferred.key_types.empty? + + unless ComplexType.new(inferred.key_types).conforms_to?(api_map, + ComplexType.new(expected.key_types), + situation, + rules, + variance: inferred.parameter_variance(situation)) + return false + end + + true + end + + def subtypes_conform? + return true if expected.subtypes.empty? + + return true if expected.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) + + return true if inferred.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) + + return true if inferred.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic) + + return true if expected.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic) + + return false if inferred.subtypes.empty? + + ComplexType.new(inferred.subtypes).conforms_to?(api_map, + ComplexType.new(expected.subtypes), + situation, + rules, + variance: inferred.parameter_variance(situation)) + end + + # @return [self] + # @param inferred [ComplexType::UniqueType] + # @param expected [ComplexType::UniqueType] + def with_new_types inferred, expected + self.class.new(api_map, inferred, expected, situation, rules, variance: variance) + end + + attr_reader :api_map, :inferred, :expected, :situation, :rules, :variance + end + end +end diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index a0e99cdee..5a1775c50 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -54,11 +54,11 @@ def duck_type? # @return [Boolean] def nil_type? - @nil_type ||= (name.casecmp('nil') == 0) + @nil_type ||= name.casecmp('nil').zero? end def tuple? - @tuple_type ||= (name == 'Tuple') || (name == 'Array' && subtypes.length >= 1 && fixed_parameters?) + @tuple ||= (name == 'Tuple') || (name == 'Array' && subtypes.length >= 1 && fixed_parameters?) end def void? @@ -73,9 +73,21 @@ def undefined? name == 'undefined' end + # Variance of the type ignoring any type parameters + # @return [Symbol] + # @param situation [Symbol] The situation in which the variance is being considered. + def erased_variance situation = :method_call + # :nocov: + unless %i[method_call return_type assignment].include?(situation) + raise "Unknown situation: #{situation.inspect}" + end + # :nocov: + :covariant + end + # @param generics_to_erase [Enumerable] # @return [self] - def erase_generics(generics_to_erase) + def erase_generics generics_to_erase transform do |type| if type.name == ComplexType::GENERIC_TAG_NAME if type.all_params.length == 1 && generics_to_erase.include?(type.all_params.first.to_s) @@ -130,7 +142,7 @@ def namespace @namespace ||= lambda do return 'Object' if duck_type? return 'NilClass' if nil_type? - return (name == 'Class' || name == 'Module') && !subtypes.empty? ? subtypes.first.name : name + %w[Class Module].include?(name) && !subtypes.empty? ? subtypes.first.name : name end.call end @@ -138,7 +150,7 @@ def namespace def namespace_type return ComplexType.parse('::Object') if duck_type? return ComplexType.parse('::NilClass') if nil_type? - return subtypes.first if (name == 'Class' || name == 'Module') && !subtypes.empty? + return subtypes.first if %w[Class Module].include?(name) && !subtypes.empty? self end @@ -165,36 +177,33 @@ def rooted_substring end # @return [String] - def generate_substring_from(&to_str) + def generate_substring_from &to_str key_types_str = key_types.map(&to_str).join(', ') subtypes_str = subtypes.map(&to_str).join(', ') - if key_types.none?(&:defined?) && subtypes.none?(&:defined?) - '' - elsif key_types.empty? && subtypes.empty? + if (key_types.none?(&:defined?) && subtypes.none?(&:defined?)) || + (key_types.empty? && subtypes.empty?) '' elsif hash_parameters? "{#{key_types_str} => #{subtypes_str}}" elsif fixed_parameters? "(#{subtypes_str})" + elsif name == 'Hash' + "<#{key_types_str}, #{subtypes_str}>" else - if name == 'Hash' - "<#{key_types_str}, #{subtypes_str}>" - else - "<#{key_types_str}#{subtypes_str}>" - end + "<#{key_types_str}#{subtypes_str}>" end end # @return [::Symbol] :class or :instance def scope @scope ||= :instance if duck_type? || nil_type? - @scope ||= (name == 'Class' || name == 'Module') && !subtypes.empty? ? :class : :instance + @scope ||= %w[Class Module].include?(name) && !subtypes.empty? ? :class : :instance end # @param other [Object] def == other return false unless self.class == other.class - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow sensitive typing should support .class == .class tag == other.tag end @@ -218,7 +227,9 @@ def qualify api_map, context = '' end # @yieldparam [UniqueType] - # @return [Enumerator] + # @return [void] + # @overload each_unique_type() + # @return [Enumerator] def each_unique_type &block return enum_for(__method__) unless block_given? yield self diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index e490bff35..067b01082 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -11,11 +11,6 @@ class UniqueType attr_reader :all_params, :subtypes, :key_types - # @sg-ignore Fix "Not enough arguments to Module#protected" - protected def equality_fields - [@name, @all_params, @subtypes, @key_types] - end - # Create a UniqueType with the specified name and an optional substring. # The substring is the parameter section of a parametrized type, e.g., # for the type `Array`, the name is `Array` and the substring is @@ -26,11 +21,9 @@ class UniqueType # @param make_rooted [Boolean, nil] # @return [UniqueType] def self.parse name, substring = '', make_rooted: nil - if name.start_with?(':::') - raise ComplexTypeError, "Illegal prefix: #{name}" - end + raise ComplexTypeError, "Illegal prefix: #{name}" if name.start_with?(':::') if name.start_with?('::') - name = name[2..-1] + name = name[2..] rooted = true elsif !can_root_name?(name) rooted = true @@ -46,15 +39,20 @@ def self.parse name, substring = '', make_rooted: nil parameters_type = nil unless substring.empty? subs = ComplexType.parse(substring[1..-2], partial: true) + # @sg-ignore Need to add nil check here parameters_type = PARAMETERS_TYPE_BY_STARTING_TAG.fetch(substring[0]) if parameters_type == :hash - raise ComplexTypeError, "Bad hash type: name=#{name}, substring=#{substring}" unless !subs.is_a?(ComplexType) and subs.length == 2 and !subs[0].is_a?(UniqueType) and !subs[1].is_a?(UniqueType) + unless !subs.is_a?(ComplexType) && (subs.length == 2) && !subs[0].is_a?(UniqueType) && !subs[1].is_a?(UniqueType) + raise ComplexTypeError, + "Bad hash type: name=#{name}, substring=#{substring}" + end key_types.concat(subs[0].map { |u| ComplexType.new([u]) }) subtypes.concat(subs[1].map { |u| ComplexType.new([u]) }) elsif parameters_type == :list && name == 'Hash' # Treat Hash as Hash{A => B} if subs.length != 2 - raise ComplexTypeError, "Bad hash type: name=#{name}, substring=#{substring} - must have exactly two parameters" + raise ComplexTypeError, + "Bad hash type: name=#{name}, substring=#{substring} - must have exactly two parameters" end key_types.concat(subs[0].map { |u| ComplexType.new([u]) }) subtypes.concat(subs[1].map { |u| ComplexType.new([u]) }) @@ -62,6 +60,7 @@ def self.parse name, substring = '', make_rooted: nil subtypes.concat subs end end + # @sg-ignore Need to add nil check here new(name, key_types, subtypes, rooted: rooted, parameters_type: parameters_type) end @@ -70,9 +69,9 @@ def self.parse name, substring = '', make_rooted: nil # @param subtypes [Array] # @param rooted [Boolean] # @param parameters_type [Symbol, nil] - def initialize(name, key_types = [], subtypes = [], rooted:, parameters_type: nil) - if parameters_type.nil? - raise "You must supply parameters_type if you provide parameters" unless key_types.empty? && subtypes.empty? + def initialize name, key_types = [], subtypes = [], rooted:, parameters_type: nil + if parameters_type.nil? && !(key_types.empty? && subtypes.empty?) + raise 'You must supply parameters_type if you provide parameters' end raise "Please remove leading :: and set rooted instead - #{name.inspect}" if name.start_with?('::') @name = name @@ -94,7 +93,7 @@ def implicit_union? # @todo use api_map to establish number of generics in type; # if only one is allowed but multiple are passed in, treat # those as implicit unions - ['Hash', 'Array', 'Set', '_ToAry', 'Enumerable', '_Each'].include?(name) && parameters_type != :fixed + %w[Hash Array Set _ToAry Enumerable _Each].include?(name) && parameters_type != :fixed end def to_s @@ -109,6 +108,44 @@ def simplify_literals end end + # @param exclude_types [ComplexType, nil] + # @param api_map [ApiMap] + # @return [ComplexType, self] + def exclude exclude_types, api_map + return self if exclude_types.nil? + + types = items - exclude_types.items + types = [ComplexType::UniqueType::UNDEFINED] if types.empty? + ComplexType.new(types) + end + + # @see https://en.wikipedia.org/wiki/Intersection_type + # + # @param intersection_type [ComplexType, ComplexType::UniqueType, nil] + # @param api_map [ApiMap] + # @return [self, ComplexType] + def intersect_with intersection_type, api_map + return self if intersection_type.nil? + return intersection_type if undefined? + types = [] + # try to find common types via conformance + items.each do |ut| + intersection_type.each do |int_type| + if ut.conforms_to?(api_map, int_type, :assignment) + types << ut + elsif int_type.conforms_to?(api_map, ut, :assignment) + types << int_type + end + end + end + types = [ComplexType::UniqueType::UNDEFINED] if types.empty? + ComplexType.new(types) + end + + def simplifyable_literal? + literal? && name != 'nil' + end + def literal? non_literal_name != name end @@ -118,6 +155,13 @@ def non_literal_name @non_literal_name ||= determine_non_literal_name end + # @return [self] + def without_nil + return UniqueType::UNDEFINED if nil_type? + + self + end + # @return [String] def determine_non_literal_name # https://github.com/ruby/rbs/blob/master/docs/syntax.md @@ -129,37 +173,114 @@ def determine_non_literal_name # | `false` return name if name.empty? return 'NilClass' if name == 'nil' - return 'Boolean' if ['true', 'false'].include?(name) + return 'Boolean' if %w[true false].include?(name) return 'Symbol' if name[0] == ':' + # @sg-ignore Need to add nil check here return 'String' if ['"', "'"].include?(name[0]) return 'Integer' if name.match?(/^-?\d+$/) name end - def eql?(other) + def eql? other self.class == other.class && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow sensitive typing should support .class == .class @name == other.name && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow sensitive typing should support .class == .class @key_types == other.key_types && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow sensitive typing should support .class == .class @subtypes == other.subtypes && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow sensitive typing should support .class == .class @rooted == other.rooted? && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow sensitive typing should support .class == .class @all_params == other.all_params && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow sensitive typing should support .class == .class @parameters_type == other.parameters_type end - def ==(other) + def == other eql?(other) end + # https://www.playfulpython.com/type-hinting-covariance-contra-variance/ + + # "[Expected] type variables that are COVARIANT can be substituted with + # a more specific [inferred] type without causing errors" + # + # "[Expected] type variables that are CONTRAVARIANT can be substituted + # with a more general [inferred] type without causing errors" + # + # "[Expected] types where neither is possible are INVARIANT" + # + # @param _situation [:method_call, :return_type] + # @param default [Symbol] The default variance to return if the type is not one of the special cases + # + # @return [:invariant, :covariant, :contravariant] + def parameter_variance _situation, default = :covariant + # @todo RBS can specify variance - maybe we can use that info + # and also let folks specify? + # + # Array/Set: ideally invariant, since we don't know if user is + # going to add new stuff into it or read it. But we don't + # have a way to specify, so we use covariant + # Enumerable: covariant: can't be changed, so we can pass + # in more specific subtypes + # Hash: read-only would be covariant, read-write would be + # invariant if we could distinguish that - should default to + # covariant + # contravariant?: Proc - can be changed, so we can pass + # in less specific super types + if %w[Hash Tuple Array Set Enumerable].include?(name) && fixed_parameters? + :covariant + else + default + end + end + + # Whether this is an RBS interface like _ToAry or _Each. + def interface? + name.start_with?('_') + end + + # @param other [UniqueType] + def erased_version_of? other + name == other.name && (all_params.empty? || all_params.all?(&:undefined?)) + end + + # @param api_map [ApiMap] + # @param expected [ComplexType::UniqueType, ComplexType] + # @param situation [:method_call, :assignment, :return_type] + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic>] + # @param variance [:invariant, :covariant, :contravariant] + def conforms_to? api_map, expected, situation, rules = [], + variance: erased_variance(situation) + return true if undefined? && rules.include?(:allow_undefined) + + # @todo teach this to validate duck types as inferred type + return true if duck_type? + + # complex types as expectations are unions - we only need to + # match one of their unique types + expected.any? do |expected_unique_type| + # :nocov: + unless expected_unique_type.instance_of?(UniqueType) + raise "Expected type must be a UniqueType, got #{expected_unique_type.class} in #{expected.inspect}" + end + # :nocov: + conformance = Conformance.new(api_map, self, expected_unique_type, situation, + rules, variance: variance) + conformance.conforms_to_unique_type? + end + end + def hash [self.class, @name, @key_types, @sub_types, @rooted, @all_params, @parameters_type].hash end + # @return [self] + def erase_parameters + UniqueType.new(name, rooted: rooted?, parameters_type: parameters_type) + end + # @return [Array] def items [self] @@ -181,6 +302,7 @@ def desc rooted_tags end + # @sg-ignore Need better if/elseanalysis # @return [String] def to_rbs if duck_type? @@ -190,10 +312,10 @@ def to_rbs elsif name.downcase == 'nil' 'nil' elsif name == GENERIC_TAG_NAME - all_params.first.name - elsif ['Class', 'Module'].include?(name) + all_params.first&.name + elsif %w[Class Module].include?(name) rbs_name - elsif ['Tuple', 'Array'].include?(name) && fixed_parameters? + elsif %w[Tuple Array].include?(name) && fixed_parameters? # tuples don't have a name; they're just [foo, bar, baz]. if substring == '()' # but there are no zero element tuples, so we go with an array @@ -218,7 +340,7 @@ def parameters? # @param types [Array] # @return [String] - def rbs_union(types) + def rbs_union types if types.length == 1 types.first.to_rbs else @@ -242,16 +364,13 @@ def generic? name == GENERIC_TAG_NAME || all_params.any?(&:generic?) end - # @param api_map [ApiMap] The ApiMap that performs qualification - # @param atype [ComplexType] type which may be assigned to this type - def can_assign?(api_map, atype) - logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect})" } - downcasted_atype = atype.downcast_to_literal_if_possible - out = downcasted_atype.all? do |autype| - autype.name == name || api_map.super_and_sub?(name, autype.name) - end - logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect}) => #{out}" } - out + def nullable? + nil_type? + end + + # @yieldreturn [Boolean] + def all? &block + block.yield self end # @return [UniqueType] @@ -260,37 +379,44 @@ def downcast_to_literal_if_possible end # @param generics_to_resolve [Enumerable] - # @param context_type [UniqueType, nil] + # @param context_type [ComplexType, UniqueType, nil] # @param resolved_generic_values [Hash{String => ComplexType, ComplexType::UniqueType}] Added to as types are encountered or resolved # @return [UniqueType, ComplexType] def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {} if name == ComplexType::GENERIC_TAG_NAME type_param = subtypes.first&.name + # @sg-ignore flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo) return self unless generics_to_resolve.include? type_param + # @sg-ignore flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo) unless context_type.nil? || !resolved_generic_values[type_param].nil? new_binding = true + # @sg-ignore flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo) resolved_generic_values[type_param] = context_type end if new_binding resolved_generic_values.transform_values! do |complex_type| - complex_type.resolve_generics_from_context(generics_to_resolve, nil, resolved_generic_values: resolved_generic_values) + complex_type.resolve_generics_from_context(generics_to_resolve, nil, + resolved_generic_values: resolved_generic_values) end end + # @sg-ignore flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo) return resolved_generic_values[type_param] || self end # @todo typechecking should complain when the method being called has no @yieldparam tag - new_key_types = resolve_param_generics_from_context(generics_to_resolve, context_type, resolved_generic_values, &:key_types) - new_subtypes = resolve_param_generics_from_context(generics_to_resolve, context_type, resolved_generic_values, &:subtypes) + new_key_types = resolve_param_generics_from_context(generics_to_resolve, context_type, resolved_generic_values, + &:key_types) + new_subtypes = resolve_param_generics_from_context(generics_to_resolve, context_type, resolved_generic_values, + &:subtypes) recreate(new_key_types: new_key_types, new_subtypes: new_subtypes) end # @param generics_to_resolve [Enumerable] - # @param context_type [UniqueType, nil] + # @param context_type [UniqueType, ComplexType, nil] # @param resolved_generic_values [Hash{String => ComplexType}] # @yieldreturn [Array] # @return [Array] - def resolve_param_generics_from_context(generics_to_resolve, context_type, resolved_generic_values) + def resolve_param_generics_from_context generics_to_resolve, context_type, resolved_generic_values types = yield self types.each_with_index.flat_map do |ct, i| ct.items.flat_map do |ut| @@ -298,10 +424,12 @@ def resolve_param_generics_from_context(generics_to_resolve, context_type, resol if context_params && context_params[i] type_arg = context_params[i] type_arg.map do |new_unique_context_type| - ut.resolve_generics_from_context generics_to_resolve, new_unique_context_type, resolved_generic_values: resolved_generic_values + ut.resolve_generics_from_context generics_to_resolve, new_unique_context_type, + resolved_generic_values: resolved_generic_values end else - ut.resolve_generics_from_context generics_to_resolve, nil, resolved_generic_values: resolved_generic_values + ut.resolve_generics_from_context generics_to_resolve, nil, + resolved_generic_values: resolved_generic_values end end end @@ -323,7 +451,7 @@ def resolve_generics definitions, context_type idx = definitions.generics.index(generic_name) next t if idx.nil? if context_type.parameters_type == :hash - if idx == 0 + if idx.zero? next ComplexType.new(context_type.key_types) elsif idx == 1 next ComplexType.new(context_type.subtypes) @@ -331,12 +459,13 @@ def resolve_generics definitions, context_type next ComplexType::UNDEFINED end elsif context_type.all?(&:implicit_union?) - if idx == 0 && !context_type.all_params.empty? + if idx.zero? && !context_type.all_params.empty? ComplexType.new(context_type.all_params) else ComplexType::UNDEFINED end else + # @sg-ignore Need to add nil check here context_type.all_params[idx] || definitions.generic_defaults[generic_name] || ComplexType::UNDEFINED end else @@ -352,6 +481,13 @@ def map &block [block.yield(self)] end + # @yieldparam t [self] + # @yieldreturn [self] + # @return [Enumerable] + def each &block + [self].each(&block) + end + # @return [Array] def to_a [self] @@ -360,16 +496,17 @@ def to_a # @param new_name [String, nil] # @param make_rooted [Boolean, nil] # @param new_key_types [Array, nil] - # @param rooted [Boolean, nil] + # @param make_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) + def recreate new_name: nil, make_rooted: nil, new_key_types: nil, new_subtypes: nil raise "Please remove leading :: and set rooted instead - #{new_name}" if new_name&.start_with?('::') new_name ||= name new_key_types ||= @key_types new_subtypes ||= @subtypes make_rooted = @rooted if make_rooted.nil? + # @sg-ignore flow sensitive typing needs better handling of ||= on lvars UniqueType.new(new_name, new_key_types, new_subtypes, rooted: make_rooted, parameters_type: parameters_type) end @@ -396,8 +533,10 @@ def force_rooted # @yieldparam t [UniqueType] # @yieldreturn [self] # @return [self] - def transform(new_name = nil, &transform_type) - raise "Please remove leading :: and set rooted with recreate() instead - #{new_name}" if new_name&.start_with?('::') + def transform new_name = nil, &transform_type + if new_name&.start_with?('::') + raise "Please remove leading :: and set rooted with recreate() instead - #{new_name}" + end if name == ComplexType::GENERIC_TAG_NAME # doesn't make sense to manipulate the name of the generic new_key_types = @key_types @@ -406,14 +545,15 @@ def transform(new_name = nil, &transform_type) new_key_types = @key_types.flat_map { |ct| ct.items.map { |ut| ut.transform(&transform_type) } } new_subtypes = @subtypes.flat_map { |ct| ct.items.map { |ut| ut.transform(&transform_type) } } end - new_type = recreate(new_name: new_name || name, new_key_types: new_key_types, new_subtypes: new_subtypes, make_rooted: @rooted) + new_type = recreate(new_name: new_name || name, new_key_types: new_key_types, new_subtypes: new_subtypes, + make_rooted: @rooted) yield new_type end # Generate a ComplexType that fully qualifies this type's namespaces. # # @param api_map [ApiMap] The ApiMap that performs qualification - # @param context [String] The namespace from which to resolve names + # @param gates [Array] The namespaces from which to resolve names # @return [self, ComplexType, UniqueType] The generated ComplexType def qualify api_map, *gates transform do |t| @@ -443,6 +583,22 @@ def self_to_type dst end end + # @yieldreturn [Boolean] + def any? &block + block.yield self + end + + # @return [ComplexType] + def reduce_class_type + new_items = items.flat_map do |type| + next type unless %w[Module Class].include?(type.name) + next type if type.all_params.empty? + + type.all_params + end + ComplexType.new(new_items) + end + def all_rooted? return true if name == GENERIC_TAG_NAME rooted? && all_params.all?(&:rooted?) @@ -453,12 +609,12 @@ def rooted? end # @param name_to_check [String] - def can_root_name?(name_to_check = name) + def can_root_name? name_to_check = name self.class.can_root_name?(name_to_check) end # @param name [String] - def self.can_root_name?(name) + def self.can_root_name? name # name is not lowercase !name.empty? && name != name.downcase end @@ -475,8 +631,13 @@ def self.can_root_name?(name) '::NilClass' => UniqueType::NIL }.freeze - include Logging + + protected + + def equality_fields + [@name, @all_params, @subtypes, @key_types] + end end end end diff --git a/lib/solargraph/convention.rb b/lib/solargraph/convention.rb index 89eac82b7..5e73eb3bf 100644 --- a/lib/solargraph/convention.rb +++ b/lib/solargraph/convention.rb @@ -30,7 +30,7 @@ def self.unregister convention # @param source_map [SourceMap] # @return [Environ] - def self.for_local(source_map) + def self.for_local source_map result = Environ.new @@conventions.each do |conv| result.merge conv.local(source_map) @@ -40,7 +40,7 @@ def self.for_local(source_map) # @param doc_map [DocMap] # @return [Environ] - def self.for_global(doc_map) + def self.for_global doc_map result = Environ.new @@conventions.each do |conv| result.merge conv.global(doc_map) diff --git a/lib/solargraph/convention/data_definition.rb b/lib/solargraph/convention/data_definition.rb index 8efe27932..960852caa 100644 --- a/lib/solargraph/convention/data_definition.rb +++ b/lib/solargraph/convention/data_definition.rb @@ -17,6 +17,7 @@ def process type: :class, location: loc, closure: region.closure, + # @sg-ignore flow sensitive typing needs to handle attrs name: data_definition_node.class_name, comments: comments_for(node), visibility: :public, @@ -39,6 +40,7 @@ def process # Solargraph::SourceMap::Clip#complete_keyword_parameters does not seem to currently take into account [Pin::Method#signatures] hence we only one for :kwarg pins.push initialize_method_pin + # @sg-ignore flow sensitive typing needs to handle attrs data_definition_node.attributes.map do |attribute_node, attribute_name| initialize_method_pin.parameters.push( Pin::Parameter.new( @@ -51,6 +53,7 @@ def process end # define attribute readers and instance variables + # @sg-ignore flow sensitive typing needs to handle attrs data_definition_node.attributes.each do |attribute_node, attribute_name| name = attribute_name.to_s method_pin = Pin::Method.new( @@ -78,7 +81,7 @@ def process private - # @return [DataDefintionNode, nil] + # @return [DataDefinition::DataDefintionNode, DataDefinition::DataAssignmentNode, nil] def data_definition_node @data_definition_node ||= if DataDefintionNode.match?(node) DataDefintionNode.new(node) @@ -90,7 +93,7 @@ def data_definition_node # @param attribute_node [Parser::AST::Node] # @param attribute_name [String] # @return [String, nil] - def attribute_comments(attribute_node, attribute_name) + def attribute_comments attribute_node, attribute_name data_comments = comments_for(attribute_node) return if data_comments.nil? || data_comments.empty? diff --git a/lib/solargraph/convention/data_definition/data_assignment_node.rb b/lib/solargraph/convention/data_definition/data_assignment_node.rb index cffe77494..97ef272cf 100644 --- a/lib/solargraph/convention/data_definition/data_assignment_node.rb +++ b/lib/solargraph/convention/data_definition/data_assignment_node.rb @@ -23,7 +23,7 @@ class << self # s(:args), # s(:send, nil, :bar)))) # @param node [::Parser::AST::Node] - def match?(node) + def match? node return false unless node&.type == :casgn return false if node.children[2].nil? diff --git a/lib/solargraph/convention/data_definition/data_definition_node.rb b/lib/solargraph/convention/data_definition/data_definition_node.rb index e86161c2d..5c4b2276a 100644 --- a/lib/solargraph/convention/data_definition/data_definition_node.rb +++ b/lib/solargraph/convention/data_definition/data_definition_node.rb @@ -27,7 +27,7 @@ class << self # s(:send, nil, :bar))) # # @param node [Parser::AST::Node] - def match?(node) + def match? node return false unless node&.type == :class data_definition_node?(node.children[1]) @@ -37,7 +37,7 @@ def match?(node) # @param data_node [Parser::AST::Node] # @return [Boolean] - def data_definition_node?(data_node) + def data_definition_node? data_node return false unless data_node.is_a?(::Parser::AST::Node) return false unless data_node&.type == :send return false unless data_node.children[0]&.type == :const @@ -49,7 +49,7 @@ def data_definition_node?(data_node) end # @param node [Parser::AST::Node] - def initialize(node) + def initialize node @node = node end @@ -66,7 +66,7 @@ def attributes end.compact end - # @return [Parser::AST::Node] + # @return [Parser::AST::Node, nil] def body_node node.children[2] end @@ -81,9 +81,11 @@ def data_node node.children[1] end + # @sg-ignore Need to add nil check here # @return [Array] def data_attribute_nodes - data_node.children[2..-1] + # @sg-ignore Need to add nil check here + data_node.children[2..] end end end diff --git a/lib/solargraph/convention/gemfile.rb b/lib/solargraph/convention/gemfile.rb index 8484f6247..bcebe1de9 100644 --- a/lib/solargraph/convention/gemfile.rb +++ b/lib/solargraph/convention/gemfile.rb @@ -5,7 +5,7 @@ module Convention class Gemfile < Base def local source_map return EMPTY_ENVIRON unless File.basename(source_map.filename) == 'Gemfile' - @environ ||= Environ.new( + @local ||= Environ.new( requires: ['bundler'], domains: ['Bundler::Dsl'] ) diff --git a/lib/solargraph/convention/gemspec.rb b/lib/solargraph/convention/gemspec.rb index 66357ecbb..ce4ce78c7 100644 --- a/lib/solargraph/convention/gemspec.rb +++ b/lib/solargraph/convention/gemspec.rb @@ -5,7 +5,7 @@ module Convention class Gemspec < Base def local source_map return Convention::Base::EMPTY_ENVIRON unless File.basename(source_map.filename).end_with?('.gemspec') - @environ ||= Environ.new( + @local ||= Environ.new( requires: ['rubygems'], pins: [ Solargraph::Pin::Reference::Override.from_comment( diff --git a/lib/solargraph/convention/rakefile.rb b/lib/solargraph/convention/rakefile.rb index 17be306ae..c5286bd54 100644 --- a/lib/solargraph/convention/rakefile.rb +++ b/lib/solargraph/convention/rakefile.rb @@ -7,7 +7,7 @@ def local source_map basename = File.basename(source_map.filename) return EMPTY_ENVIRON unless basename.end_with?('.rake') || basename == 'Rakefile' - @environ ||= Environ.new( + @local ||= Environ.new( requires: ['rake'], domains: ['Rake::DSL'] ) diff --git a/lib/solargraph/convention/struct_definition.rb b/lib/solargraph/convention/struct_definition.rb index b34ae5494..f1d240363 100644 --- a/lib/solargraph/convention/struct_definition.rb +++ b/lib/solargraph/convention/struct_definition.rb @@ -17,6 +17,7 @@ def process type: :class, location: loc, closure: region.closure, + # @sg-ignore flow sensitive typing needs to handle attrs name: struct_definition_node.class_name, docstring: docstring, visibility: :public, @@ -39,6 +40,7 @@ def process pins.push initialize_method_pin + # @sg-ignore flow sensitive typing needs to handle attrs struct_definition_node.attributes.map do |attribute_node, attribute_name| initialize_method_pin.parameters.push( Pin::Parameter.new( @@ -52,6 +54,7 @@ def process end # define attribute accessors and instance variables + # @sg-ignore flow sensitive typing needs to handle attrs struct_definition_node.attributes.each do |attribute_node, attribute_name| [attribute_name, "#{attribute_name}="].each do |name| docs = docstring.tags.find { |t| t.tag_name == 'param' && t.name == attribute_name } @@ -102,7 +105,7 @@ def process private - # @return [StructDefintionNode, StructAssignmentNode, nil] + # @return [StructDefinition::StructDefintionNode, StructDefinition::StructAssignmentNode, nil] def struct_definition_node @struct_definition_node ||= if StructDefintionNode.match?(node) StructDefintionNode.new(node) @@ -121,6 +124,7 @@ def docstring # @return [YARD::Docstring] def parse_comments struct_comments = comments_for(node) || '' + # @sg-ignore Need to add nil check here struct_definition_node.attributes.each do |attr_node, attr_name| comment = comments_for(attr_node) next if comment.nil? @@ -138,7 +142,7 @@ def parse_comments # @param tag [YARD::Tags::Tag, nil] The param tag for this attribute.xtract_ # # @return [String] - def tag_string(tag) + def tag_string tag tag&.types&.join(',') || 'undefined' end @@ -146,8 +150,8 @@ def tag_string(tag) # @param for_setter [Boolean] If true, will return a @param tag instead of a @return tag # # @return [String] The formatted comment for the attribute - def attribute_comment(tag, for_setter) - return "" if tag.nil? + def attribute_comment tag, for_setter + return '' if tag.nil? suffix = "[#{tag_string(tag)}] #{tag.text}" diff --git a/lib/solargraph/convention/struct_definition/struct_assignment_node.rb b/lib/solargraph/convention/struct_definition/struct_assignment_node.rb index 141abf599..6dcafd068 100644 --- a/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +++ b/lib/solargraph/convention/struct_definition/struct_assignment_node.rb @@ -22,8 +22,9 @@ class << self # s(:def, :foo, # s(:args), # s(:send, nil, :bar)))) + # # @param node [Parser::AST::Node] - def match?(node) + def match? node return false unless node&.type == :casgn return false if node.children[2].nil? diff --git a/lib/solargraph/convention/struct_definition/struct_definition_node.rb b/lib/solargraph/convention/struct_definition/struct_definition_node.rb index 725e4227f..51518a687 100644 --- a/lib/solargraph/convention/struct_definition/struct_definition_node.rb +++ b/lib/solargraph/convention/struct_definition/struct_definition_node.rb @@ -27,7 +27,7 @@ class << self # s(:send, nil, :bar))) # # @param node [Parser::AST::Node] - def match?(node) + def match? node return false unless node&.type == :class struct_definition_node?(node.children[1]) @@ -37,7 +37,7 @@ def match?(node) # @param struct_node [Parser::AST::Node] # @return [Boolean] - def struct_definition_node?(struct_node) + def struct_definition_node? struct_node return false unless struct_node.is_a?(::Parser::AST::Node) return false unless struct_node&.type == :send return false unless struct_node.children[0]&.type == :const @@ -49,7 +49,7 @@ def struct_definition_node?(struct_node) end # @param node [Parser::AST::Node] - def initialize(node) + def initialize node @node = node end @@ -92,6 +92,7 @@ def struct_node node.children[1] end + # @sg-ignore Need to add nil check here # @return [Array] def struct_attribute_nodes struct_node.children[2..-1] diff --git a/lib/solargraph/converters/dd.rb b/lib/solargraph/converters/dd.rb index 98085da56..31c9e4b0f 100644 --- a/lib/solargraph/converters/dd.rb +++ b/lib/solargraph/converters/dd.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'nokogiri' module ReverseMarkdown diff --git a/lib/solargraph/converters/dl.rb b/lib/solargraph/converters/dl.rb index 92785622a..c4401ce87 100644 --- a/lib/solargraph/converters/dl.rb +++ b/lib/solargraph/converters/dl.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ReverseMarkdown module Converters class Dl < Base diff --git a/lib/solargraph/converters/dt.rb b/lib/solargraph/converters/dt.rb index edd420678..c7027e2ae 100644 --- a/lib/solargraph/converters/dt.rb +++ b/lib/solargraph/converters/dt.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ReverseMarkdown module Converters class Dt < Base diff --git a/lib/solargraph/converters/misc.rb b/lib/solargraph/converters/misc.rb index e45b5bc9e..febacb9ee 100644 --- a/lib/solargraph/converters/misc.rb +++ b/lib/solargraph/converters/misc.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + ReverseMarkdown::Converters.register :tt, ReverseMarkdown::Converters::Code.new diff --git a/lib/solargraph/diagnostics/require_not_found.rb b/lib/solargraph/diagnostics/require_not_found.rb index 757849e08..df42da2e5 100644 --- a/lib/solargraph/diagnostics/require_not_found.rb +++ b/lib/solargraph/diagnostics/require_not_found.rb @@ -10,6 +10,7 @@ def diagnose source, api_map return [] unless source.parsed? && source.synchronized? result = [] refs = {} + # @sg-ignore Need to add nil check here map = api_map.source_map(source.filename) map.requires.each { |ref| refs[ref.name] = ref } api_map.missing_docs.each do |r| diff --git a/lib/solargraph/diagnostics/rubocop.rb b/lib/solargraph/diagnostics/rubocop.rb index 0a55d0367..39b79d9b7 100644 --- a/lib/solargraph/diagnostics/rubocop.rb +++ b/lib/solargraph/diagnostics/rubocop.rb @@ -17,7 +17,7 @@ class Rubocop < Base 'warning' => Severities::WARNING, 'error' => Severities::ERROR, 'fatal' => Severities::ERROR - } + }.freeze # @param source [Solargraph::Source] # @param _api_map [Solargraph::ApiMap] @@ -25,6 +25,7 @@ class Rubocop < Base def diagnose source, _api_map @source = source require_rubocop(rubocop_version) + # @sg-ignore Need to add nil check here options, paths = generate_options(source.filename, source.code) store = RuboCop::ConfigStore.new runner = RuboCop::Runner.new(options, store) @@ -32,7 +33,7 @@ def diagnose source, _api_map # a time - it uses 'chdir' to read config files with ERB, # which can conflict with other chdirs. result = Solargraph::CHDIR_MUTEX.synchronize do - redirect_stdout{ runner.run(paths) } + redirect_stdout { runner.run(paths) } end return [] if result.empty? @@ -76,7 +77,7 @@ def offense_to_diagnostic off severity: SEVERITIES[off['severity']], source: 'rubocop', code: off['cop_name'], - message: off['message'].gsub(/^#{off['cop_name']}\:/, '') + message: off['message'].gsub(/^#{off['cop_name']}:/, '') } end @@ -95,22 +96,22 @@ def offense_start_position off # @param off [Hash{String => Hash{String => Integer}}] # @return [Position] def offense_ending_position off - if off['location']['start_line'] != off['location']['last_line'] - Position.new(off['location']['start_line'], 0) - else + if off['location']['start_line'] == off['location']['last_line'] start_line = off['location']['start_line'] - 1 # @type [Integer] last_column = off['location']['last_column'] line = @source.code.lines[start_line] col_off = if line.nil? || line.empty? - 1 - else - 0 - end + 1 + else + 0 + end Position.new( start_line, last_column - col_off ) + else + Position.new(off['location']['start_line'], 0) end end end diff --git a/lib/solargraph/diagnostics/rubocop_helpers.rb b/lib/solargraph/diagnostics/rubocop_helpers.rb index fc458956e..e97ca628e 100644 --- a/lib/solargraph/diagnostics/rubocop_helpers.rb +++ b/lib/solargraph/diagnostics/rubocop_helpers.rb @@ -13,17 +13,18 @@ module RubocopHelpers # @param version [String, nil] # @raise [InvalidRubocopVersionError] if _version_ is not installed # @return [void] - def require_rubocop(version = nil) + def require_rubocop version = nil begin # @type [String] gem_path = Gem::Specification.find_by_name('rubocop', version).full_gem_path gem_lib_path = File.join(gem_path, 'lib') + # @sg-ignore Should better support meaning of '&' in RBS $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}) - "\ + "could not find '#{e.name}' (#{e.requirement}) - " \ "did find: [#{specs.map { |s| s.version.version }.join(', ')}]" end require 'rubocop' @@ -50,7 +51,8 @@ def generate_options filename, code # @return [String] def fix_drive_letter path return path unless path.match(/^[a-z]:/) - path[0].upcase + path[1..-1] + # @sg-ignore Need to add nil check here + path[0].upcase + path[1..] end # @todo This is a smelly way to redirect output, but the RuboCop specs do diff --git a/lib/solargraph/diagnostics/type_check.rb b/lib/solargraph/diagnostics/type_check.rb index 80f53eb7c..b1333f9d9 100644 --- a/lib/solargraph/diagnostics/type_check.rb +++ b/lib/solargraph/diagnostics/type_check.rb @@ -10,18 +10,19 @@ class TypeCheck < Base def diagnose source, api_map # return [] unless args.include?('always') || api_map.workspaced?(source.filename) severity = Diagnostics::Severities::ERROR - level = (args.reverse.find { |a| ['normal', 'typed', 'strict', 'strong'].include?(a) }) || :normal + level = args.reverse.find { |a| %w[normal typed strict strong].include?(a) } || :normal + # @sg-ignore sensitive typing needs to handle || on nil types checker = Solargraph::TypeChecker.new(source.filename, api_map: api_map, level: level.to_sym) checker.problems - .sort { |a, b| a.location.range.start.line <=> b.location.range.start.line } - .map do |problem| - { - range: extract_first_line(problem.location, source), - severity: severity, - source: 'Typecheck', - message: problem.message - } - end + .sort { |a, b| a.location.range.start.line <=> b.location.range.start.line } + .map do |problem| + { + range: extract_first_line(problem.location, source), + severity: severity, + source: 'Typecheck', + message: problem.message + } + end end private diff --git a/lib/solargraph/diagnostics/update_errors.rb b/lib/solargraph/diagnostics/update_errors.rb index b6f9baa89..c2ca02408 100644 --- a/lib/solargraph/diagnostics/update_errors.rb +++ b/lib/solargraph/diagnostics/update_errors.rb @@ -4,16 +4,12 @@ module Solargraph module Diagnostics class UpdateErrors < Base def diagnose source, api_map - result = [] - combine_ranges(source.code, source.error_ranges).each do |range| - result.push( - range: range.to_hash, + combine_ranges(source.code, source.error_ranges).map do |range| + { range: range.to_hash, severity: Diagnostics::Severities::ERROR, source: 'Solargraph', - message: 'Syntax error' - ) + message: 'Syntax error' } end - result end private @@ -26,7 +22,7 @@ def diagnose source, api_map def combine_ranges code, ranges result = [] lines = [] - ranges.sort{|a, b| a.start.line <=> b.start.line}.each do |rng| + ranges.sort { |a, b| a.start.line <=> b.start.line }.each do |rng| next if rng.nil? || lines.include?(rng.start.line) lines.push rng.start.line next if rng.start.line >= code.lines.length diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index e45ff0b65..61589e016 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -5,122 +5,86 @@ require 'open3' module Solargraph - # A collection of pins generated from required gems. + # A collection of pins generated from specific 'require' statements + # in code. Multiple can be created per workspace, to represent the + # pins available in different files based on their particular + # 'require' lines. # class DocMap include Logging - # @return [Array] - attr_reader :requires - alias required requires - - # @return [Array] - attr_reader :preferences - - # @return [Array] - attr_reader :pins - - # @return [Array] - def uncached_gemspecs - uncached_yard_gemspecs.concat(uncached_rbs_collection_gemspecs) - .sort - .uniq { |gemspec| "#{gemspec.name}:#{gemspec.version}" } - end - - # @return [Array] - attr_reader :uncached_yard_gemspecs - - # @return [Array] - attr_reader :uncached_rbs_collection_gemspecs - - # @return [String, nil] - attr_reader :rbs_collection_path - - # @return [String, nil] - attr_reader :rbs_collection_config_path - - # @return [Workspace, nil] + # @return [Workspace] attr_reader :workspace # @return [Environ] attr_reader :environ # @param requires [Array] - # @param preferences [Array] # @param workspace [Workspace, nil] - def initialize(requires, preferences, workspace = nil) - @requires = requires.compact - @preferences = preferences.compact + # @param out [IO, nil] output stream for logging + def initialize requires, workspace, out: $stderr + @provided_requires = requires.compact @workspace = workspace - @rbs_collection_path = workspace&.rbs_collection_path - @rbs_collection_config_path = workspace&.rbs_collection_config_path - @environ = Convention.for_global(self) - @requires.concat @environ.requires if @environ - load_serialized_gem_pins - pins.concat @environ.pins + @out = out end - # @param out [IO] - # @return [void] - def cache_all!(out) - # if we log at debug level: - if logger.info? - gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ') - logger.info "Caching pins for gems: #{gem_desc}" unless uncached_gemspecs.empty? - end - logger.debug { "Caching for YARD: #{uncached_yard_gemspecs.map(&:name)}" } - logger.debug { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" } - load_serialized_gem_pins - uncached_gemspecs.each do |gemspec| - cache(gemspec, out: out) + # @return [Array] + def requires + @requires ||= @provided_requires + (workspace.global_environ&.requires || []) + end + alias required requires + + # @sg-ignore flow sensitive typing needs to understand reassignment + # @return [Array] + def uncached_gemspecs + if @uncached_gemspecs.nil? + @uncached_gemspecs = [] + pins # force lazy-loaded pin lookup end - load_serialized_gem_pins - @uncached_rbs_collection_gemspecs = [] - @uncached_yard_gemspecs = [] + @uncached_gemspecs end - # @param gemspec [Gem::Specification] - # @param out [IO] - # @return [void] - def cache_yard_pins(gemspec, out) - pins = GemPins.build_yard_pins(yard_plugins, gemspec) - PinCache.serialize_yard_gem(gemspec, pins) - logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty? + # @return [Array] + def pins + @pins ||= load_serialized_gem_pins + (workspace.global_environ&.pins || []) end - # @param gemspec [Gem::Specification] - # @param out [IO] # @return [void] - def cache_rbs_collection_pins(gemspec, out) - rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) - pins = rbs_map.pins - rbs_version_cache_key = rbs_map.cache_key - # cache pins even if result is zero, so we don't retry building pins - pins ||= [] - PinCache.serialize_rbs_collection_gem(gemspec, rbs_version_cache_key, pins) - logger.info { "Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with cache_key #{rbs_version_cache_key.inspect}" unless pins.empty? } + def reset_pins! + @uncached_gemspecs = nil + @pins = nil end - # @param gemspec [Gem::Specification] + # @return [Solargraph::PinCache] + def pin_cache + @pin_cache ||= workspace.fresh_pincache + end + + def any_uncached? + uncached_gemspecs.any? + end + + # Cache all pins needed for the sources in this doc_map + # @param out [StringIO, IO, nil] output stream for logging # @param rebuild [Boolean] whether to rebuild the pins even if they are cached - # @param out [IO, nil] output stream for logging # @return [void] - def cache(gemspec, rebuild: false, out: nil) - build_yard = uncached_yard_gemspecs.include?(gemspec) || rebuild - build_rbs_collection = uncached_rbs_collection_gemspecs.include?(gemspec) || rebuild - if build_yard || build_rbs_collection - type = [] - type << 'YARD' if build_yard - type << 'RBS collection' if build_rbs_collection - out.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}") if out + def cache_doc_map_gems! out, rebuild: false + unless uncached_gemspecs.empty? + logger.info do + gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ') + "Caching pins for gems: #{gem_desc}" + end end - cache_yard_pins(gemspec, out) if build_yard - cache_rbs_collection_pins(gemspec, out) if build_rbs_collection - end - - # @return [Array] - def gemspecs - @gemspecs ||= required_gems_map.values.compact.flatten + time = Benchmark.measure do + uncached_gemspecs.each do |gemspec| + cache(gemspec, rebuild: rebuild, out: out) + end + end + milliseconds = (time.real * 1000).round + if (milliseconds > 500) && uncached_gemspecs.any? && out && uncached_gemspecs.any? + out.puts "Built #{uncached_gemspecs.length} gems in #{milliseconds} ms" + end + reset_pins! end # @return [Array] @@ -128,312 +92,108 @@ 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 - def self.all_yard_gems_in_memory - @yard_gems_in_memory ||= {} - end - - # @return [Hash{String => Hash{Array(String, String) => Array}}] stored by RBS collection path - def self.all_rbs_collection_gems_in_memory - @rbs_collection_gems_in_memory ||= {} - end - - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def yard_pins_in_memory - self.class.all_yard_gems_in_memory - end - - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def rbs_collection_pins_in_memory - self.class.all_rbs_collection_gems_in_memory[rbs_collection_path] ||= {} - end - - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def self.all_combined_pins_in_memory - @combined_pins_in_memory ||= {} + # @return [Array] + # @param out [IO, nil] + def dependencies out: $stderr + @dependencies ||= + begin + gem_deps = gemspecs + .flat_map { |spec| workspace.fetch_dependencies(spec, out: out) } + .uniq(&:name) + stdlib_deps = gemspecs + .flat_map { |spec| workspace.stdlib_dependencies(spec.name) } + .flat_map { |dep_name| workspace.resolve_require(dep_name) } + .compact + existing_gems = gemspecs.map(&:name) + (gem_deps + stdlib_deps).reject { |gemspec| existing_gems.include? gemspec.name } + end end - # @todo this should also include an index by the hash of the RBS collection - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def combined_pins_in_memory - self.class.all_combined_pins_in_memory + # Cache gem documentation if needed for this doc_map + # + # @param gemspec [Gem::Specification] + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # @param out [StringIO, IO, nil] output stream for logging + # + # @return [void] + def cache gemspec, rebuild: false, out: nil + pin_cache.cache_gem(gemspec: gemspec, + rebuild: rebuild, + out: out) end - # @return [Array] - def yard_plugins - @environ.yard_plugins - end + private - # @return [Set] - def dependencies - @dependencies ||= (gemspecs.flat_map { |spec| fetch_dependencies(spec) } - gemspecs).to_set + # @return [Array] + def gemspecs + @gemspecs ||= required_gems_map.values.compact.flatten end - private - - # @return [void] - def load_serialized_gem_pins - @pins = [] - @uncached_yard_gemspecs = [] - @uncached_rbs_collection_gemspecs = [] + # @param out [IO, nil] + # @return [Array] + def load_serialized_gem_pins out: @out + serialized_pins = [] with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v } - # @sg-ignore Need support for RBS duck interfaces like _ToHash # @type [Array] - paths = Hash[without_gemspecs].keys - # @sg-ignore Need support for RBS duck interfaces like _ToHash + missing_paths = without_gemspecs.to_h.keys # @type [Array] - gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies.to_a - - paths.each do |path| - rbs_pins = deserialize_stdlib_rbs_map path + gemspecs = with_gemspecs.to_h.values.flatten.compact + dependencies(out: out).to_a + + # if we are type checking a gem project, we should not include + # pins from rbs or yard from that gem here - we use our own + # parser for those pins + + # @param gemspec [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification] + gemspecs.reject! do |gemspec| + gemspec.respond_to?(:source) && + gemspec.source.instance_of?(Bundler::Source::Gemspec) && + gemspec.source.respond_to?(:path) && + gemspec.source.path == Pathname.new('.') + end + + missing_paths.each do |path| + # this will load from disk if needed; no need to manage + # uncached_gemspecs to trigger that later + stdlib_name_guess = path.split('/').first + + # try to resolve the stdlib name + # @type [Array] + deps = workspace.stdlib_dependencies(stdlib_name_guess) || [] + [stdlib_name_guess, *deps].compact.each do |potential_stdlib_name| + # @sg-ignore Need to support splatting in literal array + rbs_pins = pin_cache.cache_stdlib_rbs_map potential_stdlib_name + serialized_pins.concat rbs_pins if rbs_pins + end end - logger.debug { "DocMap#load_serialized_gem_pins: Combining pins..." } + serialized_pins.length time = Benchmark.measure do gemspecs.each do |gemspec| - pins = deserialize_combined_pin_cache gemspec - @pins.concat pins if pins + # only deserializes already-cached gems + gemspec_pins = pin_cache.deserialize_combined_pin_cache gemspec + if gemspec_pins + serialized_pins.concat gemspec_pins + else + uncached_gemspecs << gemspec + end end end - logger.info { "DocMap#load_serialized_gem_pins: Loaded and processed serialized pins together in #{time.real} seconds" } - @uncached_yard_gemspecs.uniq! - @uncached_rbs_collection_gemspecs.uniq! - nil + serialized_pins.length + milliseconds = (time.real * 1000).round + if (milliseconds > 500) && out && gemspecs.any? + out.puts "Deserialized #{serialized_pins.length} gem pins from #{PinCache.base_dir} in #{milliseconds} ms" + end + uncached_gemspecs.uniq! { |gemspec| "#{gemspec.name}:#{gemspec.version}" } + serialized_pins end # @return [Hash{String => Array}] def required_gems_map - @required_gems_map ||= requires.to_h { |path| [path, resolve_path_to_gemspecs(path)] } - end - - # @return [Hash{String => Gem::Specification}] - def preference_map - @preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] } - end - - # @param gemspec [Gem::Specification] - # @return [Array, nil] - def deserialize_yard_pin_cache gemspec - if yard_pins_in_memory.key?([gemspec.name, gemspec.version]) - return yard_pins_in_memory[[gemspec.name, gemspec.version]] - end - - cached = PinCache.deserialize_yard_gem(gemspec) - if cached - logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" } - yard_pins_in_memory[[gemspec.name, gemspec.version]] = cached - cached - else - logger.debug "No YARD pin cache for #{gemspec.name}:#{gemspec.version}" - @uncached_yard_gemspecs.push gemspec - nil - end - end - - # @param gemspec [Gem::Specification] - # @return [void] - def deserialize_combined_pin_cache(gemspec) - unless combined_pins_in_memory[[gemspec.name, gemspec.version]].nil? - return combined_pins_in_memory[[gemspec.name, gemspec.version]] - end - - rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) - rbs_version_cache_key = rbs_map.cache_key - - cached = PinCache.deserialize_combined_gem(gemspec, rbs_version_cache_key) - if cached - logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" } - combined_pins_in_memory[[gemspec.name, gemspec.version]] = cached - return combined_pins_in_memory[[gemspec.name, gemspec.version]] - end - - rbs_collection_pins = deserialize_rbs_collection_cache gemspec, rbs_version_cache_key - - yard_pins = deserialize_yard_pin_cache gemspec - - if !rbs_collection_pins.nil? && !yard_pins.nil? - logger.debug { "Combining pins for #{gemspec.name}:#{gemspec.version}" } - combined_pins = GemPins.combine(yard_pins, rbs_collection_pins) - PinCache.serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins) - combined_pins_in_memory[[gemspec.name, gemspec.version]] = combined_pins - logger.info { "Generated #{combined_pins_in_memory[[gemspec.name, gemspec.version]].length} combined pins for #{gemspec.name} #{gemspec.version}" } - return combined_pins - end - - if !yard_pins.nil? - logger.debug { "Using only YARD pins for #{gemspec.name}:#{gemspec.version}" } - combined_pins_in_memory[[gemspec.name, gemspec.version]] = yard_pins - return combined_pins_in_memory[[gemspec.name, gemspec.version]] - elsif !rbs_collection_pins.nil? - logger.debug { "Using only RBS collection pins for #{gemspec.name}:#{gemspec.version}" } - combined_pins_in_memory[[gemspec.name, gemspec.version]] = rbs_collection_pins - return combined_pins_in_memory[[gemspec.name, gemspec.version]] - else - logger.debug { "Pins not yet cached for #{gemspec.name}:#{gemspec.version}" } - return nil - end - end - - # @param path [String] require path that might be in the RBS stdlib collection - # @return [void] - def deserialize_stdlib_rbs_map path - map = RbsMap::StdlibMap.load(path) - if map.resolved? - logger.debug { "Loading stdlib pins for #{path}" } - @pins.concat map.pins - logger.debug { "Loaded #{map.pins.length} stdlib pins for #{path}" } - map.pins - else - # @todo Temporarily ignoring unresolved `require 'set'` - logger.debug { "Require path #{path} could not be resolved in RBS" } unless path == 'set' - nil - end - end - - # @param gemspec [Gem::Specification] - # @param rbs_version_cache_key [String] - # @return [Array, nil] - def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key - return if rbs_collection_pins_in_memory.key?([gemspec, rbs_version_cache_key]) - cached = PinCache.deserialize_rbs_collection_gem(gemspec, rbs_version_cache_key) - if cached - logger.info { "Loaded #{cached.length} pins from RBS collection cache for #{gemspec.name}:#{gemspec.version}" } unless cached.empty? - rbs_collection_pins_in_memory[[gemspec, rbs_version_cache_key]] = cached - cached - else - logger.debug "No RBS collection pin cache for #{gemspec.name} #{gemspec.version}" - @uncached_rbs_collection_gemspecs.push gemspec - nil - end - end - - # @param path [String] - # @return [::Array, nil] - def resolve_path_to_gemspecs path - return nil if path.empty? - return gemspecs_required_from_bundler if path == 'bundler/require' - - # @type [Gem::Specification, nil] - gemspec = Gem::Specification.find_by_path(path) - if gemspec.nil? - gem_name_guess = path.split('/').first - begin - # this can happen when the gem is included via a local path in - # a Gemfile; Gem doesn't try to index the paths in that case. - # - # See if we can make a good guess: - potential_gemspec = Gem::Specification.find_by_name(gem_name_guess) - file = "lib/#{path}.rb" - gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file } - rescue Gem::MissingSpecError - logger.debug { "Require path #{path} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" } - [] - end - end - return nil if gemspec.nil? - [gemspec_or_preference(gemspec)] + @required_gems_map ||= requires.to_h { |path| [path, workspace.resolve_require(path)] } end - # @param gemspec [Gem::Specification] - # @return [Gem::Specification] - def gemspec_or_preference gemspec - # :nocov: dormant feature - return gemspec unless preference_map.key?(gemspec.name) - return gemspec if gemspec.version == preference_map[gemspec.name].version - - change_gemspec_version gemspec, preference_map[gemspec.name].version - # :nocov: - end - - # @param gemspec [Gem::Specification] - # @param version [Gem::Version] - # @return [Gem::Specification] - def change_gemspec_version gemspec, version - Gem::Specification.find_by_name(gemspec.name, "= #{version}") - rescue Gem::MissingSpecError - Solargraph.logger.info "Gem #{gemspec.name} version #{version} not found. Using #{gemspec.version} instead" - gemspec - end - - # @param gemspec [Gem::Specification] - # @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 - - # @param gemspec [Gem::Specification] - # @return [Array] - def only_runtime_dependencies gemspec - gemspec.dependencies - gemspec.development_dependencies - end - - def inspect self.class.inspect end - - # @return [Array, nil] - def gemspecs_required_from_bundler - # @todo Handle projects with custom Bundler/Gemfile setups - return unless workspace.gemfile? - - if workspace.gemfile? && Bundler.definition&.lockfile&.to_s&.start_with?(workspace.directory) - # Find only the gems bundler is now using - Bundler.definition.locked_gems.specs.flat_map do |lazy_spec| - logger.info "Handling #{lazy_spec.name}:#{lazy_spec.version}" - [Gem::Specification.find_by_name(lazy_spec.name, lazy_spec.version)] - rescue Gem::MissingSpecError => e - logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with find_by_name, falling back to guess") - # can happen in local filesystem references - specs = resolve_path_to_gemspecs lazy_spec.name - logger.warn "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil? - next specs - end.compact - else - logger.info 'Fetching gemspecs required from Bundler (bundler/require)' - gemspecs_required_from_external_bundle - end - end - - # @return [Array, nil] - def gemspecs_required_from_external_bundle - logger.info 'Fetching gemspecs required from external bundle' - return [] unless workspace&.directory - - Solargraph.with_clean_env do - cmd = [ - 'ruby', '-e', - "require 'bundler'; require 'json'; Dir.chdir('#{workspace&.directory}') { puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }.to_h.to_json }" - ] - o, e, s = Open3.capture3(*cmd) - if s.success? - Solargraph.logger.debug "External bundle: #{o}" - hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {} - hash.flat_map do |name, version| - Gem::Specification.find_by_name(name, version) - rescue Gem::MissingSpecError => e - logger.info("Could not find #{name}:#{version} with find_by_name, falling back to guess") - # can happen in local filesystem references - specs = resolve_path_to_gemspecs name - logger.warn "Gem #{name} #{version} from bundle not found: #{e}" if specs.nil? - next specs - end.compact - else - Solargraph.logger.warn "Failed to load gems from bundle at #{workspace&.directory}: #{e}" - end - end - end end end diff --git a/lib/solargraph/equality.rb b/lib/solargraph/equality.rb index f8c50ff31..0a266df79 100644 --- a/lib/solargraph/equality.rb +++ b/lib/solargraph/equality.rb @@ -10,16 +10,16 @@ module Equality # @param other [Object] # @return [Boolean] - def eql?(other) + def eql? other self.class.eql?(other.class) && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow sensitive typing should support .class == .class equality_fields.eql?(other.equality_fields) end # @param other [Object] # @return [Boolean] - def ==(other) - self.eql?(other) + def == other + eql?(other) end def hash diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index 0c0016449..9be1e8eb7 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -13,8 +13,8 @@ class << self # @param pins [Array] # @return [Array] - def self.combine_method_pins_by_path(pins) - method_pins, alias_pins = pins.partition { |pin| pin.class == Pin::Method } + def self.combine_method_pins_by_path pins + method_pins, alias_pins = pins.partition { |pin| pin.instance_of?(Pin::Method) } by_path = method_pins.group_by(&:path) by_path.transform_values! do |pins| GemPins.combine_method_pins(*pins) @@ -43,36 +43,37 @@ def self.combine_method_pins(*pins) out end - # @param yard_plugins [Array] The names of YARD plugins to use. - # @param gemspec [Gem::Specification] - # @return [Array] - def self.build_yard_pins(yard_plugins, gemspec) - Yardoc.cache(yard_plugins, gemspec) unless Yardoc.cached?(gemspec) - return [] unless Yardoc.cached?(gemspec) - yardoc = Yardoc.load!(gemspec) - YardMap::Mapper.new(yardoc, gemspec).map - end - # @param yard_pins [Array] # @param rbs_pins [Array] # # @return [Array] - def self.combine(yard_pins, rbs_pins) + def self.combine yard_pins, rbs_pins in_yard = Set.new - rbs_api_map = Solargraph::ApiMap.new(pins: rbs_pins) + rbs_store = Solargraph::ApiMap::Store.new(rbs_pins) combined = yard_pins.map do |yard_pin| in_yard.add yard_pin.path - rbs_pin = rbs_api_map.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first - next yard_pin unless rbs_pin && yard_pin.class == Pin::Method + rbs_pin = rbs_store.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first + + next yard_pin unless rbs_pin && yard_pin.is_a?(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})" } + logger.debug do + "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})" + end next yard_pin end + # at this point both yard_pins and rbs_pins are methods or + # method aliases. if not plain methods, prefer the YARD one + next yard_pin if rbs_pin.class != Pin::Method + + next rbs_pin if yard_pin.class != Pin::Method + + # both are method pins out = combine_method_pins(rbs_pin, yard_pin) - logger.debug { "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}" } + logger.debug do + "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}" + end out end in_rbs_only = rbs_pins.select do |pin| @@ -91,7 +92,7 @@ class << self # @param choices [Array] # @return [ComplexType] def best_return_type *choices - choices.find { |pin| pin.defined? } || choices.first || ComplexType::UNDEFINED + choices.find(&:defined?) || choices.first || ComplexType::UNDEFINED end end end diff --git a/lib/solargraph/language_server/error_codes.rb b/lib/solargraph/language_server/error_codes.rb index 492ca6462..7df65388a 100644 --- a/lib/solargraph/language_server/error_codes.rb +++ b/lib/solargraph/language_server/error_codes.rb @@ -5,16 +5,16 @@ module LanguageServer # The ErrorCode constants for the language server protocol. # module ErrorCodes - PARSE_ERROR = -32700 - INVALID_REQUEST = -32600 - METHOD_NOT_FOUND = -32601 - INVALID_PARAMS = -32602 - INTERNAL_ERROR = -32603 - SERVER_ERROR_START = -32099 - SERVER_ERROR_END = -32000 - SERVER_NOT_INITIALIZED = -32002 - UNKNOWN_ERROR_CODE = -32001 - REQUEST_CANCELLED = -32800 + PARSE_ERROR = -32_700 + INVALID_REQUEST = -32_600 + METHOD_NOT_FOUND = -32_601 + INVALID_PARAMS = -32_602 + INTERNAL_ERROR = -32_603 + SERVER_ERROR_START = -32_099 + SERVER_ERROR_END = -32_000 + SERVER_NOT_INITIALIZED = -32_002 + UNKNOWN_ERROR_CODE = -32_001 + REQUEST_CANCELLED = -32_800 end end end diff --git a/lib/solargraph/language_server/host.rb b/lib/solargraph/language_server/host.rb index b228bdba6..d61283567 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -53,7 +53,7 @@ def configure update logger.level = LOG_LEVELS[options['logLevel']] || DEFAULT_LOG_LEVEL end - # @return [Hash{String => [Boolean, String]}] + # @return [Hash{String => Boolean, String}] def options @options ||= default_configuration end @@ -105,6 +105,7 @@ def receive request message.process unless cancel?(request['id']) rescue StandardError => e logger.warn "Error processing request: [#{e.class}] #{e.message}" + # @sg-ignore Need to add nil check here logger.warn e.backtrace.join("\n") message.set_error Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, "[#{e.class}] #{e.message}" end @@ -118,7 +119,7 @@ def receive request nil end else - logger.warn "Invalid message received." + logger.warn 'Invalid message received.' logger.debug request nil end @@ -153,7 +154,7 @@ def delete *uris lib.delete(*filenames) end uris.each do |uri| - send_notification "textDocument/publishDiagnostics", { + send_notification 'textDocument/publishDiagnostics', { uri: uri, diagnostics: [] } @@ -208,7 +209,7 @@ def diagnose uri logger.info "Diagnosing #{uri}" begin results = library.diagnose uri_to_file(uri) - send_notification "textDocument/publishDiagnostics", { + send_notification 'textDocument/publishDiagnostics', { uri: uri, diagnostics: results } @@ -300,8 +301,11 @@ def prepare directory, name = nil end end + # @sg-ignore Need to validate config # @return [String] + # @sg-ignore Need to validate config def command_path + # @type [String] options['commandPath'] || 'solargraph' end @@ -350,7 +354,7 @@ def folders # @return [void] def send_notification method, params response = { - jsonrpc: "2.0", + jsonrpc: '2.0', method: method, params: params } @@ -373,7 +377,7 @@ def send_notification method, params def send_request method, params, &block @request_mutex.synchronize do message = { - jsonrpc: "2.0", + jsonrpc: '2.0', method: method, params: params, id: @next_request_id @@ -415,13 +419,13 @@ def register_capabilities methods # @return [void] def unregister_capabilities methods logger.debug "Unregistering capabilities: #{methods}" - unregisterations = methods.select{|m| registered?(m)}.map{ |m| + unregisterations = methods.select { |m| registered?(m) }.map do |m| @registered_capabilities.delete m { id: m, method: m } - } + end return if unregisterations.empty? send_request 'client/unregisterCapability', { unregisterations: unregisterations } end @@ -489,7 +493,7 @@ def locate_pins params params['data']['location']['range']['end']['character'] ) ) - result.concat library.locate_pins(location).select{ |pin| pin.name == params['label'] } + result.concat(library.locate_pins(location).select { |pin| pin.name == params['label'] }) end if params['data']['path'] result.concat library.path_pins(params['data']['path']) @@ -543,7 +547,7 @@ def completions_at uri, line, column end # @return [Bool] if has pending completion request - def has_pending_completions? + def pending_completions? message_worker.messages.reverse_each.any? { |req| req['method'] == 'textDocument/completion' } end @@ -644,7 +648,7 @@ def show_message text, type = LanguageServer::MessageTypes::INFO # @param text [String] # @param type [Integer] A MessageType constant # @param actions [Array] Response options for the client - # @param block The block that processes the response + # @param block [Proc] The block that processes the response # @yieldparam [String] The action received from the client # @return [void] def show_message_request text, type, actions, &block @@ -663,7 +667,7 @@ def pending_requests requests.keys end - # @return [Hash{String => [Boolean,String]}] + # @return [Hash{String => Boolean,String}] def default_configuration { 'completion' => true, @@ -729,9 +733,11 @@ def requests end # @param path [String] + # @sg-ignore Need to be able to choose signature on String#gsub # @return [String] def normalize_separators path return path if File::ALT_SEPARATOR.nil? + # @sg-ignore flow sensitive typing needs to handle constants path.gsub(File::ALT_SEPARATOR, File::SEPARATOR) end @@ -742,9 +748,12 @@ def generate_updater params params['contentChanges'].each do |recvd| chng = check_diff(params['textDocument']['uri'], recvd) changes.push Solargraph::Source::Change.new( - (chng['range'].nil? ? - nil : - Solargraph::Range.from_to(chng['range']['start']['line'], chng['range']['start']['character'], chng['range']['end']['line'], chng['range']['end']['character']) + (if chng['range'].nil? + nil + else + Solargraph::Range.from_to(chng['range']['start']['line'], chng['range']['start']['character'], + chng['range']['end']['line'], chng['range']['end']['character']) + end ), chng['text'] ) @@ -764,8 +773,7 @@ def check_diff uri, change source = sources.find(uri) return change if source.code.length + 1 != change['text'].length diffs = Diff::LCS.diff(source.code, change['text']) - return change if diffs.length.zero? || diffs.length > 1 || diffs.first.length > 1 - # @sg-ignore push this upstream + return change if diffs.empty? || diffs.length > 1 || diffs.first.length > 1 # @type [Diff::LCS::Change] diff = diffs.first.first return change unless diff.adding? && ['.', ':', '(', ',', ' '].include?(diff.element) @@ -808,10 +816,10 @@ def dynamic_capability_options # documentSymbolProvider: true, # workspaceSymbolProvider: true, # workspace: { - # workspaceFolders: { - # supported: true, - # changeNotifications: true - # } + # workspaceFolders: { + # supported: true, + # changeNotifications: true + # } # } 'textDocument/definition' => { definitionProvider: true @@ -858,7 +866,7 @@ def library_map library end # @param library [Library] - # @param uuid [String, nil] + # # @return [void] def sync_library_map library total = library.workspace.sources.length diff --git a/lib/solargraph/language_server/host/diagnoser.rb b/lib/solargraph/language_server/host/diagnoser.rb index e69ae16f9..8c259c131 100644 --- a/lib/solargraph/language_server/host/diagnoser.rb +++ b/lib/solargraph/language_server/host/diagnoser.rb @@ -56,7 +56,7 @@ def start # @return [void] def tick return if queue.empty? || host.synchronizing? - if !host.options['diagnostics'] + unless host.options['diagnostics'] mutex.synchronize { queue.clear } return end diff --git a/lib/solargraph/language_server/host/dispatch.rb b/lib/solargraph/language_server/host/dispatch.rb index 1ff1227b8..b3c78ffca 100644 --- a/lib/solargraph/language_server/host/dispatch.rb +++ b/lib/solargraph/language_server/host/dispatch.rb @@ -33,6 +33,7 @@ def libraries # @return [void] def update_libraries uri src = sources.find(uri) + # @sg-ignore Need to add nil check here using = libraries.select { |lib| lib.contain?(src.filename) } using.push library_for(uri) if using.empty? using.each { |lib| lib.merge src } @@ -44,12 +45,11 @@ def update_libraries uri # @param uri [String] # @return [Library] def library_for uri - result = explicit_library_for(uri) || + explicit_library_for(uri) || implicit_library_for(uri) || generic_library_for(uri) # previous library for already call attach. avoid call twice # result.attach sources.find(uri) if sources.include?(uri) - result end # Find an explicit library match for the given URI. An explicit match @@ -118,8 +118,8 @@ def generic_library .tap { |lib| lib.add_observer self } end - # @param library [Solargraph::Library] # @param progress [Solargraph::LanguageServer::Progress, nil] + # # @return [void] def update progress progress&.send(self) diff --git a/lib/solargraph/language_server/host/message_worker.rb b/lib/solargraph/language_server/host/message_worker.rb index ec426b99f..440e2c7cc 100644 --- a/lib/solargraph/language_server/host/message_worker.rb +++ b/lib/solargraph/language_server/host/message_worker.rb @@ -20,7 +20,7 @@ class MessageWorker ].freeze # @param host [Host] - def initialize(host) + def initialize host @host = host @mutex = Mutex.new @resource = ConditionVariable.new @@ -28,7 +28,7 @@ def initialize(host) end # pending handle messages - # @return [Array] + # @return [Array undefined}>] def messages @messages ||= [] end @@ -44,7 +44,7 @@ def stop # @param message [Hash] The message to handle. Will be forwarded to Host#receive # @return [void] - def queue(message) + def queue message @mutex.synchronize do messages.push(message) @resource.signal @@ -66,6 +66,7 @@ def tick @resource.wait(@mutex) if messages.empty? next_message end + # @sg-ignore Need to add nil check here handler = @host.receive(message) handler&.send_response end diff --git a/lib/solargraph/language_server/host/sources.rb b/lib/solargraph/language_server/host/sources.rb index da0c63b93..72a155d73 100644 --- a/lib/solargraph/language_server/host/sources.rb +++ b/lib/solargraph/language_server/host/sources.rb @@ -13,7 +13,7 @@ class Sources # @param uri [String] # @return [void] - def add_uri(uri) + def add_uri uri queue.push(uri) end @@ -55,6 +55,7 @@ def update uri, updater # @raise [FileNotFoundError] if the URI does not match an open source. # # @param uri [String] + # @sg-ignore flow ensitive typing should understand raise # @return [Solargraph::Source] def find uri open_source_hash[uri] || raise(Solargraph::FileNotFoundError, "Host could not find #{uri}") diff --git a/lib/solargraph/language_server/message.rb b/lib/solargraph/language_server/message.rb index ab74c2eb4..170bfdb4f 100644 --- a/lib/solargraph/language_server/message.rb +++ b/lib/solargraph/language_server/message.rb @@ -38,7 +38,7 @@ def register path, message_class # @param path [String] # @return [Class] def select path - if method_map.has_key?(path) + if method_map.key?(path) method_map[path] elsif path.start_with?('$/') MethodNotImplemented diff --git a/lib/solargraph/language_server/message/base.rb b/lib/solargraph/language_server/message/base.rb index cc72d99b5..6b61101c4 100644 --- a/lib/solargraph/language_server/message/base.rb +++ b/lib/solargraph/language_server/message/base.rb @@ -69,7 +69,7 @@ def send_response } response[:result] = result unless result.nil? response[:error] = error unless error.nil? - response[:result] = nil if result.nil? and error.nil? + response[:result] = nil if result.nil? && error.nil? json = response.to_json envelope = "Content-Length: #{json.bytesize}\r\n\r\n#{json}" Solargraph.logger.debug envelope diff --git a/lib/solargraph/language_server/message/client/register_capability.rb b/lib/solargraph/language_server/message/client/register_capability.rb index a9af8748e..67469def8 100644 --- a/lib/solargraph/language_server/message/client/register_capability.rb +++ b/lib/solargraph/language_server/message/client/register_capability.rb @@ -5,9 +5,7 @@ module LanguageServer module Message module Client class RegisterCapability < Solargraph::LanguageServer::Message::Base - def process - - end + def process; end end end end diff --git a/lib/solargraph/language_server/message/completion_item/resolve.rb b/lib/solargraph/language_server/message/completion_item/resolve.rb index 85e03ad4f..83cc5c1fe 100644 --- a/lib/solargraph/language_server/message/completion_item/resolve.rb +++ b/lib/solargraph/language_server/message/completion_item/resolve.rb @@ -21,9 +21,9 @@ def merge pins docs = pins .reject { |pin| pin.documentation.empty? && pin.return_type.undefined? } result = params - .transform_keys(&:to_sym) - .merge(pins.first.resolve_completion_item) - .merge(documentation: markup_content(join_docs(docs))) + .transform_keys(&:to_sym) + .merge(pins.first.resolve_completion_item) + .merge(documentation: markup_content(join_docs(docs))) result[:detail] = pins.first.detail result end @@ -43,12 +43,10 @@ def markup_content text def join_docs pins result = [] last_link = nil - pins.each_with_index do |pin| + pins.each do |pin| this_link = host.options['enablePages'] ? pin.link_documentation : pin.text_documentation - if this_link && this_link != last_link && this_link != 'undefined' - result.push this_link - end - result.push pin.documentation unless result.last && result.last.end_with?(pin.documentation) + result.push this_link if this_link && this_link != last_link && this_link != 'undefined' + result.push pin.documentation unless result.last&.end_with?(pin.documentation) last_link = this_link end result.join("\n\n") diff --git a/lib/solargraph/language_server/message/extended/check_gem_version.rb b/lib/solargraph/language_server/message/extended/check_gem_version.rb index ead1eeaf2..8909406a4 100644 --- a/lib/solargraph/language_server/message/extended/check_gem_version.rb +++ b/lib/solargraph/language_server/message/extended/check_gem_version.rb @@ -1,12 +1,5 @@ # frozen_string_literal: true - -# @todo PR the RBS gem to add this -# @!parse -# module ::Gem -# class SpecFetcher; end -# end - module Solargraph module LanguageServer module Message @@ -23,8 +16,8 @@ def self.fetcher # @param obj [Gem::SpecFetcher] # @return [Gem::SpecFetcher] - def self.fetcher= obj - @fetcher = obj + class << self + attr_writer :fetcher end GEM_ZERO = Gem::Version.new('0.0.0') @@ -47,11 +40,11 @@ def process ['Update now'] do |result| next unless result == 'Update now' cmd = if host.options['useBundler'] - 'bundle update solargraph' - else - 'gem update solargraph' - end - o, s = Open3.capture2(cmd) + 'bundle update solargraph' + else + 'gem update solargraph' + end + _, s = Open3.capture2(cmd) if s == 0 host.show_message 'Successfully updated the Solargraph gem.', LanguageServer::MessageTypes::INFO host.send_notification '$/solargraph/restart', {} @@ -64,12 +57,13 @@ def process end elsif fetched? Solargraph::Logging.logger.warn error + # @sg-ignore Need to add nil check here host.show_message(error, MessageTypes::ERROR) if params['verbose'] end set_result({ - installed: current, - available: available - }) + installed: current, + available: available + }) end private @@ -78,12 +72,12 @@ def process attr_reader :current # @return [Gem::Version] + # @sg-ignore Need to add nil check here def available if !@available && !@fetched @fetched = true begin @available ||= begin - # @sg-ignore Variable type could not be inferred for tuple # @type [Gem::Dependency, nil] tuple = CheckGemVersion.fetcher.search_for_dependency(Gem::Dependency.new('solargraph')).flatten.first if tuple.nil? diff --git a/lib/solargraph/language_server/message/extended/document.rb b/lib/solargraph/language_server/message/extended/document.rb index 836fc005e..f379d0a6b 100644 --- a/lib/solargraph/language_server/message/extended/document.rb +++ b/lib/solargraph/language_server/message/extended/document.rb @@ -14,6 +14,7 @@ def process ) rescue StandardError => e Solargraph.logger.warn "Error processing document: [#{e.class}] #{e.message}" + # @sg-ignore Need to add nil check here Solargraph.logger.debug e.backtrace.join("\n") end end diff --git a/lib/solargraph/language_server/message/extended/document_gems.rb b/lib/solargraph/language_server/message/extended/document_gems.rb index f1cfca0b8..403632390 100644 --- a/lib/solargraph/language_server/message/extended/document_gems.rb +++ b/lib/solargraph/language_server/message/extended/document_gems.rb @@ -13,16 +13,16 @@ class DocumentGems < Base def process cmd = [host.command_path, 'gems'] cmd.push '--rebuild' if params['rebuild'] - o, s = Open3.capture2(*cmd) - if s != 0 - host.show_message "An error occurred while building gem documentation.", LanguageServer::MessageTypes::ERROR + _, s = Open3.capture2(*cmd) + if s == 0 set_result({ - status: 'err' - }) + status: 'ok' + }) else + host.show_message 'An error occurred while building gem documentation.', LanguageServer::MessageTypes::ERROR set_result({ - status: 'ok' - }) + status: 'err' + }) end end end diff --git a/lib/solargraph/language_server/message/extended/download_core.rb b/lib/solargraph/language_server/message/extended/download_core.rb index 7310757a1..4e0775c14 100644 --- a/lib/solargraph/language_server/message/extended/download_core.rb +++ b/lib/solargraph/language_server/message/extended/download_core.rb @@ -10,7 +10,8 @@ module Extended # class DownloadCore < Base def process - host.show_message "Downloading cores is deprecated. Solargraph currently uses RBS for core and stdlib documentation", LanguageServer::MessageTypes::INFO + host.show_message 'Downloading cores is deprecated. Solargraph currently uses RBS for core and stdlib documentation', + LanguageServer::MessageTypes::INFO end end end diff --git a/lib/solargraph/language_server/message/extended/search.rb b/lib/solargraph/language_server/message/extended/search.rb index 1f09a8890..312c048bb 100644 --- a/lib/solargraph/language_server/message/extended/search.rb +++ b/lib/solargraph/language_server/message/extended/search.rb @@ -8,7 +8,7 @@ class Search < Base def process results = host.search(params['query']) page = Solargraph::Page.new(host.options['viewsPath']) - content = page.render('search', layout: true, locals: {query: params['query'], results: results}) + content = page.render('search', layout: true, locals: { query: params['query'], results: results }) set_result( content: content ) diff --git a/lib/solargraph/language_server/message/initialize.rb b/lib/solargraph/language_server/message/initialize.rb index 3f3e1338b..f0f54f22e 100644 --- a/lib/solargraph/language_server/message/initialize.rb +++ b/lib/solargraph/language_server/message/initialize.rb @@ -26,20 +26,27 @@ def process } } # FIXME: lsp default is utf-16, may have different position - result[:capabilities][:positionEncoding] = "utf-32" if params.dig("capabilities", "general", "positionEncodings")&.include?("utf-32") + result[:capabilities][:positionEncoding] = 'utf-32' if params.dig('capabilities', 'general', + 'positionEncodings')&.include?('utf-32') result[:capabilities].merge! static_completion unless dynamic_registration_for?('textDocument', 'completion') - result[:capabilities].merge! static_signature_help unless dynamic_registration_for?('textDocument', 'signatureHelp') + result[:capabilities].merge! static_signature_help unless dynamic_registration_for?('textDocument', + 'signatureHelp') # result[:capabilities].merge! static_on_type_formatting unless dynamic_registration_for?('textDocument', 'onTypeFormatting') result[:capabilities].merge! static_hover unless dynamic_registration_for?('textDocument', 'hover') - result[:capabilities].merge! static_document_formatting unless dynamic_registration_for?('textDocument', 'formatting') - result[:capabilities].merge! static_document_symbols unless dynamic_registration_for?('textDocument', 'documentSymbol') + result[:capabilities].merge! static_document_formatting unless dynamic_registration_for?('textDocument', + 'formatting') + result[:capabilities].merge! static_document_symbols unless dynamic_registration_for?('textDocument', + 'documentSymbol') result[:capabilities].merge! static_definitions unless dynamic_registration_for?('textDocument', 'definition') - result[:capabilities].merge! static_type_definitions unless dynamic_registration_for?('textDocument', 'typeDefinition') + result[:capabilities].merge! static_type_definitions unless dynamic_registration_for?('textDocument', + 'typeDefinition') result[:capabilities].merge! static_rename unless dynamic_registration_for?('textDocument', 'rename') result[:capabilities].merge! static_references unless dynamic_registration_for?('textDocument', 'references') result[:capabilities].merge! static_workspace_symbols unless dynamic_registration_for?('workspace', 'symbol') - result[:capabilities].merge! static_folding_range unless dynamic_registration_for?('textDocument', 'foldingRange') - result[:capabilities].merge! static_highlights unless dynamic_registration_for?('textDocument', 'documentHighlight') + result[:capabilities].merge! static_folding_range unless dynamic_registration_for?('textDocument', + 'foldingRange') + result[:capabilities].merge! static_highlights unless dynamic_registration_for?('textDocument', + 'documentHighlight') # @todo Temporarily disabled # result[:capabilities].merge! static_code_action unless dynamic_registration_for?('textDocument', 'codeAction') set_result result @@ -71,7 +78,7 @@ def static_completion def static_code_action { codeActionProvider: true, - codeActionKinds: ["quickfix"] + codeActionKinds: ['quickfix'] } end @@ -144,11 +151,10 @@ def static_type_definitions # @return [Hash{Symbol => Hash{Symbol => Boolean}}] def static_rename { - renameProvider: {prepareProvider: true} + renameProvider: { prepareProvider: true } } end - # @return [Hash{Symbol => Boolean}] def static_references return {} unless host.options['references'] @@ -178,10 +184,10 @@ def static_highlights # enforce strict true/false-ness # @sg-ignore def dynamic_registration_for? section, capability - result = (params['capabilities'] && - params['capabilities'][section] && - params['capabilities'][section][capability] && - params['capabilities'][section][capability]['dynamicRegistration']) + result = params['capabilities'] && + params['capabilities'][section] && + params['capabilities'][section][capability] && + params['capabilities'][section][capability]['dynamicRegistration'] host.allow_registration("#{section}/#{capability}") if result result end diff --git a/lib/solargraph/language_server/message/text_document/completion.rb b/lib/solargraph/language_server/message/text_document/completion.rb index ef7ad1be4..d2f352a3d 100644 --- a/lib/solargraph/language_server/message/text_document/completion.rb +++ b/lib/solargraph/language_server/message/text_document/completion.rb @@ -6,7 +6,7 @@ module Message module TextDocument class Completion < Base def process - return set_error(ErrorCodes::REQUEST_CANCELLED, "cancelled by so many request") if host.has_pending_completions? + return set_error(ErrorCodes::REQUEST_CANCELLED, 'cancelled by so many request') if host.pending_completions? line = params['position']['line'] col = params['position']['character'] @@ -15,15 +15,16 @@ def process items = [] last_context = nil idx = -1 + # @sg-ignore Need to add nil check here completion.pins.each do |pin| idx += 1 if last_context != pin.context items.push pin.completion_item.merge({ - textEdit: { - range: completion.range.to_hash, - newText: pin.name.sub(/=$/, ' = ').sub(/:$/, ': ') - }, - sortText: "#{idx.to_s.rjust(4, '0')}#{pin.name}" - }) + textEdit: { + range: completion.range.to_hash, + newText: pin.name.sub(/=$/, ' = ').sub(/:$/, ': ') + }, + sortText: "#{idx.to_s.rjust(4, '0')}#{pin.name}" + }) items.last[:data][:uri] = params['textDocument']['uri'] last_context = pin.context end @@ -31,12 +32,13 @@ def process isIncomplete: false, items: items ) - rescue InvalidOffsetError => e + rescue InvalidOffsetError Logging.logger.info "Completion ignored invalid offset: #{params['textDocument']['uri']}, line #{line}, character #{col}" set_result empty_result end rescue FileNotFoundError => e Logging.logger.warn "[#{e.class}] #{e.message}" + # @sg-ignore Need to add nil check here Logging.logger.warn e.backtrace.join("\n") set_result empty_result end diff --git a/lib/solargraph/language_server/message/text_document/definition.rb b/lib/solargraph/language_server/message/text_document/definition.rb index ea0942dd5..1bac9b36c 100644 --- a/lib/solargraph/language_server/message/text_document/definition.rb +++ b/lib/solargraph/language_server/message/text_document/definition.rb @@ -1,40 +1,49 @@ # frozen_string_literal: true -module Solargraph::LanguageServer::Message::TextDocument - class Definition < Base - def process - @line = params['position']['line'] - @column = params['position']['character'] - set_result(code_location || require_location || []) - end +module Solargraph + module LanguageServer + module Message + module TextDocument + class Definition < Base + def process + @line = params['position']['line'] + @column = params['position']['character'] + set_result(code_location || require_location || []) + end - private + private - # @return [Array, nil] - def code_location - suggestions = host.definitions_at(params['textDocument']['uri'], @line, @column) - return nil if suggestions.empty? - suggestions.reject { |pin| pin.best_location.nil? || pin.best_location.filename.nil? }.map do |pin| - { - uri: file_to_uri(pin.best_location.filename), - range: pin.best_location.range.to_hash - } - end - end + # @return [Array, nil] + def code_location + suggestions = host.definitions_at(params['textDocument']['uri'], @line, @column) + # @sg-ignore Need to add nil check here + return nil if suggestions.empty? + # @sg-ignore Need to add nil check here + suggestions.reject { |pin| pin.best_location.nil? || pin.best_location.filename.nil? }.map do |pin| + { + uri: file_to_uri(pin.best_location.filename), + range: pin.best_location.range.to_hash + } + end + end - # @return [Array, nil] - def require_location - # @todo Terrible hack - lib = host.library_for(params['textDocument']['uri']) - rloc = Solargraph::Location.new(uri_to_file(params['textDocument']['uri']), Solargraph::Range.from_to(@line, @column, @line, @column)) - dloc = lib.locate_ref(rloc) - return nil if dloc.nil? - [ - { - uri: file_to_uri(dloc.filename), - range: dloc.range.to_hash - } - ] + # @return [Array, nil] + def require_location + # @todo Terrible hack + lib = host.library_for(params['textDocument']['uri']) + rloc = Solargraph::Location.new(uri_to_file(params['textDocument']['uri']), + Solargraph::Range.from_to(@line, @column, @line, @column)) + dloc = lib.locate_ref(rloc) + return nil if dloc.nil? + [ + { + uri: file_to_uri(dloc.filename), + range: dloc.range.to_hash + } + ] + end + end + end end end end diff --git a/lib/solargraph/language_server/message/text_document/document_highlight.rb b/lib/solargraph/language_server/message/text_document/document_highlight.rb index 2aded7641..e1ecfcb29 100644 --- a/lib/solargraph/language_server/message/text_document/document_highlight.rb +++ b/lib/solargraph/language_server/message/text_document/document_highlight.rb @@ -1,16 +1,23 @@ # frozen_string_literal: true -module Solargraph::LanguageServer::Message::TextDocument - class DocumentHighlight < Base - def process - locs = host.references_from(params['textDocument']['uri'], params['position']['line'], params['position']['character'], strip: true, only: true) - result = locs.map do |loc| - { - range: loc.range.to_hash, - kind: 1 - } +module Solargraph + module LanguageServer + module Message + module TextDocument + class DocumentHighlight < Base + def process + locs = host.references_from(params['textDocument']['uri'], params['position']['line'], + params['position']['character'], strip: true, only: true) + result = locs.map do |loc| + { + range: loc.range.to_hash, + kind: 1 + } + end + set_result result + end + end end - set_result result end end end diff --git a/lib/solargraph/language_server/message/text_document/document_symbol.rb b/lib/solargraph/language_server/message/text_document/document_symbol.rb index 2490f5c6d..04b706358 100644 --- a/lib/solargraph/language_server/message/text_document/document_symbol.rb +++ b/lib/solargraph/language_server/message/text_document/document_symbol.rb @@ -1,26 +1,36 @@ # frozen_string_literal: true -class Solargraph::LanguageServer::Message::TextDocument::DocumentSymbol < Solargraph::LanguageServer::Message::Base - include Solargraph::LanguageServer::UriHelpers +module Solargraph + module LanguageServer + module Message + module TextDocument + class DocumentSymbol < Solargraph::LanguageServer::Message::Base + include Solargraph::LanguageServer::UriHelpers - def process - pins = host.document_symbols params['textDocument']['uri'] - info = pins.map do |pin| - next nil unless pin.best_location&.filename + def process + pins = host.document_symbols params['textDocument']['uri'] + info = pins.map do |pin| + next nil unless pin.best_location&.filename - result = { - name: pin.name, - containerName: pin.namespace, - kind: pin.symbol_kind, - location: { - uri: file_to_uri(pin.best_location.filename), - range: pin.best_location.range.to_hash - }, - deprecated: pin.deprecated? - } - result - end.compact + result = { + name: pin.name, + containerName: pin.namespace, + kind: pin.symbol_kind, + location: { + # @sg-ignore Need to add nil check here + uri: file_to_uri(pin.best_location.filename), + # @sg-ignore Need to add nil check here + range: pin.best_location.range.to_hash + }, + deprecated: pin.deprecated? + } + result + end.compact - set_result info + set_result info + end + end + end + end end end diff --git a/lib/solargraph/language_server/message/text_document/formatting.rb b/lib/solargraph/language_server/message/text_document/formatting.rb index d67a0b414..c6cc3353a 100644 --- a/lib/solargraph/language_server/message/text_document/formatting.rb +++ b/lib/solargraph/language_server/message/text_document/formatting.rb @@ -43,7 +43,7 @@ def process # @param corrections [String] # @return [void] - def log_corrections(corrections) + def log_corrections corrections corrections = corrections&.strip return if corrections&.empty? @@ -56,7 +56,7 @@ def log_corrections(corrections) # @param file_uri [String] # @return [Hash{String => undefined}] - def config_for(file_uri) + def config_for file_uri conf = host.formatter_config(file_uri) return {} unless conf.is_a?(Hash) @@ -71,10 +71,10 @@ def cli_args file_uri, config args = [ config['cops'] == 'all' ? '-A' : '-a', '--cache', 'false', - '--format', formatter_class(config).name, + '--format', formatter_class(config).name ] - ['except', 'only'].each do |arg| + %w[except only].each do |arg| cops = cop_list(config[arg]) args += ["--#{arg}", cops] if cops end @@ -86,7 +86,7 @@ def cli_args file_uri, config # @param config [Hash{String => String}] # @sg-ignore # @return [Class] - def formatter_class(config) + def formatter_class config if self.class.const_defined?('BlankRubocopFormatter') # @sg-ignore BlankRubocopFormatter @@ -98,9 +98,11 @@ def formatter_class(config) end # @param value [Array, String] + # # @return [String, nil] - def cop_list(value) + def cop_list value # @type [String] + # @sg-ignore Translate to something flow sensitive typing understands value = value.join(',') if value.respond_to?(:join) return nil if value == '' || !value.is_a?(String) value diff --git a/lib/solargraph/language_server/message/text_document/hover.rb b/lib/solargraph/language_server/message/text_document/hover.rb index 72eff4296..6b60969e1 100644 --- a/lib/solargraph/language_server/message/text_document/hover.rb +++ b/lib/solargraph/language_server/message/text_document/hover.rb @@ -11,12 +11,11 @@ def process contents = [] suggestions = host.definitions_at(params['textDocument']['uri'], line, col) last_link = nil + # @sg-ignore Need to add nil check here suggestions.each do |pin| parts = [] this_link = host.options['enablePages'] ? pin.link_documentation : pin.text_documentation - if !this_link.nil? && this_link != last_link - parts.push this_link - end + parts.push this_link if !this_link.nil? && this_link != last_link parts.push "`#{pin.detail}`" unless pin.is_a?(Pin::Namespace) || pin.detail.nil? parts.push pin.documentation unless pin.documentation.nil? || pin.documentation.empty? unless parts.empty? @@ -31,6 +30,7 @@ def process ) rescue FileNotFoundError => e Logging.logger.warn "[#{e.class}] #{e.message}" + # @sg-ignore Need to add nil check here Logging.logger.warn e.backtrace.join("\n") set_result nil end @@ -41,8 +41,8 @@ def process # @return [Hash{Symbol => Hash{Symbol => String}}, nil] def contents_or_nil contents stripped = contents - .map(&:strip) - .reject { |c| c.empty? } + .map(&:strip) + .reject(&:empty?) return nil if stripped.empty? { contents: { diff --git a/lib/solargraph/language_server/message/text_document/prepare_rename.rb b/lib/solargraph/language_server/message/text_document/prepare_rename.rb index 4a37410dd..2f742047a 100644 --- a/lib/solargraph/language_server/message/text_document/prepare_rename.rb +++ b/lib/solargraph/language_server/message/text_document/prepare_rename.rb @@ -1,11 +1,18 @@ # frozen_string_literal: true -module Solargraph::LanguageServer::Message::TextDocument - class PrepareRename < Base - def process - line = params['position']['line'] - col = params['position']['character'] - set_result host.sources.find(params['textDocument']['uri']).cursor_at(Solargraph::Position.new(line, col)).range.to_hash +module Solargraph + module LanguageServer + module Message + module TextDocument + class PrepareRename < Base + def process + line = params['position']['line'] + col = params['position']['character'] + set_result host.sources.find(params['textDocument']['uri']).cursor_at(Solargraph::Position.new(line, + col)).range.to_hash + end + end + end end end end diff --git a/lib/solargraph/language_server/message/text_document/references.rb b/lib/solargraph/language_server/message/text_document/references.rb index 2de266214..6a894ecd9 100644 --- a/lib/solargraph/language_server/message/text_document/references.rb +++ b/lib/solargraph/language_server/message/text_document/references.rb @@ -1,16 +1,23 @@ # frozen_string_literal: true -module Solargraph::LanguageServer::Message::TextDocument - class References < Base - def process - locs = host.references_from(params['textDocument']['uri'], params['position']['line'], params['position']['character']) - result = locs.map do |loc| - { - uri: file_to_uri(loc.filename), - range: loc.range.to_hash - } +module Solargraph + module LanguageServer + module Message + module TextDocument + class References < Base + def process + locs = host.references_from(params['textDocument']['uri'], params['position']['line'], + params['position']['character']) + result = locs.map do |loc| + { + uri: file_to_uri(loc.filename), + range: loc.range.to_hash + } + end + set_result result + end + end end - set_result result end end end diff --git a/lib/solargraph/language_server/message/text_document/rename.rb b/lib/solargraph/language_server/message/text_document/rename.rb index 997e9595b..b0aff3c8a 100644 --- a/lib/solargraph/language_server/message/text_document/rename.rb +++ b/lib/solargraph/language_server/message/text_document/rename.rb @@ -1,19 +1,26 @@ # frozen_string_literal: true -module Solargraph::LanguageServer::Message::TextDocument - class Rename < Base - def process - locs = host.references_from(params['textDocument']['uri'], params['position']['line'], params['position']['character'], strip: true) - changes = {} - locs.each do |loc| - uri = file_to_uri(loc.filename) - changes[uri] ||= [] - changes[uri].push({ - range: loc.range.to_hash, - newText: params['newName'] - }) +module Solargraph + module LanguageServer + module Message + module TextDocument + class Rename < Base + def process + locs = host.references_from(params['textDocument']['uri'], params['position']['line'], + params['position']['character'], strip: true) + changes = {} + locs.each do |loc| + uri = file_to_uri(loc.filename) + changes[uri] ||= [] + changes[uri].push({ + range: loc.range.to_hash, + newText: params['newName'] + }) + end + set_result changes: changes + end + end end - set_result changes: changes end end end diff --git a/lib/solargraph/language_server/message/text_document/signature_help.rb b/lib/solargraph/language_server/message/text_document/signature_help.rb index e4e8795db..675daaebe 100644 --- a/lib/solargraph/language_server/message/text_document/signature_help.rb +++ b/lib/solargraph/language_server/message/text_document/signature_help.rb @@ -10,10 +10,11 @@ def process col = params['position']['character'] suggestions = host.signatures_at(params['textDocument']['uri'], line, col) set_result({ - signatures: suggestions.flat_map { |pin| pin.signature_help } - }) + signatures: suggestions.flat_map(&:signature_help) + }) rescue FileNotFoundError => e Logging.logger.warn "[#{e.class}] #{e.message}" + # @sg-ignore Need to add nil check here Logging.logger.warn e.backtrace.join("\n") set_result nil end diff --git a/lib/solargraph/language_server/message/text_document/type_definition.rb b/lib/solargraph/language_server/message/text_document/type_definition.rb index adb24038b..1035869c9 100644 --- a/lib/solargraph/language_server/message/text_document/type_definition.rb +++ b/lib/solargraph/language_server/message/text_document/type_definition.rb @@ -1,24 +1,32 @@ # frozen_string_literal: true -module Solargraph::LanguageServer::Message::TextDocument - class TypeDefinition < Base - def process - @line = params['position']['line'] - @column = params['position']['character'] - set_result(code_location || []) - end +module Solargraph + module LanguageServer + module Message + module TextDocument + class TypeDefinition < Base + def process + @line = params['position']['line'] + @column = params['position']['character'] + set_result(code_location || []) + end - private + private - # @return [Array, nil] - def code_location - suggestions = host.type_definitions_at(params['textDocument']['uri'], @line, @column) - return nil if suggestions.empty? - suggestions.reject { |pin| pin.best_location.nil? || pin.best_location.filename.nil? }.map do |pin| - { - uri: file_to_uri(pin.best_location.filename), - range: pin.best_location.range.to_hash - } + # @return [Array, nil] + def code_location + suggestions = host.type_definitions_at(params['textDocument']['uri'], @line, @column) + # @sg-ignore Need to add nil check here + return nil if suggestions.empty? + # @sg-ignore Need to add nil check here + suggestions.reject { |pin| pin.best_location.nil? || pin.best_location.filename.nil? }.map do |pin| + { + uri: file_to_uri(pin.best_location.filename), + range: pin.best_location.range.to_hash + } + end + end + end end end end diff --git a/lib/solargraph/language_server/message/workspace/did_change_configuration.rb b/lib/solargraph/language_server/message/workspace/did_change_configuration.rb index 5a15ab0a5..2c9bd8d1c 100644 --- a/lib/solargraph/language_server/message/workspace/did_change_configuration.rb +++ b/lib/solargraph/language_server/message/workspace/did_change_configuration.rb @@ -1,35 +1,41 @@ # frozen_string_literal: true -module Solargraph::LanguageServer::Message::Workspace - class DidChangeConfiguration < Solargraph::LanguageServer::Message::Base - def process - return unless params['settings'] - update = params['settings']['solargraph'] - host.configure update - register_from_options - end +module Solargraph + module LanguageServer + module Message + module Workspace + class DidChangeConfiguration < Solargraph::LanguageServer::Message::Base + def process + return unless params['settings'] + update = params['settings']['solargraph'] + host.configure update + register_from_options + end - private + private - # @return [void] - def register_from_options - Solargraph.logger.debug "Registering capabilities from options: #{host.options.inspect}" - # @type [Array] - y = [] - # @type [Array] - n = [] - (host.options['completion'] ? y : n).push('textDocument/completion') - (host.options['hover'] ? y : n).push('textDocument/hover', 'textDocument/signatureHelp') - (host.options['autoformat'] ? y : n).push('textDocument/onTypeFormatting') - (host.options['formatting'] ? y : n).push('textDocument/formatting') - (host.options['symbols'] ? y : n).push('textDocument/documentSymbol', 'workspace/symbol') - (host.options['definitions'] ? y : n).push('textDocument/definition') - (host.options['typeDefinitions'] ? y : n).push('textDocument/typeDefinition') - (host.options['references'] ? y : n).push('textDocument/references') - (host.options['folding'] ? y : n).push('textDocument/folding') - (host.options['highlights'] ? y : n).push('textDocument/documentHighlight') - host.register_capabilities y - host.unregister_capabilities n + # @return [void] + def register_from_options + Solargraph.logger.debug "Registering capabilities from options: #{host.options.inspect}" + # @type [Array] + y = [] + # @type [Array] + n = [] + (host.options['completion'] ? y : n).push('textDocument/completion') + (host.options['hover'] ? y : n).push('textDocument/hover', 'textDocument/signatureHelp') + (host.options['autoformat'] ? y : n).push('textDocument/onTypeFormatting') + (host.options['formatting'] ? y : n).push('textDocument/formatting') + (host.options['symbols'] ? y : n).push('textDocument/documentSymbol', 'workspace/symbol') + (host.options['definitions'] ? y : n).push('textDocument/definition') + (host.options['typeDefinitions'] ? y : n).push('textDocument/typeDefinition') + (host.options['references'] ? y : n).push('textDocument/references') + (host.options['folding'] ? y : n).push('textDocument/folding') + (host.options['highlights'] ? y : n).push('textDocument/documentHighlight') + host.register_capabilities y + host.unregister_capabilities n + end + end + end end end end diff --git a/lib/solargraph/language_server/message/workspace/did_change_watched_files.rb b/lib/solargraph/language_server/message/workspace/did_change_watched_files.rb index 32b2c1c50..d44c24097 100644 --- a/lib/solargraph/language_server/message/workspace/did_change_watched_files.rb +++ b/lib/solargraph/language_server/message/workspace/did_change_watched_files.rb @@ -1,40 +1,48 @@ # frozen_string_literal: true -module Solargraph::LanguageServer::Message::Workspace - class DidChangeWatchedFiles < Solargraph::LanguageServer::Message::Base - CREATED = 1 - CHANGED = 2 - DELETED = 3 +module Solargraph + module LanguageServer + module Message + module Workspace + class DidChangeWatchedFiles < Solargraph::LanguageServer::Message::Base + CREATED = 1 + CHANGED = 2 + DELETED = 3 - include Solargraph::LanguageServer::UriHelpers + include Solargraph::LanguageServer::UriHelpers - def process - need_catalog = false - to_create = [] - to_delete = [] + def process + need_catalog = false + to_create = [] + to_delete = [] - # @param change [Hash] - params['changes'].each do |change| - if change['type'] == CREATED - to_create << change['uri'] - need_catalog = true - elsif change['type'] == CHANGED - next if host.open?(change['uri']) - to_create << change['uri'] - need_catalog = true - elsif change['type'] == DELETED - to_delete << change['uri'] - need_catalog = true - else - set_error Solargraph::LanguageServer::ErrorCodes::INVALID_PARAMS, "Unknown change type ##{change['type']} for #{uri_to_file(change['uri'])}" - end - end + # @param change [Hash] + params['changes'].each do |change| + case change['type'] + when CREATED + to_create << change['uri'] + need_catalog = true + when CHANGED + next if host.open?(change['uri']) + to_create << change['uri'] + need_catalog = true + when DELETED + to_delete << change['uri'] + need_catalog = true + else + set_error Solargraph::LanguageServer::ErrorCodes::INVALID_PARAMS, + "Unknown change type ##{change['type']} for #{uri_to_file(change['uri'])}" + end + end - host.create *to_create - host.delete *to_delete + host.create(*to_create) + host.delete(*to_delete) - # Force host to catalog libraries after file changes (see castwide/solargraph#139) - host.catalog if need_catalog + # Force host to catalog libraries after file changes (see castwide/solargraph#139) + host.catalog if need_catalog + end + end + end end end end diff --git a/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb b/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb index e1e83fc1e..7ba16b66c 100644 --- a/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb +++ b/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb @@ -1,25 +1,31 @@ # frozen_string_literal: true -module Solargraph::LanguageServer::Message::Workspace - class DidChangeWorkspaceFolders < Solargraph::LanguageServer::Message::Base - def process - add_folders - remove_folders - end +module Solargraph + module LanguageServer + module Message + module Workspace + class DidChangeWorkspaceFolders < Solargraph::LanguageServer::Message::Base + def process + add_folders + remove_folders + end - private + private - # @return [void] - def add_folders - return unless params['event'] && params['event']['added'] - host.prepare_folders params['event']['added'] - end + # @return [void] + def add_folders + return unless params['event'] && params['event']['added'] + host.prepare_folders params['event']['added'] + end - # @return [void] - def remove_folders - return unless params['event'] && params['event']['removed'] - params['event']['removed'].each do |folder| - host.remove_folders params['event']['removed'] + # @return [void] + def remove_folders + return unless params['event'] && params['event']['removed'] + params['event']['removed'].each do |_folder| + host.remove_folders params['event']['removed'] + end + end + end end end end diff --git a/lib/solargraph/language_server/message/workspace/workspace_symbol.rb b/lib/solargraph/language_server/message/workspace/workspace_symbol.rb index 780e4aa0b..8e8c884a7 100644 --- a/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +++ b/lib/solargraph/language_server/message/workspace/workspace_symbol.rb @@ -1,23 +1,33 @@ # frozen_string_literal: true -class Solargraph::LanguageServer::Message::Workspace::WorkspaceSymbol < Solargraph::LanguageServer::Message::Base - include Solargraph::LanguageServer::UriHelpers +module Solargraph + module LanguageServer + module Message + module Workspace + class WorkspaceSymbol < Solargraph::LanguageServer::Message::Base + include Solargraph::LanguageServer::UriHelpers - def process - pins = host.query_symbols(params['query']) - info = pins.map do |pin| - uri = file_to_uri(pin.best_location.filename) - { - name: pin.path, - containerName: pin.namespace, - kind: pin.symbol_kind, - location: { - uri: uri, - range: pin.best_location.range.to_hash - }, - deprecated: pin.deprecated? - } + def process + pins = host.query_symbols(params['query']) + info = pins.map do |pin| + # @sg-ignore Need to add nil check here + uri = file_to_uri(pin.best_location.filename) + { + name: pin.path, + containerName: pin.namespace, + kind: pin.symbol_kind, + location: { + uri: uri, + # @sg-ignore Need to add nil check here + range: pin.best_location.range.to_hash + }, + deprecated: pin.deprecated? + } + end + set_result info + end + end + end end - set_result info end end diff --git a/lib/solargraph/language_server/request.rb b/lib/solargraph/language_server/request.rb index 2cc874613..2a2a29221 100644 --- a/lib/solargraph/language_server/request.rb +++ b/lib/solargraph/language_server/request.rb @@ -4,18 +4,20 @@ module Solargraph module LanguageServer class Request # @param id [Integer] - # @param &block The block that processes the client's response + # @param block [Proc] The block that processes the client's response def initialize id, &block @id = id @block = block end + # @sg-ignore Solargraph::LanguageServer::Request#process return + # type could not be inferred # @param result [Object] # @generic T # @yieldreturn [generic] # @return [generic, nil] def process result - @block.call(result) unless @block.nil? + @block&.call(result) end # @return [void] diff --git a/lib/solargraph/language_server/transport/data_reader.rb b/lib/solargraph/language_server/transport/data_reader.rb index 3fc3ed311..5145e41e6 100644 --- a/lib/solargraph/language_server/transport/data_reader.rb +++ b/lib/solargraph/language_server/transport/data_reader.rb @@ -33,8 +33,8 @@ def receive data @buffer.concat char if @in_header prepare_to_parse_message if @buffer.end_with?("\r\n\r\n") - else - parse_message_from_buffer if @buffer.bytesize == @content_length + elsif @buffer.bytesize == @content_length + parse_message_from_buffer end end end @@ -56,17 +56,15 @@ def prepare_to_parse_message # @return [void] def parse_message_from_buffer - begin - msg = JSON.parse(@buffer) - @message_handler.call msg unless @message_handler.nil? - rescue JSON::ParserError => e - Solargraph::Logging.logger.warn "Failed to parse request: #{e.message}" - Solargraph::Logging.logger.debug "Buffer: #{@buffer}" - ensure - @buffer.clear - @in_header = true - @content_length = 0 - end + msg = JSON.parse(@buffer) + @message_handler&.call msg + rescue JSON::ParserError => e + Solargraph::Logging.logger.warn "Failed to parse request: #{e.message}" + Solargraph::Logging.logger.debug "Buffer: #{@buffer}" + ensure + @buffer.clear + @in_header = true + @content_length = 0 end end end diff --git a/lib/solargraph/language_server/uri_helpers.rb b/lib/solargraph/language_server/uri_helpers.rb index c7e55afb8..fcf2e0dcb 100644 --- a/lib/solargraph/language_server/uri_helpers.rb +++ b/lib/solargraph/language_server/uri_helpers.rb @@ -14,7 +14,7 @@ module UriHelpers # @param uri [String] # @return [String] def uri_to_file uri - decode(uri).sub(/^file\:(?:\/\/)?/, '').sub(/^\/([a-z]\:)/i, '\1') + decode(uri).sub(%r{^file:(?://)?}, '').sub(%r{^/([a-z]:)}i, '\1') end # Convert a file path to a URI. @@ -22,7 +22,7 @@ def uri_to_file uri # @param file [String] # @return [String] def file_to_uri file - "file://#{encode(file.gsub(/^([a-z]\:)/i, '/\1'))}" + "file://#{encode(file.gsub(/^([a-z]:)/i, '/\1'))}" end # Encode text to be used as a URI path component in LSP. diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 5c7851201..8cf806275 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -1,9 +1,16 @@ # frozen_string_literal: true +require 'rubygems' require 'pathname' require 'observer' require 'open3' +# @!parse +# class ::Gem::Specification +# # @return [String] +# def name; end +# end + module Solargraph # A Library handles coordination between a Workspace and an ApiMap. # @@ -33,6 +40,7 @@ def initialize workspace = Solargraph::Workspace.new, name = nil # @type [Source, nil] @current = nil @sync_count = 0 + @cache_progress = nil end def inspect @@ -57,8 +65,11 @@ def synchronized? # @param source [Source, nil] # @return [void] def attach source + # @sg-ignore Need to add nil check here if @current && (!source || @current.filename != source.filename) && source_map_hash.key?(@current.filename) && !workspace.has_file?(@current.filename) + # @sg-ignore Need to add nil check here source_map_hash.delete @current.filename + # @sg-ignore Need to add nil check here source_map_external_require_hash.delete @current.filename @external_requires = nil end @@ -116,8 +127,8 @@ def create filename, text # @return [Boolean] True if at least one file was added to the workspace. def create_from_disk *filenames sources = filenames - .reject { |filename| File.directory?(filename) || !File.exist?(filename) } - .map { |filename| Solargraph::Source.load_string(File.read(filename), filename) } + .reject { |filename| File.directory?(filename) || !File.exist?(filename) } + .map { |filename| Solargraph::Source.load_string(File.read(filename), filename) } result = workspace.merge(*sources) sources.each { |source| maybe_map source } result @@ -182,9 +193,14 @@ def definitions_at filename, line, column if cursor.comment? source = read(filename) offset = Solargraph::Position.to_offset(source.code, Solargraph::Position.new(line, column)) - lft = source.code[0..offset-1].match(/\[[a-z0-9_:<, ]*?([a-z0-9_:]*)\z/i) - rgt = source.code[offset..-1].match(/^([a-z0-9_]*)(:[a-z0-9_:]*)?[\]>, ]/i) + # @sg-ignore Need to add nil check here + # @type [MatchData, nil] + lft = source.code[0..(offset - 1)].match(/\[[a-z0-9_:<, ]*?([a-z0-9_:]*)\z/i) + # @sg-ignore Need to add nil check here + # @type [MatchData, nil] + rgt = source.code[offset..].match(/^([a-z0-9_]*)(:[a-z0-9_:]*)?[\]>, ]/i) if lft && rgt + # @sg-ignore Need to add nil check here tag = (lft[1] + rgt[1]).sub(/:+$/, '') clip = mutex.synchronize { api_map.clip(cursor) } clip.translate tag @@ -248,22 +264,26 @@ def references_from filename, line, column, strip: false, only: false return [] unless pin result = [] files = if only - [api_map.source_map(filename)] - else - (workspace.sources + (@current ? [@current] : [])) - end + [api_map.source_map(filename)] + else + (workspace.sources + (@current ? [@current] : [])) + end files.uniq(&:filename).each do |source| found = source.references(pin.name) found.select! do |loc| + # @sg-ignore Need to add nil check here + # @type [Solargraph::Pin::Base, nil] referenced = definitions_at(loc.filename, loc.range.ending.line, loc.range.ending.character).first referenced&.path == pin.path end if pin.path == 'Class#new' + # @todo flow sensitive typing should allow shadowing of Kernel#caller caller = cursor.chain.base.infer(api_map, clip.send(:closure), clip.locals).first if caller.defined? found.select! do |loc| clip = api_map.clip_at(loc.filename, loc.range.start) other = clip.send(:cursor).chain.base.infer(api_map, clip.send(:closure), clip.locals).first + # @todo flow sensitive typing should allow shadowing of Kernel#caller caller == other end else @@ -271,14 +291,15 @@ def references_from filename, line, column, strip: false, only: false end end # HACK: for language clients that exclude special characters from the start of variable names - if strip && match = cursor.word.match(/^[^a-z0-9_]+/i) + if strip && (match = cursor.word.match(/^[^a-z0-9_]+/i)) found.map! do |loc| - Solargraph::Location.new(loc.filename, Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line, loc.range.ending.column)) + Solargraph::Location.new(loc.filename, + # @sg-ignore flow sensitive typing needs to handle if foo = bar + Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line, + loc.range.ending.column)) end end - result.concat(found.sort do |a, b| - a.range.start.line <=> b.range.start.line - end) + result.concat(found.sort { |a, b| a.range.start.line <=> b.range.start.line }) end result.uniq end @@ -299,18 +320,17 @@ def locate_pins location def locate_ref location map = source_map_hash[location.filename] return if map.nil? + # @sg-ignore Need to add nil check here pin = map.requires.select { |p| p.location.range.contain?(location.range.start) }.first return nil if pin.nil? # @param full [String] return_if_match = proc do |full| - if source_map_hash.key?(full) - return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) - end + return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) if source_map_hash.key?(full) end workspace.require_paths.each do |path| full = File.join path, pin.name - return_if_match.(full) - return_if_match.(full << ".rb") + return_if_match.call(full) + return_if_match.call(full << '.rb') end nil rescue FileNotFoundError @@ -403,6 +423,7 @@ def diagnose filename workspace.config.reporters.each do |line| if line == 'all!' Diagnostics.reporters.each do |reporter_name| + # @sg-ignore Need to add nil check here repargs[Diagnostics.reporter(reporter_name)] ||= [] end else @@ -410,7 +431,9 @@ def diagnose filename name = args.shift reporter = Diagnostics.reporter(name) raise DiagnosticsError, "Diagnostics reporter #{name} does not exist" if reporter.nil? + # @sg-ignore flow sensitive typing needs to handle 'raise if' repargs[reporter] ||= [] + # @sg-ignore flow sensitive typing needs to handle 'raise if' repargs[reporter].concat args end end @@ -433,6 +456,7 @@ def bench source_maps: source_map_hash.values, workspace: workspace, external_requires: external_requires, + # @sg-ignore Need to add nil check here live_map: @current ? source_map_hash[@current.filename] : nil ) end @@ -469,10 +493,13 @@ def mapped? # @return [SourceMap, Boolean] def next_map return false if mapped? + # @sg-ignore Need to add nil check here src = workspace.sources.find { |s| !source_map_hash.key?(s.filename) } if src Logging.logger.debug "Mapping #{src.filename}" + # @sg-ignore Need to add nil check here source_map_hash[src.filename] = Solargraph::SourceMap.map(src) + # @sg-ignore Need to add nil check here source_map_hash[src.filename] else false @@ -482,7 +509,9 @@ def next_map # @return [self] def map! workspace.sources.each do |src| + # @sg-ignore Need to add nil check here source_map_hash[src.filename] = Solargraph::SourceMap.map(src) + # @sg-ignore Need to add nil check here find_external_requires source_map_hash[src.filename] end self @@ -500,6 +529,11 @@ def external_requires private + # @return [PinCache] + def pin_cache + workspace.pin_cache + end + # @return [Hash{String => Array}] def source_map_external_require_hash @source_map_external_require_hash ||= {} @@ -509,14 +543,15 @@ def source_map_external_require_hash # @return [void] def find_external_requires source_map # @type [Set] - new_set = source_map.requires.map(&:name).to_set + new_set = source_map.requires.to_set(&:name) # return if new_set == source_map_external_require_hash[source_map.filename] _filenames = nil - filenames = ->{ _filenames ||= workspace.filenames.to_set } + filenames = -> { _filenames ||= workspace.filenames.to_set } + # @sg-ignore Need to add nil check here source_map_external_require_hash[source_map.filename] = new_set.reject do |path| workspace.require_paths.any? do |base| full = File.join(base, path) - filenames[].include?(full) or filenames[].include?(full << ".rb") + filenames[].include?(full) or filenames[].include?(full << '.rb') end end @external_requires = nil @@ -550,23 +585,24 @@ def read filename # @param error [FileNotFoundError] # @return [nil] def handle_file_not_found filename, error - if workspace.source(filename) - Solargraph.logger.debug "#{filename} is not cataloged in the ApiMap" - nil - else - raise error - end + raise error unless workspace.source(filename) + Solargraph.logger.debug "#{filename} is not cataloged in the ApiMap" + nil end # @param source [Source, nil] # @return [void] def maybe_map source return unless source + # @sg-ignore Need to add nil check here return unless @current == source || workspace.has_file?(source.filename) + # @sg-ignore Need to add nil check here if source_map_hash.key?(source.filename) new_map = Solargraph::SourceMap.map(source) + # @sg-ignore Need to add nil check here source_map_hash[source.filename] = new_map else + # @sg-ignore Need to add nil check here source_map_hash[source.filename] = Solargraph::SourceMap.map(source) end end @@ -585,7 +621,7 @@ def cache_next_gemspec pending = api_map.uncached_gemspecs.length - cache_errors.length - 1 - if Yardoc.processing?(spec) + if pin_cache.yardoc_processing?(spec) logger.info "Enqueuing cache of #{spec.name} #{spec.version} (already being processed)" queued_gemspec_cache.push(spec) return if pending - queued_gemspec_cache.length < 1 @@ -596,7 +632,10 @@ def cache_next_gemspec logger.info "Caching #{spec.name} #{spec.version}" Thread.new do report_cache_progress spec.name, pending - _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s) + kwargs = {} + kwargs[:chdir] = workspace.directory.to_s if workspace.directory && !workspace.directory.empty? + _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s, + **kwargs) if s.success? logger.info "Cached #{spec.name} #{spec.version}" else @@ -613,8 +652,7 @@ def cache_next_gemspec # @return [Array] def cacheable_specs - cacheable = api_map.uncached_yard_gemspecs + - api_map.uncached_rbs_collection_gemspecs - + cacheable = api_map.uncached_gemspecs + queued_gemspec_cache - cache_errors.to_a return cacheable unless cacheable.empty? @@ -632,26 +670,30 @@ def queued_gemspec_cache # @return [void] def report_cache_progress gem_name, pending @total ||= pending + # @sg-ignore flow sensitive typing needs better handling of ||= on lvars @total = pending if pending > @total + # @sg-ignore flow sensitive typing needs better handling of ||= on lvars finished = @total - pending + # @sg-ignore flow sensitive typing needs better handling of ||= on lvars pct = if @total.zero? - 0 - else - ((finished.to_f / @total.to_f) * 100).to_i - end - message = "#{gem_name}#{pending > 0 ? " (+#{pending})" : ''}" + 0 + else + # @sg-ignore flow sensitive typing needs better handling of ||= on lvars + ((finished.to_f / @total) * 100).to_i + end + message = "#{gem_name}#{" (+#{pending})" if pending.positive?}" # " - if @cache_progress - @cache_progress.report(message, pct) - else + unless @cache_progress @cache_progress = LanguageServer::Progress.new('Caching gem') # If we don't send both a begin and a report, the progress notification # might get stuck in the status bar forever + # @sg-ignore flow sensitive typing should be able to handle redefinition @cache_progress.begin(message, pct) changed notify_observers @cache_progress - @cache_progress.report(message, pct) end + # @sg-ignore flow sensitive typing should be able to handle redefinition + @cache_progress.report(message, pct) changed notify_observers @cache_progress end @@ -666,15 +708,14 @@ def end_cache_progress # @return [void] def sync_catalog - return if @sync_count == 0 + return if @sync_count.zero? mutex.synchronize do logger.info "Cataloging #{workspace.directory.empty? ? 'generic workspace' : workspace.directory}" - source_map_hash.values.each { |map| find_external_requires(map) } + source_map_hash.each_value { |map| find_external_requires(map) } api_map.catalog bench logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)" - logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs" - logger.info "#{api_map.uncached_rbs_collection_gemspecs.length} uncached RBS collection gemspecs" + logger.info "#{api_map.uncached_gemspecs.length} uncached gemspecs" cache_next_gemspec @sync_count = 0 end diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index df92668bf..2317c3cb4 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -6,6 +6,7 @@ module Solargraph # class Location include Equality + include Comparable # @return [String] attr_reader :filename @@ -13,20 +14,17 @@ class Location # @return [Solargraph::Range] attr_reader :range - # @param filename [String] + # @param filename [String, nil] # @param range [Solargraph::Range] def initialize filename, range + raise 'Use nil to represent no-file' if filename&.empty? + @filename = filename @range = range end - # @sg-ignore Fix "Not enough arguments to Module#protected" - protected def equality_fields - [filename, range] - end - # @param other [self] - def <=>(other) + def <=> other return nil unless other.is_a?(Location) if filename == other.filename range <=> other.range @@ -44,10 +42,6 @@ def contain? location range.contain?(location.range.start) && range.contain?(location.range.ending) && filename == location.filename end - def inspect - "<#{self.class.name}: filename=#{filename}, range=#{range.inspect}>" - end - def to_s inspect end @@ -62,21 +56,30 @@ def to_hash # @param node [Parser::AST::Node, nil] # @return [Location, nil] - def self.from_node(node) + def self.from_node node return nil if node.nil? || node.loc.nil? + filename = node.loc.expression.source_buffer.name + # @sg-ignore flow sensitive typing needs to create separate ranges for postfix if + filename = nil if filename.empty? range = Range.from_node(node) - self.new(node.loc.expression.source_buffer.name, range) + # @sg-ignore Need to add nil check here + new(filename, range) end # @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 def inspect "#<#{self.class} #{filename}, #{range.inspect}>" end + + protected + + def equality_fields + [filename, range] + end end end diff --git a/lib/solargraph/logging.rb b/lib/solargraph/logging.rb index 8f3edaba2..d472438e2 100644 --- a/lib/solargraph/logging.rb +++ b/lib/solargraph/logging.rb @@ -10,7 +10,7 @@ module Logging 'warn' => Logger::WARN, 'info' => Logger::INFO, 'debug' => Logger::DEBUG - } + }.freeze configured_level = ENV.fetch('SOLARGRAPH_LOG', nil) level = if LOG_LEVELS.keys.include?(configured_level) LOG_LEVELS.fetch(configured_level) @@ -21,17 +21,37 @@ module Logging end DEFAULT_LOG_LEVEL end - @@logger = Logger.new(STDERR, level: level) + @@logger = Logger.new($stderr, level: level) # @sg-ignore Fix cvar issue - @@logger.formatter = proc do |severity, datetime, progname, msg| + @@logger.formatter = proc do |severity, _datetime, _progname, msg| "[#{severity}] #{msg}\n" end module_function + # override this in your class to temporarily set a custom + # filtering log level for the class (e.g., suppress any debug + # message by setting it to :info even if it is set elsewhere, or + # show existing debug messages by setting to :debug). + # + # @return [Symbol] + def log_level + :warn + end + # @return [Logger] def logger - @@logger + if LOG_LEVELS[log_level.to_s] == DEFAULT_LOG_LEVEL + @@logger + else + new_log_level = LOG_LEVELS[log_level.to_s] + logger = Logger.new($stderr, level: new_log_level) + + # @sg-ignore Wrong argument type for Logger#formatter=: arg_0 + # expected nil, received Logger::_Formatter, nil + logger.formatter = @@logger.formatter + logger + end end end end diff --git a/lib/solargraph/page.rb b/lib/solargraph/page.rb index 12782da90..207b84665 100644 --- a/lib/solargraph/page.rb +++ b/lib/solargraph/page.rb @@ -49,7 +49,7 @@ def ruby_to_html code # @param directory [String] def initialize directory = VIEWS_PATH - directory = VIEWS_PATH if directory.nil? or !File.directory?(directory) + directory = VIEWS_PATH if directory.nil? || !File.directory?(directory) directories = [directory] directories.push VIEWS_PATH if directory != VIEWS_PATH # @type [Proc] diff --git a/lib/solargraph/parser.rb b/lib/solargraph/parser.rb index 1f02befe7..7479f4fcc 100644 --- a/lib/solargraph/parser.rb +++ b/lib/solargraph/parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Solargraph module Parser autoload :CommentRipper, 'solargraph/parser/comment_ripper' diff --git a/lib/solargraph/parser/comment_ripper.rb b/lib/solargraph/parser/comment_ripper.rb index 92373df20..89d4a2c91 100644 --- a/lib/solargraph/parser/comment_ripper.rb +++ b/lib/solargraph/parser/comment_ripper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'ripper' module Solargraph @@ -23,37 +25,50 @@ def on_comment *args # @sg-ignore # @type [Array(Symbol, String, Array([Integer, nil], [Integer, nil]))] result = super + # @sg-ignore Need to add nil check here if @buffer_lines[result[2][0]][0..result[2][1]].strip =~ /^#/ chomped = result[1].chomp - if result[2][0] == 0 && chomped.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '').match(/^#\s*frozen_string_literal:/) + if result[2][0].zero? && chomped.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, + replace: '').match(/^#\s*frozen_string_literal:/) chomped = '#' end - @comments[result[2][0]] = Snippet.new(Range.from_to(result[2][0], result[2][1], result[2][0], result[2][1] + chomped.length), chomped) + @comments[result[2][0]] = + Snippet.new(Range.from_to(result[2][0], result[2][1], result[2][0], result[2][1] + chomped.length), chomped) end result end # @param result [Array(Symbol, String, Array([Integer, nil], [Integer, nil]))] # @return [void] - def create_snippet(result) + def create_snippet result chomped = result[1].chomp - @comments[result[2][0]] = Snippet.new(Range.from_to(result[2][0] || 0, result[2][1] || 0, result[2][0] || 0, (result[2][1] || 0) + chomped.length), chomped) + @comments[result[2][0]] = + Snippet.new( + Range.from_to(result[2][0] || 0, result[2][1] || 0, result[2][0] || 0, + (result[2][1] || 0) + chomped.length), chomped + ) end + # @sg-ignore @override is adding, not overriding def on_embdoc_beg *args result = super + # @sg-ignore @override is adding, not overriding create_snippet(result) result end + # @sg-ignore @override is adding, not overriding def on_embdoc *args result = super + # @sg-ignore @override is adding, not overriding create_snippet(result) result end + # @sg-ignore @override is adding, not overriding def on_embdoc_end *args result = super + # @sg-ignore @override is adding, not overriding create_snippet(result) result end diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 41ce6eeaf..32e4866d1 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -1,20 +1,29 @@ +# frozen_string_literal: true + module Solargraph module Parser class FlowSensitiveTyping include Solargraph::Parser::NodeMethods - # @param locals [Array] + # @param locals [Array] + # @param ivars [Array] # @param enclosing_breakable_pin [Solargraph::Pin::Breakable, nil] - def initialize(locals, enclosing_breakable_pin = nil) + # @param enclosing_compound_statement_pin [Solargraph::Pin::CompoundStatement, nil] + def initialize locals, ivars, enclosing_breakable_pin, enclosing_compound_statement_pin @locals = locals + @ivars = ivars @enclosing_breakable_pin = enclosing_breakable_pin + @enclosing_compound_statement_pin = enclosing_compound_statement_pin end # @param and_node [Parser::AST::Node] # @param true_ranges [Array] + # @param false_ranges [Array] # # @return [void] - def process_and(and_node, true_ranges = []) + def process_and and_node, true_ranges = [], false_ranges = [] + return unless and_node.type == :and + # @type [Parser::AST::Node] lhs = and_node.children[0] # @type [Parser::AST::Node] @@ -25,13 +34,64 @@ def process_and(and_node, true_ranges = []) rhs_presence = Range.new(before_rhs_pos, get_node_end_position(rhs)) - process_isa(lhs, true_ranges + [rhs_presence]) + + # can't assume if an and is false that every single condition + # is false, so don't provide any false ranges to assert facts + # on + process_expression(lhs, true_ranges + [rhs_presence], []) + process_expression(rhs, true_ranges, []) + end + + # @param or_node [Parser::AST::Node] + # @param true_ranges [Array] + # @param false_ranges [Array] + # + # @return [void] + def process_or or_node, true_ranges = [], false_ranges = [] + return unless or_node.type == :or + + # @type [Parser::AST::Node] + lhs = or_node.children[0] + # @type [Parser::AST::Node] + rhs = or_node.children[1] + + before_rhs_loc = rhs.location.expression.adjust(begin_pos: -1) + before_rhs_pos = Position.new(before_rhs_loc.line, before_rhs_loc.column) + + rhs_presence = Range.new(before_rhs_pos, + get_node_end_position(rhs)) + + # can assume if an or is false that every single condition is + # false, so provide false ranges to assert facts on + + # can't assume if an or is true that every single condition is + # true, so don't provide true ranges to assert facts on + + process_expression(lhs, [], false_ranges + [rhs_presence]) + process_expression(rhs, [], false_ranges) + end + + # @param node [Parser::AST::Node] + # @param true_presences [Array] + # @param false_presences [Array] + # + # @return [void] + def process_calls node, true_presences, false_presences + return unless node.type == :send + + process_isa(node, true_presences, false_presences) + process_nilp(node, true_presences, false_presences) + process_bang(node, true_presences, false_presences) end # @param if_node [Parser::AST::Node] + # @param true_ranges [Array] + # @param false_ranges [Array] # # @return [void] - def process_if(if_node) + def process_if if_node, true_ranges = [], false_ranges = [] + return if if_node.type != :if + # # See if we can refine a type based on the result of 'if foo.nil?' # @@ -44,23 +104,37 @@ def process_if(if_node) # s(:send, nil, :bar)) # [4] pry(main)> conditional_node = if_node.children[0] - # @type [Parser::AST::Node] + # @type [Parser::AST::Node, nil] then_clause = if_node.children[1] - # @type [Parser::AST::Node] + # @type [Parser::AST::Node, nil] else_clause = if_node.children[2] - true_ranges = [] - if always_breaks?(else_clause) - unless enclosing_breakable_pin.nil? - rest_of_breakable_body = Range.new(get_node_end_position(if_node), - get_node_end_position(enclosing_breakable_pin.node)) - true_ranges << rest_of_breakable_body - end + unless enclosing_breakable_pin.nil? + rest_of_breakable_body = Range.new(get_node_end_position(if_node), + get_node_end_position(enclosing_breakable_pin.node)) + + false_ranges << rest_of_breakable_body if always_breaks?(then_clause) + + true_ranges << rest_of_breakable_body if always_breaks?(else_clause) + end + + unless enclosing_compound_statement_pin.node.nil? + rest_of_returnable_body = Range.new(get_node_end_position(if_node), + get_node_end_position(enclosing_compound_statement_pin.node)) + + # + # if one of the clauses always leaves the compound + # statement, we can assume things about the rest of the + # compound statement + # + false_ranges << rest_of_returnable_body if always_leaves_compound_statement?(then_clause) + + true_ranges << rest_of_returnable_body if always_leaves_compound_statement?(else_clause) end unless then_clause.nil? # - # Add specialized locals for the then clause range + # If the condition is true we can assume things about the then clause # before_then_clause_loc = then_clause.location.expression.adjust(begin_pos: -1) before_then_clause_pos = Position.new(before_then_clause_loc.line, before_then_clause_loc.column) @@ -68,170 +142,308 @@ def process_if(if_node) get_node_end_position(then_clause)) end - process_conditional(conditional_node, true_ranges) - end + unless else_clause.nil? + # + # If the condition is true we can assume things about the else clause + # + before_else_clause_loc = else_clause.location.expression.adjust(begin_pos: -1) + before_else_clause_pos = Position.new(before_else_clause_loc.line, before_else_clause_loc.column) + false_ranges << Range.new(before_else_clause_pos, + get_node_end_position(else_clause)) + end - class << self - include Logging + process_expression(conditional_node, true_ranges, false_ranges) end - # Find a variable pin by name and where it is used. - # - # Resolves our most specific view of this variable's type by - # preferring pins created by flow-sensitive typing when we have - # them based on the Closure and Location. - # - # @param pins [Array] - # @param name [String] - # @param closure [Pin::Closure] - # @param location [Location] + # @param while_node [Parser::AST::Node] + # @param true_ranges [Array] + # @param false_ranges [Array] # - # @return [Array] - def self.visible_pins(pins, name, closure, location) - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location})" } - pins_with_name = pins.select { |p| p.name == name } - if pins_with_name.empty? - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => [] - no pins with name" } - return [] - end - pins_with_specific_visibility = pins.select { |p| p.name == name && p.presence && p.visible_at?(closure, location) } - if pins_with_specific_visibility.empty? - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{pins_with_name} - no pins with specific visibility" } - return pins_with_name - end - visible_pins_specific_to_this_closure = pins_with_specific_visibility.select { |p| p.closure == closure } - if visible_pins_specific_to_this_closure.empty? - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{pins_with_specific_visibility} - no visible pins specific to this closure (#{closure})}" } - return pins_with_specific_visibility - end - flow_defined_pins = pins_with_specific_visibility.select { |p| p.presence_certain? } - if flow_defined_pins.empty? - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{visible_pins_specific_to_this_closure} - no flow-defined pins" } - return visible_pins_specific_to_this_closure + # @return [void] + def process_while while_node, true_ranges = [], false_ranges = [] + return if while_node.type != :while + + # + # See if we can refine a type based on the result of 'if foo.nil?' + # + # [3] pry(main)> Parser::CurrentRuby.parse("while a; b; c; end") + # => s(:while, + # s(:send, nil, :a), + # s(:begin, + # s(:send, nil, :b), + # s(:send, nil, :c))) + # [4] pry(main)> + conditional_node = while_node.children[0] + # @type [Parser::AST::Node, nil] + do_clause = while_node.children[1] + + unless do_clause.nil? + # + # If the condition is true we can assume things about the do clause + # + before_do_clause_loc = do_clause.location.expression.adjust(begin_pos: -1) + before_do_clause_pos = Position.new(before_do_clause_loc.line, before_do_clause_loc.column) + true_ranges << Range.new(before_do_clause_pos, + get_node_end_position(do_clause)) end - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{flow_defined_pins}" } + process_expression(conditional_node, true_ranges, false_ranges) + end - flow_defined_pins + class << self + include Logging end include Logging private - # @param pin [Pin::LocalVariable] - # @param downcast_type_name [String] + # @param pin [Pin::BaseVariable] # @param presence [Range] + # @param downcast_type [ComplexType, nil] + # @param downcast_not_type [ComplexType, nil] # # @return [void] - def add_downcast_local(pin, downcast_type_name, presence) - # @todo Create pin#update method - new_pin = Solargraph::Pin::LocalVariable.new( - location: pin.location, - closure: pin.closure, - name: pin.name, - assignment: pin.assignment, - comments: pin.comments, - presence: presence, - return_type: ComplexType.try_parse(downcast_type_name), - presence_certain: true, - source: :flow_sensitive_typing - ) - locals.push(new_pin) - end - - # @param facts_by_pin [Hash{Pin::LocalVariable => Array String}>}] + def add_downcast_var pin, presence:, downcast_type:, downcast_not_type: + new_pin = pin.downcast(exclude_return_type: downcast_not_type, + intersection_return_type: downcast_type, + source: :flow_sensitive_typing, + presence: presence) + if pin.is_a?(Pin::LocalVariable) + locals.push(new_pin) + elsif pin.is_a?(Pin::InstanceVariable) + ivars.push(new_pin) + else + raise "Tried to add invalid pin type #{pin.class} in FlowSensitiveTyping" + end + end + + # @param facts_by_pin [Hash{Pin::BaseVariable => Array ComplexType}>}] # @param presences [Array] # # @return [void] - def process_facts(facts_by_pin, presences) + def process_facts facts_by_pin, presences # - # Add specialized locals for the rest of the block + # Add specialized vars for the rest of the block # facts_by_pin.each_pair do |pin, facts| facts.each do |fact| - downcast_type_name = fact.fetch(:type) + downcast_type = fact.fetch(:type, nil) + downcast_not_type = fact.fetch(:not_type, nil) presences.each do |presence| - add_downcast_local(pin, downcast_type_name, presence) + add_downcast_var(pin, + presence: presence, + downcast_type: downcast_type, + downcast_not_type: downcast_not_type) end end end end - # @param conditional_node [Parser::AST::Node] + # @param expression_node [Parser::AST::Node] # @param true_ranges [Array] + # @param false_ranges [Array] # # @return [void] - def process_conditional(conditional_node, true_ranges) - if conditional_node.type == :send - process_isa(conditional_node, true_ranges) - elsif conditional_node.type == :and - process_and(conditional_node, true_ranges) - end + def process_expression expression_node, true_ranges, false_ranges + process_calls(expression_node, true_ranges, false_ranges) + process_and(expression_node, true_ranges, false_ranges) + process_or(expression_node, true_ranges, false_ranges) + process_variable(expression_node, true_ranges, false_ranges) end - # @param isa_node [Parser::AST::Node] - # @return [Array(String, String), nil] - def parse_isa(isa_node) - return unless isa_node&.type == :send && isa_node.children[1] == :is_a? + # @param call_node [Parser::AST::Node] + # @param method_name [Symbol] + # @return [Array(String, String), nil] Tuple of rgument to + # function, then receiver of function if it's a variable, + # otherwise nil if no simple variable receiver + def parse_call call_node, method_name + return unless call_node&.type == :send && call_node.children[1] == method_name # Check if conditional node follows this pattern: # s(:send, # s(:send, nil, :foo), :is_a?, # s(:const, nil, :Baz)), - isa_receiver = isa_node.children[0] - isa_type_name = type_name(isa_node.children[2]) - return unless isa_type_name + # + call_receiver = call_node.children[0] + call_arg = type_name(call_node.children[2]) - # check if isa_receiver looks like this: + # check if call_receiver looks like this: # s(:send, nil, :foo) # and set variable_name to :foo - if isa_receiver&.type == :send && isa_receiver.children[0].nil? && isa_receiver.children[1].is_a?(Symbol) - variable_name = isa_receiver.children[1].to_s + if call_receiver&.type == :send && call_receiver.children[0].nil? && call_receiver.children[1].is_a?(Symbol) + variable_name = call_receiver.children[1].to_s end # or like this: # (lvar :repr) - variable_name = isa_receiver.children[0].to_s if isa_receiver&.type == :lvar + # @sg-ignore Need to look at Tuple#include? handling + variable_name = call_receiver.children[0].to_s if %i[lvar ivar].include?(call_receiver&.type) return unless variable_name - [isa_type_name, variable_name] + [call_arg, variable_name] + end + + # @param isa_node [Parser::AST::Node] + # @return [Array(String, String), nil] + def parse_isa isa_node + call_type_name, variable_name = parse_call(isa_node, :is_a?) + + return unless call_type_name + + [call_type_name, variable_name] end # @param variable_name [String] # @param position [Position] # - # @return [Solargraph::Pin::LocalVariable, nil] - def find_local(variable_name, position) - pins = locals.select { |pin| pin.name == variable_name && pin.presence.include?(position) } - return unless pins.length == 1 - pins.first + # @sg-ignore Solargraph::Parser::FlowSensitiveTyping#find_var + # return type could not be inferred + # @return [Solargraph::Pin::LocalVariable, Solargraph::Pin::InstanceVariable, nil] + def find_var variable_name, position + if variable_name.start_with?('@') + # @sg-ignore flow sensitive typing needs to handle attrs + ivars.find { |ivar| ivar.name == variable_name && (!ivar.presence || ivar.presence.include?(position)) } + else + # @sg-ignore flow sensitive typing needs to handle attrs + locals.find { |pin| pin.name == variable_name && (!pin.presence || pin.presence.include?(position)) } + end end # @param isa_node [Parser::AST::Node] # @param true_presences [Array] + # @param false_presences [Array] # # @return [void] - def process_isa(isa_node, true_presences) + def process_isa isa_node, true_presences, false_presences isa_type_name, variable_name = parse_isa(isa_node) return if variable_name.nil? || variable_name.empty? + # @sg-ignore Need to add nil check here isa_position = Range.from_node(isa_node).start - pin = find_local(variable_name, isa_position) + pin = find_var(variable_name, isa_position) + return unless pin + + # @type Hash{Pin::BaseVariable => Array ComplexType}>} + if_true = {} + if_true[pin] ||= [] + if_true[pin] << { type: ComplexType.parse(isa_type_name) } + process_facts(if_true, true_presences) + + # @type Hash{Pin::BaseVariable => Array ComplexType}>} + if_false = {} + if_false[pin] ||= [] + if_false[pin] << { not_type: ComplexType.parse(isa_type_name) } + process_facts(if_false, false_presences) + end + + # @param nilp_node [Parser::AST::Node] + # @return [Array(String, String), nil] + def parse_nilp nilp_node + parse_call(nilp_node, :nil?) + end + + # @param nilp_node [Parser::AST::Node] + # @param true_presences [Array] + # @param false_presences [Array] + # + # @return [void] + def process_nilp nilp_node, true_presences, false_presences + nilp_arg, variable_name = parse_nilp(nilp_node) + return if variable_name.nil? || variable_name.empty? + # if .nil? got an argument, move on, this isn't the situation + # we're looking for and typechecking will cover any invalid + # ones + return unless nilp_arg.nil? + # @sg-ignore Need to add nil check here + nilp_position = Range.from_node(nilp_node).start + + pin = find_var(variable_name, nilp_position) return unless pin + # @type Hash{Pin::LocalVariable => Array ComplexType}>} if_true = {} if_true[pin] ||= [] - if_true[pin] << { type: isa_type_name } + if_true[pin] << { type: ComplexType::NIL } process_facts(if_true, true_presences) + + # @type Hash{Pin::LocalVariable => Array ComplexType}>} + if_false = {} + if_false[pin] ||= [] + if_false[pin] << { not_type: ComplexType::NIL } + process_facts(if_false, false_presences) + end + + # @param bang_node [Parser::AST::Node] + # @return [Array(String, String), nil] + def parse_bang bang_node + parse_call(bang_node, :!) + end + + # @param bang_node [Parser::AST::Node] + # @param true_presences [Array] + # @param false_presences [Array] + # + # @return [void] + def process_bang bang_node, true_presences, false_presences + # pry(main)> require 'parser/current'; Parser::CurrentRuby.parse("!2") + # => s(:send, + # s(:int, 2), :!) + # end + return unless bang_node.type == :send && bang_node.children[1] == :! + + receiver = bang_node.children[0] + + # swap the two presences + process_expression(receiver, false_presences, true_presences) + end + + # @param var_node [Parser::AST::Node] + # + # @return [String, nil] Variable name referenced + def parse_variable var_node + return if var_node.children.length != 1 + + var_node.children[0]&.to_s + end + + # @return [void] + # @param node [Parser::AST::Node] + # @param true_presences [Array] + # @param false_presences [Array] + def process_variable node, true_presences, false_presences + return unless %i[lvar ivar cvar gvar].include?(node.type) + + variable_name = parse_variable(node) + return if variable_name.nil? + + # @sg-ignore Need to add nil check here + var_position = Range.from_node(node).start + + pin = find_var(variable_name, var_position) + return unless pin + + # @type Hash{Pin::LocalVariable => Array ComplexType}>} + if_true = {} + if_true[pin] ||= [] + if_true[pin] << { not_type: ComplexType::NIL } + process_facts(if_true, true_presences) + + # @type Hash{Pin::LocalVariable => Array ComplexType}>} + if_false = {} + if_false[pin] ||= [] + if_false[pin] << { type: ComplexType.parse('nil, false') } + process_facts(if_false, false_presences) end # @param node [Parser::AST::Node] # # @return [String, nil] - def type_name(node) + def type_name node # e.g., # s(:const, nil, :Baz) return unless node&.type == :const + # @type [Parser::AST::Node, nil] module_node = node.children[0] + # @type [Parser::AST::Node, nil] class_node = node.children[1] return class_node.to_s if module_node.nil? @@ -242,14 +454,20 @@ def type_name(node) "#{module_type_name}::#{class_node}" end - # @param clause_node [Parser::AST::Node] - def always_breaks?(clause_node) + # @param clause_node [Parser::AST::Node, nil] + # @sg-ignore need boolish support for ? methods + def always_breaks? clause_node clause_node&.type == :break end - attr_reader :locals + # @param clause_node [Parser::AST::Node, nil] + def always_leaves_compound_statement? clause_node + # https://docs.ruby-lang.org/en/2.2.0/keywords_rdoc.html + # @sg-ignore Need to look at Tuple#include? handling + %i[return raise next redo retry].include?(clause_node&.type) + end - attr_reader :enclosing_breakable_pin + attr_reader :locals, :ivars, :enclosing_breakable_pin, :enclosing_compound_statement_pin end end end diff --git a/lib/solargraph/parser/node_processor.rb b/lib/solargraph/parser/node_processor.rb index dbe0b7cd5..d3579df80 100644 --- a/lib/solargraph/parser/node_processor.rb +++ b/lib/solargraph/parser/node_processor.rb @@ -35,27 +35,28 @@ def deregister type, cls # @param node [Parser::AST::Node] # @param region [Region] # @param pins [Array] - # @param locals [Array] - # @return [Array(Array, Array)] - def self.process node, region = Region.new, pins = [], locals = [] + # @param locals [Array] + # @param ivars [Array] + # @return [Array(Array, Array, Array)] + def self.process node, region = Region.new, pins = [], locals = [], ivars = [] if pins.empty? pins.push Pin::Namespace.new( location: region.source.location, name: '', - source: :parser, + source: :parser ) end - return [pins, locals] unless Parser.is_ast_node?(node) + return [pins, locals, ivars] unless Parser.is_ast_node?(node) node_processor_classes = @@processors[node.type] || [NodeProcessor::Base] node_processor_classes.each do |klass| - processor = klass.new(node, region, pins, locals) + processor = klass.new(node, region, pins, locals, ivars) process_next = processor.process break unless process_next end - [pins, locals] + [pins, locals, ivars] end end end diff --git a/lib/solargraph/parser/node_processor/base.rb b/lib/solargraph/parser/node_processor/base.rb index fad31e95b..48fd070f6 100644 --- a/lib/solargraph/parser/node_processor/base.rb +++ b/lib/solargraph/parser/node_processor/base.rb @@ -16,15 +16,20 @@ class Base # @return [Array] attr_reader :locals + # @return [Array] + attr_reader :ivars + # @param node [Parser::AST::Node] # @param region [Region] # @param pins [Array] # @param locals [Array] - def initialize node, region, pins, locals + # @param ivars [Array] + def initialize node, region, pins, locals, ivars @node = node @region = region @pins = pins @locals = locals + @ivars = ivars @processed_children = false end @@ -40,6 +45,28 @@ def process private + # @return [Solargraph::Location] + def location + get_node_location(node) + end + + # @return [Solargraph::Position] + def position + Position.new(node.loc.line, node.loc.column) + end + + # @sg-ignore downcast output of Enumerable#select + # @return [Solargraph::Pin::Breakable, nil] + def enclosing_breakable_pin + pins.select { |pin| pin.is_a?(Pin::Breakable) && pin.location&.range&.contain?(position) }.last + end + + # @todo downcast output of Enumerable#select + # @return [Solargraph::Pin::CompoundStatement, nil] + def enclosing_compound_statement_pin + pins.select { |pin| pin.is_a?(Pin::CompoundStatement) && pin.location&.range&.contain?(position) }.last + end + # @param subregion [Region] # @return [void] def process_children subregion = region @@ -47,20 +74,20 @@ def process_children subregion = region @processed_children = true node.children.each do |child| next unless Parser.is_ast_node?(child) - NodeProcessor.process(child, subregion, pins, locals) + NodeProcessor.process(child, subregion, pins, locals, ivars) end end # @param node [Parser::AST::Node] # @return [Solargraph::Location] - def get_node_location(node) + def get_node_location node range = Parser.node_range(node) Location.new(region.filename, range) end # @param node [Parser::AST::Node] # @return [String, nil] - def comments_for(node) + def comments_for node region.source.comments_for(node) end @@ -68,6 +95,7 @@ def comments_for(node) # @return [Pin::Closure, nil] def named_path_pin position pins.select do |pin| + # @sg-ignore Need to add nil check here pin.is_a?(Pin::Closure) && pin.path && !pin.path.empty? && pin.location.range.contain?(position) end.last end @@ -77,6 +105,7 @@ def named_path_pin position # @return [Pin::Closure, nil] def block_pin position # @todo determine if this can return a Pin::Block + # @sg-ignore Need to add nil check here pins.select { |pin| pin.is_a?(Pin::Closure) && pin.location.range.contain?(position) }.last end @@ -84,6 +113,7 @@ def block_pin position # @param position [Solargraph::Position] # @return [Pin::Closure, nil] def closure_pin position + # @sg-ignore Need to add nil check here pins.select { |pin| pin.is_a?(Pin::Closure) && pin.location.range.contain?(position) }.last end end diff --git a/lib/solargraph/parser/parser_gem.rb b/lib/solargraph/parser/parser_gem.rb index 857e17768..3aca3804f 100644 --- a/lib/solargraph/parser/parser_gem.rb +++ b/lib/solargraph/parser/parser_gem.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Solargraph module Parser module ParserGem diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index 2daf22fc7..dfaf7925a 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -8,19 +8,23 @@ module ParserGem module ClassMethods # @param code [String] # @param filename [String, nil] + # @param starting_line [Integer] must be provided so that we + # can find relevant local variables later even if this is just + # a subset of the file in question # @return [Array(Parser::AST::Node, Hash{Integer => Solargraph::Parser::Snippet})] - def parse_with_comments code, filename = nil - node = parse(code, filename) + def parse_with_comments code, filename = nil, starting_line = 0 + node = parse(code, filename, starting_line) comments = CommentRipper.new(code, filename, 0).parse [node, comments] end # @param code [String] # @param filename [String, nil] - # @param line [Integer] + # @param starting_line [Integer] + # @sg-ignore need to understand that raise does not return # @return [Parser::AST::Node] - def parse code, filename = nil, line = 0 - buffer = ::Parser::Source::Buffer.new(filename, line) + def parse code, filename = nil, starting_line = 0 + buffer = ::Parser::Source::Buffer.new(filename, starting_line) buffer.source = code parser.parse(buffer) rescue ::Parser::SyntaxError, ::Parser::UnknownEncodingInMagicComment => e @@ -30,35 +34,43 @@ def parse code, filename = nil, line = 0 # @return [::Parser::Base] def parser @parser ||= Prism::Translation::Parser.new(FlawedBuilder.new).tap do |parser| + # @sg-ignore Unresolved call to diagnostics on Prism::Translation::Parser parser.diagnostics.all_errors_are_fatal = true + # @sg-ignore Unresolved call to diagnostics on Prism::Translation::Parser parser.diagnostics.ignore_warnings = true end end # @param source [Source] - # @return [Array(Array, Array)] + # @return [Array(Array, Array)] def map source - NodeProcessor.process(source.node, Region.new(source: source)) + # @sg-ignore Need to add nil check here + pins, locals, ivars = NodeProcessor.process(source.node, Region.new(source: source)) + pins.concat(ivars) + [pins, locals] end # @param source [Source] # @param name [String] # @return [Array] def references source, name - if name.end_with?("=") + if name.end_with?('=') reg = /#{Regexp.escape name[0..-2]}\s*=/ # @param code [String] # @param offset [Integer] # @return [Array(Integer, Integer), Array(nil, nil)] + # @sg-ignore Need to add nil check here extract_offset = ->(code, offset) { reg.match(code, offset).offset(0) } else # @param code [String] # @param offset [Integer] # @return [Array(Integer, Integer), Array(nil, nil)] + # @sg-ignore Need to add nil check here extract_offset = ->(code, offset) { [soff = code.index(name, offset), soff + name.length] } end inner_node_references(name, source.node).map do |n| rng = Range.from_node(n) + # @sg-ignore Need to add nil check here offset = Position.to_offset(source.code, rng.start) soff, eoff = extract_offset[source.code, offset] Location.new( @@ -86,20 +98,20 @@ def inner_node_references name, top # @return [Source::Chain] def chain *args - NodeChainer.chain *args + NodeChainer.chain(*args) end # @return [Source::Chain] def chain_string *args - NodeChainer.load_string *args + NodeChainer.load_string(*args) end # @return [Array(Array, Array)] def process_node *args - Solargraph::Parser::NodeProcessor.process *args + Solargraph::Parser::NodeProcessor.process(*args) end - # @param node [Parser::AST::Node] + # @param node [Parser::AST::Node, nil] # @return [String, nil] def infer_literal_node_type node NodeMethods.infer_literal_node_type node @@ -110,7 +122,7 @@ def version parser.version end - # @param node [BasicObject] + # @param node [BasicObject, nil] # @return [Boolean] def is_ast_node? node node.is_a?(::Parser::AST::Node) @@ -124,19 +136,25 @@ def node_range node Range.new(st, en) end - # @param node [Parser::AST::Node] + # @param node [Parser::AST::Node, nil] # @return [Array] def string_ranges node return [] unless is_ast_node?(node) result = [] + # @sg-ignore Translate to something flow sensitive typing understands result.push Range.from_node(node) if node.type == :str + # @sg-ignore Translate to something flow sensitive typing understands node.children.each do |child| result.concat string_ranges(child) end + # @sg-ignore Translate to something flow sensitive typing understands if node.type == :dstr && node.children.last.nil? + # @sg-ignore Translate to something flow sensitive typing understands last = node.children[-2] + # @sg-ignore Need to add nil check here unless last.nil? rng = Range.from_node(last) + # @sg-ignore Need to add nil check here pos = Position.new(rng.ending.line, rng.ending.column - 1) result.push Range.new(pos, pos) end diff --git a/lib/solargraph/parser/parser_gem/flawed_builder.rb b/lib/solargraph/parser/parser_gem/flawed_builder.rb index acf665e16..341042f22 100644 --- a/lib/solargraph/parser/parser_gem/flawed_builder.rb +++ b/lib/solargraph/parser/parser_gem/flawed_builder.rb @@ -10,7 +10,7 @@ class FlawedBuilder < ::Parser::Builders::Default # @param token [::Parser::AST::Node] # @return [String] # @sg-ignore - def string_value(token) + def string_value token value(token) end end diff --git a/lib/solargraph/parser/parser_gem/node_chainer.rb b/lib/solargraph/parser/parser_gem/node_chainer.rb index d8d46319b..be6e287cf 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -22,7 +22,7 @@ def initialize node, filename = nil, parent = nil # @return [Source::Chain] def chain links = generate_links(@node) - Chain.new(links, @node, (Parser.is_ast_node?(@node) && @node.type == :splat)) + Chain.new(links, @node, Parser.is_ast_node?(@node) && @node.type == :splat) end class << self @@ -35,9 +35,12 @@ def chain node, filename = nil, parent = nil end # @param code [String] + # @param filename [String] + # @param starting_line [Integer] + # # @return [Source::Chain] - def load_string(code) - node = Parser.parse(code.sub(/\.$/, '')) + def load_string code, filename, starting_line + node = Parser.parse(code.sub(/\.$/, ''), filename, starting_line) chain = NodeChainer.new(node).chain chain.links.push(Chain::Link.new) if code.end_with?('.') chain @@ -60,9 +63,9 @@ def generate_links n result.concat generate_links(n.children[0]) result.push Chain::Call.new(n.children[1].to_s, Location.from_node(n), node_args(n), passed_block(n)) elsif n.children[0].nil? - args = [] - n.children[2..-1].each do |c| - args.push NodeChainer.chain(c, @filename, n) + # @sg-ignore Need to add nil check here + n.children[2..].map do |c| + NodeChainer.chain(c, @filename, n) end result.push Chain::Call.new(n.children[1].to_s, Location.from_node(n), node_args(n), passed_block(n)) else @@ -90,27 +93,46 @@ def generate_links n elsif n.type == :const const = unpack_name(n) result.push Chain::Constant.new(const) - elsif [:lvar, :lvasgn].include?(n.type) + elsif %i[lvar lvasgn].include?(n.type) result.push Chain::Call.new(n.children[0].to_s, Location.from_node(n)) - elsif [:ivar, :ivasgn].include?(n.type) - result.push Chain::InstanceVariable.new(n.children[0].to_s) - elsif [:cvar, :cvasgn].include?(n.type) + elsif %i[ivar ivasgn].include?(n.type) + result.push Chain::InstanceVariable.new(n.children[0].to_s, n, Location.from_node(n)) + elsif %i[cvar cvasgn].include?(n.type) result.push Chain::ClassVariable.new(n.children[0].to_s) - elsif [:gvar, :gvasgn].include?(n.type) + elsif %i[gvar gvasgn].include?(n.type) result.push Chain::GlobalVariable.new(n.children[0].to_s) elsif n.type == :or_asgn - new_node = n.updated(n.children[0].type, n.children[0].children + [n.children[1]]) - result.concat generate_links new_node - elsif [:class, :module, :def, :defs].include?(n.type) + # @bar ||= 123 translates to: + # + # s(:or_asgn, + # s(:ivasgn, :@bar), + # s(:int, 123)) + lhs_chain = NodeChainer.chain n.children[0] # s(:ivasgn, :@bar) + rhs_chain = NodeChainer.chain n.children[1] # s(:int, 123) + or_link = Chain::Or.new([lhs_chain, rhs_chain]) + # this is just for a call chain, so we don't need to record the assignment + result.push(or_link) + elsif %i[class module def defs].include?(n.type) # @todo Undefined or what? result.push Chain::UNDEFINED_CALL elsif n.type == :and result.concat generate_links(n.children.last) elsif n.type == :or - result.push Chain::Or.new([NodeChainer.chain(n.children[0], @filename), NodeChainer.chain(n.children[1], @filename, n)]) + result.push Chain::Or.new([NodeChainer.chain(n.children[0], @filename), + NodeChainer.chain(n.children[1], @filename, n)]) elsif n.type == :if - result.push Chain::If.new([NodeChainer.chain(n.children[1], @filename), NodeChainer.chain(n.children[2], @filename, n)]) - elsif [:begin, :kwbegin].include?(n.type) + then_clause = if n.children[1] + NodeChainer.chain(n.children[1], @filename, n) + else + Source::Chain.new([Source::Chain::Literal.new('nil', nil)], n) + end + else_clause = if n.children[2] + NodeChainer.chain(n.children[2], @filename, n) + else + Source::Chain.new([Source::Chain::Literal.new('nil', nil)], n) + end + result.push Chain::If.new([then_clause, else_clause]) + elsif %i[begin kwbegin].include?(n.type) result.concat generate_links(n.children.last) elsif n.type == :block_pass block_variable_name_node = n.children[0] @@ -118,12 +140,10 @@ def generate_links n # anonymous block forwarding (e.g., "&") # added in Ruby 3.1 - https://bugs.ruby-lang.org/issues/11256 result.push Chain::BlockVariable.new(nil) + elsif block_variable_name_node.type == :sym + result.push Chain::BlockSymbol.new(block_variable_name_node.children[0].to_s) else - if block_variable_name_node.type == :sym - result.push Chain::BlockSymbol.new("#{block_variable_name_node.children[0].to_s}") - else - result.push Chain::BlockVariable.new("&#{block_variable_name_node.children[0].to_s}") - end + result.push Chain::BlockVariable.new("&#{block_variable_name_node.children[0]}") end elsif n.type == :hash result.push Chain::Hash.new('::Hash', n, hash_is_splatted?(n)) @@ -132,7 +152,7 @@ def generate_links n result.push Source::Chain::Array.new(chained_children, n) else lit = infer_literal_node_type(n) - result.push (lit ? Chain::Literal.new(lit, n) : Chain::Link.new) + result.push(lit ? Chain::Literal.new(lit, n) : Chain::Link.new) end result end @@ -141,7 +161,9 @@ def generate_links n def hash_is_splatted? node return false unless Parser.is_ast_node?(node) && node.type == :hash return false unless Parser.is_ast_node?(node.children.last) && node.children.last.type == :kwsplat - return false if Parser.is_ast_node?(node.children.last.children[0]) && node.children.last.children[0].type == :hash + if Parser.is_ast_node?(node.children.last.children[0]) && node.children.last.children[0].type == :hash + return false + end true end @@ -150,13 +172,16 @@ def hash_is_splatted? node def passed_block node return unless node == @node && @parent&.type == :block + # @sg-ignore Need to add nil check here NodeChainer.chain(@parent.children[2], @filename) end # @param node [Parser::AST::Node] + # @sg-ignore Need to add nil check here # @return [Array] def node_args node - node.children[2..-1].map do |child| + # @sg-ignore Need to add nil check here + node.children[2..].map do |child| NodeChainer.chain(child, @filename, node) end end diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index b77c4cd47..3cbed9547 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -12,17 +12,17 @@ module NodeMethods # @param node [Parser::AST::Node] # @return [String] - def unpack_name(node) - pack_name(node).join("::") + def unpack_name node + pack_name(node).join('::') end # @param node [Parser::AST::Node] # @return [Array] - def pack_name(node) + def pack_name node # @type [Array] parts = [] if node.is_a?(AST::Node) - node.children.each { |n| + node.children.each do |n| if n.is_a?(AST::Node) if n.type == :cbase parts = [''] + pack_name(n) @@ -32,16 +32,16 @@ def pack_name(node) else parts.push n unless n.nil? end - } + end end parts end - # @param node [Parser::AST::Node] + # @param node [Parser::AST::Node, nil] # @return [String, nil] def infer_literal_node_type node return nil unless node.is_a?(AST::Node) - if node.type == :str || node.type == :dstr + if %i[str dstr].include?(node.type) return '::String' elsif node.type == :array return '::Array' @@ -53,30 +53,30 @@ def infer_literal_node_type node return '::Integer' elsif node.type == :float return '::Float' - elsif node.type == :sym || node.type == :dsym + elsif %i[sym dsym].include?(node.type) return '::Symbol' elsif node.type == :regexp return '::Regexp' elsif node.type == :irange return '::Range' - elsif node.type == :true || node.type == :false + elsif %i[true false].include?(node.type) return '::Boolean' # @todo Support `nil` keyword in types - # elsif node.type == :nil - # return 'NilClass' + # elsif node.type == :nil + # return 'NilClass' end nil end # @param node [Parser::AST::Node] # @return [Position] - def get_node_start_position(node) + def get_node_start_position node Position.new(node.loc.line, node.loc.column) end # @param node [Parser::AST::Node] # @return [Position] - def get_node_end_position(node) + def get_node_end_position node Position.new(node.loc.last_line, node.loc.last_column) end @@ -86,40 +86,42 @@ def get_node_end_position(node) # @return [String] def drill_signature node, signature return signature unless node.is_a?(AST::Node) - if node.type == :const or node.type == :cbase - unless node.children[0].nil? - signature += drill_signature(node.children[0], signature) - end + if %i[const cbase].include?(node.type) + signature += drill_signature(node.children[0], signature) unless node.children[0].nil? signature += '::' unless signature.empty? signature += node.children[1].to_s - elsif node.type == :lvar or node.type == :ivar or node.type == :cvar + elsif %i[lvar ivar cvar].include?(node.type) signature += '.' unless signature.empty? signature += node.children[0].to_s elsif node.type == :send - unless node.children[0].nil? - signature += drill_signature(node.children[0], signature) - end + signature += drill_signature(node.children[0], signature) unless node.children[0].nil? signature += '.' unless signature.empty? signature += node.children[1].to_s end signature end - # @param node [Parser::AST::Node] + # @param node [Parser::AST::Node, nil] # @return [Hash{Symbol => Chain}] def convert_hash node return {} unless Parser.is_ast_node?(node) + # @sg-ignore Translate to something flow sensitive typing understands return convert_hash(node.children[0]) if node.type == :kwsplat - return convert_hash(node.children[0]) if Parser.is_ast_node?(node.children[0]) && node.children[0].type == :kwsplat + # @sg-ignore Translate to something flow sensitive typing understands + if Parser.is_ast_node?(node.children[0]) && node.children[0].type == :kwsplat + # @sg-ignore Translate to something flow sensitive typing understands + return convert_hash(node.children[0]) + end + # @sg-ignore Translate to something flow sensitive typing understands return {} unless node.type == :hash result = {} + # @sg-ignore Translate to something flow sensitive typing understands node.children.each do |pair| result[pair.children[0].children[0]] = Solargraph::Parser.chain(pair.children[1]) end 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] @@ -148,7 +150,7 @@ def splatted_call? node end # @param nodes [Enumerable] - def any_splatted_call?(nodes) + def any_splatted_call? nodes nodes.any? { |n| splatted_call?(n) } end @@ -161,14 +163,17 @@ def call_nodes_from node if node.type == :block result.push node if Parser.is_ast_node?(node.children[0]) && node.children[0].children.length > 2 - node.children[0].children[2..-1].each { |child| result.concat call_nodes_from(child) } + # @sg-ignore Need to add nil check here + node.children[0].children[2..].each { |child| result.concat call_nodes_from(child) } end - node.children[1..-1].each { |child| result.concat call_nodes_from(child) } + # @sg-ignore Need to add nil check here + node.children[1..].each { |child| result.concat call_nodes_from(child) } elsif node.type == :send result.push node result.concat call_nodes_from(node.children.first) - node.children[2..-1].each { |child| result.concat call_nodes_from(child) } - elsif [:super, :zsuper].include?(node.type) + # @sg-ignore Need to add nil check here + node.children[2..].each { |child| result.concat call_nodes_from(child) } + elsif %i[super zsuper].include?(node.type) result.push node node.children.each { |child| result.concat call_nodes_from(child) } else @@ -199,40 +204,44 @@ def returns_from_method_body node # @return [Array] low-level value nodes in # value position. Does not include explicit return # statements - def value_position_nodes_only(node) + def value_position_nodes_only node DeepInference.value_position_nodes_only(node).map { |n| n || NIL_NODE } end # @param cursor [Solargraph::Source::Cursor] # @return [Parser::AST::Node, nil] def find_recipient_node cursor - return repaired_find_recipient_node(cursor) if cursor.source.repaired? && cursor.source.code[cursor.offset - 1] == '(' + if cursor.source.repaired? && cursor.source.code[cursor.offset - 1] == '(' + return repaired_find_recipient_node(cursor) + end source = cursor.source position = cursor.position offset = cursor.offset tree = if source.synchronized? - match = source.code[0..offset-1].match(/,\s*\z/) - if match - source.tree_at(position.line, position.column - match[0].length) - else - source.tree_at(position.line, position.column) - end - else - source.tree_at(position.line, position.column - 1) - end + # @sg-ignore Need to add nil check here + match = source.code[0..(offset - 1)].match(/,\s*\z/) + if match + # @sg-ignore Need to add nil check here + source.tree_at(position.line, position.column - match[0].length) + else + source.tree_at(position.line, position.column) + end + else + source.tree_at(position.line, position.column - 1) + end # @type [AST::Node, nil] prev = nil tree.each do |node| if node.type == :send - args = node.children[2..-1] + args = node.children[2..] + # @sg-ignore Need to add nil check here if !args.empty? + # @sg-ignore Need to add nil check here return node if prev && args.include?(prev) - else - if source.synchronized? - return node if source.code[0..offset-1] =~ /\(\s*\z/ && source.code[offset..-1] =~ /^\s*\)/ - else - return node if source.code[0..offset-1] =~ /\([^(]*\z/ - end + elsif source.synchronized? + return node if source.code[0..(offset - 1)] =~ /\(\s*\z/ && source.code[offset..] =~ /^\s*\)/ + elsif source.code[0..(offset - 1)] =~ /\([^(]*\z/ + return node end end prev = node @@ -245,7 +254,7 @@ def find_recipient_node cursor def repaired_find_recipient_node cursor cursor = cursor.source.cursor_at([cursor.position.line, cursor.position.column - 1]) node = cursor.source.tree_at(cursor.position.line, cursor.position.column).first - return node if node && node.type == :send + node if node && node.type == :send end # @@ -302,19 +311,15 @@ def repaired_find_recipient_node cursor # statements in value positions. module DeepInference class << self - CONDITIONAL_ALL_BUT_FIRST = [:if, :unless] - CONDITIONAL_ALL = [:or] - ONLY_ONE_CHILD = [:return] - FIRST_TWO_CHILDREN = [:rescue] - COMPOUND_STATEMENTS = [:begin, :kwbegin] - SKIPPABLE = [:def, :defs, :class, :sclass, :module] - FUNCTION_VALUE = [:block] - CASE_STATEMENT = [:case] + CONDITIONAL_ALL_BUT_FIRST = %i[if unless].freeze + ONLY_ONE_CHILD = [:return].freeze + FIRST_TWO_CHILDREN = [:rescue].freeze + COMPOUND_STATEMENTS = %i[begin kwbegin].freeze + SKIPPABLE = %i[def defs class sclass module].freeze + FUNCTION_VALUE = [:block].freeze + CASE_STATEMENT = [:case].freeze # @param node [AST::Node] a method body compound statement - # @param include_explicit_returns [Boolean] If true, - # include the value nodes of the parameter of the - # 'return' statements in the type returned. # @return [Array] low-level value nodes from # both nodes in value position as well as explicit # return statements in the method's closure. @@ -327,14 +332,14 @@ def from_method_body node # @return [Array] low-level value nodes in # value position. Does not include explicit return # statements - def value_position_nodes_only(node) + def value_position_nodes_only node from_value_position_statement(node, include_explicit_returns: false) end # Look at known control statements and use them to find # more specific return nodes. # - # @param node [Parser::AST::Node] Statement which is in + # @param node [AST::Node] Statement which is in # value position for a method body # @param include_explicit_returns [Boolean] If true, # include the value nodes of the parameter of the @@ -348,10 +353,9 @@ def from_value_position_statement node, include_explicit_returns: true if COMPOUND_STATEMENTS.include?(node.type) result.concat from_value_position_compound_statement node elsif CONDITIONAL_ALL_BUT_FIRST.include?(node.type) - result.concat reduce_to_value_nodes(node.children[1..-1]) + # @sg-ignore Need to add nil check here + result.concat reduce_to_value_nodes(node.children[1..]) # result.push NIL_NODE unless node.children[2] - elsif CONDITIONAL_ALL.include?(node.type) - result.concat reduce_to_value_nodes(node.children) elsif ONLY_ONE_CHILD.include?(node.type) result.concat reduce_to_value_nodes([node.children[0]]) elsif FIRST_TWO_CHILDREN.include?(node.type) @@ -362,9 +366,12 @@ def from_value_position_statement node, include_explicit_returns: true # @todo any explicit returns actually return from # scope in which the proc is run. This asssumes # that the function is executed here. - result.concat explicit_return_values_from_compound_statement(node.children[2]) if include_explicit_returns + if include_explicit_returns + result.concat explicit_return_values_from_compound_statement(node.children[2]) + end elsif CASE_STATEMENT.include?(node.type) - node.children[1..-1].each do |cc| + # @sg-ignore Need to add nil check here + node.children[1..].each do |cc| if cc.nil? result.push NIL_NODE elsif cc.type == :when @@ -397,7 +404,7 @@ def from_value_position_statement node, include_explicit_returns: true # @return [Array] def from_value_position_compound_statement parent result = [] - nodes = parent.children.select{|n| n.is_a?(AST::Node)} + nodes = parent.children.select { |n| n.is_a?(AST::Node) } nodes.each_with_index do |node, idx| if node.type == :block result.concat explicit_return_values_from_compound_statement(node.children[2]) @@ -422,7 +429,10 @@ def from_value_position_compound_statement parent # value position. we already have the explicit values # from above; now we need to also gather the value # position nodes - result.concat from_value_position_statement(nodes.last, include_explicit_returns: false) if idx == nodes.length - 1 + if idx == nodes.length - 1 + result.concat from_value_position_statement(nodes.last, + include_explicit_returns: false) + end end result end @@ -438,7 +448,7 @@ def from_value_position_compound_statement parent def explicit_return_values_from_compound_statement parent return [] unless parent.is_a?(::Parser::AST::Node) result = [] - nodes = parent.children.select{|n| n.is_a?(::Parser::AST::Node)} + nodes = parent.children.select { |n| n.is_a?(::Parser::AST::Node) } nodes.each do |node| next if SKIPPABLE.include?(node.type) if node.type == :return @@ -460,17 +470,28 @@ def reduce_to_value_nodes nodes nodes.each do |node| if !node.is_a?(::Parser::AST::Node) result.push nil + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check elsif COMPOUND_STATEMENTS.include?(node.type) result.concat from_value_position_compound_statement(node) + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check elsif CONDITIONAL_ALL_BUT_FIRST.include?(node.type) - result.concat reduce_to_value_nodes(node.children[1..-1]) + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check + result.concat reduce_to_value_nodes(node.children[1..]) + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check elsif node.type == :return + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check result.concat reduce_to_value_nodes([node.children[0]]) + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check elsif node.type == :or + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check result.concat reduce_to_value_nodes(node.children) + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check elsif node.type == :block + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check result.concat explicit_return_values_from_compound_statement(node.children[2]) + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check elsif node.type == :resbody + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check result.concat reduce_to_value_nodes([node.children[2]]) else result.push node diff --git a/lib/solargraph/parser/parser_gem/node_processors.rb b/lib/solargraph/parser/parser_gem/node_processors.rb index e2cb828da..5f1634bba 100644 --- a/lib/solargraph/parser/parser_gem/node_processors.rb +++ b/lib/solargraph/parser/parser_gem/node_processors.rb @@ -27,8 +27,10 @@ module NodeProcessors autoload :SymNode, 'solargraph/parser/parser_gem/node_processors/sym_node' autoload :ResbodyNode, 'solargraph/parser/parser_gem/node_processors/resbody_node' autoload :UntilNode, 'solargraph/parser/parser_gem/node_processors/until_node' + autoload :WhenNode, 'solargraph/parser/parser_gem/node_processors/when_node' autoload :WhileNode, 'solargraph/parser/parser_gem/node_processors/while_node' autoload :AndNode, 'solargraph/parser/parser_gem/node_processors/and_node' + autoload :OrNode, 'solargraph/parser/parser_gem/node_processors/or_node' end end @@ -63,8 +65,10 @@ module NodeProcessor register :op_asgn, ParserGem::NodeProcessors::OpasgnNode register :sym, ParserGem::NodeProcessors::SymNode register :until, ParserGem::NodeProcessors::UntilNode + register :when, ParserGem::NodeProcessors::WhenNode register :while, ParserGem::NodeProcessors::WhileNode register :and, ParserGem::NodeProcessors::AndNode + register :or, ParserGem::NodeProcessors::OrNode end end end 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 a761ae38c..83f14a415 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/and_node.rb @@ -10,10 +10,10 @@ class AndNode < Parser::NodeProcessor::Base 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) + FlowSensitiveTyping.new(locals, + ivars, + enclosing_breakable_pin, + enclosing_compound_statement_pin).process_and(node) end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/args_node.rb b/lib/solargraph/parser/parser_gem/node_processors/args_node.rb index 8d601bf6e..9a22b8edd 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/args_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/args_node.rb @@ -14,16 +14,17 @@ def process node.children.each do |u| loc = get_node_location(u) locals.push Solargraph::Pin::Parameter.new( - location: loc, - closure: callable, - comments: comments_for(node), - name: u.children[0].to_s, - assignment: u.children[1], - asgn_code: u.children[1] ? region.code_for(u.children[1]) : nil, - presence: callable.location.range, - decl: get_decl(u), - source: :parser - ) + location: loc, + closure: callable, + comments: comments_for(node), + name: u.children[0].to_s, + assignment: u.children[1], + asgn_code: u.children[1] ? region.code_for(u.children[1]) : nil, + # @sg-ignore Need to add nil check here + presence: callable.location.range, + decl: get_decl(u), + source: :parser + ) callable.parameters.push locals.last end end @@ -35,11 +36,12 @@ def process # @param callable [Pin::Callable] # @return [void] - def forward(callable) + def forward callable loc = get_node_location(node) locals.push Solargraph::Pin::Parameter.new( location: loc, closure: callable, + # @sg-ignore Need to add nil check here presence: region.closure.location.range, decl: get_decl(node), source: :parser diff --git a/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb b/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb index b52b9d3c6..56139dffd 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb @@ -6,6 +6,15 @@ module ParserGem module NodeProcessors class BeginNode < Parser::NodeProcessor::Base def process + # We intentionally don't create a CompoundStatement pin + # here, as this is not necessarily a control flow block - + # e.g., a begin...end without rescue or ensure should be + # treated by flow sensitive typing as if the begin and end + # didn't exist at all. As such, we create the + # CompoundStatement pins around the things which actually + # result in control flow changes - like + # if/while/rescue/etc + process_children end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb index d773e8e50..dadc1e3ad 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb @@ -9,23 +9,22 @@ class BlockNode < Parser::NodeProcessor::Base def process location = get_node_location(node) - parent = if other_class_eval? - Solargraph::Pin::Namespace.new( - location: location, - type: :class, - name: unpack_name(node.children[0].children[0]), - source: :parser, - ) - else - region.closure + scope = region.scope || region.closure.context.scope + if other_class_eval? + clazz_name = unpack_name(node.children[0].children[0]) + # instance variables should come from the Class type + # - i.e., treated as class instance variables + context = ComplexType.try_parse("Class<#{clazz_name}>") + scope = :class end block_pin = Solargraph::Pin::Block.new( location: location, - closure: parent, + closure: region.closure, node: node, + context: context, receiver: node.children[0], comments: comments_for(node), - scope: region.scope || region.closure.context.scope, + scope: scope, source: :parser ) pins.push block_pin @@ -37,7 +36,8 @@ def process def other_class_eval? node.children[0].type == :send && node.children[0].children[1] == :class_eval && - [:cbase, :const].include?(node.children[0].children[0]&.type) + # @sg-ignore Need to add nil check here + %i[cbase const].include?(node.children[0].children[0]&.type) end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb index 47c01e728..f45f5544d 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb @@ -8,38 +8,45 @@ class DefNode < Parser::NodeProcessor::Base def process name = node.children[0].to_s scope = region.scope || (region.closure.is_a?(Pin::Singleton) ? :class : :instance) + # specify context explicitly instead of relying on + # closure, as they may differ (e.g., defs inside + # class_eval) + method_context = scope == :instance ? region.closure.binder.namespace_type : region.closure.binder methpin = Solargraph::Pin::Method.new( location: get_node_location(node), closure: region.closure, name: name, + context: method_context, comments: comments_for(node), scope: scope, visibility: scope == :instance && name == 'initialize' ? :private : region.visibility, node: node, - source: :parser, + source: :parser ) if region.visibility == :module_function pins.push Solargraph::Pin::Method.new( location: methpin.location, closure: methpin.closure, name: methpin.name, + context: method_context, comments: methpin.comments, scope: :class, visibility: :public, parameters: methpin.parameters, node: methpin.node, - source: :parser, + source: :parser ) pins.push Solargraph::Pin::Method.new( location: methpin.location, closure: methpin.closure, name: methpin.name, + context: method_context, comments: methpin.comments, scope: :instance, visibility: :private, parameters: methpin.parameters, node: methpin.node, - source: :parser, + source: :parser ) else pins.push methpin diff --git a/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb b/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb index 5f40457e9..09679c7f7 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb @@ -11,13 +11,14 @@ def process s_visi = region.visibility s_visi = :public if s_visi == :module_function || region.scope != :class loc = get_node_location(node) - if node.children[0].is_a?(AST::Node) && node.children[0].type == :self - closure = region.closure - else - closure = Solargraph::Pin::Namespace.new( - name: unpack_name(node.children[0]) - ) - end + closure = if node.children[0].is_a?(AST::Node) && node.children[0].type == :self + region.closure + else + Solargraph::Pin::Namespace.new( + name: unpack_name(node.children[0]), + source: :parser + ) + end pins.push Solargraph::Pin::Method.new( location: loc, closure: closure, @@ -26,7 +27,7 @@ def process scope: :class, visibility: s_visi, node: node, - source: :parser, + source: :parser ) process_children region.update(closure: pins.last, scope: :class) end diff --git a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb index 2452b9cc5..0b9a75e77 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb @@ -8,13 +8,43 @@ class IfNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process - process_children + FlowSensitiveTyping.new(locals, + ivars, + enclosing_breakable_pin, + enclosing_compound_statement_pin).process_if(node) + condition_node = node.children[0] + if condition_node + pins.push Solargraph::Pin::CompoundStatement.new( + location: get_node_location(condition_node), + closure: region.closure, + node: condition_node, + source: :parser + ) + NodeProcessor.process(condition_node, region, pins, locals, ivars) + end + then_node = node.children[1] + if then_node + pins.push Solargraph::Pin::CompoundStatement.new( + location: get_node_location(then_node), + closure: region.closure, + node: then_node, + source: :parser + ) + NodeProcessor.process(then_node, region, pins, locals, ivars) + end - position = get_node_start_position(node) - # @sg-ignore - # @type [Solargraph::Pin::Breakable, nil] - enclosing_breakable_pin = pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location.range.contain?(position)}.last - FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_if(node) + else_node = node.children[2] + if else_node + pins.push Solargraph::Pin::CompoundStatement.new( + location: get_node_location(else_node), + closure: region.closure, + node: else_node, + source: :parser + ) + NodeProcessor.process(else_node, region, pins, locals, ivars) + end + + true end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb index 021ae0ab1..0c4099d41 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb @@ -9,7 +9,7 @@ class IvasgnNode < Parser::NodeProcessor::Base def process loc = get_node_location(node) - pins.push Solargraph::Pin::InstanceVariable.new( + ivars.push Solargraph::Pin::InstanceVariable.new( location: loc, closure: region.closure, name: node.children[0].to_s, @@ -19,11 +19,13 @@ def process ) if region.visibility == :module_function here = get_node_start_position(node) + # @type [Pin::Closure, nil] named_path = named_path_pin(here) if named_path.is_a?(Pin::Method) - pins.push Solargraph::Pin::InstanceVariable.new( + ivars.push Solargraph::Pin::InstanceVariable.new( location: loc, - closure: Pin::Namespace.new(type: :module, closure: region.closure.closure, name: region.closure.name), + closure: Pin::Namespace.new(type: :module, closure: region.closure.closure, + name: region.closure.name), name: node.children[0].to_s, comments: comments_for(node), assignment: node.children[1], diff --git a/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb index 938483652..63e2c55dc 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb @@ -9,6 +9,7 @@ class LvasgnNode < Parser::NodeProcessor::Base def process here = get_node_start_position(node) + # @sg-ignore Need to add nil check here presence = Range.new(here, region.closure.location.range.ending) loc = get_node_location(node) locals.push Solargraph::Pin::LocalVariable.new( diff --git a/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb index dbef1e2d7..b5a7805d9 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb @@ -37,8 +37,10 @@ def process pin = if lhs.type == :lvasgn # lvasgn is a local variable locals.find { |l| l.location == location } + elsif lhs.type == :ivasgn + # ivasgn is an instance variable assignment + ivars.find { |iv| iv.location == location } else - # e.g., ivasgn is an instance variable, etc pins.find { |iv| iv.location == location && iv.is_a?(Pin::BaseVariable) } end # @todo in line below, nothing in typechecking alerts 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 a4359af9d..aab8106aa 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb @@ -57,7 +57,7 @@ def process_send_target call, operator, argument [callee, asgn_method, node.updated(:send, [call, operator, argument])]) - NodeProcessor.process(new_send, region, pins, locals) + NodeProcessor.process(new_send, region, pins, locals, ivars) end # @param asgn [Parser::AST::Node] the target of the assignment @@ -88,8 +88,8 @@ def process_vasgn_target asgn, operator, argument argument ] send_node = node.updated(:send, send_children) - new_asgn = node.updated(asgn.type, [variable_name, send_node]) - NodeProcessor.process(new_asgn, region, pins, locals) + new_asgn = node.updated(asgn.type, [variable_name, send_node]) + NodeProcessor.process(new_asgn, region, pins, locals, ivars) end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/or_node.rb b/lib/solargraph/parser/parser_gem/node_processors/or_node.rb new file mode 100644 index 000000000..6c54f1c8c --- /dev/null +++ b/lib/solargraph/parser/parser_gem/node_processors/or_node.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Solargraph + module Parser + module ParserGem + module NodeProcessors + class OrNode < Parser::NodeProcessor::Base + include ParserGem::NodeMethods + + def process + process_children + + FlowSensitiveTyping.new(locals, + ivars, + enclosing_breakable_pin, + enclosing_compound_statement_pin).process_or(node) + end + end + end + end + end +end diff --git a/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb index 105b78828..17480adfb 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb @@ -8,7 +8,7 @@ class OrasgnNode < Parser::NodeProcessor::Base # @return [void] def process new_node = node.updated(node.children[0].type, node.children[0].children + [node.children[1]]) - NodeProcessor.process(new_node, region, pins, locals) + NodeProcessor.process(new_node, region, pins, locals, ivars) end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb b/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb index 21e32bd22..24846748f 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb @@ -11,6 +11,7 @@ class ResbodyNode < Parser::NodeProcessor::Base def process if node.children[1] # Exception local variable name here = get_node_start_position(node.children[1]) + # @sg-ignore Need to add nil check here presence = Range.new(here, region.closure.location.range.ending) loc = get_node_location(node.children[1]) types = if node.children[0].nil? @@ -29,7 +30,7 @@ def process source: :parser ) end - NodeProcessor.process(node.children[2], region, pins, locals) + NodeProcessor.process(node.children[2], region, pins, locals, ivars) end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb b/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb index 1b573ed93..2d3d967cc 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb @@ -5,6 +5,7 @@ module Parser module ParserGem module NodeProcessors class SclassNode < Parser::NodeProcessor::Base + # @sg-ignore @override is adding, not overriding def process sclass = node.children[0] # @todo Changing Parser::AST::Node to AST::Node below will @@ -23,16 +24,14 @@ def process if sclass.children[0].nil? && names.last != sclass.children[1].to_s names << sclass.children[1].to_s else - names.concat [NodeMethods.unpack_name(sclass.children[0]), sclass.children[1].to_s] + names.push NodeMethods.unpack_name(sclass.children[0]), sclass.children[1].to_s end name = names.reject(&:empty?).join('::') closure = Solargraph::Pin::Namespace.new(name: name, location: region.closure.location, source: :parser) elsif sclass.is_a?(::Parser::AST::Node) && sclass.type == :const names = [region.closure.namespace, region.closure.name] also = NodeMethods.unpack_name(sclass) - if also != region.closure.name - names << also - end + names << also if also != region.closure.name name = names.reject(&:empty?).join('::') closure = Solargraph::Pin::Namespace.new(name: name, location: region.closure.location, source: :parser) else @@ -41,7 +40,7 @@ def process pins.push Solargraph::Pin::Singleton.new( location: get_node_location(node), closure: closure, - source: :parser, + source: :parser ) process_children region.update(visibility: :public, scope: :class, closure: pins.last) end 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 861d6b157..a9e60cb65 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb @@ -7,22 +7,24 @@ module NodeProcessors class SendNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods + # @sg-ignore @override is adding, not overriding def process # @sg-ignore Variable type could not be inferred for method_name # @type [Symbol] method_name = node.children[1] # :nocov: unless method_name.instance_of?(Symbol) - Solargraph.assert_or_log(:parser_method_name, "Expected method name to be a Symbol, got #{method_name.class} for node #{node.inspect}") + Solargraph.assert_or_log(:parser_method_name, + "Expected method name to be a Symbol, got #{method_name.class} for node #{node.inspect}") return process_children end # :nocov: if node.children[0].nil? - if [:private, :public, :protected].include?(method_name) + if %i[private public protected].include?(method_name) process_visibility elsif method_name == :module_function process_module_function - elsif [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) + elsif %i[attr_reader attr_writer attr_accessor].include?(method_name) process_attribute elsif method_name == :include process_include @@ -43,7 +45,10 @@ def process return if process_private_class_method end 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) + pins.push Pin::Reference::Require.new( + Solargraph::Location.new(region.filename, + Solargraph::Range.from_to(0, 0, 0, 0)), 'bundler/require', source: :parser + ) end process_children end @@ -52,20 +57,22 @@ def process # @return [void] def process_visibility - if (node.children.length > 2) - node.children[2..-1].each do |child| + if node.children.length > 2 + # @sg-ignore Need to add nil check here + node.children[2..].each do |child| # @sg-ignore Variable type could not be inferred for method_name # @type [Symbol] visibility = node.children[1] # :nocov: unless visibility.instance_of?(Symbol) - Solargraph.assert_or_log(:parser_visibility, "Expected visibility name to be a Symbol, got #{visibility.class} for node #{node.inspect}") + Solargraph.assert_or_log(:parser_visibility, + "Expected visibility name to be a Symbol, got #{visibility.class} for node #{node.inspect}") return process_children end # :nocov: - if child.is_a?(::Parser::AST::Node) && (child.type == :sym || child.type == :str) + if child.is_a?(::Parser::AST::Node) && %i[sym str].include?(child.type) name = child.children[0].to_s - matches = pins.select{ |pin| pin.is_a?(Pin::Method) && pin.name == name && pin.namespace == region.closure.full_context.namespace && pin.context.scope == (region.scope || :instance)} + matches = pins.select { |pin| pin.is_a?(Pin::Method) && pin.name == name && pin.namespace == region.closure.full_context.namespace && pin.context.scope == (region.scope || :instance) } matches.each do |pin| # @todo Smelly instance variable access pin.instance_variable_set(:@visibility, visibility) @@ -82,11 +89,12 @@ def process_visibility # @return [void] def process_attribute - node.children[2..-1].each do |a| + # @sg-ignore Need to add nil check here + node.children[2..].each do |a| loc = get_node_location(node) clos = region.closure cmnt = comments_for(node) - if node.children[1] == :attr_reader || node.children[1] == :attr_accessor + if %i[attr_reader attr_accessor].include?(node.children[1]) pins.push Solargraph::Pin::Method.new( location: loc, closure: clos, @@ -98,60 +106,62 @@ def process_attribute source: :parser ) end - if node.children[1] == :attr_writer || node.children[1] == :attr_accessor - method_pin = Solargraph::Pin::Method.new( - location: loc, - closure: clos, - name: "#{a.children[0]}=", - comments: cmnt, - scope: region.scope || :instance, - visibility: region.visibility, - attribute: true, - source: :parser - ) - pins.push method_pin - method_pin.parameters.push Pin::Parameter.new(name: 'value', decl: :arg, closure: pins.last, source: :parser) - if method_pin.return_type.defined? - pins.last.docstring.add_tag YARD::Tags::Tag.new(:param, '', pins.last.return_type.items.map(&:rooted_tags), 'value') - end + next unless %i[attr_writer attr_accessor].include?(node.children[1]) + method_pin = Solargraph::Pin::Method.new( + location: loc, + closure: clos, + name: "#{a.children[0]}=", + comments: cmnt, + scope: region.scope || :instance, + visibility: region.visibility, + attribute: true, + source: :parser + ) + pins.push method_pin + method_pin.parameters.push Pin::Parameter.new(name: 'value', decl: :arg, closure: pins.last, + source: :parser) + if method_pin.return_type.defined? + pins.last.docstring.add_tag YARD::Tags::Tag.new(:param, '', + pins.last.return_type.items.map(&:rooted_tags), 'value') end end end # @return [void] def process_include - if node.children[2].is_a?(AST::Node) && node.children[2].type == :const - cp = region.closure - node.children[2..-1].each do |i| - type = region.scope == :class ? Pin::Reference::Extend : Pin::Reference::Include - pins.push type.new( - location: get_node_location(i), - closure: cp, - name: unpack_name(i), - source: :parser - ) - end + return unless node.children[2].is_a?(AST::Node) && node.children[2].type == :const + cp = region.closure + # @sg-ignore Need to add nil check here + node.children[2..].each do |i| + type = region.scope == :class ? Pin::Reference::Extend : Pin::Reference::Include + pins.push type.new( + location: get_node_location(i), + closure: cp, + name: unpack_name(i), + source: :parser + ) end end # @return [void] def process_prepend - if node.children[2].is_a?(AST::Node) && node.children[2].type == :const - cp = region.closure - node.children[2..-1].each do |i| - pins.push Pin::Reference::Prepend.new( - location: get_node_location(i), - closure: cp, - name: unpack_name(i), - source: :parser - ) - end + return unless node.children[2].is_a?(AST::Node) && node.children[2].type == :const + cp = region.closure + # @sg-ignore Need to add nil check here + node.children[2..].each do |i| + pins.push Pin::Reference::Prepend.new( + location: get_node_location(i), + closure: cp, + name: unpack_name(i), + source: :parser + ) end end # @return [void] def process_extend - node.children[2..-1].each do |i| + # @sg-ignore Need to add nil check here + node.children[2..].each do |i| loc = get_node_location(node) if i.type == :self pins.push Pin::Reference::Extend.new( @@ -173,18 +183,16 @@ def process_extend # @return [void] def process_require - if node.children[2].is_a?(AST::Node) && node.children[2].type == :str - path = node.children[2].children[0].to_s - pins.push Pin::Reference::Require.new(get_node_location(node), path, source: :parser) - end + return unless node.children[2].is_a?(AST::Node) && node.children[2].type == :str + path = node.children[2].children[0].to_s + pins.push Pin::Reference::Require.new(get_node_location(node), path, source: :parser) end # @return [void] def process_autoload - if node.children[3].is_a?(AST::Node) && node.children[3].type == :str - path = node.children[3].children[0].to_s - pins.push Pin::Reference::Require.new(get_node_location(node), path, source: :parser) - end + return unless node.children[3].is_a?(AST::Node) && node.children[3].type == :str + path = node.children[3].children[0].to_s + pins.push Pin::Reference::Require.new(get_node_location(node), path, source: :parser) end # @return [void] @@ -192,76 +200,77 @@ def process_module_function if node.children[2].nil? # @todo Smelly instance variable access region.instance_variable_set(:@visibility, :module_function) - elsif node.children[2].type == :sym || node.children[2].type == :str - node.children[2..-1].each do |x| + elsif %i[sym str].include?(node.children[2].type) + # @sg-ignore Need to add nil check here + node.children[2..].each do |x| cn = x.children[0].to_s # @type [Pin::Method, nil] ref = pins.find { |p| p.is_a?(Pin::Method) && p.namespace == region.closure.full_context.namespace && p.name == cn } - unless ref.nil? - pins.delete ref - mm = Solargraph::Pin::Method.new( - location: ref.location, - closure: ref.closure, - name: ref.name, - parameters: ref.parameters, - comments: ref.comments, - scope: :class, - visibility: :public, - node: ref.node, + next if ref.nil? + pins.delete ref + mm = Solargraph::Pin::Method.new( + location: ref.location, + closure: ref.closure, + name: ref.name, + parameters: ref.parameters, + comments: ref.comments, + scope: :class, + visibility: :public, + node: ref.node, + source: :parser + ) + cm = Solargraph::Pin::Method.new( + location: ref.location, + closure: ref.closure, + name: ref.name, + parameters: ref.parameters, + comments: ref.comments, + scope: :instance, + visibility: :private, + node: ref.node, + source: :parser + ) + pins.push mm, cm + ivars.select { |pin| pin.is_a?(Pin::InstanceVariable) && pin.closure.path == ref.path }.each do |ivar| + ivars.delete ivar + ivars.push Solargraph::Pin::InstanceVariable.new( + location: ivar.location, + closure: cm, + name: ivar.name, + comments: ivar.comments, + assignment: ivar.assignment, + source: :parser + ) + ivars.push Solargraph::Pin::InstanceVariable.new( + location: ivar.location, + closure: mm, + name: ivar.name, + comments: ivar.comments, + assignment: ivar.assignment, source: :parser ) - cm = Solargraph::Pin::Method.new( - location: ref.location, - closure: ref.closure, - name: ref.name, - parameters: ref.parameters, - comments: ref.comments, - scope: :instance, - visibility: :private, - node: ref.node, - source: :parser) - pins.push mm, cm - pins.select{|pin| pin.is_a?(Pin::InstanceVariable) && pin.closure.path == ref.path}.each do |ivar| - pins.delete ivar - pins.push Solargraph::Pin::InstanceVariable.new( - location: ivar.location, - closure: cm, - name: ivar.name, - comments: ivar.comments, - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 - assignment: ivar.assignment, - source: :parser - ) - pins.push Solargraph::Pin::InstanceVariable.new( - location: ivar.location, - closure: mm, - name: ivar.name, - comments: ivar.comments, - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 - assignment: ivar.assignment, - source: :parser - ) - end end end elsif node.children[2].type == :def - NodeProcessor.process node.children[2], region.update(visibility: :module_function), pins, locals + NodeProcessor.process node.children[2], region.update(visibility: :module_function), pins, locals, ivars end end # @return [void] def process_private_constant - if node.children[2] && (node.children[2].type == :sym || node.children[2].type == :str) - cn = node.children[2].children[0].to_s - ref = pins.select{|p| [Solargraph::Pin::Namespace, Solargraph::Pin::Constant].include?(p.class) && p.namespace == region.closure.full_context.namespace && p.name == cn}.first - # HACK: Smelly instance variable access - ref.instance_variable_set(:@visibility, :private) unless ref.nil? - end + return unless node.children[2] && %i[sym str].include?(node.children[2].type) + cn = node.children[2].children[0].to_s + ref = pins.select do |p| + [Solargraph::Pin::Namespace, + Solargraph::Pin::Constant].include?(p.class) && p.namespace == region.closure.full_context.namespace && p.name == cn + end.first + # HACK: Smelly instance variable access + ref&.instance_variable_set(:@visibility, :private) end # @return [void] def process_alias_method - loc = get_node_location(node) + get_node_location(node) pins.push Solargraph::Pin::MethodAlias.new( location: get_node_location(node), closure: region.closure, @@ -274,10 +283,12 @@ def process_alias_method # @return [Boolean] def process_private_class_method - if node.children[2].type == :sym || node.children[2].type == :str - ref = pins.select { |p| p.is_a?(Pin::Method) && p.namespace == region.closure.full_context.namespace && p.name == node.children[2].children[0].to_s }.first + if %i[sym str].include?(node.children[2].type) + ref = pins.select do |p| + p.is_a?(Pin::Method) && p.namespace == region.closure.full_context.namespace && p.name == node.children[2].children[0].to_s + end.first # HACK: Smelly instance variable access - ref.instance_variable_set(:@visibility, :private) unless ref.nil? + ref&.instance_variable_set(:@visibility, :private) false else process_children region.update(scope: :class, visibility: :private) diff --git a/lib/solargraph/parser/parser_gem/node_processors/until_node.rb b/lib/solargraph/parser/parser_gem/node_processors/until_node.rb index bcfae287d..2e091f41d 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/until_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/until_node.rb @@ -18,7 +18,7 @@ def process closure: region.closure, node: node, comments: comments_for(node), - source: :parser, + source: :parser ) process_children region end diff --git a/lib/solargraph/parser/parser_gem/node_processors/when_node.rb b/lib/solargraph/parser/parser_gem/node_processors/when_node.rb new file mode 100644 index 000000000..915eb57e6 --- /dev/null +++ b/lib/solargraph/parser/parser_gem/node_processors/when_node.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Solargraph + module Parser + module ParserGem + module NodeProcessors + class WhenNode < Parser::NodeProcessor::Base + include ParserGem::NodeMethods + + def process + pins.push Solargraph::Pin::CompoundStatement.new( + location: get_node_location(node), + closure: region.closure, + node: node, + source: :parser + ) + process_children + end + end + end + end + end +end diff --git a/lib/solargraph/parser/parser_gem/node_processors/while_node.rb b/lib/solargraph/parser/parser_gem/node_processors/while_node.rb index c9211448e..6c4fe33d8 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/while_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/while_node.rb @@ -8,7 +8,11 @@ class WhileNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process - location = get_node_location(node) + FlowSensitiveTyping.new(locals, + ivars, + enclosing_breakable_pin, + enclosing_compound_statement_pin).process_while(node) + # Note - this should not be considered a block, as the # while statement doesn't create a closure - e.g., # variables created inside can be seen from outside as @@ -18,7 +22,7 @@ def process closure: region.closure, node: node, comments: comments_for(node), - source: :parser, + source: :parser ) process_children region end diff --git a/lib/solargraph/parser/region.rb b/lib/solargraph/parser/region.rb index a6559bc8a..8c4caf6ac 100644 --- a/lib/solargraph/parser/region.rb +++ b/lib/solargraph/parser/region.rb @@ -22,7 +22,6 @@ class Region attr_reader :lvars # @param source [Source] - # @param namespace [String] # @param closure [Pin::Closure, nil] # @param scope [Symbol, nil] # @param visibility [Symbol] @@ -30,18 +29,25 @@ class Region def initialize source: Solargraph::Source.load_string(''), closure: nil, scope: nil, visibility: :public, lvars: [] @source = source - # @closure = closure @closure = closure || Pin::Namespace.new(name: '', location: source.location, source: :parser) @scope = scope @visibility = visibility @lvars = lvars end - # @return [String] + # @return [String, nil] def filename source.filename end + # @return [Pin::Namespace, nil] + def namespace_pin + ns = closure + # @sg-ignore flow sensitive typing needs to handle while + ns = ns.closure while ns && !ns.is_a?(Pin::Namespace) + ns + end + # Generate a new Region with the provided attribute changes. # # @param closure [Pin::Closure, nil] diff --git a/lib/solargraph/parser/snippet.rb b/lib/solargraph/parser/snippet.rb index 081dec3e0..7282a44d6 100644 --- a/lib/solargraph/parser/snippet.rb +++ b/lib/solargraph/parser/snippet.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module Solargraph module Parser class Snippet - # @return [Range] + # @return [Solargraph::Range] attr_reader :range # @return [String] attr_reader :text diff --git a/lib/solargraph/pin.rb b/lib/solargraph/pin.rb index 526ac6fc3..6cd6fcaf9 100644 --- a/lib/solargraph/pin.rb +++ b/lib/solargraph/pin.rb @@ -38,6 +38,8 @@ module Pin autoload :Until, 'solargraph/pin/until' autoload :While, 'solargraph/pin/while' autoload :Callable, 'solargraph/pin/callable' + autoload :CompoundStatement, + 'solargraph/pin/compound_statement' ROOT_PIN = Pin::Namespace.new(type: :class, name: '', closure: nil, source: :pin_rb) end diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 57d083453..240d46b44 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -41,12 +41,13 @@ def presence_certain? # @param type_location [Solargraph::Location, nil] # @param closure [Solargraph::Pin::Closure, nil] # @param name [String] - # @param comments [String] + # @param comments [String, nil] # @param source [Symbol, nil] # @param docstring [YARD::Docstring, nil] # @param directives [::Array, nil] # @param combine_priority [::Numeric, nil] See attr_reader for combine_priority - def initialize location: nil, type_location: nil, closure: nil, source: nil, name: '', comments: '', docstring: nil, directives: nil, combine_priority: nil + def initialize location: nil, type_location: nil, closure: nil, source: nil, name: '', comments: '', + docstring: nil, directives: nil, combine_priority: nil @location = location @type_location = type_location @closure = closure @@ -57,6 +58,8 @@ def initialize location: nil, type_location: nil, closure: nil, source: nil, nam @docstring = docstring @directives = directives @combine_priority = combine_priority + # @type [ComplexType, ComplexType::UniqueType, nil] + @binder = nil assert_source_provided assert_location_provided @@ -66,13 +69,16 @@ def initialize location: nil, type_location: nil, closure: nil, source: nil, nam def assert_location_provided return unless best_location.nil? && %i[yardoc source rbs].include?(source) - Solargraph.assert_or_log(:best_location, "Neither location nor type_location provided - #{path} #{source} #{self.class}") + Solargraph.assert_or_log(:best_location, + "Neither location nor type_location provided - #{path} #{source} #{self.class}") end # @return [Pin::Closure, nil] def closure - Solargraph.assert_or_log(:closure, "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") unless @closure - # @type [Pin::Closure, nil] + unless @closure + Solargraph.assert_or_log(:closure, + "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") + end @closure end @@ -80,8 +86,7 @@ def closure # @param attrs [Hash{::Symbol => Object}] # # @return [self] - def combine_with(other, attrs={}) - raise "tried to combine #{other.class} with #{self.class}" unless other.class == self.class + def combine_with other, attrs = {} priority_choice = choose_priority(other) return priority_choice unless priority_choice.nil? @@ -92,7 +97,7 @@ def combine_with(other, attrs={}) location: location, type_location: type_location, name: combined_name, - closure: choose_pin_attr_with_same_name(other, :closure), + closure: combine_closure(other), comments: choose_longer(other, :comments), source: :combined, docstring: choose(other, :docstring), @@ -100,7 +105,9 @@ def combine_with(other, attrs={}) combine_priority: combine_priority }.merge(attrs) assert_same_macros(other) - logger.debug { "Base#combine_with(path=#{path}) - other.comments=#{other.comments.inspect}, self.comments = #{self.comments}" } + logger.debug do + "Base#combine_with(path=#{path}) - other.comments=#{other.comments.inspect}, self.comments = #{comments}" + end out = self.class.new(**new_attrs) out.reset_generated! out @@ -109,7 +116,7 @@ def combine_with(other, attrs={}) # @param other [self] # @return [self, nil] Returns either the pin chosen based on priority or nil # A nil return means that the combination process must proceed - def choose_priority(other) + def choose_priority other if combine_priority.nil? && !other.combine_priority.nil? return other elsif other.combine_priority.nil? && !combine_priority.nil? @@ -129,7 +136,7 @@ def choose_priority(other) # @param attr [::Symbol] # @sg-ignore # @return [undefined] - def choose_longer(other, attr) + def choose_longer other, attr # @type [undefined] val1 = send(attr) # @type [undefined] @@ -140,16 +147,24 @@ def choose_longer(other, attr) end # @param other [self] + # # @return [::Array, nil] - def combine_directives(other) - return self.directives if other.directives.empty? + def combine_directives other + return directives if other.directives.empty? return other.directives if directives.empty? - [directives + other.directives].uniq + (directives + other.directives).uniq end # @param other [self] + # @return [Pin::Closure, nil] + def combine_closure other + choose_pin_attr_with_same_name(other, :closure) + end + + # @param other [self] + # @sg-ignore @type should override probed type # @return [String] - def combine_name(other) + def combine_name other if needs_consistent_name? || other.needs_consistent_name? assert_same(other, :name) else @@ -170,6 +185,9 @@ def reset_generated! # Same with @directives, @macros, @maybe_directives, which # regenerate docstring @deprecated = nil + @context = nil + @binder = nil + @path = nil reset_conversions end @@ -177,25 +195,26 @@ def needs_consistent_name? true end - # @sg-ignore def should infer as symbol - "Not enough arguments to Module#protected" - protected def equality_fields - [name, location, type_location, closure, source] - end - # @param other [self] # @return [ComplexType] - def combine_return_type(other) + def combine_return_type other if return_type.undefined? other.return_type elsif other.return_type.undefined? return_type + elsif return_type.erased_version_of?(other.return_type) + other.return_type + elsif other.return_type.erased_version_of?(return_type) + return_type elsif dodgy_return_type_source? && !other.dodgy_return_type_source? other.return_type elsif other.dodgy_return_type_source? && !dodgy_return_type_source? return_type else all_items = return_type.items + other.return_type.items - if all_items.any? { |item| item.selfy? } && all_items.any? { |item| item.rooted_tag == context.reduce_class_type.rooted_tag } + if all_items.any?(&:selfy?) && all_items.any? do |item| + item.rooted_tag == context.reduce_class_type.rooted_tag + end # assume this was a declaration that should have said 'self' all_items.delete_if { |item| item.rooted_tag == context.reduce_class_type.rooted_tag } end @@ -203,9 +222,12 @@ def combine_return_type(other) end end + # @sg-ignore need boolish support for ? methods def dodgy_return_type_source? # uses a lot of 'Object' instead of 'self' - location&.filename&.include?('core_ext/object/') + location&.filename&.include?('core_ext/object/') || + # ditto + location&.filename&.include?('stdlib/date/0/date.rbs') end # when choices are arbitrary, make sure the choice is consistent @@ -213,14 +235,19 @@ def dodgy_return_type_source? # @param other [Pin::Base] # @param attr [::Symbol] # - # @return [Object, nil] - def choose(other, attr) + # @sg-ignore + # @return [undefined, nil] + def choose other, attr results = [self, other].map(&attr).compact # true and false are different classes and can't be sorted - return true if results.any? { |r| r == true || r == false } + + # @sg-ignore Wrong argument type for Array#include?: object + # expected Boolean, received Proc + return true if results.any? { |r| [true, false].include?(r) } + return results.first if results.any? { |r| r.is_a? AST::Node } results.min - rescue - STDERR.puts("Problem handling #{attr} for \n#{self.inspect}\n and \n#{other.inspect}\n\n#{self.send(attr).inspect} vs #{other.send(attr).inspect}") + rescue StandardError + warn("Problem handling #{attr} for \n#{inspect}\n and \n#{other.inspect}\n\n#{send(attr).inspect} vs #{other.send(attr).inspect}") raise end @@ -228,7 +255,7 @@ def choose(other, attr) # @param attr [::Symbol] # @sg-ignore # @return [undefined] - def choose_node(other, attr) + def choose_node other, attr if other.object_id < attr.object_id other.send(attr) else @@ -240,9 +267,9 @@ def choose_node(other, attr) # @param attr [::Symbol] # @sg-ignore # @return [undefined] - def prefer_rbs_location(other, attr) + def prefer_rbs_location other, attr if rbs_location? && !other.rbs_location? - self.send(attr) + send(attr) elsif !rbs_location? && other.rbs_location? other.send(attr) else @@ -250,14 +277,15 @@ def prefer_rbs_location(other, attr) end end + # @sg-ignore need boolish support for ? methods def rbs_location? type_location&.rbs? end # @param other [self] # @return [void] - def assert_same_macros(other) - return unless self.source == :yardoc && other.source == :yardoc + def assert_same_macros other + return unless source == :yardoc && other.source == :yardoc assert_same_count(other, :macros) # @param [YARD::Tags::MacroDirective] assert_same_array_content(other, :macros) { |macro| macro.tag.name } @@ -267,7 +295,7 @@ def assert_same_macros(other) # @param attr [::Symbol] # @return [void] # @todo strong typechecking should complain when there are no block-related tags - def assert_same_array_content(other, attr, &block) + def assert_same_array_content other, attr, &block arr1 = send(attr) raise "Expected #{attr} on #{self} to be an Enumerable, got #{arr1.class}" unless arr1.is_a?(::Enumerable) # @type arr1 [::Enumerable] @@ -281,7 +309,7 @@ def assert_same_array_content(other, attr, &block) values2 = arr2.map(&block) # @sg-ignore return arr1 if values1 == values2 - Solargraph.assert_or_log("combine_with_#{attr}".to_sym, + Solargraph.assert_or_log(:"combine_with_#{attr}", "Inconsistent #{attr.inspect} values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self values = #{values1}\nother values =#{attr} = #{values2}") arr1 end @@ -290,15 +318,15 @@ def assert_same_array_content(other, attr, &block) # @param attr [::Symbol] # # @return [::Enumerable] - def assert_same_count(other, attr) + def assert_same_count other, attr # @type [::Enumerable] - arr1 = self.send(attr) + arr1 = send(attr) raise "Expected #{attr} on #{self} to be an Enumerable, got #{arr1.class}" unless arr1.is_a?(::Enumerable) # @type [::Enumerable] arr2 = other.send(attr) raise "Expected #{attr} on #{other} to be an Enumerable, got #{arr2.class}" unless arr2.is_a?(::Enumerable) return arr1 if arr1.count == arr2.count - Solargraph.assert_or_log("combine_with_#{attr}".to_sym, + Solargraph.assert_or_log(:"combine_with_#{attr}", "Inconsistent #{attr.inspect} count value between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{arr1.inspect}\nother.#{attr} = #{arr2.inspect}") arr1 end @@ -308,12 +336,16 @@ def assert_same_count(other, attr) # # @sg-ignore # @return [undefined] - def assert_same(other, attr) - return false if other.nil? + def assert_same other, attr + if other.nil? + Solargraph.assert_or_log(:"combine_with_#{attr}_nil", + "Other was passed in nil in assert_same on #{self}") + return send(attr) + end val1 = send(attr) val2 = other.send(attr) return val1 if val1 == val2 - Solargraph.assert_or_log("combine_with_#{attr}".to_sym, + Solargraph.assert_or_log(:"combine_with_#{attr}", "Inconsistent #{attr.inspect} values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") val1 end @@ -322,15 +354,17 @@ def assert_same(other, attr) # @param attr [::Symbol] # @sg-ignore # @return [undefined] - def choose_pin_attr_with_same_name(other, attr) + def choose_pin_attr_with_same_name other, attr # @type [Pin::Base, nil] val1 = send(attr) # @type [Pin::Base, nil] val2 = other.send(attr) - raise "Expected pin for #{attr} on\n#{self.inspect},\ngot #{val1.inspect}" unless val1.nil? || val1.is_a?(Pin::Base) - raise "Expected pin for #{attr} on\n#{other.inspect},\ngot #{val2.inspect}" unless val2.nil? || val2.is_a?(Pin::Base) + raise "Expected pin for #{attr} on\n#{inspect},\ngot #{val1.inspect}" unless val1.nil? || val1.is_a?(Pin::Base) + unless val2.nil? || val2.is_a?(Pin::Base) + raise "Expected pin for #{attr} on\n#{other.inspect},\ngot #{val2.inspect}" + end if val1&.name != val2&.name - Solargraph.assert_or_log("combine_with_#{attr}_name".to_sym, + Solargraph.assert_or_log(:"combine_with_#{attr}_name", "Inconsistent #{attr.inspect} name values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") end choose_pin_attr(other, attr) @@ -341,14 +375,14 @@ def choose_pin_attr_with_same_name(other, attr) # # @sg-ignore Missing @return tag for Solargraph::Pin::Base#choose_pin_attr # @return [undefined] - def choose_pin_attr(other, attr) + def choose_pin_attr other, attr # @type [Pin::Base, nil] val1 = send(attr) # @type [Pin::Base, nil] val2 = other.send(attr) if val1.class != val2.class # :nocov: - Solargraph.assert_or_log("combine_with_#{attr}_class".to_sym, + Solargraph.assert_or_log(:"combine_with_#{attr}_class", "Inconsistent #{attr.inspect} class values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") return val1 # :nocov: @@ -358,8 +392,11 @@ def choose_pin_attr(other, attr) [ # maximize number of gates, as types in other combined pins may # depend on those gates + + # @sg-ignore Need better handling of #compact closure.gates.length, # use basename so that results don't vary system to system + # @sg-ignore Need better handling of #compact File.basename(closure.best_location.to_s) ] end @@ -376,11 +413,10 @@ def comments end # @param generics_to_resolve [Enumerable] - # @param return_type_context [ComplexType, nil] - # @param context [ComplexType] + # @param return_type_context [ComplexType, ComplexType::UniqueType, nil] # @param resolved_generic_values [Hash{String => ComplexType}] # @return [self] - def resolve_generics_from_context(generics_to_resolve, return_type_context = nil, resolved_generic_values: {}) + def resolve_generics_from_context generics_to_resolve, return_type_context = nil, resolved_generic_values: {} proxy return_type.resolve_generics_from_context(generics_to_resolve, return_type_context, resolved_generic_values: resolved_generic_values) @@ -389,7 +425,7 @@ def resolve_generics_from_context(generics_to_resolve, return_type_context = nil # @yieldparam [ComplexType] # @yieldreturn [ComplexType] # @return [self] - def transform_types(&transform) + def transform_types &transform proxy return_type.transform(&transform) end @@ -401,7 +437,7 @@ def transform_types(&transform) # @param context_type [ComplexType] The receiver type # @return [self] def resolve_generics definitions, context_type - transform_types { |t| t.resolve_generics(definitions, context_type) if t } + transform_types { |t| t&.resolve_generics(definitions, context_type) } end def all_rooted? @@ -410,7 +446,7 @@ def all_rooted? # @param generics_to_erase [::Array] # @return [self] - def erase_generics(generics_to_erase) + def erase_generics generics_to_erase return self if generics_to_erase.empty? transform_types { |t| t.erase_generics(generics_to_erase) } end @@ -418,6 +454,7 @@ def erase_generics(generics_to_erase) # @return [String, nil] def filename return nil if location.nil? + # @sg-ignore flow sensitive typing needs to handle attrs location.filename end @@ -452,12 +489,20 @@ def best_location # @param other [Solargraph::Pin::Base, Object] # @return [Boolean] def nearly? other - self.class == other.class && + instance_of?(other.class) && + # @sg-ignore Translate to something flow sensitive typing understands name == other.name && + # @sg-ignore flow sensitive typing needs to handle attrs (closure == other.closure || (closure && closure.nearly?(other.closure))) && + # @sg-ignore Translate to something flow sensitive typing understands (comments == other.comments || - (((maybe_directives? == false && other.maybe_directives? == false) || compare_directives(directives, other.directives)) && - compare_docstring_tags(docstring, other.docstring)) + # @sg-ignore Translate to something flow sensitive typing understands + (((maybe_directives? == false && other.maybe_directives? == false) || + compare_directives(directives, + # @sg-ignore Translate to something flow sensitive typing understands + other.directives)) && + # @sg-ignore Translate to something flow sensitive typing understands + compare_docstring_tags(docstring, other.docstring)) ) end @@ -484,6 +529,7 @@ def docstring @docstring ||= Solargraph::Source.parse_docstring('').to_docstring end + # @sg-ignore parse_comments will always set @directives # @return [::Array] def directives parse_comments unless @directives @@ -520,7 +566,7 @@ def deprecated? # provided ApiMap. # # @param api_map [ApiMap] - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] def typify api_map return_type.qualify(api_map, *(closure&.gates || [''])) end @@ -528,16 +574,17 @@ def typify api_map # Infer the pin's return type via static code analysis. # # @param api_map [ApiMap] - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] def probe api_map typify api_map end # @deprecated Use #typify and/or #probe instead # @param api_map [ApiMap] - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] def infer api_map - Solargraph::Logging.logger.warn "WARNING: Pin #infer methods are deprecated. Use #typify or #probe instead." + Solargraph.assert_or_log(:pin_infer, + 'WARNING: Pin #infer methods are deprecated. Use #typify or #probe instead.') type = typify(api_map) return type unless type.undefined? probe api_map @@ -568,7 +615,7 @@ def realize api_map # the return type and the #proxied? setting, the proxy should be a clone # of the original. # - # @param return_type [ComplexType] + # @param return_type [ComplexType, ComplexType::UniqueType, nil] # @return [self] def proxy return_type result = dup @@ -607,7 +654,7 @@ def type_desc rbs = return_type.rooted_tags if return_type.name == 'Class' if path if rbs - path + ' ' + rbs + "#{path} #{rbs}" else path end @@ -618,7 +665,7 @@ def type_desc # @return [String] def inner_desc - closure_info = closure&.desc + closure_info = closure&.name.inspect binder_info = binder&.desc "name=#{name.inspect} return_type=#{type_desc}, context=#{context.rooted_tags}, closure=#{closure_info}, binder=#{binder_info}" end @@ -630,7 +677,7 @@ def desc # @return [String] def inspect - "#<#{self.class} `#{self.inner_desc}`#{all_location_text} via #{source.inspect}>" + "#<#{self.class} `#{inner_desc}`#{all_location_text} via #{source.inspect}>" end # @return [String] @@ -646,24 +693,23 @@ def all_location_text end end - # @return [void] - def reset_generated! - end - protected + # @sg-ignore def should infer as symbol - "Not enough arguments to Module#protected" + def equality_fields + [name, location, type_location, closure, source] + end + # @return [Boolean] attr_writer :probed # @return [Boolean] attr_writer :proxied - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType, nil] attr_writer :return_type - attr_writer :docstring - - attr_writer :directives + attr_writer :docstring, :directives private @@ -685,13 +731,13 @@ def parse_comments # True if two docstrings have the same tags, regardless of any other # differences. # - # @param d1 [YARD::Docstring] - # @param d2 [YARD::Docstring] + # @param docstring1 [YARD::Docstring] + # @param docstring2 [YARD::Docstring] # @return [Boolean] - def compare_docstring_tags d1, d2 - return false if d1.tags.length != d2.tags.length - d1.tags.each_index do |i| - return false unless compare_tags(d1.tags[i], d2.tags[i]) + def compare_docstring_tags docstring1, docstring2 + return false if docstring1.tags.length != docstring2.tags.length + docstring1.tags.each_index do |i| + return false unless compare_tags(docstring1.tags[i], docstring2.tags[i]) end true end @@ -711,7 +757,7 @@ def compare_directives dir1, dir2 # @param tag2 [YARD::Tags::Tag] # @return [Boolean] def compare_tags tag1, tag2 - tag1.class == tag2.class && + tag1.instance_of?(tag2.class) && tag1.tag_name == tag2.tag_name && tag1.text == tag2.text && tag1.name == tag2.name && @@ -722,7 +768,7 @@ def compare_tags tag1, tag2 def collect_macros return [] unless maybe_directives? parse = Solargraph::Source.parse_docstring(comments) - parse.directives.select{ |d| d.tag.tag_name == 'macro' } + parse.directives.select { |d| d.tag.tag_name == 'macro' } end end end diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index ab7bd3961..c7945e599 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -6,31 +6,127 @@ class BaseVariable < Base # include Solargraph::Source::NodeMethods include Solargraph::Parser::NodeMethods - # @return [Parser::AST::Node, nil] - attr_reader :assignment + # @return [Array] + attr_reader :assignments attr_accessor :mass_assignment + # @return [Range, nil] + attr_reader :presence + # @param return_type [ComplexType, nil] + # @param assignment [Parser::AST::Node, nil] First assignment + # that was made to this variable + # @param assignments [Array] Possible + # assignments that may have been made to this variable # @param mass_assignment [::Array(Parser::AST::Node, Integer), nil] - # @param assignment [Parser::AST::Node, nil] - def initialize assignment: nil, return_type: nil, mass_assignment: nil, **splat + # @param assignment [Parser::AST::Node, nil] First assignment + # that was made to this variable + # @param assignments [Array] Possible + # assignments that may have been made to this variable + # @param exclude_return_type [ComplexType, nil] Ensure any + # return type returned will never include any of these unique + # types in the unique types of its complex type. + # + # Example: If a return type is 'Float | Integer | nil' and the + # exclude_return_type is 'Integer', the resulting return + # type will be 'Float | nil' because Integer is excluded. + # @param intersection_return_type [ComplexType, nil] Ensure each unique + # return type is compatible with at least one element of this + # complex type. If a ComplexType used as a return type is an + # union type - we can return any of these - these are + # intersection types - everything we return needs to meet at least + # one of these unique types. + # + # Example: If a return type is 'Numeric | nil' and the + # intersection_return_type is 'Float | nil', the resulting return + # type will be 'Float | nil' because Float is compatible + # with Numeric and nil is compatible with nil. + # @see https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types + # @see https://en.wikipedia.org/wiki/Intersection_type#TypeScript_example + # @param presence [Range, nil] + # @param [Hash{Symbol => Object}] splat + def initialize assignment: nil, assignments: [], mass_assignment: nil, + presence: nil, return_type: nil, + intersection_return_type: nil, exclude_return_type: nil, + **splat super(**splat) - @assignment = assignment + @assignments = (assignment.nil? ? [] : [assignment]) + assignments # @type [nil, ::Array(Parser::AST::Node, Integer)] - @mass_assignment = nil + @mass_assignment = mass_assignment @return_type = return_type + @intersection_return_type = intersection_return_type + @exclude_return_type = exclude_return_type + @presence = presence end - def combine_with(other, attrs={}) + # @param presence [Range] + # @param exclude_return_type [ComplexType, nil] + # @param intersection_return_type [ComplexType, nil] + # @param source [::Symbol] + # + # @return [self] + def downcast presence:, exclude_return_type: nil, intersection_return_type: nil, + source: self.source + result = dup + result.exclude_return_type = exclude_return_type + result.intersection_return_type = intersection_return_type + result.source = source + result.presence = presence + result.reset_generated! + result + end + + def reset_generated! + @assignment = nil + super + end + + def combine_with other, attrs = {} + new_assignments = combine_assignments(other) new_attrs = attrs.merge({ - assignment: assert_same(other, :assignment), - mass_assignment: assert_same(other, :mass_assignment), - return_type: combine_return_type(other), - }) + # default values don't exist in RBS parameters; it just + # tells you if the arg is optional or not. Prefer a + # provided value if we have one here since we can't rely on + # it from RBS so we can infer from it and typecheck on it. + assignment: choose(other, :assignment), + assignments: new_assignments, + mass_assignment: combine_mass_assignment(other), + return_type: combine_return_type(other), + intersection_return_type: combine_types(other, :intersection_return_type), + exclude_return_type: combine_types(other, :exclude_return_type), + presence: combine_presence(other) + }) super(other, new_attrs) end + # @param other [self] + # + # @return [Array(AST::Node, Integer), nil] + def combine_mass_assignment other + # @todo pick first non-nil arbitrarily - we don't yet support + # mass assignment merging + mass_assignment || other.mass_assignment + end + + # @return [Parser::AST::Node, nil] + def assignment + @assignment ||= assignments.last + end + + # @param other [self] + # + # @return [::Array] + def combine_assignments other + (other.assignments + assignments).uniq + end + + def inner_desc + super + ", presence=#{presence.inspect}, assignments=#{assignments}, " \ + "intersection_return_type=#{intersection_return_type&.rooted_tags.inspect}, " \ + "exclude_return_type=#{exclude_return_type&.rooted_tags.inspect}" + end + def completion_item_kind Solargraph::LanguageServer::CompletionItemKinds::VARIABLE end @@ -40,10 +136,6 @@ def symbol_kind Solargraph::LanguageServer::SymbolKinds::VARIABLE end - def return_type - @return_type ||= generate_complex_type - end - def nil_assignment? # this will always be false - should it be return_type == # ComplexType::NIL or somesuch? @@ -57,7 +149,7 @@ def variable? # @param parent_node [Parser::AST::Node] # @param api_map [ApiMap] # @return [::Array] - def return_types_from_node(parent_node, api_map) + def return_types_from_node parent_node, api_map types = [] value_position_nodes_only(parent_node).each do |node| # Nil nodes may not have a location @@ -67,10 +159,12 @@ def return_types_from_node(parent_node, api_map) rng = Range.from_node(node) next if rng.nil? pos = rng.ending + # @sg-ignore Need to add nil check here clip = api_map.clip_at(location.filename, pos) # Use the return node for inference. The clip might infer from the # first node in a method call instead of the entire call. chain = Parser.chain(node, nil, nil) + # @sg-ignore Need to add nil check here result = chain.infer(api_map, closure, clip.locals).self_to_type(closure.context) types.push result unless result.undefined? end @@ -79,13 +173,15 @@ def return_types_from_node(parent_node, api_map) end # @param api_map [ApiMap] - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] def probe api_map - unless @assignment.nil? - types = return_types_from_node(@assignment, api_map) - return ComplexType.new(types.uniq) unless types.empty? - end + assignment_types = assignments.flat_map { |node| return_types_from_node(node, api_map) } + type_from_assignment = ComplexType.new(assignment_types.flat_map(&:items).uniq) unless assignment_types.empty? + return adjust_type api_map, type_from_assignment unless type_from_assignment.nil? + # @todo should handle merging types from mass assignments as + # well so that we can do better flow sensitive typing with + # multiple assignments unless @mass_assignment.nil? mass_node, index = @mass_assignment types = return_types_from_node(mass_node, api_map) @@ -96,7 +192,10 @@ def probe api_map type.all_params.first end end.compact! - return ComplexType.new(types.uniq) unless types.empty? + + return ComplexType::UNDEFINED if types.empty? + + return adjust_type api_map, ComplexType.new(types.uniq).qualify(api_map, *gates) end ComplexType::UNDEFINED @@ -113,13 +212,163 @@ def type_desc "#{super} = #{assignment&.type.inspect}" end + # @return [ComplexType, nil] + def return_type + generate_complex_type || @return_type || intersection_return_type || ComplexType::UNDEFINED + end + + def typify api_map + raw_return_type = super + + adjust_type(api_map, raw_return_type) + end + + # @sg-ignore need boolish support for ? methods + def presence_certain? + exclude_return_type || intersection_return_type + end + + # @param other_loc [Location] + # @sg-ignore flow sensitive typing needs to handle attrs + def starts_at? other_loc + location&.filename == other_loc.filename && + presence && + # @sg-ignore flow sensitive typing needs to handle attrs + presence.start == other_loc.range.start + end + + # Narrow the presence range to the intersection of both. + # + # @param other [self] + # + # @return [Range, nil] + def combine_presence other + return presence || other.presence if presence.nil? || other.presence.nil? + + # @sg-ignore flow sensitive typing needs to handle attrs + Range.new([presence.start, other.presence.start].max, [presence.ending, other.presence.ending].min) + end + + # @param other [self] + # @return [Pin::Closure, nil] + def combine_closure other + return closure if closure == other.closure + + # choose first defined, as that establishes the scope of the variable + if closure.nil? || other.closure.nil? + Solargraph.assert_or_log(:varible_closure_missing) do + 'One of the local variables being combined is missing a closure: ' \ + "#{inspect} vs #{other.inspect}" + end + return closure || other.closure + end + + # @sg-ignore flow sensitive typing needs to handle attrs + if closure.location.nil? || other.closure.location.nil? + # @sg-ignore flow sensitive typing needs to handle attrs + return closure.location.nil? ? other.closure : closure + end + + # if filenames are different, this will just pick one + # @sg-ignore flow sensitive typing needs to handle attrs + return closure if closure.location <= other.closure.location + + other.closure + end + + # @param other_closure [Pin::Closure] + # @param other_loc [Location] + def visible_at? other_closure, other_loc + # @sg-ignore flow sensitive typing needs to handle attrs + location.filename == other_loc.filename && + # @sg-ignore flow sensitive typing needs to handle attrs + (!presence || presence.include?(other_loc.range.start)) && + visible_in_closure?(other_closure) + end + + protected + + attr_accessor :exclude_return_type, :intersection_return_type + + # @return [Range] + attr_writer :presence + private - # @return [ComplexType] + # @param api_map [ApiMap] + # @param raw_return_type [ComplexType, ComplexType::UniqueType] + # + # @return [ComplexType, ComplexType::UniqueType] + def adjust_type api_map, raw_return_type + qualified_exclude = exclude_return_type&.qualify(api_map, *(closure&.gates || [''])) + minus_exclusions = raw_return_type.exclude qualified_exclude, api_map + qualified_intersection = intersection_return_type&.qualify(api_map, *(closure&.gates || [''])) + minus_exclusions.intersect_with qualified_intersection, api_map + end + + # See if this variable is visible within 'viewing_closure' + # + # @param viewing_closure [Pin::Closure] + # @return [Boolean] + def visible_in_closure? viewing_closure + return false if closure.nil? + + # if we're declared at top level, we can't be seen from within + # methods declared tere + + # @sg-ignore Need to add nil check here + return false if viewing_closure.is_a?(Pin::Method) && closure.context.tags == 'Class<>' + + # @sg-ignore Need to add nil check here + return true if viewing_closure.binder.namespace == closure.binder.namespace + + # @sg-ignore Need to add nil check here + return true if viewing_closure.return_type == closure.context + + # classes and modules can't see local variables declared + # in their parent closure, so stop here + return false if scope == :instance && viewing_closure.is_a?(Pin::Namespace) + + parent_of_viewing_closure = viewing_closure.closure + + return false if parent_of_viewing_closure.nil? + + visible_in_closure?(parent_of_viewing_closure) + end + + # @param other [self] + # @return [ComplexType, nil] + def combine_return_type other + combine_types(other, :return_type) + end + + # @param other [self] + # @param attr [::Symbol] + # + # @return [ComplexType, nil] + def combine_types other, attr + # @type [ComplexType, nil] + type1 = send(attr) + # @type [ComplexType, nil] + type2 = other.send(attr) + if type1 && type2 + types = (type1.items + type2.items).uniq + ComplexType.new(types) + else + type1 || type2 + end + end + + # @return [::Symbol] + def scope + :instance + end + + # @return [ComplexType, nil] def generate_complex_type tag = docstring.tag(:type) return ComplexType.try_parse(*tag.types) unless tag.nil? || tag.types.nil? || tag.types.empty? - ComplexType.new + nil end end end diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 0c6ecd258..1ad503317 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -15,12 +15,14 @@ class Block < Callable # @param node [Parser::AST::Node, nil] # @param context [ComplexType, nil] # @param args [::Array] + # @param [Hash{Symbol => Object}] splat def initialize receiver: nil, args: [], context: nil, node: nil, **splat super(**splat, parameters: args) @receiver = receiver @context = context @return_type = ComplexType.parse('::Proc') @node = node + @name = '' end # @param api_map [ApiMap] @@ -30,14 +32,20 @@ def rebind api_map end def binder - @rebind&.defined? ? @rebind : closure.binder + out = @rebind if @rebind&.defined? + out ||= super + end + + def context + @context = @rebind if @rebind&.defined? + super end # @param yield_types [::Array] # @param parameters [::Array] # # @return [::Array] - def destructure_yield_types(yield_types, parameters) + def destructure_yield_types yield_types, parameters # yielding a tuple into a block will destructure the tuple if yield_types.length == 1 yield_type = yield_types.first @@ -48,16 +56,19 @@ def destructure_yield_types(yield_types, parameters) # @param api_map [ApiMap] # @return [::Array] - def typify_parameters(api_map) + def typify_parameters api_map chain = Parser.chain(receiver, filename, node) + # @sg-ignore Need to add nil check here clip = api_map.clip_at(location.filename, location.range.start) locals = clip.locals - [self] + # @sg-ignore Need to add nil check here 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? + # @sg-ignore flow sensitive typing needs to handle attrs yield_types = meth.block.parameters.map(&:return_type) # 'arguments' is what the method says it will yield to the # block; 'parameters' is what the block accepts @@ -67,6 +78,7 @@ def typify_parameters(api_map) param_type = chain.base.infer(api_map, param, locals) unless arg_type.nil? if arg_type.generic? && param_type.defined? + # @sg-ignore Need to add nil check here namespace_pin = api_map.get_namespace_pins(meth.namespace, closure.namespace).first arg_type.resolve_generics(namespace_pin, param_type) else @@ -86,16 +98,27 @@ def typify_parameters(api_map) def maybe_rebind api_map return ComplexType::UNDEFINED unless receiver - chain = Parser.chain(receiver, location.filename) + # @sg-ignore Need to add nil check here + chain = Parser.chain(receiver, location.filename, node) + # @sg-ignore Need to add nil check here locals = api_map.source_map(location.filename).locals_at(location) + # @sg-ignore Need to add nil check here receiver_pin = chain.define(api_map, closure, locals).first return ComplexType::UNDEFINED unless receiver_pin types = receiver_pin.docstring.tag(:yieldreceiver)&.types return ComplexType::UNDEFINED unless types&.any? - target = chain.base.infer(api_map, receiver_pin, locals) - target = full_context unless target.defined? + name_pin = self + # if we have Foo.bar { |x| ... }, and the bar method references self... + target = if chain.base.defined? + # figure out Foo + chain.base.infer(api_map, name_pin, locals) + else + # if not, any self there must be the context of our closure + # @sg-ignore Need to add nil check here + closure.full_context + end ComplexType.try_parse(*types).qualify(api_map, *receiver_pin.gates).self_to_type(target) end diff --git a/lib/solargraph/pin/breakable.rb b/lib/solargraph/pin/breakable.rb index 05907b1bb..f5e4bfd4a 100644 --- a/lib/solargraph/pin/breakable.rb +++ b/lib/solargraph/pin/breakable.rb @@ -1,9 +1,15 @@ +# frozen_string_literal: true + module Solargraph module Pin - # Mix-in for pins which enclose code which the 'break' statement works with-in - e.g., blocks, when, until, ... + # Mix-in for pins which enclose code which the 'break' statement + # works with-in - e.g., blocks, when, until, ... module Breakable # @return [Parser::AST::Node] attr_reader :node + + # @return [Location, nil] + attr_reader :location end end end diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index edbc3f941..2ad4bbf62 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -14,6 +14,7 @@ class Callable < Closure # @param block [Signature, nil] # @param return_type [ComplexType, nil] # @param parameters [::Array] + # @param [Hash{Symbol => Object}] splat def initialize block: nil, return_type: nil, parameters: [], **splat super(**splat) @block = block @@ -21,15 +22,22 @@ def initialize block: nil, return_type: nil, parameters: [], **splat @parameters = parameters end + def reset_generated! + parameters.each(&:reset_generated!) + super + end + + # @sg-ignore Need to add nil check here # @return [String] def method_namespace + # @sg-ignore Need to add nil check here closure.namespace end # @param other [self] # # @return [Pin::Signature, nil] - def combine_blocks(other) + def combine_blocks other if block.nil? other.block elsif other.block.nil? @@ -44,10 +52,10 @@ def combine_blocks(other) # @param attrs [Hash{Symbol => Object}] # # @return [self] - def combine_with(other, attrs={}) + def combine_with other, attrs = {} new_attrs = { block: combine_blocks(other), - return_type: combine_return_type(other), + return_type: combine_return_type(other) }.merge(attrs) new_attrs[:parameters] = choose_parameters(other).clone.freeze unless new_attrs.key?(:parameters) super(other, new_attrs) @@ -65,8 +73,10 @@ def generics # @param other [self] # # @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 + def choose_parameters other + if other.arity != arity + raise "Trying to combine two pins with different arities - \nself =#{inspect}, \nother=#{other.inspect}, \n\n self.arity=#{arity}, \nother.arity=#{other.arity}" + end # @param param [Pin::Parameter] # @param other_param [Pin::Parameter] parameters.zip(other.parameters).map do |param, other_param| @@ -80,6 +90,7 @@ def choose_parameters(other) end end + # @sg-ignore Need to add nil check here # @return [Array] def blockless_parameters if parameters.last&.block? @@ -89,25 +100,47 @@ def blockless_parameters end end - # @return [Array] + # e.g., [["T"], "", "?", "foo:"] - parameter arity declarations, + # ignoring positional names. Used to match signatures. + # + # @return [Array, String, nil>] def arity [generics, blockless_parameters.map(&:arity_decl), block&.arity] end + # e.g., [["T"], "1", "?3", "foo:5"] - parameter arity + # declarations, including the number of unique types in each + # parameter. Used to determine whether combining two + # signatures has lost useful information mapping specific + # parameter types to specific return types. + # + # @return [Array] + def type_arity + [generics, blockless_parameters.map(&:type_arity_decl), block&.type_arity] + end + + # Same as type_arity, but includes return type arity at the front. + # + # @return [Array] + def full_type_arity + # @sg-ignore flow sensitive typing needs to handle attrs + [return_type ? return_type.items.count.to_s : nil] + type_arity + end + # @param generics_to_resolve [Enumerable] # @param arg_types [Array, nil] # @param return_type_context [ComplexType, nil] # @param yield_arg_types [Array, nil] # @param yield_return_type_context [ComplexType, nil] - # @param context [ComplexType, nil] # @param resolved_generic_values [Hash{String => ComplexType}] + # # @return [self] - def resolve_generics_from_context(generics_to_resolve, + def resolve_generics_from_context generics_to_resolve, arg_types = nil, return_type_context = nil, yield_arg_types = nil, yield_return_type_context = nil, - resolved_generic_values: {}) + resolved_generic_values: {} callable = super(generics_to_resolve, return_type_context, resolved_generic_values: resolved_generic_values) callable.parameters = callable.parameters.each_with_index.map do |param, i| if arg_types.nil? @@ -118,10 +151,12 @@ def resolve_generics_from_context(generics_to_resolve, resolved_generic_values: resolved_generic_values) end end - callable.block = block.resolve_generics_from_context(generics_to_resolve, - yield_arg_types, - yield_return_type_context, - resolved_generic_values: resolved_generic_values) if callable.block? + if callable.block? + callable.block = block.resolve_generics_from_context(generics_to_resolve, + yield_arg_types, + yield_return_type_context, + resolved_generic_values: resolved_generic_values) + end callable end @@ -137,9 +172,11 @@ def typify api_map end end + # @sg-ignore Need to add nil check here # @return [String] def method_name - raise "closure was nil in #{self.inspect}" if closure.nil? + raise "closure was nil in #{inspect}" if closure.nil? + # @sg-ignore Need to add nil check here @method_name ||= closure.name end @@ -148,15 +185,15 @@ def method_name # @param return_type_context [ComplexType, nil] # @param yield_arg_types [Array, nil] # @param yield_return_type_context [ComplexType, nil] - # @param context [ComplexType, nil] # @param resolved_generic_values [Hash{String => ComplexType}] + # # @return [self] - def resolve_generics_from_context_until_complete(generics_to_resolve, + def resolve_generics_from_context_until_complete generics_to_resolve, arg_types = nil, return_type_context = nil, yield_arg_types = nil, yield_return_type_context = nil, - resolved_generic_values: {}) + resolved_generic_values: {} # See # https://github.com/soutaro/steep/tree/master/lib/steep/type_inference # and @@ -174,7 +211,7 @@ def resolve_generics_from_context_until_complete(generics_to_resolve, resolved_generic_values: resolved_generic_values) if last_resolved_generic_values == resolved_generic_values # erase anything unresolved - return new_pin.erase_generics(self.generics) + return new_pin.erase_generics(generics) end new_pin.resolve_generics_from_context_until_complete(generics_to_resolve, arg_types, @@ -184,11 +221,10 @@ def resolve_generics_from_context_until_complete(generics_to_resolve, resolved_generic_values: resolved_generic_values) end - # @return [Array] # @yieldparam [ComplexType] # @yieldreturn [ComplexType] # @return [self] - def transform_types(&transform) + def transform_types &transform # @todo 'super' alone should work here I think, but doesn't typecheck at level typed callable = super(&transform) callable.block = block.transform_types(&transform) if block? @@ -206,6 +242,9 @@ def arity_matches? arguments, with_block parcount = mandatory_positional_param_count parcount -= 1 if !parameters.empty? && parameters.last.block? return false if block? && !with_block + # @todo this and its caller should be changed so that this can + # look at the kwargs provided and check names against what + # we acccept return false if argcount < parcount && !(argcount == parcount - 1 && parameters.last.restarg?) true end @@ -215,8 +254,13 @@ def mandatory_positional_param_count parameters.count(&:arg?) end + # @return [String] + def parameters_to_rbs + "#{rbs_generics}(#{parameters.map(&:to_rbs).join(', ')}) #{"{ #{block.to_rbs} } " unless block.nil?}" + end + def to_rbs - rbs_generics + '(' + parameters.map { |param| param.to_rbs }.join(', ') + ') ' + (block.nil? ? '' : '{ ' + block.to_rbs + ' } ') + '-> ' + return_type.to_rbs + "#{parameters_to_rbs}-> #{return_type&.to_rbs || 'untyped'}" end def block? diff --git a/lib/solargraph/pin/closure.rb b/lib/solargraph/pin/closure.rb index a7b37e01b..4b5738cfc 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -2,14 +2,15 @@ module Solargraph module Pin - class Closure < Base + class Closure < CompoundStatement # @return [::Symbol] :class or :instance attr_reader :scope # @param scope [::Symbol] :class or :instance - # @param generics [::Array, nil] + # @param generics [::Array, nil] # @param generic_defaults [Hash{String => ComplexType}] - def initialize scope: :class, generics: nil, generic_defaults: {}, **splat + # @param [Hash{Symbol => Object}] splat + def initialize scope: :class, generics: nil, generic_defaults: {}, **splat super(**splat) @scope = scope @generics = generics @@ -25,10 +26,10 @@ def generic_defaults # @param attrs [Hash{Symbol => Object}] # # @return [self] - def combine_with(other, attrs={}) + def combine_with other, attrs = {} new_attrs = { scope: assert_same(other, :scope), - generics: generics.empty? ? other.generics : generics, + generics: generics.empty? ? other.generics : generics }.merge(attrs) super(other, new_attrs) end @@ -44,10 +45,6 @@ def context end end - def binder - @binder || context - end - # @param api_map [Solargraph::ApiMap] # @return [void] def rebind api_map; end @@ -65,7 +62,7 @@ def to_rbs def rbs_generics return '' if generics.empty? - '[' + generics.map { |gen| gen.to_s }.join(', ') + '] ' + "[#{generics.map(&:to_s).join(', ')}] " end end end diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index 062099ee4..d52502afe 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -6,15 +6,30 @@ module Common # @!method source # @abstract # @return [Source, nil] + # @!method reset_generated! + # @abstract + # @return [void] # @type @closure [Pin::Closure, nil] + # @type @binder [ComplexType, ComplexType::UniqueType, nil] + + # @todo Missed nil violation + # @return [Location, nil] + attr_accessor :location - # @return [Location] - attr_reader :location + # @param value [Pin::Closure] + # @return [void] + def closure= value + @closure = value + # remove cached values generated from closure + reset_generated! + end - # @sg-ignore Solargraph::Pin::Common#closure return type could not be inferred # @return [Pin::Closure, nil] def closure - Solargraph.assert_or_log(:closure, "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") unless @closure + unless @closure + Solargraph.assert_or_log(:closure, + "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") + end @closure end @@ -23,12 +38,13 @@ def name @name ||= '' end + # @todo redundant with Base#return_type? # @return [ComplexType] def return_type @return_type ||= ComplexType::UNDEFINED end - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] def context # Get the static context from the nearest namespace @context ||= find_context @@ -40,7 +56,8 @@ def namespace context.namespace.to_s end - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] + # @sg-ignore https://github.com/castwide/solargraph/pull/1100 def binder @binder || context end @@ -70,6 +87,7 @@ def find_context elsif here.is_a?(Pin::Method) return here.context end + # @sg-ignore Need to add nil check here here = here.closure end ComplexType::ROOT diff --git a/lib/solargraph/pin/compound_statement.rb b/lib/solargraph/pin/compound_statement.rb new file mode 100644 index 000000000..39d9cf2d5 --- /dev/null +++ b/lib/solargraph/pin/compound_statement.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Solargraph + module Pin + # A series of statements where if a given statement executes, /all + # of the previous statements in the sequence must have executed as + # well/. In other words, the statements are run from the top in + # sequence, until interrupted by something like a + # return/break/next/raise/etc. + # + # This mix-in is used in flow sensitive typing to determine how + # far we can assume a given assertion about a type can be trusted + # to be true. + # + # Some examples in Ruby: + # + # * Bodies of methods and Ruby blocks + # * Branches of conditionals and loops - if/elsif/else, + # unless/else, when, until, ||=, ?:, switch/case/else + # * The body of begin-end/try/rescue/ensure statements + # + # Compare/contrast with: + # + # * Scope - a sequence where variables declared are not available + # after the end of the scope. Note that this is not necessarily + # true for a compound statement. + # * Compound statement - synonym + # * Block - in Ruby this has a special meaning (a closure passed to a method), but + # in general parlance this is also a synonym. + # * Closure - a sequence which is also a scope + # * Namespace - a named sequence which is also a scope and a + # closure + # + # See: + # https://cse.buffalo.edu/~regan/cse305/RubyBNF.pdf + # https://ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html + # https://en.wikipedia.org/wiki/Block_(programming) + # + # Note: + # + # Just because statement #1 in a sequence is executed, it doesn't + # mean that future ones will. Consider the effect of + # break/next/return/raise/etc. on control flow. + class CompoundStatement < Pin::Base + attr_reader :node + + # @param node [Parser::AST::Node, nil] + # @param [Hash{Symbol => Object}] splat + def initialize node: nil, **splat + super(**splat) + @node = node + end + end + end +end diff --git a/lib/solargraph/pin/constant.rb b/lib/solargraph/pin/constant.rb index 94a968e7e..a7696fd1f 100644 --- a/lib/solargraph/pin/constant.rb +++ b/lib/solargraph/pin/constant.rb @@ -33,12 +33,10 @@ def path # @return [ComplexType] def generate_complex_type - tags = docstring.tags(:return).map(&:types).flatten.reject(&:nil?) - if tags.empty? - tags = docstring.tags(:type).map(&:types).flatten.reject(&:nil?) - end + tags = docstring.tags(:return).map(&:types).flatten.compact + tags = docstring.tags(:type).map(&:types).flatten.compact if tags.empty? return ComplexType::UNDEFINED if tags.empty? - ComplexType.try_parse *tags + ComplexType.try_parse(*tags) end end end diff --git a/lib/solargraph/pin/conversions.rb b/lib/solargraph/pin/conversions.rb index e40cc8990..1c3aeac99 100644 --- a/lib/solargraph/pin/conversions.rb +++ b/lib/solargraph/pin/conversions.rb @@ -43,7 +43,7 @@ def completion_item data: { path: path, return_type: return_type.tag, - location: (location ? location.to_hash : nil), + location: location&.to_hash, deprecated: deprecated? } } @@ -72,7 +72,13 @@ def detail # This property is not cached in an instance variable because it can # change when pins get proxied. detail = String.new - detail += "=#{probed? ? '~' : (proxied? ? '^' : '>')} #{return_type.to_s}" unless return_type.undefined? + unless return_type.undefined? + detail += "=#{if probed? + '~' + else + (proxied? ? '^' : '>') + end} #{return_type}" + end detail.strip! return nil if detail.empty? detail @@ -80,7 +86,7 @@ def detail # Get a markdown-flavored link to a documentation page. # - # @return [String] + # @return [String, nil] def link_documentation @link_documentation ||= generate_link end @@ -116,7 +122,7 @@ def generate_link # @return [String] def escape_brackets text # text.gsub(/(\<|\>)/, "\\#{$1}") - text.gsub("<", '\<').gsub(">", '\>') + text.gsub('<', '\<').gsub('>', '\>') end end end diff --git a/lib/solargraph/pin/delegated_method.rb b/lib/solargraph/pin/delegated_method.rb index bcf5b5912..7e17f5042 100644 --- a/lib/solargraph/pin/delegated_method.rb +++ b/lib/solargraph/pin/delegated_method.rb @@ -13,10 +13,12 @@ class DelegatedMethod < Pin::Method # # @param method [Method, nil] an already resolved method pin. # @param receiver [Source::Chain, nil] the source code used to resolve the receiver for this delegated method. - # @param name [String] - # @param receiver_method_name [String] the method name that will be called on the receiver (defaults to :name). + # @param name [String, nil] + # @param receiver_method_name [String, nil] the method name that will be called on the receiver (defaults to :name). + # @param [Hash{Symbol => Object}] splat def initialize(method: nil, receiver: nil, name: method&.name, receiver_method_name: name, **splat) raise ArgumentError, 'either :method or :receiver is required' if (method && receiver) || (!method && !receiver) + # @sg-ignore Need to add nil check here super(name: name, **splat) @receiver_chain = receiver @@ -34,7 +36,6 @@ def location @resolved_method&.send(:location) end - def type_location return super if super @@ -58,7 +59,7 @@ def type_location end # @param api_map [ApiMap] - def resolvable?(api_map) + def resolvable? api_map resolve_method(api_map) !!@resolved_method end @@ -69,30 +70,40 @@ def resolvable?(api_map) # # @param api_map [ApiMap] # @return [Pin::Method, nil] + # @sg-ignore Declared return type ::Solargraph::Pin::Method, nil + # does not match inferred type nil, false for + # Solargraph::Pin::DelegatedMethod#resolve_method def resolve_method api_map return if @resolved_method + # @sg-ignore Need to add nil check here resolver = @receiver_chain.define(api_map, self, []).first unless resolver - Solargraph.logger.warn \ - "Delegated receiver for #{path} was resolved to nil from `#{print_chain(@receiver_chain)}'" + # @sg-ignore Need to add nil check here + Solargraph.logger.warn "Delegated receiver for #{path} was resolved to nil from `#{print_chain(@receiver_chain)}'" return end + # @sg-ignore Need to add nil check here receiver_type = resolver.return_type + # @sg-ignore Need to add nil check here return if receiver_type.undefined? receiver_path, method_scope = + # @sg-ignore Need to add nil check here if @receiver_chain.constant? # HACK: the `return_type` of a constant is Class, but looking up a method expects # the arguments `"Whatever"` and `scope: :class`. + # @sg-ignore Need to add nil check here [receiver_type.to_s.sub(/^Class<(.+)>$/, '\1'), :class] else + # @sg-ignore Need to add nil check here [receiver_type.to_s, :instance] end + # @sg-ignore Need to add nil check here method_stack = api_map.get_method_stack(receiver_path, @receiver_method_name, scope: method_scope) @resolved_method = method_stack.first end @@ -101,10 +112,10 @@ def resolve_method api_map # # @param chain [Source::Chain] # @return [String] - def print_chain(chain) + def print_chain chain out = +'' chain.links.each_with_index do |link, index| - if index > 0 + if index.positive? if Source::Chain::Constant out << '::' unless link.word.start_with?('::') else diff --git a/lib/solargraph/pin/documenting.rb b/lib/solargraph/pin/documenting.rb index bd8b1fe9a..94bd8a551 100644 --- a/lib/solargraph/pin/documenting.rb +++ b/lib/solargraph/pin/documenting.rb @@ -62,7 +62,7 @@ def to_s # @return [String] def to_code - "\n```ruby\n#{Documenting.normalize_indentation(@plaintext)}#{@plaintext.end_with?("\n") ? '' : "\n"}```\n\n" + "\n```ruby\n#{Documenting.normalize_indentation(@plaintext)}#{"\n" unless @plaintext.end_with?("\n")}```\n\n" end # @return [String] @@ -78,7 +78,8 @@ def documentation # line and at least two spaces of indentation. This is a common # convention in Ruby core documentation, e.g., String#split. sections = [DocSection.new(false)] - Documenting.normalize_indentation(Documenting.strip_html_comments(docstring.to_s.gsub("\t", ' '))).lines.each do |l| + Documenting.normalize_indentation(Documenting.strip_html_comments(docstring.to_s.gsub("\t", + ' '))).lines.each do |l| if l.start_with?(' ') # Code block sections.push DocSection.new(true) unless sections.last.code? @@ -104,6 +105,7 @@ def self.normalize_indentation text left = text.lines.map do |line| match = line.match(/^ +/) next 0 unless match + # @sg-ignore Need to add nil check here match[0].length end.min return text if left.nil? || left.zero? diff --git a/lib/solargraph/pin/instance_variable.rb b/lib/solargraph/pin/instance_variable.rb index c06fdd93e..b3c69f09c 100644 --- a/lib/solargraph/pin/instance_variable.rb +++ b/lib/solargraph/pin/instance_variable.rb @@ -3,13 +3,17 @@ module Solargraph module Pin class InstanceVariable < BaseVariable - # @return [ComplexType] + # @sg-ignore Need to add nil check here + # @return [ComplexType, ComplexType::UniqueType] def binder + # @sg-ignore Need to add nil check here closure.binder end + # @sg-ignore Need to add nil check here # @return [::Symbol] def scope + # @sg-ignore Need to add nil check here closure.binder.scope end diff --git a/lib/solargraph/pin/keyword.rb b/lib/solargraph/pin/keyword.rb index 089d0a417..08ea1c6e0 100644 --- a/lib/solargraph/pin/keyword.rb +++ b/lib/solargraph/pin/keyword.rb @@ -11,10 +11,6 @@ def initialize(name, **kwargs) def closure @closure ||= Pin::ROOT_PIN end - - def name - @name - end end end end diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index cb2dda140..077da21be 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -3,72 +3,28 @@ module Solargraph module Pin class LocalVariable < BaseVariable - # @return [Range] - attr_reader :presence - - def presence_certain? - @presence_certain - end + # @param api_map [ApiMap] + # @return [ComplexType, ComplexType::UniqueType] + def probe api_map + if presence_certain? && return_type&.defined? + # flow sensitive typing has already figured out this type + # has been downcast - use the type it figured out + # @sg-ignore flow sensitive typing should support ivars + return adjust_type api_map, return_type.qualify(api_map, *gates) + end - # @param assignment [AST::Node, nil] - # @param presence [Range, nil] - # @param presence_certain [Boolean] - # @param splat [Hash] - def initialize assignment: nil, presence: nil, presence_certain: false, **splat - super(**splat) - @assignment = assignment - @presence = presence - @presence_certain = presence_certain + super end - def combine_with(other, attrs={}) - new_attrs = { - assignment: assert_same(other, :assignment), - presence_certain: assert_same(other, :presence_certain?), - }.merge(attrs) - new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence) + def combine_with other, attrs = {} + # keep this as a parameter + return other.combine_with(self, attrs) if other.is_a?(Parameter) && !is_a?(Parameter) - super(other, new_attrs) - end - - # @param other_closure [Pin::Closure] - # @param other_loc [Location] - def visible_at?(other_closure, other_loc) - location.filename == other_loc.filename && - presence.include?(other_loc.range.start) && - match_named_closure(other_closure, closure) + super end def to_rbs - (name || '(anon)') + ' ' + (return_type&.to_rbs || 'untyped') - end - - private - - # @param tag1 [String] - # @param tag2 [String] - # @return [Boolean] - def match_tags tag1, tag2 - # @todo This is an unfortunate hack made necessary by a discrepancy in - # how tags indicate the root namespace. The long-term solution is to - # standardize it, whether it's `Class<>`, an empty string, or - # something else. - tag1 == tag2 || - (['', 'Class<>'].include?(tag1) && ['', 'Class<>'].include?(tag2)) - end - - # @param needle [Pin::Base] - # @param haystack [Pin::Base] - # @return [Boolean] - def match_named_closure needle, haystack - return true if needle == haystack || haystack.is_a?(Pin::Block) - cursor = haystack - until cursor.nil? - return true if needle.path == cursor.path - return false if cursor.path && !cursor.path.empty? - cursor = cursor.closure - end - false + "#{name || '(anon)'} #{return_type&.to_rbs || 'untyped'}" end end end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 86bf1cd09..f06a563ed 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -22,8 +22,10 @@ class Method < Callable # @param attribute [Boolean] # @param signatures [::Array, nil] # @param anon_splat [Boolean] + # @param context [ComplexType, ComplexType::UniqueType, nil] + # @param [Hash{Symbol => Object}] splat def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false, - **splat + context: nil, **splat super(**splat) @visibility = visibility @explicit = explicit @@ -32,31 +34,12 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil @attribute = attribute @signatures = signatures @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) - end - end - by_arity.values.flatten + @context = context if context end # @param other [Pin::Method] # @return [::Symbol] - def combine_visibility(other) + def combine_visibility other if dodgy_visibility_source? && !other.dodgy_visibility_source? other.visibility elsif other.dodgy_visibility_source? && !dodgy_visibility_source? @@ -66,33 +49,18 @@ def combine_visibility(other) end end - # @param other [Pin::Method] - # @return [Array] - def combine_signatures(other) - all_undefined = signatures.all? { |sig| sig.return_type.undefined? } - other_all_undefined = other.signatures.all? { |sig| sig.return_type.undefined? } - if all_undefined && !other_all_undefined - other.signatures - elsif other_all_undefined && !all_undefined - signatures - else - combine_all_signature_pins(*signatures, *other.signatures) - end - end - - def combine_with(other, attrs = {}) + def combine_with other, attrs = {} priority_choice = choose_priority(other) return priority_choice unless priority_choice.nil? sigs = combine_signatures(other) - parameters = if sigs.length > 0 - [].freeze - else - choose(other, :parameters).clone.freeze - end + parameters = if sigs.length.positive? + [].freeze + else + choose(other, :parameters).clone.freeze + 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), @@ -110,7 +78,7 @@ def == other super && other.node == node end - def transform_types(&transform) + def transform_types &transform # @todo 'super' alone should work here I think, but doesn't typecheck at level typed m = super(&transform) m.signatures = m.signatures.map do |sig| @@ -125,14 +93,12 @@ def transform_types(&transform) def reset_generated! super unless signatures.empty? - return_type = nil @block = :undefined - parameters = [] + [] end block&.reset_generated! @signatures&.each(&:reset_generated!) - signature_help = nil - documentation = nil + nil end def all_rooted? @@ -141,7 +107,7 @@ def all_rooted? # @param signature [Pin::Signature] # @return [Pin::Method] - def with_single_signature(signature) + def with_single_signature signature m = proxy signature.return_type m.reset_generated! # @todo populating the single parameters/return_type/block @@ -160,6 +126,8 @@ def block? !block.nil? end + # @sg-ignore flow sensitive typing needs to remove literal with + # this unless block # @return [Pin::Signature, nil] def block return @block unless @block == :undefined @@ -179,9 +147,10 @@ def return_type end # @param parameters [::Array] - # @param return_type [ComplexType] + # @param return_type [ComplexType, nil] # @return [Signature] - def generate_signature(parameters, return_type) + def generate_signature parameters, return_type + # @type [Pin::Signature, nil] block = nil yieldparam_tags = docstring.tags(:yieldparam) yieldreturn_tags = docstring.tags(:yieldreturn) @@ -202,7 +171,7 @@ def generate_signature(parameters, return_type) comments: p.text, name: name, decl: decl, - presence: location ? location.range : nil, + presence: location&.range, return_type: ComplexType.try_parse(*p.types), source: source ) @@ -223,7 +192,11 @@ def signatures top_type = generate_complex_type result = [] result.push generate_signature(parameters, top_type) if top_type.defined? - result.concat(overloads.map { |meth| generate_signature(meth.parameters, meth.return_type) }) unless overloads.empty? + unless overloads.empty? + result.concat(overloads.map do |meth| + generate_signature(meth.parameters, meth.return_type) + end) + end result.push generate_signature(parameters, @return_type || ComplexType::UNDEFINED) if result.empty? result end @@ -243,11 +216,18 @@ def detail # change when pins get proxied. detail = String.new detail += if signatures.length > 1 - "(*) " - else - "(#{signatures.first.parameters.map(&:full).join(', ')}) " unless signatures.first.parameters.empty? - end.to_s - detail += "=#{probed? ? '~' : (proxied? ? '^' : '>')} #{return_type.to_s}" unless return_type.undefined? + '(*) ' + else + "(#{signatures.first.parameters.map(&:full).join(', ')}) " unless signatures.first.parameters.empty? + end.to_s + # @sg-ignore Need to add nil check here + unless return_type.undefined? + detail += "=#{if probed? + '~' + else + (proxied? ? '^' : '>') + end} #{return_type}" + end detail.strip! return nil if detail.empty? detail @@ -257,7 +237,7 @@ def detail def signature_help @signature_help ||= signatures.map do |sig| { - label: name + '(' + sig.parameters.map(&:full).join(', ') + ')', + label: "#{name}(#{sig.parameters.map(&:full).join(', ')})", documentation: documentation } end @@ -276,6 +256,7 @@ def to_rbs return nil if signatures.empty? rbs = "def #{name}: #{signatures.first.to_rbs}" + # @sg-ignore Need to add nil check here signatures[1..].each do |sig| rbs += "\n" rbs += (' ' * (4 + name.length)) @@ -285,7 +266,7 @@ def to_rbs end def path - @path ||= "#{namespace}#{(scope == :instance ? '#' : '.')}#{name}" + @path ||= "#{namespace}#{scope == :instance ? '#' : '.'}#{name}" end # @return [String] @@ -294,15 +275,21 @@ def method_name end def typify api_map - logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context.rooted_tags}, return_type=#{return_type.rooted_tags}) - starting" } + logger.debug do + # @sg-ignore Need to add nil check here + "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context.rooted_tags}, return_type=#{return_type.rooted_tags}) - starting" + end decl = super unless decl.undefined? - logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}) => #{decl.rooted_tags.inspect} - decl found" } + logger.debug do + "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}) => #{decl.rooted_tags.inspect} - decl found" + end return decl end type = see_reference(api_map) || typify_from_super(api_map) logger.debug { "Method#typify(self=#{self}) - type=#{type&.rooted_tags.inspect}" } unless type.nil? + # @sg-ignore Need to add nil check here qualified = type.qualify(api_map, *closure.gates) logger.debug { "Method#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } return qualified @@ -314,26 +301,26 @@ def documentation if @documentation.nil? method_docs ||= super || '' param_tags = docstring.tags(:param) - unless param_tags.nil? or param_tags.empty? + unless param_tags.nil? || param_tags.empty? method_docs += "\n\n" unless method_docs.empty? method_docs += "Params:\n" lines = [] param_tags.each do |p| l = "* #{p.name}" - l += " [#{escape_brackets(p.types.join(', '))}]" unless p.types.nil? or p.types.empty? + l += " [#{escape_brackets(p.types.join(', '))}]" unless p.types.nil? || p.types.empty? l += " #{p.text}" lines.push l end method_docs += lines.join("\n") end yieldparam_tags = docstring.tags(:yieldparam) - unless yieldparam_tags.nil? or yieldparam_tags.empty? + unless yieldparam_tags.nil? || yieldparam_tags.empty? method_docs += "\n\n" unless method_docs.empty? method_docs += "Block Params:\n" lines = [] yieldparam_tags.each do |p| l = "* #{p.name}" - l += " [#{escape_brackets(p.types.join(', '))}]" unless p.types.nil? or p.types.empty? + l += " [#{escape_brackets(p.types.join(', '))}]" unless p.types.nil? || p.types.empty? l += " #{p.text}" lines.push l end @@ -345,8 +332,8 @@ def documentation method_docs += "Block Returns:\n" lines = [] yieldreturn_tags.each do |r| - l = "*" - l += " [#{escape_brackets(r.types.join(', '))}]" unless r.types.nil? or r.types.empty? + l = '*' + l += " [#{escape_brackets(r.types.join(', '))}]" unless r.types.nil? || r.types.empty? l += " #{r.text}" lines.push l end @@ -358,8 +345,8 @@ def documentation method_docs += "Returns:\n" lines = [] return_tags.each do |r| - l = "*" - l += " [#{escape_brackets(r.types.join(', '))}]" unless r.types.nil? or r.types.empty? + l = '*' + l += " [#{escape_brackets(r.types.join(', '))}]" unless r.types.nil? || r.types.empty? l += " #{r.text}" lines.push l end @@ -396,7 +383,7 @@ def probe api_map attribute? ? infer_from_iv(api_map) : infer_from_return_nodes(api_map) end - # @return [::Array] + # @return [::Array] def overloads # Ignore overload tags with nil parameters. If it's not an array, the # tag's source is likely malformed. @@ -414,14 +401,14 @@ def overloads comments: tag.docstring.all.to_s, name: name, decl: decl, - presence: location ? location.range : nil, + presence: location&.range, return_type: param_type_from_name(tag, src.first), source: :overloads ) end, closure: self, return_type: ComplexType.try_parse(*tag.docstring.tags(:return).flat_map(&:types)), - source: :overloads, + source: :overloads ) end @overloads @@ -440,13 +427,13 @@ def resolve_ref_tag api_map return self unless docstring.ref_tags.any? docstring.ref_tags.each do |tag| ref = if tag.owner.to_s.start_with?(/[#.]/) - api_map.get_methods(namespace) - .select { |pin| pin.path.end_with?(tag.owner.to_s) } - .first - else - # @todo Resolve relative namespaces - api_map.get_path_pins(tag.owner.to_s).first - end + api_map.get_methods(namespace) + .select { |pin| pin.path.end_with?(tag.owner.to_s) } + .first + else + # @todo Resolve relative namespaces + api_map.get_path_pins(tag.owner.to_s).first + end next unless ref docstring.add_tag(*ref.docstring.tags(:param)) @@ -462,29 +449,90 @@ def rest_of_stack api_map protected - attr_writer :block - - attr_writer :signature_help - - attr_writer :documentation + attr_writer :block, :signature_help, :documentation, :return_type + # @sg-ignore Need to add nil check here def dodgy_visibility_source? # as of 2025-03-12, the RBS generator used for # e.g. activesupport did not understand 'private' markings # inside 'class << self' blocks, but YARD did OK at it - source == :rbs && scope == :class && type_location&.filename&.include?('generated') && return_type.undefined? || + # @sg-ignore Need to add nil check here + (source == :rbs && scope == :class && type_location&.filename&.include?('generated') && return_type.undefined?) || # YARD's RBS generator seems to miss a lot of should-be protected instance methods - source == :rbs && scope == :instance && namespace.start_with?('YARD::') || + (source == :rbs && scope == :instance && namespace.start_with?('YARD::')) || # private on attr_readers seems to be broken in Prism's auto-generator script - source == :rbs && scope == :instance && namespace.start_with?('Prism::') || + (source == :rbs && scope == :instance && namespace.start_with?('Prism::')) || # The RBS for the RBS gem itself seems to use private as a # 'is this a public API' concept, more aggressively than the # actual code. Let's respect that and ignore the actual .rb file. - source == :yardoc && scope == :instance && namespace.start_with?('RBS::') + (source == :yardoc && scope == :instance && namespace.start_with?('RBS::')) end private + # @param other [Pin::Method] + # @return [Array] + def combine_signatures other + all_undefined = signatures.all? { |sig| !sig.return_type&.defined? } + other_all_undefined = other.signatures.all? { |sig| !sig.return_type&.defined? } + if all_undefined && !other_all_undefined + other.signatures + elsif other_all_undefined && !all_undefined + signatures + else + combine_signatures_by_type_arity(*signatures, *other.signatures) + end + end + + # @param signature_pins [Array] + # + # @return [Array] + def combine_signatures_by_type_arity(*signature_pins) + # @type [Hash{Array => Array}] + by_type_arity = {} + signature_pins.each do |signature_pin| + by_type_arity[signature_pin.type_arity] ||= [] + by_type_arity[signature_pin.type_arity] << signature_pin + end + + by_type_arity.transform_values! do |same_type_arity_signatures| + combine_same_type_arity_signatures same_type_arity_signatures + end + by_type_arity.values.flatten + end + + # @param same_type_arity_signatures [Array] + # + # @return [Array] + def combine_same_type_arity_signatures same_type_arity_signatures + # This is an O(n^2) operation, so bail out if n is not small + return same_type_arity_signatures if same_type_arity_signatures.length > 10 + + # @param old_signatures [Array] + # @param new_signature [Pin::Signature] + same_type_arity_signatures.reduce([]) do |old_signatures, new_signature| + next old_signatures + [new_signature] if old_signatures.empty? + old_signatures.flat_map do |old_signature| + potential_new_signature = old_signature.combine_with(new_signature) + + if potential_new_signature.type_arity == old_signature.type_arity + # the number of types in each parameter and return type + # match, so we found compatible signatures to merge. If + # we increased the number of types, we'd potentially + # have taken away the ability to use parameter types to + # choose the correct return type (while Ruby doesn't + # dispatch based on type, RBS does distinguish overloads + # based on types, not just arity, allowing for type + # information describing how methods behave based on + # their input types) + old_signatures - [old_signature] + [potential_new_signature] + else + old_signatures + [new_signature] + end + end + end + end + # @param name [String] # @param asgn [Boolean] # @@ -517,7 +565,7 @@ def clean_param name # @param name [String] # # @return [ComplexType] - def param_type_from_name(tag, name) + 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 @@ -528,23 +576,24 @@ def param_type_from_name(tag, name) def generate_complex_type tags = docstring.tags(:return).map(&:types).flatten.compact return ComplexType::UNDEFINED if tags.empty? - ComplexType.try_parse *tags + ComplexType.try_parse(*tags) end # @param api_map [ApiMap] - # @return [ComplexType, nil] + # @return [ComplexType, ComplexType::UniqueType, nil] def see_reference api_map # This should actually be an intersection type - # @param ref [YARD::Tags::Tag, Solargraph::Yard::Tags::RefTag] + # @param ref [YARD::Tags::Tag, 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 + # @sg-ignore should actually be an intersection type result = resolve_reference(ref.owner.to_s, api_map) return result unless result.nil? end match = comments.match(/^[ \t]*\(see (.*)\)/m) return nil if match.nil? + # @sg-ignore Need to add nil check here resolve_reference match[1], api_map end @@ -559,6 +608,7 @@ def typify_from_super api_map stack = rest_of_stack api_map return nil if stack.empty? stack.each do |pin| + # @sg-ignore Need to add nil check here return pin.return_type unless pin.return_type.undefined? end nil @@ -566,7 +616,7 @@ def typify_from_super api_map # @param ref [String] # @param api_map [ApiMap] - # @return [ComplexType, nil] + # @return [ComplexType, ComplexType::UniqueType, nil] def resolve_reference ref, api_map parts = ref.split(/[.#]/) if parts.first.empty? || parts.one? @@ -574,6 +624,7 @@ def resolve_reference ref, api_map else fqns = api_map.qualify(parts.first, *gates) return ComplexType::UNDEFINED if fqns.nil? + # @sg-ignore Need to add nil check here path = fqns + ref[parts.first.length] + parts.last end pins = api_map.get_path_pins(path) @@ -589,7 +640,7 @@ def method_body_node return nil if node.nil? return node.children[1].children.last if node.type == :DEFN return node.children[2].children.last if node.type == :DEFS - return node.children[2] if node.type == :def || node.type == :DEFS + return node.children[2] if %i[def DEFS].include?(node.type) return node.children[3] if node.type == :defs nil end @@ -602,16 +653,18 @@ def infer_from_return_nodes api_map has_nil = false return ComplexType::NIL if method_body_node.nil? returns_from_method_body(method_body_node).each do |n| - if n.nil? || [:NIL, :nil].include?(n.type) + if n.nil? || %i[NIL nil].include?(n.type) has_nil = true next end rng = Range.from_node(n) next unless rng clip = api_map.clip_at( + # @sg-ignore Need to add nil check here location.filename, rng.ending ) + # @sg-ignore Need to add nil check here chain = Solargraph::Parser.chain(n, location.filename) type = chain.infer(api_map, self, clip.locals) result.push type unless type.undefined? @@ -640,12 +693,12 @@ def infer_from_iv api_map # # @param name [String] # @return [::Array(String, ::Symbol)] - def parse_overload_param(name) + def parse_overload_param name # @todo this needs to handle mandatory vs not args, kwargs, blocks, etc if name.start_with?('**') - [name[2..-1], :kwrestarg] + [name[2..], :kwrestarg] elsif name.start_with?('*') - [name[1..-1], :restarg] + [name[1..], :restarg] else [name, :arg] end @@ -663,10 +716,6 @@ def concat_example_tags .join("\n") .concat("```\n") end - - protected - - attr_writer :return_type end end end diff --git a/lib/solargraph/pin/method_alias.rb b/lib/solargraph/pin/method_alias.rb index 28be6d8b1..feb6baccc 100644 --- a/lib/solargraph/pin/method_alias.rb +++ b/lib/solargraph/pin/method_alias.rb @@ -26,6 +26,14 @@ def visibility :public end + def to_rbs + if scope == :class + "alias self.#{name} self.#{original}" + else + "alias #{name} #{original}" + end + end + def path @path ||= namespace + (scope == :instance ? '#' : '.') + name end diff --git a/lib/solargraph/pin/namespace.rb b/lib/solargraph/pin/namespace.rb index 95bd1089a..55bb52b0e 100644 --- a/lib/solargraph/pin/namespace.rb +++ b/lib/solargraph/pin/namespace.rb @@ -20,14 +20,14 @@ class Namespace < Closure # @param visibility [::Symbol] :public or :private # @param gates [::Array] # @param name [String] + # @param [Hash{Symbol => Object}] splat def initialize type: :class, visibility: :public, gates: [''], name: '', **splat # super(location, namespace, name, comments) super(**splat, name: name) @type = type @visibility = visibility if name.start_with?('::') - # @type [String] - name = name[2..-1] || '' + name = name[2..] || '' @closure = Solargraph::Pin::ROOT_PIN end @open_gates = gates @@ -37,10 +37,11 @@ def initialize type: :class, visibility: :public, gates: [''], name: '', **splat parts = name.split('::') name = parts.pop closure_name = if [Solargraph::Pin::ROOT_PIN, nil].include?(closure) - '' - else - closure.full_context.namespace + '::' - end + '' + else + # @sg-ignore Need to add nil check here + "#{closure.full_context.namespace}::" + end closure_name += parts.join('::') @closure = Pin::Namespace.new(name: closure_name, gates: [parts.join('::')], source: :namespace) @context = nil @@ -48,8 +49,14 @@ def initialize type: :class, visibility: :public, gates: [''], name: '', **splat @name = name end + def reset_generated! + @return_type = nil + @full_context = nil + @path = nil + end + def to_rbs - "#{@type.to_s} #{return_type.all_params.first.to_rbs}#{rbs_generics}".strip + "#{@type} #{return_type.all_params.first.to_rbs}#{rbs_generics}".strip end def inner_desc @@ -91,7 +98,7 @@ def path end def return_type - @return_type ||= ComplexType.try_parse( (type == :class ? '::Class' : '::Module') + "<::#{path}>") + @return_type ||= ComplexType.try_parse((type == :class ? '::Class' : '::Module') + "<::#{path}>") end # @return [Array] @@ -105,10 +112,10 @@ def typify api_map def gates @gates ||= if path.empty? - @open_gates - else - [path] + @open_gates - end + @open_gates + else + [path] + @open_gates + end end end end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 91c205921..972753f7d 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -15,6 +15,7 @@ class Parameter < LocalVariable # @param decl [::Symbol] :arg, :optarg, :kwarg, :kwoptarg, :restarg, :kwrestarg, :block, :blockarg # @param asgn_code [String, nil] + # @param [Hash{Symbol => Object}] splat def initialize decl: :arg, asgn_code: nil, **splat super(**splat) @asgn_code = asgn_code @@ -29,21 +30,39 @@ def location super || closure&.type_location end - def combine_with(other, attrs={}) - new_attrs = { - decl: assert_same(other, :decl), - presence: choose(other, :presence), - asgn_code: choose(other, :asgn_code), - }.merge(attrs) - super(other, new_attrs) + def combine_with other, attrs = {} + # Parameters can be combined with local variables + new_attrs = if other.is_a?(Parameter) + { + decl: assert_same(other, :decl), + asgn_code: choose(other, :asgn_code) + } + else + { + decl: decl, + asgn_code: asgn_code + } + end + super(other, new_attrs.merge(attrs)) + end + + def combine_return_type other + out = super + if out&.undefined? + # allow our return_type method to provide a better type + # using :param tag + out = nil + end + out end def keyword? - [:kwarg, :kwoptarg].include?(decl) + %i[kwarg kwoptarg].include?(decl) end def kwrestarg? - decl == :kwrestarg || (assignment && [:HASH, :hash].include?(assignment.type)) + # @sg-ignore flow sensitive typing needs to handle attrs + decl == :kwrestarg || (assignment && %i[HASH hash].include?(assignment.type)) end def needs_consistent_name? @@ -52,26 +71,31 @@ def needs_consistent_name? # @return [String] def arity_decl - name = (self.name || '(anon)') - type = (return_type&.to_rbs || 'untyped') + name = self.name || '(anon)' + return_type&.to_rbs || 'untyped' case decl when :arg - "" + '' when :optarg - "?" + '?' when :kwarg "#{name}:" when :kwoptarg "?#{name}:" when :restarg - "*" + '*' when :kwrestarg - "**" + '**' else "(unknown decl: #{decl})" end end + # @return [String] + def type_arity_decl + arity_decl + return_type.items.count.to_s + end + def arg? decl == :arg end @@ -80,12 +104,20 @@ def restarg? decl == :restarg end + def mandatory_positional? + decl == :arg + end + + def positional? + !keyword? + end + def rest? - decl == :restarg || decl == :kwrestarg + %i[restarg kwrestarg].include?(decl) end def block? - [:block, :blockarg].include?(decl) + %i[block blockarg].include?(decl) end def to_rbs @@ -123,6 +155,11 @@ def full_name end end + def reset_generated! + @return_type = nil if param_tag + super + end + # @return [String] def full full_name + case decl @@ -135,50 +172,67 @@ def full end end + # @sg-ignore super always sets @return_type to something # @return [ComplexType] def return_type if @return_type.nil? @return_type = ComplexType::UNDEFINED found = param_tag - @return_type = ComplexType.try_parse(*found.types) unless found.nil? or found.types.nil? + @return_type = ComplexType.try_parse(*found.types) unless found.nil? || found.types.nil? + # @sg-ignore flow sensitive typing should be able to handle redefinition if @return_type.undefined? - if decl == :restarg + case decl + when :restarg @return_type = ComplexType.try_parse('::Array') - elsif decl == :kwrestarg + when :kwrestarg @return_type = ComplexType.try_parse('::Hash') - elsif decl == :blockarg + when :blockarg @return_type = ComplexType.try_parse('::Proc') end end end super - @return_type end # The parameter's zero-based location in the block's signature. # + # @sg-ignore Need to add nil check here # @return [Integer] def index - # @type [Method, Block] method_pin = closure + # @sg-ignore Need to add nil check here method_pin.parameter_names.index(name) end # @param api_map [ApiMap] def typify api_map - return return_type.qualify(api_map, *closure.gates) unless return_type.undefined? - closure.is_a?(Pin::Block) ? typify_block_param(api_map) : typify_method_param(api_map) + new_type = super + return new_type if new_type.defined? + + # sniff based on param tags + new_type = closure.is_a?(Pin::Block) ? typify_block_param(api_map) : typify_method_param(api_map) + + return adjust_type api_map, new_type.self_to_type(full_context) if new_type.defined? + + adjust_type api_map, super.self_to_type(full_context) end # @param atype [ComplexType] # @param api_map [ApiMap] - def compatible_arg?(atype, api_map) + def compatible_arg? atype, api_map # make sure we get types from up the method # inheritance chain if we don't have them on this pin ptype = typify api_map - ptype.undefined? || ptype.can_assign?(api_map, atype) || ptype.generic? + return true if ptype.undefined? + + return true if atype.conforms_to?(api_map, + ptype, + :method_call, + %i[allow_empty_params allow_undefined]) + ptype.generic? end + # @sg-ignore flow sensitive typing needs to handle attrs def documentation tag = param_tag return '' if tag.nil? || tag.text.nil? @@ -187,12 +241,19 @@ def documentation private + def generate_complex_type + nil + end + # @return [YARD::Tags::Tag, nil] def param_tag + # @sg-ignore Need to add nil check here params = closure.docstring.tags(:param) + # @sg-ignore Need to add nil check here params.each do |p| return p if p.name == name end + # @sg-ignore Need to add nil check here params[index] if index && params[index] && (params[index].name.nil? || params[index].name.empty?) end @@ -200,15 +261,14 @@ def param_tag # @return [ComplexType] def typify_block_param api_map block_pin = closure - if block_pin.is_a?(Pin::Block) && block_pin.receiver - return block_pin.typify_parameters(api_map)[index] - end + return block_pin.typify_parameters(api_map)[index] if block_pin.is_a?(Pin::Block) && block_pin.receiver && index ComplexType::UNDEFINED end # @param api_map [ApiMap] # @return [ComplexType] def typify_method_param api_map + # @sg-ignore Need to add nil check here meths = api_map.get_method_stack(closure.full_context.tag, closure.name, scope: closure.scope) # meths.shift # Ignore the first one meths.each do |meth| @@ -219,10 +279,14 @@ def typify_method_param api_map found = p break end - if found.nil? and !index.nil? - found = params[index] if params[index] && (params[index].name.nil? || params[index].name.empty?) + if found.nil? && !index.nil? && params[index] && (params[index].name.nil? || params[index].name.empty?) + found = params[index] + end + unless found.nil? || found.types.nil? + return ComplexType.try_parse(*found.types).qualify(api_map, + # @sg-ignore Need to add nil check here + *meth.closure.gates) end - return ComplexType.try_parse(*found.types).qualify(api_map, *meth.closure.gates) unless found.nil? || found.types.nil? end ComplexType::UNDEFINED end @@ -230,6 +294,7 @@ def typify_method_param api_map # @param heredoc [YARD::Docstring] # @param api_map [ApiMap] # @param skip [::Array] + # # @return [::Array] def see_reference heredoc, api_map, skip = [] # This should actually be an intersection type @@ -237,7 +302,7 @@ def see_reference heredoc, api_map, skip = [] 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 + # @todo ref should actually be an intersection type result = resolve_reference(ref.owner.to_s, api_map, skip) return result unless result.nil? end @@ -257,14 +322,13 @@ def resolve_reference ref, api_map, skip else fqns = api_map.qualify(parts.first, namespace) return nil if fqns.nil? + # @sg-ignore Need to add nil check here path = fqns + ref[parts.first.length] + parts.last end pins = api_map.get_path_pins(path) pins.each do |pin| params = pin.docstring.tags(:param) return params unless params.empty? - end - pins.each do |pin| params = see_reference(pin.docstring, api_map, skip) return params unless params.empty? end diff --git a/lib/solargraph/pin/proxy_type.rb b/lib/solargraph/pin/proxy_type.rb index 452536834..fd37bab85 100644 --- a/lib/solargraph/pin/proxy_type.rb +++ b/lib/solargraph/pin/proxy_type.rb @@ -3,10 +3,11 @@ module Solargraph module Pin class ProxyType < Base - # @param return_type [ComplexType] + # @param return_type [ComplexType, ComplexType::UniqueType] # @param gates [Array, nil] Namespaces to try while resolving non-rooted types # @param binder [ComplexType, ComplexType::UniqueType, nil] # @param gates [Array, nil] + # @param [Hash{Symbol => Object}] splat def initialize return_type: ComplexType::UNDEFINED, binder: nil, gates: nil, **splat super(**splat) @gates = gates @@ -22,9 +23,11 @@ def context # @param closure [Pin::Namespace, nil] Used as the closure for this pin # @param binder [ComplexType, ComplexType::UniqueType, nil] # @return [ProxyType] + # @param [Hash{Symbol => Object}] kwargs def self.anonymous context, closure: nil, binder: nil, **kwargs unless closure parts = context.namespace.split('::') + # @sg-ignore Need to add nil check here namespace = parts[0..-2].join('::').to_s closure = Solargraph::Pin::Namespace.new(name: namespace, source: :proxy_type) end diff --git a/lib/solargraph/pin/reference.rb b/lib/solargraph/pin/reference.rb index d456fbbf8..fc54db021 100644 --- a/lib/solargraph/pin/reference.rb +++ b/lib/solargraph/pin/reference.rb @@ -12,7 +12,24 @@ class Reference < Base attr_reader :generic_values + # A Reference is a pin that associates a type with another type. + # The existing type is marked as the closure. The name of the + # type we're associating with it is the 'name' field, and + # subtypes are in the 'generic_values' field. + # + # These pins are a little different - the name is a rooted name, + # which may be relative or absolute, preceded with ::, not a + # fully qualified namespace, which is implicitly in the root + # namespace and is never preceded by ::. + # + # @todo can the above be represented in a less subtle way? + # @todo consider refactoring so that we can replicate more + # complex types like Hash{String => Integer} and has both key + # types and subtypes. + # + # @param name [String] rooted name of the referenced type # @param generic_values [Array] + # @param [Hash{Symbol => Object}] splat def initialize generic_values: [], **splat super(**splat) @generic_values = generic_values @@ -30,8 +47,10 @@ def type ) end + # @sg-ignore Need to add nil check here # @return [Array] def reference_gates + # @sg-ignore Need to add nil check here closure.gates end end diff --git a/lib/solargraph/pin/reference/override.rb b/lib/solargraph/pin/reference/override.rb index 878c309db..76711f5dd 100644 --- a/lib/solargraph/pin/reference/override.rb +++ b/lib/solargraph/pin/reference/override.rb @@ -7,7 +7,7 @@ class Override < Reference # @return [::Array] attr_reader :tags - # @return [::Array] + # @return [::Array<::Symbol>] attr_reader :delete def closure diff --git a/lib/solargraph/pin/reference/superclass.rb b/lib/solargraph/pin/reference/superclass.rb index c50f640df..c13522648 100644 --- a/lib/solargraph/pin/reference/superclass.rb +++ b/lib/solargraph/pin/reference/superclass.rb @@ -6,7 +6,9 @@ class Reference # A Superclass reference pin. # class Superclass < Reference + # @sg-ignore Need to add nil check here def reference_gates + # @sg-ignore Need to add nil check here @reference_gates ||= closure.gates - [closure.path] end end diff --git a/lib/solargraph/pin/search.rb b/lib/solargraph/pin/search.rb index 0f9883b65..54e4d5bc5 100644 --- a/lib/solargraph/pin/search.rb +++ b/lib/solargraph/pin/search.rb @@ -46,14 +46,15 @@ def do_query # @param b [self] # @sg-ignore https://github.com/castwide/solargraph/pull/1050 .sort { |a, b| b.match <=> a.match } - .map(&:pin) + .map(&:pin) end # @param str1 [String] # @param str2 [String] + # # @return [Float] def fuzzy_string_match str1, str2 - return 1.0 + (str2.length.to_f / str1.length.to_f) if str1.downcase.include?(str2.downcase) + return 1.0 + (str2.length.to_f / str1.length) if str1.downcase.include?(str2.downcase) JaroWinkler.similarity(str1, str2, ignore_case: true) end end diff --git a/lib/solargraph/pin/signature.rb b/lib/solargraph/pin/signature.rb index 4c25e028b..221682ccd 100644 --- a/lib/solargraph/pin/signature.rb +++ b/lib/solargraph/pin/signature.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Solargraph module Pin class Signature < Callable @@ -5,11 +7,8 @@ class Signature < Callable # to the method pin attr_writer :closure - def initialize **splat - super(**splat) - end - def generics + # @type [Array<::String, nil>] @generics ||= [].freeze end @@ -17,8 +16,7 @@ def identity @identity ||= "signature#{object_id}" end - attr_writer :closure - + # @ sg-ignore need boolish support for ? methods def dodgy_return_type_source? super || closure&.dodgy_return_type_source? end @@ -32,8 +30,11 @@ def location end def typify api_map + # @sg-ignore Need to add nil check here if return_type.defined? + # @sg-ignore Need to add nil check here qualified = return_type.qualify(api_map, closure.namespace) + # @sg-ignore Need to add nil check here logger.debug { "Signature#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } return qualified end @@ -44,13 +45,15 @@ def typify api_map method_stack = closure.rest_of_stack api_map logger.debug { "Signature#typify(self=#{self}) - method_stack: #{method_stack}" } method_stack.each do |pin| - sig = pin.signatures.find { |s| s.arity == self.arity } + sig = pin.signatures.find { |s| s.arity == arity } next unless sig - unless sig.return_type.undefined? - qualified = sig.return_type.qualify(api_map, closure.namespace) - logger.debug { "Signature#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } - return qualified - end + # @sg-ignore Need to add nil check here + next if sig.return_type.undefined? + # @sg-ignore Need to add nil check here + qualified = sig.return_type.qualify(api_map, closure.namespace) + # @sg-ignore Need to add nil check here + logger.debug { "Signature#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } + return qualified end out = super logger.debug { "Signature#typify(self=#{self}) => #{out}" } diff --git a/lib/solargraph/pin/symbol.rb b/lib/solargraph/pin/symbol.rb index 294363f5f..f28fb2a71 100644 --- a/lib/solargraph/pin/symbol.rb +++ b/lib/solargraph/pin/symbol.rb @@ -3,8 +3,9 @@ module Solargraph module Pin class Symbol < Base - # @param location [Solargraph::Location] + # @param location [Solargraph::Location, nil] # @param name [String] + # @param [Hash{Symbol => Object}] kwargs def initialize(location, name, **kwargs) # @sg-ignore "Unrecognized keyword argument kwargs to Solargraph::Pin::Base#initialize" super(location: location, name: name, **kwargs) diff --git a/lib/solargraph/pin/until.rb b/lib/solargraph/pin/until.rb index 67823532b..f6da568c6 100644 --- a/lib/solargraph/pin/until.rb +++ b/lib/solargraph/pin/until.rb @@ -2,13 +2,11 @@ module Solargraph module Pin - class Until < Base + class Until < CompoundStatement include Breakable - # @param receiver [Parser::AST::Node, nil] # @param node [Parser::AST::Node, nil] - # @param context [ComplexType, nil] - # @param args [::Array] + # @param [Hash{Symbol => Object}] splat def initialize node: nil, **splat super(**splat) @node = node diff --git a/lib/solargraph/pin/while.rb b/lib/solargraph/pin/while.rb index e380aadd9..f10616891 100644 --- a/lib/solargraph/pin/while.rb +++ b/lib/solargraph/pin/while.rb @@ -2,13 +2,11 @@ module Solargraph module Pin - class While < Base + class While < CompoundStatement include Breakable - # @param receiver [Parser::AST::Node, nil] # @param node [Parser::AST::Node, nil] - # @param context [ComplexType, nil] - # @param args [::Array] + # @param [Hash{Symbol => Object}] splat def initialize node: nil, **splat super(**splat) @node = node diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index b3c162a15..886d55838 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -1,12 +1,437 @@ -require 'yard-activesupport-concern' +# frozen_string_literal: true + require 'fileutils' require 'rbs' +require 'rubygems' module Solargraph - module PinCache + class PinCache + include Logging + + attr_reader :directory, :rbs_collection_path, :rbs_collection_config_path, :yard_plugins + + # @param rbs_collection_path [String, nil] + # @param rbs_collection_config_path [String, nil] + # @param directory [String, nil] + # @param yard_plugins [Array] + def initialize rbs_collection_path:, rbs_collection_config_path:, + directory:, + yard_plugins: + @rbs_collection_path = rbs_collection_path + @rbs_collection_config_path = rbs_collection_config_path + @directory = directory + @yard_plugins = yard_plugins + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + def cached? gemspec + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + combined_gem?(gemspec, rbs_version_cache_key) + end + + # @param gemspec [Gem::Specification] + # @param rebuild [Boolean] whether to rebuild the cache regardless of whether it already exists + # @param out [StringIO, IO, nil] output stream for logging + # @return [void] + def cache_gem gemspec:, rebuild: false, out: nil + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + + build_yard, build_rbs_collection, build_combined = + calculate_build_needs(gemspec, + rebuild: rebuild, + rbs_version_cache_key: rbs_version_cache_key) + + return unless build_yard || build_rbs_collection || build_combined + + build_combine_and_cache(gemspec, + rbs_version_cache_key, + build_yard: build_yard, + build_rbs_collection: build_rbs_collection, + build_combined: build_combined, + out: out) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String, nil] + def suppress_yard_cache? gemspec, rbs_version_cache_key + if gemspec.name == 'parser' && rbs_version_cache_key != RbsMap::CACHE_KEY_UNRESOLVED + # parser takes forever to build YARD pins, but has excellent RBS collection pins + return true + end + false + end + + # @param out [StringIO, IO, nil] output stream for logging + # @param rebuild [Boolean] build pins regardless of whether we + # have cached them already + # + # @return [void] + def cache_all_stdlibs rebuild: false, out: $stderr + possible_stdlibs.each do |stdlib| + RbsMap::StdlibMap.new(stdlib, rebuild: rebuild, out: out) + end + end + + # @param path [String] require path that might be in the RBS stdlib collection + # @return [void] + def cache_stdlib_rbs_map path + # these are held in memory in RbsMap::StdlibMap + map = RbsMap::StdlibMap.load(path) + if map.resolved? + logger.debug { "Loading stdlib pins for #{path}" } + pins = map.pins + logger.debug { "Loaded #{pins.length} stdlib pins for #{path}" } + pins + else + # @todo Temporarily ignoring unresolved `require 'set'` + logger.debug { "Require path #{path} could not be resolved in RBS" } unless path == 'set' + nil + end + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # + # @return [String] + def lookup_rbs_version_cache_key gemspec + rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) + rbs_map.cache_key + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String, nil] + # @param yard_pins [Array] + # @param rbs_collection_pins [Array] + # @return [void] + def cache_combined_pins gemspec, rbs_version_cache_key, yard_pins, rbs_collection_pins + combined_pins = GemPins.combine(yard_pins, rbs_collection_pins) + serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @return [Array, nil] + def deserialize_combined_pin_cache gemspec + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + + load_combined_gem(gemspec, rbs_version_cache_key) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [StringIO, IO, nil] + # @return [void] + def uncache_gem gemspec, out: nil + PinCache.uncache(yardoc_path(gemspec), out: out) + PinCache.uncache(yard_gem_path(gemspec), out: out) + uncache_by_prefix(rbs_collection_pins_path_prefix(gemspec), out: out) + uncache_by_prefix(combined_path_prefix(gemspec), out: out) + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + combined_pins_in_memory.delete([gemspec.name, gemspec.version, rbs_version_cache_key]) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + def yardoc_processing? gemspec + Yardoc.processing?(yardoc_path(gemspec)) + end + + # @return [Array] a list of possible standard library names + def possible_stdlibs + # all dirs and .rb files in Gem::RUBYGEMS_DIR + Dir.glob(File.join(Gem::RUBYGEMS_DIR, '*')).map do |file_or_dir| + basename = File.basename(file_or_dir) + # remove .rb + # @sg-ignore flow sensitive typing should be able to handle redefinition + basename = basename[0..-4] if basename.end_with?('.rb') + basename + end.sort.uniq + rescue StandardError => e + logger.info { "Failed to get possible stdlibs: #{e.message}" } + # @sg-ignore Need to add nil check here + logger.debug { e.backtrace.join("\n") } + [] + end + + private + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rebuild [Boolean] whether to rebuild the cache regardless of whether it already exists + # @param rbs_version_cache_key [String, nil] the cache key for the gem in the RBS collection + # + # @return [Array(Boolean, Boolean, Boolean)] whether to build YARD + # pins, RBS collection pins, and combined pins + def calculate_build_needs gemspec, rebuild:, rbs_version_cache_key: + if rebuild + build_yard = true + build_rbs_collection = true + build_combined = true + else + build_yard = !yard_gem?(gemspec) + build_rbs_collection = !rbs_collection_pins?(gemspec, rbs_version_cache_key) + # @sg-ignore Need to add nil check here + build_combined = !combined_gem?(gemspec, rbs_version_cache_key) || build_yard || build_rbs_collection + end + + build_yard = false if suppress_yard_cache?(gemspec, rbs_version_cache_key) + + [build_yard, build_rbs_collection, build_combined] + end + + # @param gemspec [Gem::Specification] + # @param rbs_version_cache_key [String, nil] + # @param build_yard [Boolean] + # @param build_rbs_collection [Boolean] + # @param build_combined [Boolean] + # @param out [StringIO, IO, nil] + # + # @return [void] + def build_combine_and_cache gemspec, + rbs_version_cache_key, + build_yard:, + build_rbs_collection:, + build_combined:, + out: + log_cache_info(gemspec, rbs_version_cache_key, + build_yard: build_yard, + build_rbs_collection: build_rbs_collection, + build_combined: build_combined, + out: out) + cache_yard_pins(gemspec, out) if build_yard + # this can be nil even if we aren't told to build it - see suppress_yard_cache? + yard_pins = deserialize_yard_pin_cache(gemspec) || [] + cache_rbs_collection_pins(gemspec, out) if build_rbs_collection + rbs_collection_pins = deserialize_rbs_collection_cache(gemspec, rbs_version_cache_key) || [] + cache_combined_pins(gemspec, rbs_version_cache_key, yard_pins, rbs_collection_pins) if build_combined + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String, nil] + # @param build_yard [Boolean] + # @param build_rbs_collection [Boolean] + # @param build_combined [Boolean] + # @param out [StringIO, IO, nil] + # + # @return [void] + def log_cache_info gemspec, + rbs_version_cache_key, + build_yard:, + build_rbs_collection:, + build_combined:, + out: + type = [] + type << 'YARD' if build_yard + rbs_source_desc = RbsMap.rbs_source_desc(rbs_version_cache_key) + type << rbs_source_desc if build_rbs_collection && !rbs_source_desc.nil? + # we'll build it anyway, but it won't take long to build with + # only a single source + + # 'combining' is awkward terminology in this case + just_yard = build_yard && rbs_source_desc.nil? + + type << 'combined' if build_combined && !just_yard + out&.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}") + end + + # @param gemspec [Gem::Specification] + # @param out [StringIO, IO, nil] + # @return [Array] + def cache_yard_pins gemspec, out + gem_yardoc_path = yardoc_path(gemspec) + Yardoc.build_docs(gem_yardoc_path, yard_plugins, gemspec) unless Yardoc.docs_built?(gem_yardoc_path) + pins = Yardoc.build_pins(gem_yardoc_path, gemspec, out: out) + serialize_yard_gem(gemspec, pins) + logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty? + pins + end + + # @return [Hash{Array(String, String, String) => Array}] + def combined_pins_in_memory + PinCache.all_combined_pins_in_memory[yard_plugins] ||= {} + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param _out [StringIO, IO, nil] + # @return [Array] + def cache_rbs_collection_pins gemspec, _out + rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) + pins = rbs_map.pins + rbs_version_cache_key = rbs_map.cache_key + # cache pins even if result is zero, so we don't retry building pins + pins ||= [] + serialize_rbs_collection_pins(gemspec, rbs_version_cache_key, pins) + logger.info do + unless pins.empty? + "Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with " \ + "cache_key #{rbs_version_cache_key.inspect}" + end + end + pins + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @return [Array, nil] + def deserialize_yard_pin_cache gemspec + cached = load_yard_gem(gemspec) + if cached + cached + else + logger.debug "No YARD pin cache for #{gemspec.name}:#{gemspec.version}" + nil + end + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String, nil] + # @return [Array, nil] + def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key + cached = load_rbs_collection_pins(gemspec, rbs_version_cache_key) + Solargraph.assert_or_log(:pin_cache_rbs_collection, 'Asked for non-existent rbs collection') if cached.nil? + logger.info do + "Loaded #{cached&.length} pins from RBS collection cache for #{gemspec.name}:#{gemspec.version}" + end + cached + end + + # @return [Array] + def yard_path_components + ["yard-#{YARD::VERSION}", + yard_plugins.sort.uniq.join('-')] + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @return [String] + def yardoc_path gemspec + File.join(PinCache.base_dir, + *yard_path_components, + "#{gemspec.name}-#{gemspec.version}.yardoc") + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @return [String] + def yard_gem_path gemspec + File.join(PinCache.work_dir, *yard_path_components, "#{gemspec.name}-#{gemspec.version}.ser") + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @return [Array, nil] + def load_yard_gem gemspec + PinCache.load(yard_gem_path(gemspec)) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param pins [Array] + # @return [void] + def serialize_yard_gem gemspec, pins + PinCache.save(yard_gem_path(gemspec), pins) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @return [Boolean] + def yard_gem? gemspec + exist?(yard_gem_path(gemspec)) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String, nil] + # @return [String] + def rbs_collection_pins_path gemspec, hash + rbs_collection_pins_path_prefix(gemspec) + "#{hash || 0}.ser" + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @return [String] + def rbs_collection_pins_path_prefix gemspec + File.join(PinCache.work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-") + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String, nil] + # + # @return [Array, nil] + def load_rbs_collection_pins gemspec, hash + PinCache.load(rbs_collection_pins_path(gemspec, hash)) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String, nil] + # @param pins [Array] + # @return [void] + def serialize_rbs_collection_pins gemspec, hash, pins + PinCache.save(rbs_collection_pins_path(gemspec, hash), pins) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String, nil] + # @return [String] + def combined_path gemspec, hash + File.join(combined_path_prefix(gemspec) + "-#{hash || 0}.ser") + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @return [String] + def combined_path_prefix gemspec + File.join(PinCache.work_dir, 'combined', yard_plugins.sort.join('-'), "#{gemspec.name}-#{gemspec.version}") + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String, nil] + # @param pins [Array] + # @return [void] + def serialize_combined_gem gemspec, hash, pins + PinCache.save(combined_path(gemspec, hash), pins) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String] + def combined_gem? gemspec, hash + exist?(combined_path(gemspec, hash)) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String, nil] + # @return [Array, nil] + def load_combined_gem gemspec, hash + cached = combined_pins_in_memory[[gemspec.name, gemspec.version, hash]] + return cached if cached + loaded = PinCache.load(combined_path(gemspec, hash)) + combined_pins_in_memory[[gemspec.name, gemspec.version, hash]] = loaded if loaded + loaded + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String, nil] + def rbs_collection_pins? gemspec, hash + exist?(rbs_collection_pins_path(gemspec, hash)) + end + + include Logging + + # @param path [String] + def exist? *path + File.file? File.join(*path) + end + + # @return [void] + # @param path_segments [Array] + # @param out [StringIO, IO, nil] + def uncache_by_prefix *path_segments, out: nil + path = File.join(*path_segments) + glob = "#{path}*" + out&.puts "Clearing pin cache in #{glob}" + Dir.glob(glob).each do |file| + next unless File.file?(file) + FileUtils.rm_rf file, secure: true + out&.puts "Clearing pin cache in #{file}" + end + end + class << self include Logging + # @return [Hash{Array => Hash{Array(String, String) => + # Array}}] yard plugins, then gemspec name and + # version + def all_combined_pins_in_memory + @all_combined_pins_in_memory ||= {} + end + # The base directory where cached YARD documentation and serialized pins are serialized # # @return [String] @@ -18,6 +443,47 @@ def base_dir File.join(Dir.home, '.cache', 'solargraph') end + # @param path_segments [Array] + # @param out [IO, nil] + # @return [void] + def uncache *path_segments, out: nil + path = File.join(*path_segments) + if File.exist?(path) + FileUtils.rm_rf path, secure: true + out&.puts "Clearing pin cache in #{path}" + else + out&.puts "Pin cache file #{path} does not exist" + end + end + + # @return [void] + # @param out [IO, nil] + # @param path_segments [Array] + def uncache_by_prefix *path_segments, out: nil + path = File.join(*path_segments) + glob = "#{path}*" + out&.puts "Clearing pin cache in #{glob}" + Dir.glob(glob).each do |file| + next unless File.file?(file) + FileUtils.rm_rf file, secure: true + out&.puts "Clearing pin cache in #{file}" + end + end + + # @param out [StringIO, IO, nil] + # @return [void] + def uncache_core out: nil + uncache(core_path, out: out) + # ApiMap keep this in memory + ApiMap.reset_core(out: out) + end + + # @param out [StringIO, IO, nil] + # @return [void] + def uncache_stdlib out: nil + uncache(stdlib_path, out: out) + end + # The working directory for the current Ruby, RBS, and Solargraph versions. # # @return [String] @@ -27,15 +493,6 @@ def work_dir File.join(base_dir, "ruby-#{RUBY_VERSION}", "rbs-#{RBS::VERSION}", "solargraph-#{Solargraph::VERSION}") end - # @param gemspec [Gem::Specification] - # @return [String] - def yardoc_path gemspec - File.join(base_dir, - "yard-#{YARD::VERSION}", - "yard-activesupport-concern-#{YARD::ActiveSupport::Concern::VERSION}", - "#{gemspec.name}-#{gemspec.version}.yardoc") - end - # @return [String] def stdlib_path File.join(work_dir, 'stdlib') @@ -84,40 +541,34 @@ def yard_gem_path gemspec # @param gemspec [Gem::Specification] # @return [Array, nil] - def deserialize_yard_gem(gemspec) + def deserialize_yard_gem gemspec load(yard_gem_path(gemspec)) end # @param gemspec [Gem::Specification] # @param pins [Array] # @return [void] - def serialize_yard_gem(gemspec, pins) + def serialize_yard_gem gemspec, pins save(yard_gem_path(gemspec), pins) end - # @param gemspec [Gem::Specification] - # @return [Boolean] - def has_yard?(gemspec) - exist?(yard_gem_path(gemspec)) - end - # @param gemspec [Gem::Specification] # @param hash [String, nil] # @return [String] - def rbs_collection_path(gemspec, hash) + def rbs_collection_path gemspec, hash File.join(work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-#{hash || 0}.ser") end # @param gemspec [Gem::Specification] # @return [String] - def rbs_collection_path_prefix(gemspec) + def rbs_collection_path_prefix gemspec File.join(work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-") end # @param gemspec [Gem::Specification] # @param hash [String, nil] # @return [Array, nil] - def deserialize_rbs_collection_gem(gemspec, hash) + def deserialize_rbs_collection_gem gemspec, hash load(rbs_collection_path(gemspec, hash)) end @@ -125,20 +576,20 @@ def deserialize_rbs_collection_gem(gemspec, hash) # @param hash [String, nil] # @param pins [Array]n # @return [void] - def serialize_rbs_collection_gem(gemspec, hash, pins) + def serialize_rbs_collection_gem gemspec, hash, pins save(rbs_collection_path(gemspec, hash), pins) end # @param gemspec [Gem::Specification] # @param hash [String, nil] # @return [String] - def combined_path(gemspec, hash) + def combined_path gemspec, hash File.join(work_dir, 'combined', "#{gemspec.name}-#{gemspec.version}-#{hash || 0}.ser") end # @param gemspec [Gem::Specification] # @return [String] - def combined_path_prefix(gemspec) + def combined_path_prefix gemspec File.join(work_dir, 'combined', "#{gemspec.name}-#{gemspec.version}-") end @@ -146,7 +597,7 @@ def combined_path_prefix(gemspec) # @param hash [String, nil] # @param pins [Array] # @return [void] - def serialize_combined_gem(gemspec, hash, pins) + def serialize_combined_gem gemspec, hash, pins save(combined_path(gemspec, hash), pins) end @@ -160,38 +611,17 @@ def deserialize_combined_gem gemspec, hash # @param gemspec [Gem::Specification] # @param hash [String, nil] # @return [Boolean] - def has_rbs_collection?(gemspec, hash) + def has_rbs_collection? gemspec, hash exist?(rbs_collection_path(gemspec, hash)) end - # @return [void] - def uncache_core - uncache(core_path) - end - - # @return [void] - def uncache_stdlib - uncache(stdlib_path) - end - - # @param gemspec [Gem::Specification] - # @param out [IO, nil] - # @return [void] - def uncache_gem(gemspec, out: nil) - uncache(yardoc_path(gemspec), out: out) - uncache_by_prefix(rbs_collection_path_prefix(gemspec), out: out) - uncache(yard_gem_path(gemspec), out: out) - uncache_by_prefix(combined_path_prefix(gemspec), out: out) - end - # @return [void] def clear FileUtils.rm_rf base_dir, secure: true end - private - # @param file [String] + # @sg-ignore Marshal.load evaluates to boolean here which is wrong # @return [Array, nil] def load file return nil unless File.file?(file) @@ -202,11 +632,6 @@ def load file nil end - # @param path [String] - def exist? *path - File.file? File.join(*path) - end - # @param file [String] # @param pins [Array] # @return [void] @@ -218,27 +643,19 @@ def save file, pins logger.debug { "Cache#save: Saved #{pins.length} pins to #{file}" } end - # @param path_segments [Array] - # @return [void] - def uncache *path_segments, out: nil - path = File.join(*path_segments) - if File.exist?(path) - FileUtils.rm_rf path, secure: true - out.puts "Clearing pin cache in #{path}" unless out.nil? - end + def core? + File.file?(core_path) end - # @return [void] - # @param path_segments [Array] - def uncache_by_prefix *path_segments, out: nil - path = File.join(*path_segments) - glob = "#{path}*" - out.puts "Clearing pin cache in #{glob}" unless out.nil? - Dir.glob(glob).each do |file| - next unless File.file?(file) - FileUtils.rm_rf file, secure: true - out.puts "Clearing pin cache in #{file}" unless out.nil? - end + # @param out [StringIO, IO, nil] + # @return [Array] + def cache_core out: $stderr + RbsMap::CoreMap.new.cache_core(out: out) + end + + # @param path [String] + def exist? *path + File.file? File.join(*path) end end end diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index a0d7bbc2e..cc20f9607 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -21,13 +21,8 @@ def initialize line, character @character = character end - # @sg-ignore Fix "Not enough arguments to Module#protected" - protected def equality_fields - [line, character] - end - # @param other [Position] - def <=>(other) + def <=> other return nil unless other.is_a?(Position) if line == other.line character <=> other.character @@ -63,15 +58,16 @@ def self.to_offset text, position line = -1 last_line_index = 0 + # @sg-ignore flow sensitive typing should be able to handle redefinition while (newline_index = text.index("\n", newline_index + 1)) && line <= position.line line += 1 break if line == position.line - line_length = newline_index - last_line_index last_line_index = newline_index end - last_line_index += 1 if position.line > 0 + last_line_index += 1 if position.line.positive? + # @sg-ignore flow sensitive typing should be able to handle redefinition last_line_index + position.character end @@ -101,12 +97,15 @@ def self.from_offset text, offset character = offset newline_index = -1 + # @sg-ignore flow sensitive typing should be able to handle redefinition while (newline_index = text.index("\n", newline_index + 1)) && newline_index < offset line += 1 + # @sg-ignore flow sensitive typing should be able to handle redefinition character = offset - newline_index - 1 end - character = 0 if character.nil? and (cursor - offset).between?(0, 1) + character = 0 if character.nil? && (cursor - offset).between?(0, 1) raise InvalidOffsetError if character.nil? + # @sg-ignore flow sensitive typing needs to handle 'raise if' Position.new(line, character) end @@ -125,8 +124,13 @@ 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 + + protected + + def equality_fields + [line, character] + end end end diff --git a/lib/solargraph/range.rb b/lib/solargraph/range.rb index 86452d646..e1ed89592 100644 --- a/lib/solargraph/range.rb +++ b/lib/solargraph/range.rb @@ -19,20 +19,12 @@ def initialize start, ending @ending = ending end - # @sg-ignore Fix "Not enough arguments to Module#protected" - protected def equality_fields - [start, ending] - end - # @param other [BasicObject] - def <=>(other) + 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 @@ -40,7 +32,7 @@ def <=>(other) # Get a hash of the range. This representation is suitable for use in # the language server protocol. # - # @return [Hash] + # @return [Hash{Symbol => Position}] def to_hash { start: start.to_hash, @@ -54,8 +46,11 @@ def to_hash # @return [Boolean] def contain? position position = Position.normalize(position) + # @sg-ignore flow sensitive typing should be able to handle redefinition return false if position.line < start.line || position.line > ending.line + # @sg-ignore flow sensitive typing should be able to handle redefinition return false if position.line == start.line && position.character < start.character + # @sg-ignore flow sensitive typing should be able to handle redefinition return false if position.line == ending.line && position.character > ending.character true end @@ -63,9 +58,11 @@ def contain? position # True if the range contains the specified position and the position does not precede it. # # @param position [Position, Array(Integer, Integer)] + # @sg-ignore flow sensitive typing should be able to handle redefinition # @return [Boolean] def include? position position = Position.normalize(position) + # @sg-ignore flow sensitive typing should be able to handle redefinition contain?(position) && !(position.line == start.line && position.character == start.character) end @@ -82,12 +79,11 @@ def self.from_to l1, c1, l2, c2 # Get a range from a node. # - # @param node [Parser::AST::Node] + # @param node [::Parser::AST::Node] # @return [Range, nil] def self.from_node node - if node&.loc && node.loc.expression - from_expr(node.loc.expression) - end + return unless node&.loc&.expression + from_expr(node.loc.expression) end # Get a range from a Parser range, usually found in @@ -101,12 +97,17 @@ 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 def inspect "#<#{self.class} #{start.inspect} to #{ending.inspect}>" end + + protected + + def equality_fields + [start, ending] + end end end diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index 803e3677a..66aff1288 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -16,17 +16,14 @@ class RbsMap # @type [Hash{String => RbsMap}] @@rbs_maps_hash = {} - attr_reader :library - - attr_reader :rbs_collection_paths - - attr_reader :rbs_collection_config_path + attr_reader :library, :rbs_collection_paths, :rbs_collection_config_path # @param library [String] # @param version [String, nil] # @param rbs_collection_config_path [String, Pathname, nil] # @param rbs_collection_paths [Array] - def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: [] + # @param out [StringIO, IO, nil] where to log messages + def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: [], out: $stderr if rbs_collection_config_path.nil? && !rbs_collection_paths.empty? raise 'Please provide rbs_collection_config_path if you provide rbs_collection_paths' end @@ -37,6 +34,28 @@ def initialize library, version = nil, rbs_collection_config_path: nil, rbs_coll add_library loader, library, version end + CACHE_KEY_GEM_EXPORT = 'gem-export' + CACHE_KEY_UNRESOLVED = 'unresolved' + CACHE_KEY_STDLIB = 'stdlib' + CACHE_KEY_LOCAL = 'local' + + # @param cache_key [String, nil] + # @return [String, nil] a description of the source of the RBS info + def self.rbs_source_desc cache_key + case cache_key + when CACHE_KEY_GEM_EXPORT + 'RBS gem export' + when CACHE_KEY_UNRESOLVED + nil + when CACHE_KEY_STDLIB + 'RBS standard library' + when CACHE_KEY_LOCAL + 'local RBS shims' + else + 'RBS collection' + end + end + # @return [RBS::EnvironmentLoader] def loader @loader ||= RBS::EnvironmentLoader.new(core_root: nil, repository: repository) @@ -47,10 +66,15 @@ def loader # updated upstream for the same library and version. May change # if the config for where information comes form changes. def cache_key - @hextdigest ||= begin + return CACHE_KEY_UNRESOLVED unless resolved? + + @cache_key ||= begin # @type [String, nil] data = nil + # @type gem_config [nil, Hash{String => Hash{String => String}}] + gem_config = nil if rbs_collection_config_path + # @sg-ignore flow sensitive typing needs to handle attrs lockfile_path = RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path)) if lockfile_path.exist? collection_config = RBS::Collection::Config.from_path lockfile_path @@ -58,16 +82,22 @@ def cache_key data = gem_config&.to_s end end - if data.nil? || data.empty? - if resolved? - # definitely came from the gem itself and not elsewhere - - # only one version per gem - 'gem-export' + if gem_config.nil? + CACHE_KEY_STDLIB + else + # @type [String] + source = gem_config.dig('source', 'type') + case source + when 'rubygems' + CACHE_KEY_GEM_EXPORT + when 'local' + CACHE_KEY_LOCAL + when 'stdlib' + CACHE_KEY_STDLIB else - 'unresolved' + # @sg-ignore Need to add nil check here + Digest::SHA1.hexdigest(data) end - else - Digest::SHA1.hexdigest(data) end end end @@ -77,6 +107,10 @@ def cache_key # @param rbs_collection_config_path [String, Pathname, nil] # @return [RbsMap] def self.from_gemspec gemspec, rbs_collection_path, rbs_collection_config_path + # prefers stdlib RBS if available + rbs_map = RbsMap::StdlibMap.new(gemspec.name) + return rbs_map if rbs_map.resolved? + rbs_map = RbsMap.new(gemspec.name, gemspec.version, rbs_collection_paths: [rbs_collection_path].compact, rbs_collection_config_path: rbs_collection_config_path) @@ -88,18 +122,26 @@ def self.from_gemspec gemspec, rbs_collection_path, rbs_collection_config_path rbs_collection_config_path: rbs_collection_config_path) end + # @param out [IO, nil] where to log messages # @return [Array] - def pins - @pins ||= resolved? ? conversions.pins : [] + def pins out: $stderr + @pins ||= if resolved? + conversions.pins + else + [] + end end # @generic T # @param path [String] # @param klass [Class>] + # + # @sg-ignore Need to be able to resolve generics based on a + # Class> param # @return [generic, nil] def path_pin path, klass = Pin::Base pin = pins.find { |p| p.path == path } - pin if pin&.is_a?(klass) + pin if pin.is_a?(klass) end # @param path [String] @@ -130,29 +172,32 @@ def self.load library private - # @return [RBS::EnvironmentLoader] - def loader - @loader ||= RBS::EnvironmentLoader.new(core_root: nil, repository: repository) - end - # @return [Conversions] def conversions @conversions ||= Conversions.new(loader: loader) end + def resolve_dependencies? + # we need to resolve dependencies via gemfile.lock manually for + # YARD regardless, so use same mechanism here so we don't + # duplicate work generating pins from dependencies + false + end + # @param loader [RBS::EnvironmentLoader] # @param library [String] - # @param version [String, nil] + # @param version [String, nil] the version of the library to load, or nil for any + # @param out [StringIO, IO, nil] where to log messages # @return [Boolean] true if adding the library succeeded - def add_library loader, library, version + def add_library loader, library, version, out: $stderr @resolved = if loader.has_library?(library: library, version: version) - loader.add library: library, version: version - logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" } - true - else - logger.info { "#{short_name} did not find data for library #{library}:#{version}" } - false - end + loader.add library: library, version: version, resolve_dependencies: resolve_dependencies? + logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" } + true + else + logger.info { "#{short_name} did not find data for library #{library}:#{version}" } + false + end end # @return [String] diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 54bca0f73..b6295040d 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -23,7 +23,7 @@ def initialize visibility = :public end # @param loader [RBS::EnvironmentLoader] - def initialize(loader:) + def initialize loader: @loader = loader @pins = [] load_environment_to_pins(loader) @@ -43,12 +43,14 @@ def type_aliases end # @param loader [RBS::EnvironmentLoader] + # # @return [void] - def load_environment_to_pins(loader) + def load_environment_to_pins loader environment = RBS::Environment.from_loader(loader).resolve_type_names - cursor = pins.length if environment.declarations.empty? - Solargraph.logger.info "No RBS declarations found in environment for core_root #{loader.core_root.inspect}, libraries #{loader.libs} and directories #{loader.dirs}" + Solargraph.logger.info 'No RBS declarations found in environment for core_root ' \ + "#{loader.core_root.inspect}, libraries #{loader.libs} and " \ + "directories #{loader.dirs}" return end environment.declarations.each { |decl| convert_decl_to_pin(decl, Solargraph::Pin::ROOT_PIN) } @@ -60,22 +62,57 @@ def load_environment_to_pins(loader) def convert_decl_to_pin decl, closure case decl when RBS::AST::Declarations::Class + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.name.absolute? + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on class #{decl.inspect}") + end class_decl_to_pin decl when RBS::AST::Declarations::Interface + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.name.absolute? + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on interface #{decl.inspect}") + end # STDERR.puts "Skipping interface #{decl.name.relative!}" - interface_decl_to_pin decl, closure + interface_decl_to_pin decl when RBS::AST::Declarations::TypeAlias - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.name.absolute? + Solargraph.assert_or_log(:rbs_closure, + # @sg-ignore flow sensitive typing should support case/when + "Ignoring closure #{closure.inspect} on alias type name #{decl.name}") + end + # @sg-ignore flow sensitive typing should support case/when type_aliases[decl.name.to_s] = decl when RBS::AST::Declarations::Module + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.name.absolute? + Solargraph.assert_or_log(:rbs_closure, + # @sg-ignore flow sensitive typing should support case/when + "Ignoring closure #{closure.inspect} on alias type name #{decl.name}") + end module_decl_to_pin decl when RBS::AST::Declarations::Constant + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.name.absolute? + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on constant #{decl.inspect}") + end constant_decl_to_pin decl when RBS::AST::Declarations::ClassAlias + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.new_name.absolute? + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on class alias #{decl.inspect}") + end class_alias_decl_to_pin decl when RBS::AST::Declarations::ModuleAlias + unless closure.name == '' + Solargraph.assert_or_log(:rbs_closure, + "Ignoring closure #{closure.inspect} on module alias #{decl.inspect}") + end module_alias_decl_to_pin decl when RBS::AST::Declarations::Global + unless closure.name == '' + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on global decl #{decl.inspect}") + end global_decl_to_pin decl else Solargraph.logger.warn "Skipping declaration #{decl.class}" @@ -86,7 +123,61 @@ def convert_decl_to_pin decl, closure # @param module_pin [Pin::Namespace] # @return [void] def convert_self_types_to_pins decl, module_pin - decl.self_types.each { |self_type| context = convert_self_type_to_pins(self_type, module_pin) } + decl.self_types.each { |self_type| convert_self_type_to_pins(self_type, module_pin) } + end + + # @type [Hash{String => String}] + RBS_TO_CLASS = { + 'bool' => 'Boolean', + 'string' => 'String', + 'int' => 'Integer' + }.freeze + private_constant :RBS_TO_CLASS + + # rooted names (namespaces) use the prefix of :: when they are + # relative to the root namespace, or not if they are relative to + # the current namespace. + # + # @param type_name [RBS::TypeName] + # + # @return [String] + def rooted_name type_name + name = type_name.to_s + RBS_TO_CLASS.fetch(name, name) + end + + # fqns names are implicitly fully qualified - they are relative + # to the root namespace and are not prefixed with :: + # + # @param type_name [RBS::TypeName] + # + # @return [String] + def fqns type_name + unless type_name.absolute? + Solargraph.assert_or_log(:rbs_fqns, "Received unexpected unqualified type name: #{type_name}") + end + ns = type_name.relative!.to_s + RBS_TO_CLASS.fetch(ns, ns) + end + + # @param type_name [RBS::TypeName] + # @param type_args [Enumerable] + # @return [ComplexType::UniqueType] + def build_type type_name, type_args = [] + # we use .absolute? below to tell the type object what to + # expect + rbs_name = type_name.relative!.to_s + base = RBS_TO_CLASS.fetch(rbs_name, rbs_name) + + params = type_args.map { |a| other_type_to_type(a) } + # tuples have their own class and are handled in other_type_to_type + if base == 'Hash' && params.length == 2 + ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: type_name.absolute?, + parameters_type: :hash) + else + ComplexType::UniqueType.new(base, [], params.reject(&:undefined?), rooted: type_name.absolute?, + parameters_type: :list) + end end # @param decl [RBS::AST::Declarations::Module::Self] @@ -94,9 +185,9 @@ def convert_self_types_to_pins decl, module_pin # @return [void] def convert_self_type_to_pins decl, closure type = build_type(decl.name, decl.args) - generic_values = type.all_params.map(&:to_s) + generic_values = type.all_params.map(&:rooted_tags) include_pin = Solargraph::Pin::Reference::Include.new( - name: decl.name.relative!.to_s, + name: type.name, type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure, @@ -120,32 +211,44 @@ def convert_members_to_pins decl, closure def convert_member_to_pin member, closure, context case member when RBS::AST::Members::MethodDefinition + # @sg-ignore flow based typing needs to understand case when class pattern method_def_to_pin(member, closure, context) when RBS::AST::Members::AttrReader + # @sg-ignore flow based typing needs to understand case when class pattern attr_reader_to_pin(member, closure, context) when RBS::AST::Members::AttrWriter + # @sg-ignore flow based typing needs to understand case when class pattern attr_writer_to_pin(member, closure, context) when RBS::AST::Members::AttrAccessor + # @sg-ignore flow based typing needs to understand case when class pattern attr_accessor_to_pin(member, closure, context) when RBS::AST::Members::Include + # @sg-ignore flow based typing needs to understand case when class pattern include_to_pin(member, closure) when RBS::AST::Members::Prepend + # @sg-ignore flow based typing needs to understand case when class pattern prepend_to_pin(member, closure) when RBS::AST::Members::Extend + # @sg-ignore flow based typing needs to understand case when class pattern extend_to_pin(member, closure) when RBS::AST::Members::Alias + # @sg-ignore flow based typing needs to understand case when class pattern alias_to_pin(member, closure) when RBS::AST::Members::ClassInstanceVariable + # @sg-ignore flow based typing needs to understand case when class pattern civar_to_pin(member, closure) when RBS::AST::Members::ClassVariable + # @sg-ignore flow based typing needs to understand case when class pattern cvar_to_pin(member, closure) when RBS::AST::Members::InstanceVariable + # @sg-ignore flow based typing needs to understand case when class pattern ivar_to_pin(member, closure) when RBS::AST::Members::Public return Context.new(:public) when RBS::AST::Members::Private return Context.new(:private) when RBS::AST::Declarations::Base + # @sg-ignore flow based typing needs to understand case when class pattern convert_decl_to_pin(member, closure) else Solargraph.logger.warn "Skipping member type #{member.class}" @@ -153,18 +256,31 @@ def convert_member_to_pin member, closure, context context end + # Pull the name of type variables for a generic - not the + # values, the names (e.g., T, U, V). As such, "rooting" isn't a + # thing, these are all in the global namespace. + # + # @param decl [RBS::AST::Declarations::Class, RBS::AST::Declarations::Interface, + # RBS::AST::Declarations::Module, RBS::MethodType] + # + # @return [Array] + def type_parameter_names decl + decl.type_params.map(&:name).map(&:to_s) + end + # @param decl [RBS::AST::Declarations::Class] # @return [void] def class_decl_to_pin decl - generics = decl.type_params.map(&:name).map(&:to_s) + # @type [Hash{String => ComplexType, ComplexType::UniqueType}] generic_defaults = {} decl.type_params.each do |param| - if param.default_type - tag = other_type_to_tag param.default_type - generic_defaults[param.name.to_s] = ComplexType.parse(tag).force_rooted - end + generic_defaults[param.name.to_s] = other_type_to_type param.default_type if param.default_type end - class_name = decl.name.relative!.to_s + + class_name = fqns(decl.name) + + generics = type_parameter_names(decl) + class_pin = Solargraph::Pin::Namespace.new( type: :class, name: class_name, @@ -181,13 +297,12 @@ def class_decl_to_pin decl pins.push class_pin if decl.super_class type = build_type(decl.super_class.name, decl.super_class.args) - generic_values = type.all_params.map(&:to_s) - superclass_name = decl.super_class.name.to_s + generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Superclass.new( type_location: location_decl_to_pin_location(decl.super_class.location), closure: class_pin, generic_values: generic_values, - name: superclass_name, + name: type.rooted_name, # reference pins use rooted names source: :rbs ) end @@ -196,16 +311,15 @@ def class_decl_to_pin decl end # @param decl [RBS::AST::Declarations::Interface] - # @param closure [Pin::Closure] # @return [void] - def interface_decl_to_pin decl, closure + def interface_decl_to_pin decl class_pin = Solargraph::Pin::Namespace.new( type: :module, type_location: location_decl_to_pin_location(decl.location), - name: decl.name.relative!.to_s, + name: fqns(decl.name), closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, - generics: decl.type_params.map(&:name).map(&:to_s), + generics: type_parameter_names(decl), # HACK: Using :hidden to keep interfaces from appearing in # autocompletion visibility: :hidden, @@ -221,45 +335,51 @@ def interface_decl_to_pin decl, closure def module_decl_to_pin decl module_pin = Solargraph::Pin::Namespace.new( type: :module, - name: decl.name.relative!.to_s, + name: fqns(decl.name), type_location: location_decl_to_pin_location(decl.location), closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, - generics: decl.type_params.map(&:name).map(&:to_s), + generics: type_parameter_names(decl), source: :rbs ) pins.push module_pin convert_self_types_to_pins decl, module_pin convert_members_to_pins decl, module_pin + raise "Invalid type for module declaration: #{module_pin.class}" unless module_pin.is_a?(Pin::Namespace) + add_mixins decl, module_pin.closure end - # @param name [String] - # @param tag [String] - # @param comments [String] - # @param decl [RBS::AST::Declarations::ClassAlias, RBS::AST::Declarations::Constant, RBS::AST::Declarations::ModuleAlias] - # @param base [String, nil] Optional conversion of tag to base + # @param fqns [String] + # @param type [ComplexType, ComplexType::UniqueType] + # @param comments [String, nil] + # @param decl [RBS::AST::Declarations::ClassAlias, + # RBS::AST::Declarations::Constant, + # RBS::AST::Declarations::ModuleAlias] + # @param base [String, nil] Optional conversion of tag to + # base - valid values are Class and Module # # @return [Solargraph::Pin::Constant] - def create_constant(name, tag, comments, decl, base = nil) - parts = name.split('::') + def create_constant fqns, type, comments, decl, base = nil + parts = fqns.split('::') if parts.length > 1 - name = parts.last + fqns = parts.last + # @sg-ignore Need to add nil check here closure = pins.select { |pin| pin && pin.path == parts[0..-2].join('::') }.first else - name = parts.first + fqns = parts.first closure = Solargraph::Pin::ROOT_PIN end constant_pin = Solargraph::Pin::Constant.new( - name: name, + name: fqns, closure: closure, type_location: location_decl_to_pin_location(decl.location), comments: comments, source: :rbs ) - tag = "#{base}<#{tag}>" if base - rooted_tag = ComplexType.parse(tag).force_rooted.rooted_tags + rooted_tag = type.rooted_tags + rooted_tag = "#{base}<#{rooted_tag}>" if base constant_pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) constant_pin end @@ -268,27 +388,27 @@ def create_constant(name, tag, comments, decl, base = nil) # @return [void] def class_alias_decl_to_pin decl # See https://www.rubydoc.info/gems/rbs/3.4.3/RBS/AST/Declarations/ClassAlias - new_name = decl.new_name.relative!.to_s - old_name = decl.old_name.relative!.to_s - - pins.push create_constant(new_name, old_name, decl.comment&.string, decl, 'Class') + new_name = fqns(decl.new_name) + old_type = build_type(decl.old_name) + pins.push create_constant(new_name, old_type, decl.comment&.string, decl, '::Class') end # @param decl [RBS::AST::Declarations::ModuleAlias] # @return [void] def module_alias_decl_to_pin decl # See https://www.rubydoc.info/gems/rbs/3.4.3/RBS/AST/Declarations/ModuleAlias - new_name = decl.new_name.relative!.to_s - old_name = decl.old_name.relative!.to_s + new_name = fqns(decl.new_name) + old_type = build_type(decl.old_name) - pins.push create_constant(new_name, old_name, decl.comment&.string, decl, 'Module') + pins.push create_constant(new_name, old_type, decl.comment&.string, decl, '::Module') end # @param decl [RBS::AST::Declarations::Constant] # @return [void] def constant_decl_to_pin decl - tag = other_type_to_tag(decl.type) - pins.push create_constant(decl.name.relative!.to_s, tag, decl.comment&.string, decl) + target_type = other_type_to_type(decl.type) + constant_name = fqns(decl.name) + pins.push create_constant(constant_name, target_type, decl.comment&.string, decl) end # @param decl [RBS::AST::Declarations::Global] @@ -303,12 +423,11 @@ def global_decl_to_pin decl type_location: location_decl_to_pin_location(decl.location), source: :rbs ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags + rooted_tag = other_type_to_type(decl.type).rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end - # Visibility overrides that will allow the Solargraph project # and plugins to pass typechecking using SOLARGRAPH_ASSERTS=on, # so that we can detect any regressions/issues elsewhere in the @@ -324,42 +443,45 @@ def global_decl_to_pin decl # allow that to be extended via .solargraph.yml # @type [Hash{Array(String, Symbol, String) => Symbol} VISIBILITY_OVERRIDE = { - ["Rails::Engine", :instance, "run_tasks_blocks"] => :protected, + ['Rails::Engine', :instance, 'run_tasks_blocks'] => :protected, # Should have been marked as both instance and class method in module -e.g., 'module_function' - ["Kernel", :instance, "pretty_inspect"] => :private, + ['Kernel', :instance, 'pretty_inspect'] => :private, # marked incorrectly in RBS - ["WEBrick::HTTPUtils::FormData", :instance, "next_data"] => :protected, - ["Rails::Command", :class, "command_type"] => :private, - ["Rails::Command", :class, "lookup_paths"] => :private, - ["Rails::Command", :class, "file_lookup_paths"] => :private, - ["Rails::Railtie", :instance, "run_console_blocks"] => :protected, - ["Rails::Railtie", :instance, "run_generators_blocks"] => :protected, - ["Rails::Railtie", :instance, "run_runner_blocks"] => :protected, - ["Rails::Railtie", :instance, "run_tasks_blocks"] => :protected, - ["ActionController::Base", :instance, "_protected_ivars"] => :private, - ["ActionView::Template", :instance, "method_name"] => :public, - ["Module", :instance, "ruby2_keywords"] => :private, - ["Nokogiri::XML::Node", :instance, "coerce"] => :protected, - ["Nokogiri::XML::Document", :class, "empty_doc?"] => :private, - ["Nokogiri::Decorators::Slop", :instance, "respond_to_missing?"] => :public, - ["RuboCop::Cop::RangeHelp", :instance, "source_range"] => :private, - ["AST::Node", :instance, "original_dup"] => :private, - ["Rainbow::Presenter", :instance, "wrap_with_sgr"] => :private, - } - - # @param decl [RBS::AST::Members::MethodDefinition, RBS::AST::Members::AttrReader, RBS::AST::Members::AttrAccessor] + ['WEBrick::HTTPUtils::FormData', :instance, 'next_data'] => :protected, + ['Rails::Command', :class, 'command_type'] => :private, + ['Rails::Command', :class, 'lookup_paths'] => :private, + ['Rails::Command', :class, 'file_lookup_paths'] => :private, + ['Rails::Railtie', :instance, 'run_console_blocks'] => :protected, + ['Rails::Railtie', :instance, 'run_generators_blocks'] => :protected, + ['Rails::Railtie', :instance, 'run_runner_blocks'] => :protected, + ['Rails::Railtie', :instance, 'run_tasks_blocks'] => :protected, + ['ActionController::Base', :instance, '_protected_ivars'] => :private, + ['ActionView::Template', :instance, 'method_name'] => :public, + ['Module', :instance, 'ruby2_keywords'] => :private, + ['Nokogiri::XML::Node', :instance, 'coerce'] => :protected, + ['Nokogiri::XML::Document', :class, 'empty_doc?'] => :private, + ['Nokogiri::Decorators::Slop', :instance, 'respond_to_missing?'] => :public, + ['RuboCop::Cop::RangeHelp', :instance, 'source_range'] => :private, + ['AST::Node', :instance, 'original_dup'] => :private, + ['Rainbow::Presenter', :instance, 'wrap_with_sgr'] => :private + }.freeze + private_constant :VISIBILITY_OVERRIDE + + # @param decl [RBS::AST::Members::MethodDefinition, RBS::AST::Members::AttrReader, + # RBS::AST::Members::AttrWriter, RBS::AST::Members::AttrAccessor] # @param closure [Pin::Closure] # @param context [Context] # @param scope [Symbol] :instance or :class # @param name [String] The name of the method - # @sg-ignore # @return [Symbol] - def calculate_method_visibility(decl, context, closure, scope, name) + def calculate_method_visibility decl, context, closure, scope, name override_key = [closure.path, scope, name] visibility = VISIBILITY_OVERRIDE[override_key] simple_override_key = [closure.path, scope] visibility ||= VISIBILITY_OVERRIDE[simple_override_key] - visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(decl.name) + if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(decl.name) + visibility ||= :private + end if decl.kind == :singleton_instance # this is a 'module function' visibility ||= :private @@ -379,7 +501,9 @@ def method_def_to_pin decl, closure, context # having different type params / orders - we may need to match # this data model and have generics live in signatures to # handle those correctly - generics = decl.overloads.map(&:method_type).flat_map(&:type_params).map(&:name).map(&:to_s).uniq + generics = decl.overloads.map(&:method_type).map do |method_type| + type_parameter_names method_type + end if decl.instance? name = decl.name.to_s @@ -403,24 +527,23 @@ def method_def_to_pin decl, closure, context pin.instance_variable_set(:@return_type, ComplexType::VOID) end end - if decl.singleton? - final_scope = :class - name = decl.name.to_s - visibility = calculate_method_visibility(decl, context, closure, final_scope, name) - pin = Solargraph::Pin::Method.new( - name: name, - closure: closure, - comments: decl.comment&.string, - type_location: location_decl_to_pin_location(decl.location), - visibility: visibility, - scope: final_scope, - signatures: [], - generics: generics, - source: :rbs - ) - pin.signatures.concat method_def_to_sigs(decl, pin) - pins.push pin - end + return unless decl.singleton? + final_scope = :class + name = decl.name.to_s + visibility = calculate_method_visibility(decl, context, closure, final_scope, name) + pin = Solargraph::Pin::Method.new( + name: name, + closure: closure, + comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location), + visibility: visibility, + scope: final_scope, + signatures: [], + generics: generics, + source: :rbs + ) + pin.signatures.concat method_def_to_sigs(decl, pin) + pins.push pin end # @param decl [RBS::AST::Members::MethodDefinition] @@ -430,38 +553,45 @@ 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) + generics = type_parameter_names(overload.method_type) signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin) - block = if overload.method_type.block - block_parameters, block_return_type = parts_of_function(overload.method_type.block, pin) - Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, source: :rbs, + rbs_block = overload.method_type.block + block = if rbs_block + block_parameters, block_return_type = parts_of_function(rbs_block, pin) + Pin::Signature.new(generics: generics, parameters: block_parameters, + return_type: block_return_type, source: :rbs, type_location: type_location, closure: pin) end - Pin::Signature.new(generics: generics, parameters: signature_parameters, return_type: signature_return_type, block: block, source: :rbs, + Pin::Signature.new(generics: generics, parameters: signature_parameters, + return_type: signature_return_type, block: block, source: :rbs, type_location: type_location, closure: pin) end end # @param location [RBS::Location, nil] # @return [Solargraph::Location, nil] - def location_decl_to_pin_location(location) + def location_decl_to_pin_location location return nil if location&.name.nil? + # @sg-ignore flow sensitive typing should handle return nil if location&.name.nil? start_pos = Position.new(location.start_line - 1, location.start_column) + # @sg-ignore flow sensitive typing should handle return nil if location&.name.nil? end_pos = Position.new(location.end_line - 1, location.end_column) range = Range.new(start_pos, end_pos) + # @sg-ignore flow sensitve typing should handle return nil if location&.name.nil? Location.new(location.name.to_s, range) end - # @param type [RBS::MethodType,RBS::Types::Block] + # @param type [RBS::MethodType, RBS::Types::Block] # @param pin [Pin::Method] # @return [Array(Array, ComplexType)] def parts_of_function type, pin type_location = pin.type_location if defined?(RBS::Types::UntypedFunction) && type.type.is_a?(RBS::Types::UntypedFunction) return [ - [Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin, source: :rbs, type_location: type_location)], - ComplexType.try_parse(method_type_to_tag(type)).force_rooted + [Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin, source: :rbs, + type_location: type_location)], + method_type_to_type(type) ] end @@ -470,41 +600,43 @@ def parts_of_function type, pin 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) + parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, + # @sg-ignore RBS generic type understanding issue + return_type: other_type_to_type(param.type), + 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, + return_type: other_type_to_type(param.type), type_location: type_location, source: :rbs) end if type.type.rest_positionals name = type.type.rest_positionals.name ? type.type.rest_positionals.name.to_s : "arg_#{arg_num += 1}" - inner_rest_positional_type = - ComplexType.try_parse(other_type_to_tag(type.type.rest_positionals.type)) + inner_rest_positional_type = other_type_to_type(type.type.rest_positionals.type) rest_positional_type = ComplexType::UniqueType.new('Array', [], [inner_rest_positional_type], rooted: true, parameters_type: :list) parameters.push Solargraph::Pin::Parameter.new(decl: :restarg, name: name, closure: pin, source: :rbs, type_location: type_location, - return_type: rest_positional_type,) + 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) + 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, + return_type: other_type_to_type(param.type), source: :rbs, type_location: type_location) end type.type.optional_keywords.each do |orig, param| @@ -512,18 +644,18 @@ def parts_of_function type, pin 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, + return_type: other_type_to_type(param.type), type_location: type_location, source: :rbs) end if type.type.rest_keywords name = type.type.rest_keywords.name ? type.type.rest_keywords.name.to_s : "arg_#{arg_num += 1}" - parameters.push Solargraph::Pin::Parameter.new(decl: :kwrestarg, name: type.type.rest_keywords.name.to_s, closure: pin, + parameters.push Solargraph::Pin::Parameter.new(decl: :kwrestarg, + name: type.type.rest_keywords.name.to_s, closure: pin, source: :rbs, type_location: type_location) end - rooted_tag = method_type_to_tag(type) - return_type = ComplexType.try_parse(rooted_tag).force_rooted + return_type = method_type_to_type(type) [parameters, return_type] end @@ -531,7 +663,7 @@ def parts_of_function type, pin # @param closure [Pin::Namespace] # @param context [Context] # @return [void] - def attr_reader_to_pin(decl, closure, context) + def attr_reader_to_pin decl, closure, context name = decl.name.to_s final_scope = decl.kind == :instance ? :instance : :class visibility = calculate_method_visibility(decl, context, closure, final_scope, name) @@ -545,9 +677,11 @@ def attr_reader_to_pin(decl, closure, context) visibility: visibility, source: :rbs ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags + rooted_tag = other_type_to_type(decl.type).rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) - logger.debug { "Conversions#attr_reader_to_pin(name=#{name.inspect}, visibility=#{visibility.inspect}) => #{pin.inspect}" } + logger.debug do + "Conversions#attr_reader_to_pin(name=#{name.inspect}, visibility=#{visibility.inspect}) => #{pin.inspect}" + end pins.push pin end @@ -555,9 +689,9 @@ def attr_reader_to_pin(decl, closure, context) # @param closure [Pin::Namespace] # @param context [Context] # @return [void] - def attr_writer_to_pin(decl, closure, context) + def attr_writer_to_pin decl, closure, context final_scope = decl.kind == :instance ? :instance : :class - name = "#{decl.name.to_s}=" + name = "#{decl.name}=" visibility = calculate_method_visibility(decl, context, closure, final_scope, name) type_location = location_decl_to_pin_location(decl.location) pin = Solargraph::Pin::Method.new( @@ -574,13 +708,13 @@ def attr_writer_to_pin(decl, closure, context) pin.parameters << Solargraph::Pin::Parameter.new( name: 'value', - return_type: ComplexType.try_parse(other_type_to_tag(decl.type)).force_rooted, + return_type: other_type_to_type(decl.type), source: :rbs, closure: pin, type_location: type_location ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags - pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) + rooted_tags = other_type_to_type(decl.type).rooted_tags + pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tags)) pins.push pin end @@ -588,7 +722,7 @@ def attr_writer_to_pin(decl, closure, context) # @param closure [Pin::Namespace] # @param context [Context] # @return [void] - def attr_accessor_to_pin(decl, closure, context) + def attr_accessor_to_pin decl, closure, context attr_reader_to_pin(decl, closure, context) attr_writer_to_pin(decl, closure, context) end @@ -596,7 +730,7 @@ def attr_accessor_to_pin(decl, closure, context) # @param decl [RBS::AST::Members::InstanceVariable] # @param closure [Pin::Namespace] # @return [void] - def ivar_to_pin(decl, closure) + def ivar_to_pin decl, closure pin = Solargraph::Pin::InstanceVariable.new( name: decl.name.to_s, closure: closure, @@ -604,7 +738,7 @@ def ivar_to_pin(decl, closure) comments: decl.comment&.string, source: :rbs ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags + rooted_tag = other_type_to_type(decl.type).rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end @@ -612,7 +746,7 @@ def ivar_to_pin(decl, closure) # @param decl [RBS::AST::Members::ClassVariable] # @param closure [Pin::Namespace] # @return [void] - def cvar_to_pin(decl, closure) + def cvar_to_pin decl, closure name = decl.name.to_s pin = Solargraph::Pin::ClassVariable.new( name: name, @@ -621,7 +755,7 @@ def cvar_to_pin(decl, closure) type_location: location_decl_to_pin_location(decl.location), source: :rbs ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags + rooted_tag = other_type_to_type(decl.type).rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end @@ -629,7 +763,7 @@ def cvar_to_pin(decl, closure) # @param decl [RBS::AST::Members::ClassInstanceVariable] # @param closure [Pin::Namespace] # @return [void] - def civar_to_pin(decl, closure) + def civar_to_pin decl, closure name = decl.name.to_s pin = Solargraph::Pin::InstanceVariable.new( name: name, @@ -638,7 +772,7 @@ def civar_to_pin(decl, closure) type_location: location_decl_to_pin_location(decl.location), source: :rbs ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags + rooted_tag = other_type_to_type(decl.type).rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end @@ -648,9 +782,9 @@ def civar_to_pin(decl, closure) # @return [void] def include_to_pin decl, closure type = build_type(decl.name, decl.args) - generic_values = type.all_params.map(&:to_s) + generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Include.new( - name: decl.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure, @@ -662,9 +796,12 @@ def include_to_pin decl, closure # @param closure [Pin::Namespace] # @return [void] def prepend_to_pin decl, closure + type = build_type(decl.name, decl.args) + generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Prepend.new( - name: decl.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(decl.location), + generic_values: generic_values, closure: closure, source: :rbs ) @@ -674,9 +811,12 @@ def prepend_to_pin decl, closure # @param closure [Pin::Namespace] # @return [void] def extend_to_pin decl, closure + type = build_type(decl.name, decl.args) + generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Extend.new( - name: decl.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(decl.location), + generic_values: generic_values, closure: closure, source: :rbs ) @@ -693,114 +833,103 @@ def alias_to_pin decl, closure original: decl.old_name.to_s, closure: closure, scope: final_scope, - source: :rbs, + source: :rbs ) end - RBS_TO_YARD_TYPE = { - 'bool' => 'Boolean', - 'string' => 'String', - 'int' => 'Integer', - 'untyped' => '', - 'NilClass' => 'nil' - } - - # @param type [RBS::MethodType] - # @return [String] - def method_type_to_tag type + # @param type [RBS::MethodType, RBS::Types::Block] + # @return [ComplexType, ComplexType::UniqueType] + def method_type_to_type type if type_aliases.key?(type.type.return_type.to_s) - other_type_to_tag(type_aliases[type.type.return_type.to_s].type) + other_type_to_type(type_aliases[type.type.return_type.to_s].type) else - other_type_to_tag type.type.return_type + other_type_to_type type.type.return_type end end - # @param type_name [RBS::TypeName] - # @param type_args [Enumerable] - # @return [ComplexType::UniqueType] - def build_type(type_name, type_args = []) - base = RBS_TO_YARD_TYPE[type_name.relative!.to_s] || type_name.relative!.to_s - params = type_args.map { |a| other_type_to_tag(a) }.map do |t| - ComplexType.try_parse(t).force_rooted - end - if base == 'Hash' && params.length == 2 - ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: true, parameters_type: :hash) - else - ComplexType::UniqueType.new(base, [], params.reject(&:undefined?), rooted: true, parameters_type: :list) - end - end - - # @param type_name [RBS::TypeName] - # @param type_args [Enumerable] - # @return [String] - def type_tag(type_name, type_args = []) - build_type(type_name, type_args).tags - end - - # @param type [RBS::Types::Bases::Base] - # @return [String] - def other_type_to_tag type - if type.is_a?(RBS::Types::Optional) - "#{other_type_to_tag(type.type)}, nil" - elsif type.is_a?(RBS::Types::Bases::Any) - 'undefined' - elsif type.is_a?(RBS::Types::Bases::Bool) - 'Boolean' - elsif type.is_a?(RBS::Types::Tuple) - "Array(#{type.types.map { |t| other_type_to_tag(t) }.join(', ')})" - elsif type.is_a?(RBS::Types::Literal) - type.literal.inspect - elsif type.is_a?(RBS::Types::Union) - type.types.map { |t| other_type_to_tag(t) }.join(', ') - elsif type.is_a?(RBS::Types::Record) + # @param type [RBS::Types::Bases::Base,Object] RBS type object. + # Note: Generally these extend from RBS::Types::Bases::Base, + # but not all. + # + # @return [ComplexType, ComplexType::UniqueType] + def other_type_to_type type + case type + when RBS::Types::Optional + # @sg-ignore flow based typing needs to understand case when class pattern + ComplexType.new([other_type_to_type(type.type), + ComplexType::UniqueType::NIL]) + when RBS::Types::Bases::Any + ComplexType::UNDEFINED + when RBS::Types::Bases::Bool + ComplexType::BOOLEAN + when RBS::Types::Tuple + # @sg-ignore flow based typing needs to understand case when class pattern + tuple_types = type.types.map { |t| other_type_to_type(t) } + ComplexType::UniqueType.new('Array', [], tuple_types, rooted: true, parameters_type: :fixed) + when RBS::Types::Literal + # @sg-ignore flow based typing needs to understand case when class pattern + ComplexType.try_parse(type.literal.inspect).force_rooted + when RBS::Types::Union + # @sg-ignore flow based typing needs to understand case when class pattern + ComplexType.new(type.types.map { |t| other_type_to_type(t) }) + when RBS::Types::Record # @todo Better record support - 'Hash' - elsif type.is_a?(RBS::Types::Bases::Nil) - 'nil' - elsif type.is_a?(RBS::Types::Bases::Self) - 'self' - elsif type.is_a?(RBS::Types::Bases::Void) - 'void' - elsif type.is_a?(RBS::Types::Variable) - "#{Solargraph::ComplexType::GENERIC_TAG_NAME}<#{type.name}>" - elsif type.is_a?(RBS::Types::ClassInstance) #&& !type.args.empty? - type_tag(type.name, type.args) - elsif type.is_a?(RBS::Types::Bases::Instance) - 'self' - elsif type.is_a?(RBS::Types::Bases::Top) + ComplexType::UniqueType.new('Hash', rooted: true) + when RBS::Types::Bases::Nil + ComplexType::NIL + when RBS::Types::Bases::Self + ComplexType::SELF + when RBS::Types::Bases::Void + ComplexType::VOID + when RBS::Types::Variable + # @sg-ignore flow based typing needs to understand case when class pattern + ComplexType.parse("generic<#{type.name}>").force_rooted + when RBS::Types::ClassInstance # && !type.args.empty? + # @sg-ignore flow based typing needs to understand case when class pattern + build_type(type.name, type.args) + when RBS::Types::Bases::Instance + ComplexType::SELF + when RBS::Types::Bases::Top # top is the most super superclass - 'BasicObject' - elsif type.is_a?(RBS::Types::Bases::Bottom) + ComplexType::UniqueType.new('BasicObject', rooted: true) + when RBS::Types::Bases::Bottom # bottom is used in contexts where nothing will ever return # - e.g., it could be the return type of 'exit()' or 'raise' # # @todo define a specific bottom type and use it to # determine dead code - 'undefined' - elsif type.is_a?(RBS::Types::Intersection) - type.types.map { |member| other_type_to_tag(member) }.join(', ') - elsif type.is_a?(RBS::Types::Proc) - 'Proc' - elsif type.is_a?(RBS::Types::Alias) + ComplexType::UNDEFINED + when RBS::Types::Intersection + # @sg-ignore flow based typing needs to understand case when class pattern + ComplexType.new(type.types.map { |member| other_type_to_type(member) }) + when RBS::Types::Proc + ComplexType::UniqueType.new('Proc', rooted: true) + when RBS::Types::Alias # type-level alias use - e.g., 'bool' in "type bool = true | false" # @todo ensure these get resolved after processing all aliases # @todo handle recursive aliases - type_tag(type.name, type.args) - elsif type.is_a?(RBS::Types::Interface) + # @sg-ignore flow based typing needs to understand case when class pattern + build_type(type.name, type.args) + when RBS::Types::Interface # represents a mix-in module which can be considered a # subtype of a consumer of it - type_tag(type.name, type.args) - elsif type.is_a?(RBS::Types::ClassSingleton) + # @sg-ignore flow based typing needs to understand case when class pattern + build_type(type.name, type.args) + when RBS::Types::ClassSingleton # e.g., singleton(String) - type_tag(type.name) + # @sg-ignore flow based typing needs to understand case when class pattern + build_type(type.name) else + # RBS doesn't provide a common base class for its type AST nodes + # + # @sg-ignore all types should include location Solargraph.logger.warn "Unrecognized RBS type: #{type.class} at #{type.location}" - 'undefined' + ComplexType::UNDEFINED end end # @param decl [RBS::AST::Declarations::Class, RBS::AST::Declarations::Module] - # @param namespace [Pin::Namespace] + # @param namespace [Pin::Namespace, nil] # @return [void] def add_mixins decl, namespace # @param mixin [RBS::AST::Members::Include, RBS::AST::Members::Members::Extend, RBS::AST::Members::Members::Prepend] @@ -808,9 +937,9 @@ def add_mixins decl, namespace # @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) + generic_values = type.all_params.map(&:rooted_tags) pins.push klass.new( - name: mixin.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(mixin.location), generic_values: generic_values, closure: namespace, diff --git a/lib/solargraph/rbs_map/core_fills.rb b/lib/solargraph/rbs_map/core_fills.rb index 3bb32a0da..0116b15eb 100644 --- a/lib/solargraph/rbs_map/core_fills.rb +++ b/lib/solargraph/rbs_map/core_fills.rb @@ -19,7 +19,7 @@ module CoreFills Solargraph::Pin::Method.new(name: 'class', scope: :instance, closure: Solargraph::Pin::Namespace.new(name: 'Object', source: :core_fill), comments: '@return [::Class]', source: :core_fill) - ] + ].freeze OVERRIDES = [ Override.from_comment('BasicObject#instance_eval', '@yieldreceiver [self]', @@ -38,43 +38,50 @@ module CoreFills source: :core_fill), # RBS does not define Class with a generic, so all calls to # generic() return an 'untyped'. We can do better: - Override.method_return('Class#allocate', 'self', source: :core_fill), - ] + Override.method_return('Class#allocate', 'self', source: :core_fill) + ].freeze # @todo I don't see any direct link in RBS to build this from - # presumably RBS is using duck typing to match interfaces # against concrete classes INCLUDES = [ Solargraph::Pin::Reference::Include.new(name: '_ToAry', - closure: Solargraph::Pin::Namespace.new(name: 'Array', source: :core_fill), + closure: Solargraph::Pin::Namespace.new(name: 'Array', + source: :core_fill), generic_values: ['generic'], source: :core_fill), Solargraph::Pin::Reference::Include.new(name: '_ToAry', - closure: Solargraph::Pin::Namespace.new(name: 'Set', source: :core_fill), + closure: Solargraph::Pin::Namespace.new(name: 'Set', + source: :core_fill), generic_values: ['generic'], source: :core_fill), Solargraph::Pin::Reference::Include.new(name: '_Each', - closure: Solargraph::Pin::Namespace.new(name: 'Array', source: :core_fill), + closure: Solargraph::Pin::Namespace.new(name: 'Array', + source: :core_fill), generic_values: ['generic'], source: :core_fill), Solargraph::Pin::Reference::Include.new(name: '_Each', - closure: Solargraph::Pin::Namespace.new(name: 'Set', source: :core_fill), + closure: Solargraph::Pin::Namespace.new(name: 'Set', + source: :core_fill), generic_values: ['generic'], source: :core_fill), Solargraph::Pin::Reference::Include.new(name: '_ToS', - closure: Solargraph::Pin::Namespace.new(name: 'Object', source: :core_fill), + closure: Solargraph::Pin::Namespace.new(name: 'Object', + source: :core_fill), source: :core_fill), Solargraph::Pin::Reference::Include.new(name: '_ToS', - closure: Solargraph::Pin::Namespace.new(name: 'String', source: :core_fill), + closure: Solargraph::Pin::Namespace.new(name: 'String', + source: :core_fill), source: :core_fill) - ] + ].freeze # HACK: Add Errno exception classes errno = Solargraph::Pin::Namespace.new(name: 'Errno', source: :core_fill) errnos = [] Errno.constants.each do |const| errnos.push Solargraph::Pin::Namespace.new(type: :class, name: const.to_s, closure: errno, source: :core_fill) - errnos.push Solargraph::Pin::Reference::Superclass.new(closure: errnos.last, name: 'SystemCallError', source: :core_fill) + errnos.push Solargraph::Pin::Reference::Superclass.new(closure: errnos.last, name: 'SystemCallError', + source: :core_fill) end ERRNOS = errnos diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index d2836ffe3..a2eeed60f 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -15,31 +15,38 @@ def resolved? def initialize; end + # @param out [IO, nil] output stream for logging # @return [Enumerable] - def pins + def pins out: $stderr return @pins if @pins + @pins = cache_core(out: out) + end - @pins = [] + # @param out [StringIO, IO, nil] output stream for logging + # @return [Array] + def cache_core out: $stderr + new_pins = [] cache = PinCache.deserialize_core - if cache - @pins.replace cache - else - @pins.concat conversions.pins + return cache if cache + new_pins.concat conversions.pins + + # Avoid RBS::DuplicatedDeclarationError by loading in a different EnvironmentLoader + fill_loader = RBS::EnvironmentLoader.new(core_root: nil, repository: RBS::Repository.new(no_stdlib: false)) + fill_loader.add(path: Pathname(FILLS_DIRECTORY)) + out&.puts 'Caching RBS pins for Ruby core' + fill_conversions = Conversions.new(loader: fill_loader) + new_pins.concat fill_conversions.pins - # Avoid RBS::DuplicatedDeclarationError by loading in a different EnvironmentLoader - fill_loader = RBS::EnvironmentLoader.new(core_root: nil, repository: RBS::Repository.new(no_stdlib: false)) - fill_loader.add(path: Pathname(FILLS_DIRECTORY)) - fill_conversions = Conversions.new(loader: fill_loader) - @pins.concat fill_conversions.pins + # add some overrides + new_pins.concat RbsMap::CoreFills::ALL - @pins.concat RbsMap::CoreFills::ALL + # process overrides, then remove any which couldn't be resolved + processed = ApiMap::Store.new(new_pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } + new_pins.replace processed - processed = ApiMap::Store.new(pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } - @pins.replace processed + PinCache.serialize_core new_pins - PinCache.serialize_core @pins - end - @pins + new_pins end private diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index b6804157f..e6ebcf90f 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -12,19 +12,22 @@ class StdlibMap < RbsMap # @type [Hash{String => RbsMap}] @stdlib_maps_hash = {} + # @param rebuild [Boolean] build pins regardless of whether we + # have cached them already # @param library [String] - def initialize library + # @param out [StringIO, IO, nil] where to log messages + def initialize library, rebuild: false, out: $stderr cached_pins = PinCache.deserialize_stdlib_require library - if cached_pins + if cached_pins && !rebuild @pins = cached_pins @resolved = true @loaded = true logger.debug { "Deserialized #{cached_pins.length} cached pins for stdlib require #{library.inspect}" } - else - super + elsif self.class.source.has? library, nil + super(library, out: out) unless resolved? @pins = [] - logger.info { "Could not resolve #{library.inspect}" } + logger.debug { "StdlibMap could not resolve #{library.inspect}" } return end generated_pins = pins @@ -33,6 +36,31 @@ def initialize library end end + # @return [RBS::Collection::Sources::Stdlib] + def self.source + @source ||= RBS::Collection::Sources::Stdlib.instance + end + + # @param name [String] + # @param version [String, nil] + # @return [Array String}>, nil] + def self.stdlib_dependencies name, version = nil + if source.has?(name, version) + # @sg-ignore we are relying on undocumented behavior where + # passing version=nil gives the latest version it has + source.dependencies_of(name, version) + else + [] + end + end + + def resolve_dependencies? + # there are 'virtual' dependencies for stdlib gems in RBS that + # aren't represented in the actual gemspecs that we'd + # otherwise use + true + end + # @param library [String] # @return [StdlibMap] def self.load library diff --git a/lib/solargraph/server_methods.rb b/lib/solargraph/server_methods.rb index bdac3f19c..86cd107d0 100644 --- a/lib/solargraph/server_methods.rb +++ b/lib/solargraph/server_methods.rb @@ -7,7 +7,7 @@ module ServerMethods # @return [Integer] def available_port socket = Socket.new(:INET, :STREAM, 0) - socket.bind(Addrinfo.tcp("127.0.0.1", 0)) + socket.bind(Addrinfo.tcp('127.0.0.1', 0)) port = socket.local_address.ip_port socket.close port diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 14a1139ae..8ee13eacf 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -3,6 +3,7 @@ require 'benchmark' require 'thor' require 'yard' +require 'yaml' module Solargraph class Shell < Thor @@ -15,7 +16,7 @@ def self.exit_on_failure? map %w[--version -v] => :version - desc "--version, -v", "Print the version" + desc '--version, -v', 'Print the version' # @return [void] def version puts Solargraph::VERSION @@ -30,15 +31,15 @@ def socket port = options[:port] port = available_port if port.zero? Backport.run do - Signal.trap("INT") do + Signal.trap('INT') do Backport.stop end - Signal.trap("TERM") do + Signal.trap('TERM') do Backport.stop end # @sg-ignore Wrong argument type for Backport.prepare_tcp_server: adapter expected Backport::Adapter, received Module Backport.prepare_tcp_server host: options[:host], port: port, adapter: Solargraph::LanguageServer::Transport::Adapter - STDERR.puts "Solargraph is listening PORT=#{port} PID=#{Process.pid}" + warn "Solargraph is listening PORT=#{port} PID=#{Process.pid}" end end @@ -47,15 +48,15 @@ def socket def stdio require 'backport' Backport.run do - Signal.trap("INT") do + Signal.trap('INT') do Backport.stop end - Signal.trap("TERM") do + Signal.trap('TERM') do Backport.stop end # @sg-ignore Wrong argument type for Backport.prepare_stdio_server: adapter expected Backport::Adapter, received Module Backport.prepare_stdio_server adapter: Solargraph::LanguageServer::Transport::Adapter - STDERR.puts "Solargraph is listening on stdio PID=#{Process.pid}" + warn "Solargraph is listening on stdio PID=#{Process.pid}" end end @@ -63,11 +64,11 @@ def stdio option :extensions, type: :boolean, aliases: :e, desc: 'Add installed extensions', default: true # @param directory [String] # @return [void] - def config(directory = '.') + def config directory = '.' matches = [] if options[:extensions] Gem::Specification.each do |g| - if g.name.match(/^solargraph\-[A-Za-z0-9_\-]*?\-ext/) + if g.name.match(/^solargraph-[A-Za-z0-9_-]*?-ext/) require g.name matches.push g.name end @@ -83,7 +84,7 @@ def config(directory = '.') File.open(File.join(directory, '.solargraph.yml'), 'w') do |file| file.puts conf.to_yaml end - STDOUT.puts "Configuration file initialized." + $stdout.puts 'Configuration file initialized.' end desc 'clear', 'Delete all cached documentation' @@ -92,7 +93,7 @@ def config(directory = '.') ) # @return [void] def clear - puts "Deleting all cached documentation (gems, core and stdlib)" + puts 'Deleting all cached documentation (gems, core and stdlib)' Solargraph::PinCache.clear end map 'clear-cache' => :clear @@ -104,12 +105,11 @@ def clear # @param gem [String] # @param version [String, nil] def cache gem, version = nil - api_map = Solargraph::ApiMap.load(Dir.pwd) - spec = Gem::Specification.find_by_name(gem, version) - api_map.cache_gem(spec, rebuild: options[:rebuild], out: $stdout) + gems(gem + (version ? "=#{version}" : '')) + # ' end - desc 'uncache GEM [...GEM]', "Delete specific cached gem documentation" + desc 'uncache GEM [...GEM]', 'Delete specific cached gem documentation' long_desc %( Specify one or more gem names to clear. 'core' or 'stdlib' may also be specified to clear cached system documentation. @@ -119,39 +119,83 @@ def cache gem, version = nil # @return [void] def uncache *gems raise ArgumentError, 'No gems specified.' if gems.empty? + workspace = Solargraph::Workspace.new(Dir.pwd) + gems.each do |gem| if gem == 'core' - PinCache.uncache_core + PinCache.uncache_core(out: $stdout) next end if gem == 'stdlib' - PinCache.uncache_stdlib + PinCache.uncache_stdlib(out: $stdout) next end - spec = Gem::Specification.find_by_name(gem) - PinCache.uncache_gem(spec, out: $stdout) + spec = workspace.find_gem(gem) + raise Thor::InvocationError, "Gem '#{gem}' not found" if spec.nil? + + # @sg-ignore flow sensitive typing needs to handle 'raise if' + workspace.uncache_gem(spec, out: $stdout) end end - desc 'gems [GEM[=VERSION]]', 'Cache documentation for installed gems' + desc 'gems [GEM[=VERSION]...] [STDLIB...] [core]', 'Cache documentation for + installed libraries' + long_desc %( This command will cache the + generated type documentation for the specified libraries. While + Solargraph will generate this on the fly when needed, it takes + time. This command will generate it in advance, which can be + useful for CI scenarios. + + With no arguments, it will cache all libraries in the current + workspace. If a gem or standard library name is specified, it + will cache that library's type documentation. + + An equals sign after a gem will allow a specific gem version + to be cached. + + The 'core' argument can be used to cache the type + documentation for the core Ruby libraries. + + If the library is already cached, it will be rebuilt if the + --rebuild option is set. + + Cached documentation is stored in #{PinCache.base_dir}, which + can be stored between CI runs. + ) option :rebuild, type: :boolean, desc: 'Rebuild existing documentation', default: false # @param names [Array] # @return [void] def gems *names - api_map = ApiMap.load('.') + # print time with ms + workspace = Solargraph::Workspace.new('.') + if names.empty? - Gem::Specification.to_a.each { |spec| do_cache spec, api_map } - STDERR.puts "Documentation cached for all #{Gem::Specification.count} gems." + workspace.cache_all_for_workspace!($stdout, rebuild: options[:rebuild]) else + warn("Caching these gems: #{names}") names.each do |name| - spec = Gem::Specification.find_by_name(*name.split('=')) - do_cache spec, api_map + if name == 'core' + PinCache.cache_core(out: $stdout) if !PinCache.core? || options[:rebuild] + next + end + + gemspec = workspace.find_gem(*name.split('=')) + if gemspec.nil? + warn "Gem '#{name}' not found" + else + workspace.cache_gem(gemspec, rebuild: options[:rebuild], out: $stdout) + end rescue Gem::MissingSpecError warn "Gem '#{name}' not found" + rescue Gem::Requirement::BadRequirementError => e + warn "Gem '#{name}' failed while loading" + warn e.message + # @sg-ignore Need to add nil check here + warn e.backtrace.join("\n") end - STDERR.puts "Documentation cached for #{names.count} gems." + warn "Documentation cached for #{names.count} gems." end end @@ -168,7 +212,7 @@ def reporters Type checking levels are normal, typed, strict, and strong. ) - option :level, type: :string, aliases: [:mode, :m, :l], desc: 'Type checking level', default: 'normal' + option :level, type: :string, aliases: %i[mode m l], desc: 'Type checking level', default: 'normal' option :directory, type: :string, aliases: :d, desc: 'The workspace directory', default: '.' # @return [void] def typecheck *files @@ -176,7 +220,10 @@ def typecheck *files workspace = Solargraph::Workspace.new(directory) level = options[:level].to_sym rules = workspace.rules(level) - api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout) + api_map = + Solargraph::ApiMap.load_with_cache(directory, $stdout, + loose_unions: + !rules.require_all_unique_types_support_call?) probcount = 0 if files.empty? files = api_map.source_maps.map(&:filename) @@ -184,23 +231,28 @@ def typecheck *files files.map! { |file| File.realpath(file) } end filecount = 0 - - time = Benchmark.measure { + time = Benchmark.measure do files.each do |file| - checker = TypeChecker.new(file, api_map: api_map, level: options[:level].to_sym, workspace: workspace) + checker = TypeChecker.new(file, api_map: api_map, rules: rules, level: options[:level].to_sym, + workspace: workspace) problems = checker.problems next if problems.empty? problems.sort! { |a, b| a.location.range.start.line <=> b.location.range.start.line } - puts problems.map { |prob| "#{prob.location.filename}:#{prob.location.range.start.line + 1} - #{prob.message}" }.join("\n") + puts problems.map { |prob| + "#{prob.location.filename}:#{prob.location.range.start.line + 1} - #{prob.message}" + }.join("\n") filecount += 1 probcount += problems.length end - # " - } + end puts "Typecheck finished in #{time.real} seconds." - puts "#{probcount} problem#{probcount != 1 ? 's' : ''} found#{files.length != 1 ? " in #{filecount} of #{files.length} files" : ''}." + puts "#{probcount} problem#{if probcount != 1 + 's' + end} found#{if files.length != 1 + " in #{filecount} of #{files.length} files" + end}." # " - exit 1 if probcount > 0 + exit 1 if probcount.positive? end desc 'scan', 'Test the workspace for problems' @@ -217,21 +269,27 @@ def scan directory = File.realpath(options[:directory]) # @type [Solargraph::ApiMap, nil] api_map = nil - time = Benchmark.measure { + time = Benchmark.measure do api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout) + # @sg-ignore flow sensitive typing should be able to handle redefinition api_map.pins.each do |pin| - begin - puts pin_description(pin) if options[:verbose] - pin.typify api_map - pin.probe api_map - rescue StandardError => e - STDERR.puts "Error testing #{pin_description(pin)} #{pin.location ? "at #{pin.location.filename}:#{pin.location.range.start.line + 1}" : ''}" - STDERR.puts "[#{e.class}]: #{e.message}" - STDERR.puts e.backtrace.join("\n") - exit 1 - end + puts pin_description(pin) if options[:verbose] + pin.typify api_map + pin.probe api_map + rescue StandardError => e + # @todo to add nil check here + # @todo should warn on nil dereference below + warn "Error testing #{pin_description(pin)} #{if pin.location + "at #{pin.location.filename}:#{pin.location.range.start.line + 1}" + end}" + warn "[#{e.class}]: #{e.message}" + # @todo Need to add nil check here + # @todo flow sensitive typing should be able to handle redefinition + warn e.backtrace.join("\n") + exit 1 end - } + end + # @sg-ignore Need to add nil check here puts "Scanned #{directory} (#{api_map.pins.length} pins) in #{time.real} seconds." end @@ -245,12 +303,15 @@ def list puts "#{workspace.filenames.length} files total." end - desc 'pin [PATH]', 'Describe a pin', hide: true + desc 'pin [PATH]', 'Describe a pin' 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 :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 + 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 @@ -275,10 +336,11 @@ def pin path pin = pins.first case pin when nil - $stderr.puts "Pin not found for path '#{path}'" + warn "Pin not found for path '#{path}'" exit 1 when Pin::Namespace if options[:references] + # @sg-ignore Need to add nil check here 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 @@ -302,36 +364,130 @@ def pin path end end + desc 'profile [FILE]', 'Profile go-to-definition performance using vernier' + option :directory, type: :string, aliases: :d, desc: 'The workspace directory', default: '.' + option :output_dir, type: :string, aliases: :o, desc: 'The output directory for profiles', default: './tmp/profiles' + option :line, type: :numeric, aliases: :l, desc: 'Line number (0-based)', default: 4 + option :column, type: :numeric, aliases: :c, desc: 'Column number', default: 10 + option :memory, type: :boolean, aliases: :m, desc: 'Include memory usage counter', default: true + # @param file [String, nil] + # @return [void] + def profile file = nil + begin + require 'vernier' + rescue LoadError + warn 'vernier gem not found. Install with: gem install vernier' + return + end + + hooks = [] + hooks << :memory_usage if options[:memory] + + directory = File.realpath(options[:directory]) + FileUtils.mkdir_p(options[:output_dir]) + + host = Solargraph::LanguageServer::Host.new + host.client_capabilities.merge!({ 'window' => { 'workDoneProgress' => true } }) + # @param method [String] The message method + # @param params [Hash] The method parameters + # @return [void] + def host.send_notification method, params + puts "Notification: #{method} - #{params}" + end + + puts 'Parsing and mapping source files...' + prepare_start = Time.now + Vernier.profile(out: "#{options[:output_dir]}/parse_benchmark.json.gz", hooks: hooks) do + puts 'Mapping libraries' + host.prepare(directory) + sleep 0.2 until host.libraries.all?(&:mapped?) + end + prepare_time = Time.now - prepare_start + + puts 'Building the catalog...' + catalog_start = Time.now + Vernier.profile(out: "#{options[:output_dir]}/catalog_benchmark.json.gz", hooks: hooks) do + host.catalog + end + catalog_time = Time.now - catalog_start + + # Determine test file + if file + test_file = File.join(directory, file) + else + test_file = File.join(directory, 'lib', 'other.rb') + unless File.exist?(test_file) + # Fallback to any Ruby file in the workspace + workspace = Solargraph::Workspace.new(directory) + test_file = workspace.filenames.find { |f| f.end_with?('.rb') } + unless test_file + warn 'No Ruby files found in workspace' + return + end + end + end + + file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(File.absolute_path(test_file)) + + puts "Profiling go-to-definition for #{test_file}" + puts "Position: line #{options[:line]}, column #{options[:column]}" + + definition_start = Time.now + Vernier.profile(out: "#{options[:output_dir]}/definition_benchmark.json.gz", hooks: hooks) do + message = Solargraph::LanguageServer::Message::TextDocument::Definition.new( + host, { + 'params' => { + 'textDocument' => { 'uri' => file_uri }, + 'position' => { 'line' => options[:line], 'character' => options[:column] } + } + } + ) + puts 'Processing go-to-definition request...' + result = message.process + + puts "Result: #{result.inspect}" + end + definition_time = Time.now - definition_start + + puts "\n=== Timing Results ===" + puts "Parsing & mapping: #{(prepare_time * 1000).round(2)}ms" + puts "Catalog building: #{(catalog_time * 1000).round(2)}ms" + puts "Go-to-definition: #{(definition_time * 1000).round(2)}ms" + total_time = prepare_time + catalog_time + definition_time + puts "Total time: #{(total_time * 1000).round(2)}ms" + + puts "\nProfiles saved to:" + puts " - #{File.expand_path('parse_benchmark.json.gz', options[:output_dir])}" + puts " - #{File.expand_path('catalog_benchmark.json.gz', options[:output_dir])}" + puts " - #{File.expand_path('definition_benchmark.json.gz', options[:output_dir])}" + + puts "\nUpload the JSON files to https://vernier.prof/ to view the profiles." + puts 'Or use https://rubygems.org/gems/profile-viewer to view them locally.' + end + private # @param pin [Solargraph::Pin::Base] # @return [String] def pin_description pin desc = if pin.path.nil? || pin.path.empty? - if pin.closure - "#{pin.closure.path} | #{pin.name}" - else - "#{pin.context.namespace} | #{pin.name}" - end - else - pin.path - end + if pin.closure + # @sg-ignore Need to add nil check here + "#{pin.closure.path} | #{pin.name}" + else + "#{pin.context.namespace} | #{pin.name}" + end + else + pin.path + end + # @sg-ignore Need to add nil check here desc += " (#{pin.location.filename} #{pin.location.range.start.line})" if pin.location desc end - # @param gemspec [Gem::Specification] - # @param api_map [ApiMap] - # @return [void] - def do_cache gemspec, api_map - # @todo if the rebuild: option is passed as a positional arg, - # typecheck doesn't complain on the below line - api_map.cache_gem(gemspec, rebuild: options.rebuild, out: $stdout) - end - - # @param type [ComplexType] + # @param type [ComplexType, ComplexType::UniqueType] # @return [void] - def print_type(type) + def print_type type if options[:rbs] puts type.to_rbs else @@ -341,7 +497,7 @@ def print_type(type) # @param pin [Solargraph::Pin::Base] # @return [void] - def print_pin(pin) + def print_pin pin if options[:rbs] puts pin.to_rbs else diff --git a/lib/solargraph/source.rb b/lib/solargraph/source.rb index d8e4e2a3f..94147989e 100644 --- a/lib/solargraph/source.rb +++ b/lib/solargraph/source.rb @@ -60,11 +60,13 @@ def at range # @param c1 [Integer] # @param l2 [Integer] # @param c2 [Integer] + # + # @sg-ignore Need to add nil check here # @return [String] def from_to l1, c1, l2, c2 b = Solargraph::Position.line_char_to_offset(code, l1, c1) e = Solargraph::Position.line_char_to_offset(code, l2, c2) - code[b..e-1] + code[b..(e - 1)] end # Get the nearest node that contains the specified index. @@ -72,7 +74,7 @@ def from_to l1, c1, l2, c2 # @param line [Integer] # @param column [Integer] # @return [AST::Node] - def node_at(line, column) + def node_at line, column tree_at(line, column).first end @@ -81,8 +83,8 @@ def node_at(line, column) # # @param line [Integer] # @param column [Integer] - # @return [Array] - def tree_at(line, column) + # @return [Array] + def tree_at line, column position = Position.new(line, column) stack = [] inner_tree_at node, position, stack @@ -131,20 +133,29 @@ def string_at? position return false if Position.to_offset(code, position) >= code.length string_nodes.each do |node| range = Range.from_node(node) + # @sg-ignore Need to add nil check here next if range.ending.line < position.line + # @sg-ignore Need to add nil check here break if range.ending.line > position.line + # @sg-ignore Need to add nil check here return true if node.type == :str && range.include?(position) && range.start != position - return true if [:STR, :str].include?(node.type) && range.include?(position) && range.start != position + # @sg-ignore Need to add nil check here + return true if %i[STR str].include?(node.type) && range.include?(position) && range.start != position if node.type == :dstr inner = node_at(position.line, position.column) next if inner.nil? inner_range = Range.from_node(inner) + # @sg-ignore Need to add nil check here next unless range.include?(inner_range.ending) return true if inner.type == :str + # @sg-ignore Need to add nil check here inner_code = at(Solargraph::Range.new(inner_range.start, position)) - return true if (inner.type == :dstr && inner_range.ending.character <= position.character) && !inner_code.end_with?('}') || + # @sg-ignore Need to add nil check here + return true if (inner.type == :dstr && inner_range.ending.character <= position.character && !inner_code.end_with?('}')) || + # @sg-ignore Need to add nil check here (inner.type != :dstr && inner_range.ending.line == position.line && position.character <= inner_range.ending.character && inner_code.end_with?('}')) end + # @sg-ignore Need to add nil check here break if range.ending.line > position.line end false @@ -160,7 +171,7 @@ def string_ranges def comment_at? position comment_ranges.each do |range| return true if range.include?(position) || - (range.ending.line == position.line && range.ending.column < position.column) + (range.ending.line == position.line && range.ending.column < position.column) break if range.ending.line > position.line end false @@ -179,19 +190,24 @@ def error_ranges # @param node [Parser::AST::Node] # @return [String] - def code_for(node) + def code_for node rng = Range.from_node(node) + # @sg-ignore Need to add nil check here b = Position.line_char_to_offset(code, rng.start.line, rng.start.column) + # @sg-ignore Need to add nil check here e = Position.line_char_to_offset(code, rng.ending.line, rng.ending.column) - frag = code[b..e-1].to_s + frag = code[b..(e - 1)].to_s frag.strip.gsub(/,$/, '') end - # @param node [Parser::AST::Node] + # @param node [AST::Node] + # # @return [String, nil] def comments_for node rng = Range.from_node(node) + # @sg-ignore Need to add nil check here stringified_comments[rng.start.line] ||= begin + # @sg-ignore Need to add nil check here buff = associated_comments[rng.start.line] buff ? stringify_comment_array(buff) : nil end @@ -208,8 +224,8 @@ def location end FOLDING_NODE_TYPES = %i[ - class sclass module def defs if str dstr array while unless kwbegin hash block - ].freeze + class sclass module def defs if str dstr array while unless kwbegin hash block + ].freeze # Get an array of ranges that can be folded, e.g., the range of a class # definition or an if condition. @@ -219,6 +235,7 @@ class sclass module def defs if str dstr array while unless kwbegin hash block # @return [Array] def folding_ranges @folding_ranges ||= begin + # @type [Array] result = [] inner_folding_ranges node, result result.concat foldable_comment_block_ranges @@ -232,7 +249,7 @@ def synchronized? # Get a hash of comments grouped by the line numbers of the associated code. # - # @return [Hash{Integer => String}] + # @return [Hash{Integer => String, nil}] def associated_comments @associated_comments ||= begin # @type [Hash{Integer => String}] @@ -265,18 +282,22 @@ def first_not_empty_from line cursor end - # @param top [Parser::AST::Node] + # @param top [Parser::AST::Node, nil] # @param result [Array] # @param parent [Symbol, nil] # @return [void] def inner_folding_ranges top, result = [], parent = nil return unless Parser.is_ast_node?(top) + # @sg-ignore Translate to something flow sensitive typing understands if FOLDING_NODE_TYPES.include?(top.type) + # @sg-ignore Translate to something flow sensitive typing understands range = Range.from_node(top) - if result.empty? || range.start.line > result.last.start.line - result.push range unless range.ending.line - range.start.line < 2 + # @sg-ignore Need to add nil check here + if (result.empty? || range.start.line > result.last.start.line) && range.ending.line - range.start.line >= 2 + result.push range end end + # @sg-ignore Translate to something flow sensitive typing understands top.children.each do |child| inner_folding_ranges(child, result, top.type) end @@ -290,7 +311,7 @@ def stringify_comment_array comments ctxt = String.new('') started = false skip = nil - comments.lines.each { |l| + comments.lines.each do |l| # Trim the comment and minimum leading whitespace p = l.force_encoding('UTF-8').encode('UTF-8', invalid: :replace, replace: '?').gsub(/^#+/, '') if p.strip.empty? @@ -298,17 +319,18 @@ def stringify_comment_array comments ctxt.concat p else here = p.index(/[^ \t]/) + # @sg-ignore flow sensitive typing should be able to handle redefinition skip = here if skip.nil? || here < skip - ctxt.concat p[skip..-1] + ctxt.concat p[skip..] end started = true - } + end ctxt end # A hash of line numbers and their associated comments. # - # @return [Hash{Integer => Array, nil}] + # @return [Hash{Integer => String}] def stringified_comments @stringified_comments ||= {} end @@ -331,7 +353,7 @@ def foldable_comment_block_ranges return [] unless synchronized? result = [] grouped = [] - comments.keys.each do |l| + comments.each_key do |l| if grouped.empty? || l == grouped.last + 1 grouped.push l else @@ -348,10 +370,12 @@ def foldable_comment_block_ranges def string_nodes_in n result = [] if Parser.is_ast_node?(n) - if n.type == :str || n.type == :dstr || n.type == :STR || n.type == :DSTR + # @sg-ignore Translate to something flow sensitive typing understands + if %i[str dstr STR DSTR].include?(n.type) result.push n else - n.children.each{ |c| result.concat string_nodes_in(c) } + # @sg-ignore Translate to something flow sensitive typing understands + n.children.each { |c| result.concat string_nodes_in(c) } end end result @@ -364,13 +388,13 @@ def string_nodes_in n def inner_tree_at node, position, stack return if node.nil? here = Range.from_node(node) - if here.contain?(position) - stack.unshift node - node.children.each do |c| - next unless Parser.is_ast_node?(c) - next if c.loc.expression.nil? - inner_tree_at(c, position, stack) - end + # @sg-ignore Need to add nil check here + return unless here.contain?(position) + stack.unshift node + node.children.each do |c| + next unless Parser.is_ast_node?(c) + next if c.loc.expression.nil? + inner_tree_at(c, position, stack) end end @@ -396,10 +420,10 @@ def finalize end @finalized = true begin - @node, @comments = Solargraph::Parser.parse_with_comments(@code, filename) + @node, @comments = Solargraph::Parser.parse_with_comments(@code, filename, 0) @parsed = true @repaired = @code - rescue Parser::SyntaxError, EncodingError => e + rescue Parser::SyntaxError, EncodingError @node = nil @comments = {} @parsed = false @@ -412,9 +436,9 @@ def finalize end error_ranges.concat(changes.map(&:range)) begin - @node, @comments = Solargraph::Parser.parse_with_comments(@repaired, filename) + @node, @comments = Solargraph::Parser.parse_with_comments(@repaired, filename, 0) @parsed = true - rescue Parser::SyntaxError, EncodingError => e + rescue Parser::SyntaxError, EncodingError @node = nil @comments = {} @parsed = false @@ -427,7 +451,7 @@ def finalize # @param val [String] # @return [String] - def code=(val) + def code= val @code_lines = nil @finalized = false @code = val diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index f7a03b552..4bd1b67b6 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -48,11 +48,6 @@ class Chain attr_reader :node - # @sg-ignore Fix "Not enough arguments to Module#protected" - protected def equality_fields - [links, node] - end - # @param node [Parser::AST::Node, nil] # @param links [::Array] # @param splat [Boolean] @@ -71,6 +66,7 @@ def initialize links, node = nil, splat = false # @return [Chain] def base + # @sg-ignore Need to add nil check here @base ||= Chain.new(links[0..-2]) end @@ -78,25 +74,25 @@ def base # # @param api_map [ApiMap] # - # @param name_pin [Pin::Base] A pin - # representing the place in which expression is evaluated (e.g., - # a Method pin, or a Module or Class pin if not run within a - # method - both in terms of the closure around the chain, as well - # as the self type used for any method calls in head position. + # @param name_pin [Pin::Base] A pin representing the closure in + # which expression is evaluated (e.g., a Method pin, or a + # Module or Class pin if not run within a method - both in + # terms of the closure around the chain, as well as the self + # type used for any method calls in head position. # # Requirements for name_pin: # # * name_pin.context: This should be a type representing the - # namespace where we can look up non-local variables and - # method names. If it is a Class, we will look up - # :class scoped methods/variables. + # namespace where we can look up non-local variables. If + # it is a Class, we will look up :class scoped + # instance variables. # # * name_pin.binder: Used for method call lookups only # (Chain::Call links). For method calls as the first # element in the chain, 'name_pin.binder' should be the # same as name_pin.context above. For method calls later - # in the chain (e.g., 'b' in a.b.c), it should represent - # 'a'. + # in the chain, it changes. (e.g., for 'b' in a.b.c, it + # should represent the type of 'a'). # # @param locals [::Array] Any local # variables / method parameters etc visible by the statement @@ -113,11 +109,14 @@ def define api_map, name_pin, locals # # @todo ProxyType uses 'type' for the binder, but ' working_pin = name_pin + # @sg-ignore Need to add nil check here links[0..-2].each do |link| pins = link.resolve(api_map, working_pin, locals) type = infer_from_definitions(pins, working_pin, api_map, locals) if type.undefined? - logger.debug { "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.inspect}, locals=#{locals}) => [] - undefined type from #{link.desc}" } + logger.debug do + "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.inspect}, locals=#{locals}) => [] - undefined type from #{link.desc}" + end return [] end # We continue to use the context from the head pin, in case @@ -126,7 +125,9 @@ def define api_map, name_pin, locals # for the binder, as this is chaining off of it, and the # binder is now the lhs of the rhs we are evaluating. working_pin = Pin::ProxyType.anonymous(name_pin.context, binder: type, closure: name_pin, source: :chain) - logger.debug { "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.inspect}, locals=#{locals}) - after processing #{link.desc}, new working_pin=#{working_pin} with binder #{working_pin.binder}" } + logger.debug do + "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.inspect}, locals=#{locals}) - after processing #{link.desc}, new working_pin=#{working_pin} with binder #{working_pin.binder}" + end end links.last.last_context = working_pin links.last.resolve(api_map, working_pin, locals) @@ -138,7 +139,8 @@ def define api_map, name_pin, locals # @return [ComplexType] # @sg-ignore def infer api_map, name_pin, locals - cache_key = [node, node&.location, links, name_pin&.return_type, locals] + # includes binder as it is mutable in Pin::Block + cache_key = [node, node&.location, links, name_pin&.return_type, name_pin&.binder, locals] if @@inference_invalidation_key == api_map.hash cached = @@inference_cache[cache_key] return cached if cached @@ -147,23 +149,29 @@ def infer api_map, name_pin, locals @@inference_cache = {} end out = infer_uncached(api_map, name_pin, locals).downcast_to_literal_if_possible - logger.debug { "Chain#infer() - caching result - cache_key_hash=#{cache_key.hash}, links.map(&:hash)=#{links.map(&:hash)}, links=#{links}, cache_key.map(&:hash) = #{cache_key.map(&:hash)}, cache_key=#{cache_key}" } + logger.debug do + "Chain#infer() - caching result - cache_key_hash=#{cache_key.hash}, links.map(&:hash)=#{links.map(&:hash)}, links=#{links}, cache_key.map(&:hash) = #{cache_key.map(&:hash)}, cache_key=#{cache_key}" + end @@inference_cache[cache_key] = out end # @param api_map [ApiMap] # @param name_pin [Pin::Base] # @param locals [::Array] - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] def infer_uncached api_map, name_pin, locals pins = define(api_map, name_pin, locals) if pins.empty? - logger.debug { "Chain#infer_uncached(links=#{links.map(&:desc)}, locals=#{locals.map(&:desc)}) => undefined - no pins" } + logger.debug do + "Chain#infer_uncached(links=#{links.map(&:desc)}, locals=#{locals.map(&:desc)}) => undefined - no pins" + end return ComplexType::UNDEFINED end type = infer_from_definitions(pins, links.last.last_context, api_map, locals) out = maybe_nil(type) - logger.debug { "Chain#infer_uncached(links=#{self.links.map(&:desc)}, locals=#{locals.map(&:desc)}, name_pin=#{name_pin}, name_pin.closure=#{name_pin.closure.inspect}, name_pin.binder=#{name_pin.binder}) => #{out.rooted_tags.inspect}" } + logger.debug do + "Chain#infer_uncached(links=#{links.map(&:desc)}, locals=#{locals.map(&:desc)}, name_pin=#{name_pin}, name_pin.closure=#{name_pin.closure.inspect}, name_pin.binder=#{name_pin.binder}) => #{out.rooted_tags.inspect}" + end out end @@ -209,12 +217,12 @@ def to_s private # @param pins [::Array] - # @param context [Pin::Base] + # @param name_pin [Pin::Base] # @param api_map [ApiMap] # @param locals [::Enumerable] - # @return [ComplexType] - def infer_from_definitions pins, context, api_map, locals - # @type [::Array] + # @return [ComplexType, ComplexType::UniqueType] + def infer_from_definitions pins, name_pin, api_map, locals + # @type [::Array] types = [] unresolved_pins = [] # @todo this param tag shouldn't be needed to probe the type @@ -232,7 +240,8 @@ def infer_from_definitions pins, context, api_map, locals # @todo even at strong, no typechecking complaint # happens when a [Pin::Base,nil] is passed into a method # that accepts only [Pin::Namespace] as an argument - type = type.resolve_generics(pin.closure, context.binder) + # @sg-ignore Need to add nil check here + type = type.resolve_generics(pin.closure, name_pin.binder) end types << type else @@ -241,17 +250,13 @@ def infer_from_definitions pins, context, api_map, locals end # Limit method inference recursion - if @@inference_depth >= 10 && pins.first.is_a?(Pin::Method) - return ComplexType::UNDEFINED - end + return ComplexType::UNDEFINED if @@inference_depth >= 10 && pins.first.is_a?(Pin::Method) @@inference_depth += 1 # @param pin [Pin::Base] unresolved_pins.each do |pin| # Avoid infinite recursion - if @@inference_stack.include?(pin.identity) - next - end + next if @@inference_stack.include?(pin.identity) @@inference_stack.push(pin.identity) type = pin.probe(api_map) @@ -271,21 +276,27 @@ def infer_from_definitions pins, context, api_map, locals else ComplexType.new(types) end - if context.nil? || context.return_type.undefined? + if name_pin.nil? || name_pin.context.undefined? # up to downstream to resolve self type return type end - - type.self_to_type(context.return_type) + type.self_to_type(name_pin.context) end - # @param type [ComplexType] - # @return [ComplexType] + # @param type [ComplexType, ComplexType::UniqueType] + # @return [ComplexType, ComplexType::UniqueType] def maybe_nil type return type if type.undefined? || type.void? || type.nullable? return type unless nullable? ComplexType.new(type.items + [ComplexType::NIL]) end + + protected + + # @sg-ignore Fix "Not enough arguments to Module#protected" + def equality_fields + [links, node] + end end end end diff --git a/lib/solargraph/source/chain/array.rb b/lib/solargraph/source/chain/array.rb index 544934d8a..352ccf8f0 100644 --- a/lib/solargraph/source/chain/array.rb +++ b/lib/solargraph/source/chain/array.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Solargraph class Source class Chain @@ -20,11 +22,11 @@ def resolve api_map, name_pin, locals child_types = @children.map do |child| child.infer(api_map, name_pin, locals).simplify_literals end - type = if child_types.length == 0 || child_types.any?(&:undefined?) + type = if child_types.empty? || child_types.any?(&:undefined?) ComplexType::UniqueType.new('Array', rooted: true) elsif child_types.uniq.length == 1 && child_types.first.defined? ComplexType::UniqueType.new('Array', [], child_types.uniq, rooted: true, parameters_type: :list) - elsif child_types.length == 0 + elsif child_types.empty? ComplexType::UniqueType.new('Array', rooted: true, parameters_type: :list) else ComplexType::UniqueType.new('Array', [], child_types, rooted: true, parameters_type: :fixed) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 89a28b0fa..7d71077c1 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -28,18 +28,14 @@ class Call < Chain::Link # @param arguments [::Array] # @param block [Chain, nil] def initialize word, location = nil, arguments = [], block = nil - @word = word + super(word) + @location = location @arguments = arguments @block = block fix_block_pass end - # @sg-ignore Fix "Not enough arguments to Module#protected" - protected def equality_fields - super + [arguments, block] - end - def with_block? !!@block end @@ -50,24 +46,32 @@ def with_block? def resolve api_map, name_pin, locals return super_pins(api_map, name_pin) if word == 'super' return yield_pins(api_map, name_pin) if word == 'yield' - found = if head? - api_map.visible_pins(locals, word, name_pin, location) - else - [] - end - return inferred_pins(found, api_map, name_pin, locals) unless found.empty? - pins = name_pin.binder.each_unique_type.flat_map do |context| + found = api_map.var_at_location(locals, word, name_pin, location) if head? + + return inferred_pins([found], api_map, name_pin, locals) unless found.nil? + binder = name_pin.binder + # this is a q_call - i.e., foo&.bar - assume result of call + # will be nil or result as if binder were not nil - + # chain.rb#maybe_nil will add the nil type later, we just + # need to worry about the not-nil case + + # @sg-ignore Need to handle duck-typed method calls on union types + binder = binder.without_nil if nullable? + # @sg-ignore Need to handle duck-typed method calls on union types + pin_groups = binder.each_unique_type.map do |context| ns_tag = context.namespace == '' ? '' : context.namespace_type.tag stack = api_map.get_method_stack(ns_tag, word, scope: context.scope) [stack.first].compact end + pin_groups = [] if !api_map.loose_unions && pin_groups.any?(&:empty?) + pins = pin_groups.flatten.uniq(&:path) return [] if pins.empty? inferred_pins(pins, api_map, name_pin, locals) end private - # @param pins [::Enumerable] + # @param pins [::Enumerable] # @param api_map [ApiMap] # @param name_pin [Pin::Base] # @param locals [::Array] @@ -84,9 +88,13 @@ def inferred_pins pins, api_map, name_pin, locals # reject it regardless with_block, without_block = overloads.partition(&:block?) + # @sg-ignore flow sensitive typing should handle is_a? and next + # @type Array sorted_overloads = with_block + without_block # @type [Pin::Signature, nil] new_signature_pin = nil + # @sg-ignore flow sensitive typing should handle is_a? and next + # @param ol [Pin::Signature] sorted_overloads.each do |ol| next unless ol.arity_matches?(arguments, with_block?) match = true @@ -99,6 +107,7 @@ def inferred_pins pins, api_map, name_pin, locals break end arg_name_pin = Pin::ProxyType.anonymous(name_pin.context, + closure: name_pin.closure, gates: name_pin.gates, source: :chain) atype = atypes[idx] ||= arg.infer(api_map, arg_name_pin, locals) @@ -110,37 +119,43 @@ def inferred_pins pins, api_map, name_pin, locals if match if ol.block && with_block? block_atypes = ol.block.parameters.map(&:return_type) - if block.links.map(&:class) == [BlockSymbol] - # like the bar in foo(&:bar) - blocktype = block_symbol_call_type(api_map, name_pin.context, block_atypes, locals) - else - blocktype = block_call_type(api_map, name_pin, locals) - end + # @todo Need to add nil check here + blocktype = if block.links.map(&:class) == [BlockSymbol] + # like the bar in foo(&:bar) + block_symbol_call_type(api_map, name_pin.context, block_atypes, locals) + else + block_call_type(api_map, name_pin, locals) + end end # @type new_signature_pin [Pin::Signature] - new_signature_pin = ol.resolve_generics_from_context_until_complete(ol.generics, atypes, nil, nil, blocktype) + new_signature_pin = ol.resolve_generics_from_context_until_complete(ol.generics, atypes, nil, nil, + blocktype) new_return_type = new_signature_pin.return_type - if head? - # If we're at the head of the chain, we called a - # method somewhere that marked itself as returning - # self. Given we didn't invoke this on an object, - # this must be a method in this same class - so we - # use our own self type - self_type = name_pin.context - else - # if we're past the head in the chain, whatever the - # type of the lhs side is what 'self' will be in its - # declaration - we can't just use the type of the - # method pin, as this might be a subclass of the - # place where the method is defined - self_type = name_pin.binder - end + self_type = if head? + # If we're at the head of the chain, we called a + # method somewhere that marked itself as returning + # self. Given we didn't invoke this on an object, + # this must be a method in this same class - so we + # use our own self type + name_pin.context + else + # if we're past the head in the chain, whatever the + # type of the lhs side is what 'self' will be in its + # declaration - we can't just use the type of the + # method pin, as this might be a subclass of the + # place where the method is defined + name_pin.binder + end # This same logic applies to the YARD work done by # 'with_params()'. # # qualify(), however, happens in the namespace where # the docs were written - from the method pin. - type = with_params(new_return_type.self_to_type(self_type), self_type).qualify(api_map, *p.gates) if new_return_type.defined? + # @todo Need to add nil check here + if new_return_type.defined? + type = with_params(new_return_type.self_to_type(self_type), self_type).qualify(api_map, + *p.gates) + end type ||= ComplexType::UNDEFINED end break if type.defined? @@ -149,21 +164,28 @@ def inferred_pins pins, api_map, name_pin, locals next p.proxy(type) if type.defined? if !p.macros.empty? result = process_macro(p, api_map, name_pin.context, locals) + # @sg-ignore flow sensitive typing should be able to handle redefinition next result unless result.return_type.undefined? elsif !p.directives.empty? result = process_directive(p, api_map, name_pin.context, locals) + # @sg-ignore flow sensitive typing should be able to handle redefinition next result unless result.return_type.undefined? end p end - logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, pins=#{pins.map(&:desc)}, name_pin=#{name_pin}) - result=#{result}" } - out = result.map do |pin| + logger.debug do + "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, pins=#{pins.map(&:desc)}, name_pin=#{name_pin}) - result=#{result}" + end + result.map do |pin| if pin.path == 'Class#new' && name_pin.binder.tag != 'Class' reduced_context = name_pin.binder.reduce_class_type pin.proxy(reduced_context) else + # @sg-ignore Need to add nil check here next pin if pin.return_type.undefined? + # @sg-ignore Need to add nil check here selfy = pin.return_type.self_to_type(name_pin.binder) + # @sg-ignore Need to add nil check here selfy == pin.return_type ? pin : pin.proxy(selfy) end end @@ -171,7 +193,7 @@ def inferred_pins pins, api_map, name_pin, locals # @param pin [Pin::Base] # @param api_map [ApiMap] - # @param context [ComplexType] + # @param context [ComplexType, ComplexType::UniqueType] # @param locals [::Array] # @return [Pin::Base] def process_macro pin, api_map, context, locals @@ -190,7 +212,7 @@ def process_macro pin, api_map, context, locals # @param pin [Pin::Method] # @param api_map [ApiMap] - # @param context [ComplexType] + # @param context [ComplexType, ComplexType::UniqueType] # @param locals [::Array] # @return [Pin::ProxyType] def process_directive pin, api_map, context, locals @@ -206,21 +228,24 @@ def process_directive pin, api_map, context, locals # @param pin [Pin::Base] # @param macro [YARD::Tags::MacroDirective] # @param api_map [ApiMap] - # @param context [ComplexType] + # @param context [ComplexType, ComplexType::UniqueType] # @param locals [::Array] # @return [Pin::ProxyType] def inner_process_macro pin, macro, api_map, context, locals - vals = arguments.map{ |c| Pin::ProxyType.anonymous(c.infer(api_map, pin, locals), source: :chain) } + vals = arguments.map { |c| Pin::ProxyType.anonymous(c.infer(api_map, pin, locals), source: :chain) } txt = macro.tag.text.clone + # @sg-ignore Need to add nil check here if txt.empty? && macro.tag.name named = api_map.named_macro(macro.tag.name) txt = named.tag.text.clone if named end i = 1 vals.each do |v| + # @sg-ignore Need to add nil check here txt.gsub!(/\$#{i}/, v.context.namespace) i += 1 end + # @sg-ignore Need to add nil check here docstring = Solargraph::Source.parse_docstring(txt).to_docstring tag = docstring.tag(:return) unless tag.nil? || tag.types.nil? @@ -233,7 +258,7 @@ def inner_process_macro pin, macro, api_map, context, locals # @param context [ComplexType] # @return [ComplexType, nil] def extra_return_type docstring, context - if docstring.has_tag?('return_single_parameter') #&& context.subtypes.one? + if docstring.has_tag?('return_single_parameter') # && context.subtypes.one? return context.subtypes.first || ComplexType::UNDEFINED elsif docstring.has_tag?('return_value_parameter') && context.value_types.one? return context.value_types.first @@ -243,9 +268,10 @@ def extra_return_type docstring, context # @param name_pin [Pin::Base] # @return [Pin::Method, nil] - def find_method_pin(name_pin) + def find_method_pin name_pin method_pin = name_pin until method_pin.is_a?(Pin::Method) + # @sg-ignore Need to support this in flow sensitive typing method_pin = method_pin.closure return if method_pin.nil? end @@ -259,7 +285,7 @@ def super_pins api_map, name_pin method_pin = find_method_pin(name_pin) return [] if method_pin.nil? pins = api_map.get_method_stack(method_pin.namespace, method_pin.name, scope: method_pin.context.scope) - pins.reject{|p| p.path == name_pin.path} + pins.reject { |p| p.path == name_pin.path } end # @param api_map [ApiMap] @@ -271,13 +297,14 @@ def yield_pins api_map, name_pin # @param signature_pin [Pin::Signature] method_pin.signatures.map(&:block).compact.map do |signature_pin| + # @sg-ignore Need to add nil check here return_type = signature_pin.return_type.qualify(api_map, *name_pin.gates) signature_pin.proxy(return_type) end end # @param type [ComplexType] - # @param context [ComplexType] + # @param context [ComplexType, ComplexType::UniqueType] # @return [ComplexType] def with_params type, context return type unless type.to_s.include?('$') @@ -291,13 +318,14 @@ def fix_block_pass end # @param api_map [ApiMap] - # @param context [ComplexType] + # @param context [ComplexType, ComplexType::UniqueType] # @param block_parameter_types [::Array] # @param locals [::Array] # @return [ComplexType, nil] - def block_symbol_call_type(api_map, context, block_parameter_types, locals) + def block_symbol_call_type api_map, context, block_parameter_types, locals # Ruby's shorthand for sending the passed in method name # to the first yield parameter with no arguments + # @sg-ignore Need to add nil check here block_symbol_name = block.links.first.word block_symbol_call_path = "#{block_parameter_types.first}##{block_symbol_name}" callee = api_map.get_path_pins(block_symbol_call_path).first @@ -305,31 +333,43 @@ def block_symbol_call_type(api_map, context, block_parameter_types, locals) # @todo: Figure out why we get unresolved generics at # this point and need to assume method return types # based on the generic type + # @sg-ignore Need to add nil check here return_type ||= api_map.get_path_pins("#{context.subtypes.first}##{block.links.first.word}").first&.return_type return_type || ComplexType::UNDEFINED end # @param api_map [ApiMap] # @return [Pin::Block, nil] - def find_block_pin(api_map) + def find_block_pin api_map + # @sg-ignore Need to add nil check here node_location = Solargraph::Location.from_node(block.node) - return if node_location.nil? + return if node_location.nil? block_pins = api_map.get_block_pins + # @sg-ignore Need to add nil check here block_pins.find { |pin| pin.location.contain?(node_location) } end # @param api_map [ApiMap] # @param name_pin [Pin::Base] - # @param block_parameter_types [::Array] # @param locals [::Array] # @return [ComplexType, nil] - def block_call_type(api_map, name_pin, locals) + def block_call_type api_map, name_pin, locals return nil unless with_block? - block_context_pin = name_pin block_pin = find_block_pin(api_map) - block_context_pin = block_pin.closure if block_pin - block.infer(api_map, block_context_pin, locals) + # We use the block pin as the closure, as the parameters + # here will only be defined inside the block itself and we + # need to be able to see them + # @sg-ignore Need to add nil check here + block.infer(api_map, block_pin, locals) + end + + protected + + # @sg-ignore Fix "Not enough arguments to Module#protected" + def equality_fields + # @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array + super + [arguments, block] end end end diff --git a/lib/solargraph/source/chain/class_variable.rb b/lib/solargraph/source/chain/class_variable.rb index a804d89e5..f50028ffa 100644 --- a/lib/solargraph/source/chain/class_variable.rb +++ b/lib/solargraph/source/chain/class_variable.rb @@ -5,7 +5,7 @@ class Source class Chain class ClassVariable < Link def resolve api_map, name_pin, locals - api_map.get_class_variable_pins(name_pin.context.namespace).select{|p| p.name == word} + api_map.get_class_variable_pins(name_pin.context.namespace).select { |p| p.name == word } end end end diff --git a/lib/solargraph/source/chain/constant.rb b/lib/solargraph/source/chain/constant.rb index 2752ec136..e558a2e1f 100644 --- a/lib/solargraph/source/chain/constant.rb +++ b/lib/solargraph/source/chain/constant.rb @@ -6,18 +6,22 @@ class Chain class Constant < Link def initialize word @word = word + + super end def resolve api_map, name_pin, locals return [Pin::ROOT_PIN] if word.empty? if word.start_with?('::') - base = word[2..-1] + base = word[2..] gates = [''] else base = word gates = name_pin.gates end + # @sg-ignore Need to add nil check here fqns = api_map.resolve(base, gates) + # @sg-ignore Need to add nil check here api_map.get_path_pins(fqns) end end diff --git a/lib/solargraph/source/chain/global_variable.rb b/lib/solargraph/source/chain/global_variable.rb index 0842803a9..335b8e42c 100644 --- a/lib/solargraph/source/chain/global_variable.rb +++ b/lib/solargraph/source/chain/global_variable.rb @@ -5,7 +5,7 @@ class Source class Chain class GlobalVariable < Link def resolve api_map, name_pin, locals - api_map.get_global_variable_pins.select{|p| p.name == word} + api_map.get_global_variable_pins.select { |p| p.name == word } end end end diff --git a/lib/solargraph/source/chain/hash.rb b/lib/solargraph/source/chain/hash.rb index 045a7d116..a75963478 100644 --- a/lib/solargraph/source/chain/hash.rb +++ b/lib/solargraph/source/chain/hash.rb @@ -12,11 +12,6 @@ def initialize type, node, splatted = false @splatted = splatted end - # @sg-ignore Fix "Not enough arguments to Module#protected" - protected def equality_fields - super + [@splatted] - end - def word @word ||= "<#{@type}>" end @@ -28,6 +23,14 @@ def resolve api_map, name_pin, locals def splatted? @splatted end + + protected + + # @sg-ignore Fix "Not enough arguments to Module#protected" + def equality_fields + # @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array + super + [@splatted] + end end end end diff --git a/lib/solargraph/source/chain/if.rb b/lib/solargraph/source/chain/if.rb index 3a7fa0ca9..186f6e6f0 100644 --- a/lib/solargraph/source/chain/if.rb +++ b/lib/solargraph/source/chain/if.rb @@ -4,23 +4,25 @@ module Solargraph class Source class Chain class If < Link - def word - '' - end - # @param links [::Array] def initialize links - @links = links - end + super('') - # @sg-ignore Fix "Not enough arguments to Module#protected" - protected def equality_fields - super + [@links] + @links = links end def resolve api_map, name_pin, locals types = @links.map { |link| link.infer(api_map, name_pin, locals) } - [Solargraph::Pin::ProxyType.anonymous(Solargraph::ComplexType.try_parse(types.map(&:tag).uniq.join(', ')), source: :chain)] + [Solargraph::Pin::ProxyType.anonymous(Solargraph::ComplexType.try_parse(types.map(&:tag).uniq.join(', ')), + source: :chain)] + end + + protected + + # @sg-ignore Fix "Not enough arguments to Module#protected" + def equality_fields + # @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array + super + [@links] end end end diff --git a/lib/solargraph/source/chain/instance_variable.rb b/lib/solargraph/source/chain/instance_variable.rb index ea09f5578..60df51bfe 100644 --- a/lib/solargraph/source/chain/instance_variable.rb +++ b/lib/solargraph/source/chain/instance_variable.rb @@ -4,9 +4,32 @@ module Solargraph class Source class Chain class InstanceVariable < Link + # @param word [String] + # @param node [Parser::AST::Node, nil] The node representing the variable + # @param location [Location, nil] The location of the variable reference in the source + def initialize word, node, location + super(word) + @node = node + @location = location + end + + # @sg-ignore Declared return type + # ::Array<::Solargraph::Pin::Base> does not match inferred + # type ::Array<::Solargraph::Pin::BaseVariable, ::NilClass> + # for Solargraph::Source::Chain::InstanceVariable#resolve def resolve api_map, name_pin, locals - api_map.get_instance_variable_pins(name_pin.binder.namespace, name_pin.binder.scope).select{|p| p.name == word} + ivars = api_map.get_instance_variable_pins(name_pin.context.namespace, name_pin.context.scope).select do |p| + p.name == word + end + out = api_map.var_at_location(ivars, word, name_pin, location) + [out].compact end + + private + + # @todo: Missed nil violation + # @return [Location] + attr_reader :location end end end diff --git a/lib/solargraph/source/chain/link.rb b/lib/solargraph/source/chain/link.rb index 344f7affd..4eb6c7d5c 100644 --- a/lib/solargraph/source/chain/link.rb +++ b/lib/solargraph/source/chain/link.rb @@ -17,17 +17,6 @@ def initialize word = '' @word = word end - # @sg-ignore two problems - Declared return type - # ::Solargraph::Source::Chain::Array does not match inferred - # type ::Array(::Class<::Solargraph::Source::Chain::Link>, - # ::String) for - # Solargraph::Source::Chain::Link#equality_fields - # and - # Not enough arguments to Module#protected - protected def equality_fields - [self.class, word] - end - def undefined? word == '' end @@ -44,18 +33,12 @@ def resolve api_map, name_pin, locals [] end - # debugging description of contents; not for machine use - # @return [String] - def desc - word - end - def to_s desc end def inspect - "#<#{self.class} - `#{self.desc}`>" + "#<#{self.class} - `#{desc}`>" end def head? @@ -87,14 +70,21 @@ def desc word end - def inspect - "#<#{self.class} - `#{self.desc}`>" - end - include Logging protected + # @sg-ignore two problems - Declared return type + # ::Solargraph::Source::Chain::Array does not match inferred + # type ::Array(::Class<::Solargraph::Source::Chain::Link>, + # ::String) for + # Solargraph::Source::Chain::Link#equality_fields + # and + # Not enough arguments to Module#protected + def equality_fields + [self.class, word] + end + # Mark whether this link is the head of a chain # # @param bool [Boolean] diff --git a/lib/solargraph/source/chain/literal.rb b/lib/solargraph/source/chain/literal.rb index 2e0d65c9e..e8d9b753c 100644 --- a/lib/solargraph/source/chain/literal.rb +++ b/lib/solargraph/source/chain/literal.rb @@ -6,21 +6,23 @@ module Solargraph class Source class Chain class Literal < Link - def word - @word ||= "<#{@type}>" - end - - attr_reader :value + attr_reader :word, :value # @param type [String] # @param node [Parser::AST::Node, Object] def initialize type, node + super("<#{type}>") + if node.is_a?(::Parser::AST::Node) + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check if node.type == :true @value = true + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check elsif node.type == :false @value = false - elsif [:int, :sym].include?(node.type) + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check + elsif %i[int sym].include?(node.type) + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check @value = node.children.first end end @@ -31,6 +33,7 @@ def initialize type, node # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields + # @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array super + [@value, @type, @literal_type, @complex_type] end diff --git a/lib/solargraph/source/chain/or.rb b/lib/solargraph/source/chain/or.rb index 9264d4107..327d465b7 100644 --- a/lib/solargraph/source/chain/or.rb +++ b/lib/solargraph/source/chain/or.rb @@ -4,18 +4,24 @@ module Solargraph class Source class Chain class Or < Link - def word - '' - end + attr_reader :links # @param links [::Array] def initialize links + super('') + @links = links end def resolve api_map, name_pin, locals types = @links.map { |link| link.infer(api_map, name_pin, locals) } - [Solargraph::Pin::ProxyType.anonymous(Solargraph::ComplexType.new(types.uniq), source: :chain)] + combined_type = Solargraph::ComplexType.new(types) + unless types.all?(&:nullable?) + # @sg-ignore flow sensitive typing should be able to handle redefinition + combined_type = combined_type.without_nil + end + + [Solargraph::Pin::ProxyType.anonymous(combined_type, source: :chain)] end end end diff --git a/lib/solargraph/source/chain/q_call.rb b/lib/solargraph/source/chain/q_call.rb index 811594f7d..7247cf4cc 100644 --- a/lib/solargraph/source/chain/q_call.rb +++ b/lib/solargraph/source/chain/q_call.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Solargraph class Source class Chain diff --git a/lib/solargraph/source/chain/variable.rb b/lib/solargraph/source/chain/variable.rb index 975fbf6f5..8bf424e3b 100644 --- a/lib/solargraph/source/chain/variable.rb +++ b/lib/solargraph/source/chain/variable.rb @@ -5,7 +5,9 @@ class Source class Chain class Variable < Link def resolve api_map, name_pin, locals - api_map.get_instance_variable_pins(name_pin.context.namespace, name_pin.context.scope).select{|p| p.name == word} + api_map.get_instance_variable_pins(name_pin.context.namespace, name_pin.context.scope).select do |p| + p.name == word + end end end end diff --git a/lib/solargraph/source/chain/z_super.rb b/lib/solargraph/source/chain/z_super.rb index 5b0106c92..f6ef77106 100644 --- a/lib/solargraph/source/chain/z_super.rb +++ b/lib/solargraph/source/chain/z_super.rb @@ -11,9 +11,7 @@ class ZSuper < Call attr_reader :arguments # @param word [String] - # @param arguments [::Array] # @param with_block [Boolean] True if the chain is inside a block - # @param head [Boolean] True if the call is the start of its chain def initialize word, with_block = false super(word, nil, [], with_block) end @@ -22,7 +20,7 @@ def initialize word, with_block = false # @param name_pin [Pin::Base] # @param locals [::Array] def resolve api_map, name_pin, locals - return super_pins(api_map, name_pin) + super_pins(api_map, name_pin) end end end diff --git a/lib/solargraph/source/change.rb b/lib/solargraph/source/change.rb index 65c47c7e0..acea51b67 100644 --- a/lib/solargraph/source/change.rb +++ b/lib/solargraph/source/change.rb @@ -7,13 +7,13 @@ class Source class Change include EncodingFixes - # @return [Range] + # @return [Range, nil] attr_reader :range # @return [String] attr_reader :new_text - # @param range [Range] The starting and ending positions of the change. + # @param range [Range, nil] The starting and ending positions of the change. # If nil, the original text will be overwritten. # @param new_text [String] The text to be changed. def initialize range, new_text @@ -28,12 +28,14 @@ def initialize range, new_text # syntax errors will be repaired. # @return [String] The updated text. def write text, nullable = false - if nullable and !range.nil? and new_text.match(/[.\[{(@$:]$/) + if nullable && !range.nil? && new_text.match(/[.\[{(@$:]$/) [':', '@'].each do |dupable| next unless new_text == dupable + # @sg-ignore flow sensitive typing needs to handle attrs offset = Position.to_offset(text, range.start) if text[offset - 1] == dupable p = Position.from_offset(text, offset - 1) + # @sg-ignore flow sensitive typing needs to handle attrs r = Change.new(Range.new(p, range.start), ' ') text = r.write(text) end @@ -58,10 +60,13 @@ def repair text fixed else result = commit text, fixed + # @sg-ignore flow sensitive typing needs to handle attrs off = Position.to_offset(text, range.start) + # @sg-ignore Need to add nil check here match = result[0, off].match(/[.:]+\z/) if match - result = result[0, off].sub(/#{match[0]}\z/, ' ' * match[0].length) + result[off..-1] + # @sg-ignore flow sensitive typing should be able to handle redefinition + result = result[0, off].sub(/#{match[0]}\z/, ' ' * match[0].length) + result[off..] end result end @@ -73,9 +78,11 @@ def repair text # @param insert [String] # @return [String] def commit text, insert + # @sg-ignore Need to add nil check here start_offset = Position.to_offset(text, range.start) + # @sg-ignore Need to add nil check here end_offset = Position.to_offset(text, range.ending) - (start_offset == 0 ? '' : text[0..start_offset-1].to_s) + normalize(insert) + text[end_offset..-1].to_s + (start_offset.zero? ? '' : text[0..(start_offset - 1)].to_s) + normalize(insert) + text[end_offset..].to_s end end end diff --git a/lib/solargraph/source/cursor.rb b/lib/solargraph/source/cursor.rb index a8226eb07..1afa1c76c 100644 --- a/lib/solargraph/source/cursor.rb +++ b/lib/solargraph/source/cursor.rb @@ -19,7 +19,7 @@ def initialize source, position @position = Position.normalize(position) end - # @return [String] + # @return [String, nil] def filename source.filename end @@ -35,13 +35,17 @@ def word # The part of the word before the current position. Given the text # `foo.bar`, the start_of_word at position(0, 6) is `ba`. # + # @sg-ignore Need to add nil check here # @return [String] def start_of_word @start_of_word ||= begin - match = source.code[0..offset-1].to_s.match(start_word_pattern) + match = source.code[0..(offset - 1)].to_s.match(start_word_pattern) result = (match ? match[0] : '') # Including the preceding colon if the word appears to be a symbol - result = ":#{result}" if source.code[0..offset-result.length-1].end_with?(':') and !source.code[0..offset-result.length-1].end_with?('::') + # @sg-ignore Need to add nil check here + if source.code[0..(offset - result.length - 1)].end_with?(':') && !source.code[0..(offset - result.length - 1)].end_with?('::') + result = ":#{result}" + end result end end @@ -50,16 +54,17 @@ def start_of_word # `foo.bar`, the end_of_word at position (0,6) is `r`. # # @return [String] + # @sg-ignore Need to add nil check here def end_of_word @end_of_word ||= begin - match = source.code[offset..-1].to_s.match(end_word_pattern) + match = source.code[offset..].to_s.match(end_word_pattern) match ? match[0] : '' end end # @return [Boolean] def start_of_constant? - source.code[offset-2, 2] == '::' + source.code[offset - 2, 2] == '::' end # The range of the word at the current position. @@ -110,6 +115,7 @@ def string? def recipient @recipient ||= begin node = recipient_node + # @sg-ignore Need to add nil check here node ? Cursor.new(source, Range.from_node(node).ending) : nil end end @@ -122,18 +128,18 @@ def node # @return [Position] def node_position - @node_position ||= begin - if start_of_word.empty? - match = source.code[0, offset].match(/\s*(\.|:+)\s*$/) - if match - Position.from_offset(source.code, offset - match[0].length) - else - position - end - else - position - end - end + @node_position ||= if start_of_word.empty? + # @sg-ignore Need to add nil check here + match = source.code[0, offset].match(/\s*(\.|:+)\s*$/) + if match + # @sg-ignore Need to add nil check here + Position.from_offset(source.code, offset - match[0].length) + else + position + end + else + position + end end # @return [Parser::AST::Node, nil] diff --git a/lib/solargraph/source/encoding_fixes.rb b/lib/solargraph/source/encoding_fixes.rb index 2ed70037c..0bb8d7f7e 100644 --- a/lib/solargraph/source/encoding_fixes.rb +++ b/lib/solargraph/source/encoding_fixes.rb @@ -10,13 +10,12 @@ module EncodingFixes # @param string [String] # @return [String] def normalize string - begin - string.dup.force_encoding('UTF-8') - rescue ::Encoding::CompatibilityError, ::Encoding::UndefinedConversionError, ::Encoding::InvalidByteSequenceError => e - # @todo Improve error handling - Solargraph::Logging.logger.warn "Normalize error: #{e.message}" - string - end + string.dup.force_encoding('UTF-8') + rescue ::Encoding::CompatibilityError, ::Encoding::UndefinedConversionError, + ::Encoding::InvalidByteSequenceError => e + # @todo Improve error handling + Solargraph::Logging.logger.warn "Normalize error: #{e.message}" + string end end end diff --git a/lib/solargraph/source/source_chainer.rb b/lib/solargraph/source/source_chainer.rb index 5758a9d35..b410e0214 100644 --- a/lib/solargraph/source/source_chainer.rb +++ b/lib/solargraph/source/source_chainer.rb @@ -32,26 +32,47 @@ def initialize source, position # @return [Source::Chain] def chain # Special handling for files that end with an integer and a period - return Chain.new([Chain::Literal.new('Integer', Integer(phrase[0..-2])), Chain::UNDEFINED_CALL]) if phrase =~ /^[0-9]+\.$/ - return Chain.new([Chain::Literal.new('Symbol', phrase[1..].to_sym)]) if phrase.start_with?(':') && !phrase.start_with?('::') - return SourceChainer.chain(source, Position.new(position.line, position.character + 1)) if end_of_phrase.strip == '::' && source.code[Position.to_offset(source.code, position)].to_s.match?(/[a-z]/i) + if phrase =~ /^[0-9]+\.$/ + return Chain.new([Chain::Literal.new('Integer', Integer(phrase[0..-2])), + Chain::UNDEFINED_CALL]) + end + if phrase.start_with?(':') && !phrase.start_with?('::') + return Chain.new([Chain::Literal.new('Symbol', + # @sg-ignore Need to add nil check here + phrase[1..].to_sym)]) + end + if end_of_phrase.strip == '::' && source.code[Position.to_offset( + source.code, position + )].to_s.match?(/[a-z]/i) + return SourceChainer.chain(source, + Position.new(position.line, + position.character + 1)) + end begin return Chain.new([]) if phrase.end_with?('..') + # @type [::Parser::AST::Node, nil] node = nil + # @type [::Parser::AST::Node, nil] parent = nil if !source.repaired? && source.parsed? && source.synchronized? tree = source.tree_at(position.line, position.column) node, parent = tree[0..2] elsif source.parsed? && source.repaired? && end_of_phrase == '.' node, parent = source.tree_at(fixed_position.line, fixed_position.column)[0..2] - node = Parser.parse(fixed_phrase) if node.nil? + # provide filename and line so that we can look up local variables there later + node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) if node.nil? elsif source.repaired? - node = Parser.parse(fixed_phrase) + node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) else - node, parent = source.tree_at(fixed_position.line, fixed_position.column)[0..2] unless source.error_ranges.any?{|r| r.nil? || r.include?(fixed_position)} + unless source.error_ranges.any? do |r| + r.nil? || r.include?(fixed_position) + end + node, parent = source.tree_at(fixed_position.line, + fixed_position.column)[0..2] + end # Exception for positions that chain literal nodes in unsynchronized sources node = nil unless source.synchronized? || !Parser.infer_literal_node_type(node).nil? - node = Parser.parse(fixed_phrase) if node.nil? + node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) if node.nil? end rescue Parser::SyntaxError return Chain.new([Chain::UNDEFINED_CALL]) @@ -79,14 +100,16 @@ def chain # @return [Solargraph::Source] attr_reader :source + # @sg-ignore Need to add nil check here # @return [String] def phrase - @phrase ||= source.code[signature_data..offset-1] + @phrase ||= source.code[signature_data..(offset - 1)] end + # @sg-ignore Need to add nil check here # @return [String] def fixed_phrase - @fixed_phrase ||= phrase[0..-(end_of_phrase.length+1)] + @fixed_phrase ||= phrase[0..-(end_of_phrase.length + 1)] end # @return [Position] @@ -95,6 +118,7 @@ def fixed_position end # @return [String] + # @sg-ignore Need to add nil check here def end_of_phrase @end_of_phrase ||= begin match = phrase.match(/\s*(\.{1}|::)\s*$/) @@ -137,47 +161,47 @@ def get_signature_data_at index brackets = 0 squares = 0 parens = 0 - index -=1 + index -= 1 in_whitespace = false while index >= 0 pos = Position.from_offset(@source.code, index) - break if index > 0 and @source.comment_at?(pos) - break if brackets > 0 or parens > 0 or squares > 0 + break if index.positive? && @source.comment_at?(pos) + break if brackets.positive? || parens.positive? || squares.positive? char = @source.code[index, 1] break if char.nil? # @todo Is this the right way to handle this? - if brackets.zero? and parens.zero? and squares.zero? and [' ', "\r", "\n", "\t"].include?(char) + if brackets.zero? && parens.zero? && squares.zero? && [' ', "\r", "\n", "\t"].include?(char) in_whitespace = true else - if brackets.zero? and parens.zero? and squares.zero? and in_whitespace - unless char == '.' or @source.code[index+1..-1].strip.start_with?('.') - old = @source.code[index+1..-1] - nxt = @source.code[index+1..-1].lstrip - index += (@source.code[index+1..-1].length - @source.code[index+1..-1].lstrip.length) - break - end + # @sg-ignore Need to add nil check here + if brackets.zero? && parens.zero? && squares.zero? && in_whitespace && !((char == '.') || @source.code[(index + 1)..].strip.start_with?('.')) + @source.code[(index + 1)..] + # @sg-ignore Need to add nil check here + @source.code[(index + 1)..].lstrip + # @sg-ignore Need to add nil check here + index += (@source.code[(index + 1)..].length - @source.code[(index + 1)..].lstrip.length) + break end - if char == ')' - parens -=1 - elsif char == ']' - squares -=1 - elsif char == '}' + case char + when ')' + parens -= 1 + when ']' + squares -= 1 + when '}' brackets -= 1 - elsif char == '(' + when '(' parens += 1 - elsif char == '{' + when '{' brackets += 1 - elsif char == '[' + when '[' squares += 1 end - if brackets.zero? and parens.zero? and squares.zero? + if brackets.zero? && parens.zero? && squares.zero? break if ['"', "'", ',', ';', '%'].include?(char) break if ['!', '?'].include?(char) && index < offset - 1 break if char == '$' if char == '@' index -= 1 - if @source.code[index, 1] == '@' - index -= 1 - end + index -= 1 if @source.code[index, 1] == '@' break end elsif parens == 1 || brackets == 1 || squares == 1 diff --git a/lib/solargraph/source/updater.rb b/lib/solargraph/source/updater.rb index 496d534ab..9debc0283 100644 --- a/lib/solargraph/source/updater.rb +++ b/lib/solargraph/source/updater.rb @@ -29,14 +29,18 @@ def initialize filename, version, changes # @param text [String] # @param nullable [Boolean] + # @sg-ignore changes doesn't mutate @output, so this can never be nil # @return [String] def write text, nullable = false can_nullify = (nullable and changes.length == 1) - return @output if @input == text and can_nullify == @did_nullify + return @output if (@input == text) && (can_nullify == @did_nullify) @input = text @output = text @did_nullify = can_nullify changes.each do |ch| + # @sg-ignore Wrong argument type for + # Solargraph::Source::Change#write: text expected String, + # received String, nil @output = ch.write(@output, can_nullify) end @output diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index 15b747760..18f623993 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -34,6 +34,8 @@ def locals # @param source [Source] def initialize source @source = source + # @type [Array, nil] + @convention_pins = nil conventions_environ.merge Convention.for_local(self) unless filename.nil? # FIXME: unmemoizing the document_symbols in case it was called and memoized from any of conventions above @@ -60,10 +62,12 @@ def pins_by_class klass # # @return [Integer] def api_hash - @api_hash ||= (pins_by_class(Pin::Constant) + pins_by_class(Pin::Namespace).select { |pin| pin.namespace.to_s > '' } + pins_by_class(Pin::Reference) + pins_by_class(Pin::Method).map(&:node) + locals).hash + @api_hash ||= (pins_by_class(Pin::Constant) + pins_by_class(Pin::Namespace).select do |pin| + pin.namespace.to_s > '' + end + pins_by_class(Pin::Reference) + pins_by_class(Pin::Method).map(&:node) + locals).hash end - # @return [String] + # @return [String, nil] def filename source.filename end @@ -84,6 +88,7 @@ def conventions_environ end # all pins except Solargraph::Pin::Reference::Reference + # # @return [Array] def document_symbols @document_symbols ||= (pins + convention_pins).select do |pin| @@ -97,7 +102,7 @@ def query_symbols query Pin::Search.new(document_symbols, query).results end - # @param position [Position] + # @param position [Position, Array(Integer, Integer)] # @return [Source::Cursor] def cursor_at position Source::Cursor.new(source, position) @@ -125,7 +130,7 @@ def locate_named_path_pin line, character # @param line [Integer] # @param character [Integer] - # @return [Pin::Namespace,Pin::Method,Pin::Block] + # @return [Pin::Closure] def locate_closure_pin line, character _locate_pin line, character, Pin::Closure end @@ -141,9 +146,9 @@ def references name # @param location [Location] # @return [Array] - def locals_at(location) + def locals_at location return [] if location.filename != filename - closure = locate_named_path_pin(location.range.start.line, location.range.start.character) + closure = locate_closure_pin(location.range.start.line, location.range.start.character) locals.select { |pin| pin.visible_at?(closure, location) } end @@ -178,6 +183,7 @@ def map source # @return [Hash{Class => Array}] def pin_class_hash + # @todo Need to support generic resolution in classify and transform_values @pin_class_hash ||= pins.to_set.classify(&:class).transform_values(&:to_a) end @@ -191,10 +197,12 @@ def convention_pins @convention_pins || [] end + # @generic T # @param line [Integer] # @param character [Integer] - # @param klasses [Array] - # @return [Pin::Base, nil] + # @param klasses [Array>>] + # @return [generic, nil] + # @sg-ignore Need better generic inference here def _locate_pin line, character, *klasses position = Position.new(line, character) found = nil @@ -202,7 +210,11 @@ def _locate_pin line, character, *klasses # @todo Attribute pins should not be treated like closures, but # there's probably a better way to handle it next if pin.is_a?(Pin::Method) && pin.attribute? - found = pin if (klasses.empty? || klasses.any? { |kls| pin.is_a?(kls) } ) && pin.location.range.contain?(position) + found = pin if (klasses.empty? || klasses.any? do |kls| + pin.is_a?(kls) + # @sg-ignore Need to add nil check here + end) && pin.location.range.contain?(position) + # @sg-ignore Need to add nil check here break if pin.location.range.start.line > line end # Assuming the root pin is always valid diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index 3d198ac1e..c1e7c24af 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -12,7 +12,10 @@ def initialize api_map, cursor @api_map = api_map @cursor = cursor closure_pin = closure - closure_pin.rebind(api_map) if closure_pin.is_a?(Pin::Block) && !Solargraph::Range.from_node(closure_pin.receiver).contain?(cursor.range.start) + # @sg-ignore Need to add nil check here + if closure_pin.is_a?(Pin::Block) && !Solargraph::Range.from_node(closure_pin.receiver).contain?(cursor.range.start) + closure_pin.rebind(api_map) + end end # @return [Array] Relevant pins for infering the type of the Cursor's position @@ -20,7 +23,12 @@ def define return [] if cursor.comment? || cursor.chain.literal? result = cursor.chain.define(api_map, closure, locals) result.concat file_global_methods - result.concat((source_map.pins + source_map.locals).select{ |p| p.name == cursor.word && p.location.range.contain?(cursor.position) }) if result.empty? + if result.empty? + result.concat((source_map.pins + source_map.locals).select do |p| + # @sg-ignore Need to add nil check here + p.name == cursor.word && p.location.range.contain?(cursor.position) + end) + end result end @@ -32,7 +40,9 @@ def types # @return [Completion] def complete return package_completions([]) if !source_map.source.parsed? || cursor.string? - return package_completions(api_map.get_symbols) if cursor.chain.literal? && cursor.chain.links.last.word == '' + if cursor.chain.literal? && cursor.chain.links.last.word == '' + return package_completions(api_map.get_symbols) + end return Completion.new([], cursor.range) if cursor.chain.literal? if cursor.comment? tag_complete @@ -78,7 +88,7 @@ def gates # @param phrase [String] # @return [Array] def translate phrase - chain = Parser.chain(Parser.parse(phrase)) + chain = Parser.chain(Parser.parse(phrase, cursor.filename, cursor.position.line)) chain.define(api_map, closure, locals) end @@ -92,6 +102,7 @@ def translate phrase # @return [SourceMap] def source_map + # @sg-ignore Need to add nil check here @source_map ||= api_map.source_map(cursor.filename) end @@ -125,12 +136,11 @@ def complete_keyword_parameters next unless param.keyword? result.push Pin::KeywordParam.new(pin.location, "#{param.name}:") end - if !pin.parameters.empty? && pin.parameters.last.kwrestarg? - pin.docstring.tags(:param).each do |tag| - next if done.include?(tag.name) - done.push tag.name - result.push Pin::KeywordParam.new(pin.location, "#{tag.name}:") - end + next unless !pin.parameters.empty? && pin.parameters.last.kwrestarg? + pin.docstring.tags(:param).each do |tag| + next if done.include?(tag.name) + done.push tag.name + result.push Pin::KeywordParam.new(pin.location, "#{tag.name}:") end end result @@ -140,27 +150,34 @@ def complete_keyword_parameters # @return [Completion] def package_completions result frag_start = cursor.start_of_word.to_s.downcase - filtered = result.uniq(&:name).select { |s| + filtered = result.uniq(&:name).select do |s| s.name.downcase.start_with?(frag_start) && - (!s.is_a?(Pin::Method) || s.name.match(/^[a-z0-9_]+(\!|\?|=)?$/i)) - } + (!s.is_a?(Pin::Method) || s.name.match(/^[a-z0-9_]+(!|\?|=)?$/i)) + end Completion.new(filtered, cursor.range) end # @return [Completion] def tag_complete result = [] - match = source_map.code[0..cursor.offset-1].match(/[\[<, ]([a-z0-9_:]*)\z/i) + # @sg-ignore Need to add nil check here + match = source_map.code[0..(cursor.offset - 1)].match(/[\[<, ]([a-z0-9_:]*)\z/i) if match + # @sg-ignore Need to add nil check here full = match[1] + # @sg-ignore Need to add nil check here if full.include?('::') + # @sg-ignore Need to add nil check here if full.end_with?('::') + # @sg-ignore Need to add nil check here result.concat api_map.get_constants(full[0..-3], *gates) else + # @sg-ignore Need to add nil check here result.concat api_map.get_constants(full.split('::')[0..-2].join('::'), *gates) end else - result.concat api_map.get_constants('', full.end_with?('::') ? '' : context_pin.full_context.namespace, *gates) #.select { |pin| pin.name.start_with?(full) } + # @sg-ignore Need to add nil check here + result.concat api_map.get_constants('', full.end_with?('::') ? '' : context_pin.full_context.namespace, *gates) # .select { |pin| pin.name.start_with?(full) } end end package_completions(result) @@ -173,24 +190,24 @@ def code_complete if cursor.chain.constant? || cursor.start_of_constant? full = cursor.chain.links.first.word type = if cursor.chain.undefined? - cursor.chain.base.infer(api_map, context_pin, locals) - else - if full.include?('::') && cursor.chain.links.length == 1 - ComplexType.try_parse(full.split('::')[0..-2].join('::')) - elsif cursor.chain.links.length > 1 - ComplexType.try_parse(full) - else - ComplexType::UNDEFINED - end - end + cursor.chain.base.infer(api_map, context_pin, locals) + elsif full.include?('::') && cursor.chain.links.length == 1 + # @sg-ignore Need to add nil check here + ComplexType.try_parse(full.split('::')[0..-2].join('::')) + elsif cursor.chain.links.length > 1 + ComplexType.try_parse(full) + else + ComplexType::UNDEFINED + end if type.undefined? if full.include?('::') result.concat api_map.get_constants(full, *gates) else - result.concat api_map.get_constants('', cursor.start_of_constant? ? '' : context_pin.full_context.namespace, *gates) #.select { |pin| pin.name.start_with?(full) } + result.concat api_map.get_constants('', cursor.start_of_constant? ? '' : context_pin.full_context.namespace, *gates) # .select { |pin| pin.name.start_with?(full) } end else - result.concat api_map.get_constants(type.namespace, cursor.start_of_constant? ? '' : context_pin.full_context.namespace, *gates) + result.concat api_map.get_constants(type.namespace, + cursor.start_of_constant? ? '' : context_pin.full_context.namespace, *gates) end else type = cursor.chain.base.infer(api_map, closure, locals) @@ -199,14 +216,16 @@ def code_complete if cursor.word.start_with?('@@') return package_completions(api_map.get_class_variable_pins(context_pin.full_context.namespace)) elsif cursor.word.start_with?('@') - return package_completions(api_map.get_instance_variable_pins(closure.binder.namespace, closure.binder.scope)) + return package_completions(api_map.get_instance_variable_pins(closure.full_context.namespace, + closure.context.scope)) elsif cursor.word.start_with?('$') return package_completions(api_map.get_global_variable_pins) end result.concat locals result.concat file_global_methods unless closure.binder.namespace.empty? result.concat api_map.get_constants(context_pin.context.namespace, *gates) - result.concat api_map.get_methods(closure.binder.namespace, scope: closure.binder.scope, visibility: [:public, :private, :protected]) + result.concat api_map.get_methods(closure.binder.namespace, scope: closure.binder.scope, + visibility: %i[public private protected]) result.concat api_map.get_methods('Kernel') result.concat api_map.keyword_pins.to_a end diff --git a/lib/solargraph/source_map/data.rb b/lib/solargraph/source_map/data.rb index 453520414..906273fbc 100644 --- a/lib/solargraph/source_map/data.rb +++ b/lib/solargraph/source_map/data.rb @@ -8,13 +8,16 @@ def initialize source @source = source end + # @sg-ignore Translate to something flow sensitive typing understands # @return [Array] + # @sg-ignore https://github.com/castwide/solargraph/pull/1100 def pins generate @pins || [] end - # @return [Array] + # @sg-ignore Translate to something flow sensitive typing understands + # @return [Array] def locals generate @locals || [] diff --git a/lib/solargraph/source_map/mapper.rb b/lib/solargraph/source_map/mapper.rb index 5fdcb9fe6..8c306473d 100644 --- a/lib/solargraph/source_map/mapper.rb +++ b/lib/solargraph/source_map/mapper.rb @@ -12,7 +12,7 @@ class Mapper private_class_method :new - DIRECTIVE_REGEXP = /(@\!method|@\!attribute|@\!visibility|@\!domain|@\!macro|@\!parse|@\!override)/.freeze + DIRECTIVE_REGEXP = /(@!method|@!attribute|@!visibility|@!domain|@!macro|@!parse|@!override)/ # Generate the data. # @@ -24,14 +24,15 @@ def map source @code = source.code @comments = source.comments @pins, @locals = Parser.map(source) + # @param p [Solargraph::Pin::Base] @pins.each { |p| p.source = :code } @locals.each { |l| l.source = :code } process_comment_directives [@pins, @locals] - # rescue Exception => e - # Solargraph.logger.warn "Error mapping #{source.filename}: [#{e.class}] #{e.message}" - # Solargraph.logger.warn e.backtrace.join("\n") - # [[], []] + # rescue Exception => e + # Solargraph.logger.warn "Error mapping #{source.filename}: [#{e.class}] #{e.message}" + # Solargraph.logger.warn e.backtrace.join("\n") + # [[], []] end # @param filename [String] @@ -48,6 +49,7 @@ class << self # @param source [Source] # @return [Array] def map source + # @sg-ignore Need to add nil check here return new.unmap(source.filename, source.code) unless source.parsed? new.map source end @@ -61,8 +63,9 @@ def pins # @param position [Solargraph::Position] # @return [Solargraph::Pin::Closure] - def closure_at(position) - pins.select{|pin| pin.is_a?(Pin::Closure) and pin.location.range.contain?(position)}.last + def closure_at position + # @sg-ignore Need to add nil check here + pins.select { |pin| pin.is_a?(Pin::Closure) and pin.location.range.contain?(position) }.last end # @param source_position [Position] @@ -90,11 +93,13 @@ def process_comment source_position, comment_position, comment def find_directive_line_number comment, tag, start # Avoid overruning the index return start unless start < comment.lines.length - num = comment.lines[start..-1].find_index do |line| + # @sg-ignore Need to add nil check here + num = comment.lines[start..].find_index do |line| # Legacy method directives might be `@method` instead of `@!method` # @todo Legacy syntax should probably emit a warning line.include?("@!#{tag}") || (tag == 'method' && line.include?("@#{tag}")) end + # @sg-ignore Need to add nil check here num.to_i + start end @@ -103,35 +108,37 @@ def find_directive_line_number comment, tag, start # @param directive [YARD::Tags::Directive] # @return [void] def process_directive source_position, comment_position, directive + # @sg-ignore Need to add nil check here docstring = Solargraph::Source.parse_docstring(directive.tag.text).to_docstring location = Location.new(@filename, Range.new(comment_position, comment_position)) case directive.tag.tag_name when 'method' namespace = closure_at(source_position) || @pins.first - if namespace.location.range.start.line < comment_position.line - namespace = closure_at(comment_position) - end + # @sg-ignore Need to add nil check here + namespace = closure_at(comment_position) if namespace.location.range.start.line < comment_position.line begin src = Solargraph::Source.load_string("def #{directive.tag.name};end", @source.filename) region = Parser::Region.new(source: src, closure: namespace) + # @type [Array] method_gen_pins = Parser.process_node(src.node, region).first.select { |pin| pin.is_a?(Pin::Method) } gen_pin = method_gen_pins.last return if gen_pin.nil? # Move the location to the end of the line so it gets recognized # as originating from a comment - shifted = Solargraph::Position.new(comment_position.line, @code.lines[comment_position.line].to_s.chomp.length) + shifted = Solargraph::Position.new(comment_position.line, + @code.lines[comment_position.line].to_s.chomp.length) # @todo: Smelly instance variable access gen_pin.instance_variable_set(:@comments, docstring.all.to_s) gen_pin.instance_variable_set(:@location, Solargraph::Location.new(@filename, Range.new(shifted, shifted))) gen_pin.instance_variable_set(:@explicit, false) @pins.push gen_pin - rescue Parser::SyntaxError => e + rescue Parser::SyntaxError # @todo Handle error in directive end when 'attribute' return if directive.tag.name.nil? namespace = closure_at(source_position) - t = (directive.tag.types.nil? || directive.tag.types.empty?) ? nil : directive.tag.types.flatten.join('') + t = directive.tag.types.nil? || directive.tag.types.empty? ? nil : directive.tag.types.join if t.nil? || t.include?('r') pins.push Solargraph::Pin::Method.new( location: location, @@ -157,57 +164,68 @@ def process_directive source_position, comment_position, directive source: :source_map ) pins.push method_pin - method_pin.parameters.push Pin::Parameter.new(name: 'value', decl: :arg, closure: pins.last, source: :source_map) + method_pin.parameters.push Pin::Parameter.new(name: 'value', decl: :arg, closure: pins.last, + source: :source_map) if pins.last.return_type.defined? - pins.last.docstring.add_tag YARD::Tags::Tag.new(:param, '', pins.last.return_type.to_s.split(', '), 'value') + pins.last.docstring.add_tag YARD::Tags::Tag.new(:param, '', pins.last.return_type.to_s.split(', '), + 'value') end end when 'visibility' - kind = directive.tag.text&.to_sym - return unless [:private, :protected, :public].include?(kind) + kind = directive.tag.text&.to_sym + # @sg-ignore Need to look at Tuple#include? handling + return unless %i[private protected public].include?(kind) - name = directive.tag.name - closure = closure_at(source_position) || @pins.first - if closure.location.range.start.line < comment_position.line - closure = closure_at(comment_position) + name = directive.tag.name + closure = closure_at(source_position) || @pins.first + # @sg-ignore Need to add nil check here + closure = closure_at(comment_position) if closure.location.range.start.line < comment_position.line + if closure.is_a?(Pin::Method) && no_empty_lines?(comment_position.line, source_position.line) + # @todo Smelly instance variable access + closure.instance_variable_set(:@visibility, kind) + else + matches = pins.select do |pin| + pin.is_a?(Pin::Method) && pin.name == name && pin.namespace == namespace && pin.context.scope == namespace.is_a?(Pin::Singleton) ? :class : :instance end - if closure.is_a?(Pin::Method) && no_empty_lines?(comment_position.line, source_position.line) + matches.each do |pin| # @todo Smelly instance variable access - closure.instance_variable_set(:@visibility, kind) - else - matches = pins.select{ |pin| pin.is_a?(Pin::Method) && pin.name == name && pin.namespace == namespace && pin.context.scope == namespace.is_a?(Pin::Singleton) ? :class : :instance } - matches.each do |pin| - # @todo Smelly instance variable access - pin.instance_variable_set(:@visibility, kind) - end + pin.instance_variable_set(:@visibility, kind) end + end when 'parse' begin ns = closure_at(source_position) + # @sg-ignore Need to add nil check here src = Solargraph::Source.load_string(directive.tag.text, @source.filename) region = Parser::Region.new(source: src, closure: ns) # @todo These pins may need to be marked not explicit index = @pins.length loff = if @code.lines[comment_position.line].strip.end_with?('@!parse') - comment_position.line + 1 - else - comment_position.line - end - Parser.process_node(src.node, region, @pins) - @pins[index..-1].each do |p| + comment_position.line + 1 + else + comment_position.line + end + locals = [] + ivars = [] + Parser.process_node(src.node, region, @pins, locals, ivars) + @pins.concat ivars + # @sg-ignore Need to add nil check here + @pins[index..].each do |p| # @todo Smelly instance variable access p.location.range.start.instance_variable_set(:@line, p.location.range.start.line + loff) p.location.range.ending.instance_variable_set(:@line, p.location.range.ending.line + loff) end - rescue Parser::SyntaxError => e + rescue Parser::SyntaxError # @todo Handle parser errors in !parse directives end when 'domain' namespace = closure_at(source_position) || Pin::ROOT_PIN + # @sg-ignore flow sensitive typing should be able to handle redefinition namespace.domains.concat directive.tag.types unless directive.tag.types.nil? when 'override' + # @sg-ignore Need to add nil check here pins.push Pin::Reference::Override.new(location, directive.tag.name, docstring.tags, source: :source_map) when 'macro' @@ -217,7 +235,9 @@ def process_directive source_position, comment_position, directive # @param line1 [Integer] # @param line2 [Integer] - def no_empty_lines?(line1, line2) + # @sg-ignore Need to add nil check here + def no_empty_lines? line1, line2 + # @sg-ignore Need to add nil check here @code.lines[line1..line2].none? { |line| line.strip.empty? } end @@ -227,7 +247,7 @@ def remove_inline_comment_hashes comment ctxt = '' num = nil started = false - comment.lines.each { |l| + comment.lines.each do |l| # Trim the comment and minimum leading whitespace p = l.encode('UTF-8', invalid: :replace, replace: '?').gsub(/^#+/, '') if num.nil? && !p.strip.empty? @@ -235,10 +255,11 @@ def remove_inline_comment_hashes comment started = true elsif started && !p.strip.empty? cur = p.index(/[^ ]/) + # @sg-ignore Need to add nil check here num = cur if cur < num end - ctxt += "#{p[num..-1]}" if started - } + ctxt += p[num..].to_s if started + end ctxt end @@ -247,7 +268,15 @@ def process_comment_directives return unless @code.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP code_lines = @code.lines @source.associated_comments.each do |line, comments| - src_pos = line ? Position.new(line, code_lines[line].to_s.chomp.index(/[^\s]/) || 0) : Position.new(code_lines.length, 0) + src_pos = if line + Position.new(line, + code_lines[line].to_s.chomp.index(/[^\s]/) || 0) + else + Position.new( + code_lines.length, 0 + ) + end + # @sg-ignore Need to add nil check here com_pos = Position.new(line + 1 - comments.lines.length, 0) process_comment(src_pos, com_pos, comments) end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index c2bb6fc64..724340821 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -5,11 +5,10 @@ module Solargraph # class TypeChecker autoload :Problem, 'solargraph/type_checker/problem' - autoload :ParamDef, 'solargraph/type_checker/param_def' autoload :Rules, 'solargraph/type_checker/rules' - autoload :Checks, 'solargraph/type_checker/checks' - include Checks + # @!parse + # include Solargraph::Parser::ParserGem::NodeMethods include Parser::NodeMethods # @return [String] @@ -26,8 +25,6 @@ class TypeChecker # @param level [Symbol] Don't complain about anything above this level # @param workspace [Workspace, nil] Workspace to use for loading # type checker rules modified by user config - # @param type_checker_rules [Hash{Symbol => Symbol}] Overrides for - # type checker rules - e.g., :report_undefined => :strong # @param rules [Rules] Type checker rules object def initialize filename, api_map: nil, @@ -36,7 +33,8 @@ def initialize filename, rules: workspace ? workspace.rules(level) : Rules.new(level, {}) @filename = filename # @todo Smarter directory resolution - @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename)) + @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename), + loose_unions: !rules.require_all_unique_types_support_call?) @rules = rules # @type [Array] @marked_ranges = [] @@ -49,18 +47,51 @@ def source_map # @return [Source] def source - @source_map.source + source_map.source + end + + # @param inferred [ComplexType, ComplexType::UniqueType] + # @param expected [ComplexType, ComplexType::UniqueType] + def return_type_conforms_to? inferred, expected + conforms_to?(inferred, expected, :return_type) + end + + # @param inferred [ComplexType, ComplexType::UniqueType] + # @param expected [ComplexType, ComplexType::UniqueType] + def arg_conforms_to? inferred, expected + conforms_to?(inferred, expected, :method_call) + end + + # @param inferred [ComplexType, ComplexType::UniqueType] + # @param expected [ComplexType, ComplexType::UniqueType] + def assignment_conforms_to? inferred, expected + conforms_to?(inferred, expected, :assignment) + end + + # @param inferred [ComplexType, ComplexType::UniqueType] + # @param expected [ComplexType, ComplexType::UniqueType] + # @param scenario [Symbol] + def conforms_to? inferred, expected, scenario + rules_arr = [] + rules_arr << :allow_empty_params unless rules.require_inferred_type_params? + rules_arr << :allow_any_match unless rules.require_all_unique_types_match_expected? + rules_arr << :allow_undefined unless rules.require_no_undefined_args? + rules_arr << :allow_unresolved_generic unless rules.require_generics_resolved? + rules_arr << :allow_unmatched_interface unless rules.require_interfaces_resolved? + rules_arr << :allow_reverse_match unless rules.require_downcasts? + inferred.conforms_to?(api_map, expected, scenario, + rules_arr) end # @return [Array] def problems @problems ||= begin - all = method_tag_problems - .concat(variable_type_tag_problems) - .concat(const_problems) - .concat(call_problems) - unignored = without_ignored(all) - unignored.concat(unneeded_sgignore_problems) + all = method_tag_problems + .concat(variable_type_tag_problems) + .concat(const_problems) + .concat(call_problems) + unignored = without_ignored(all) + unignored.concat(unneeded_sgignore_problems) end end @@ -70,20 +101,26 @@ class << self # @return [self] def load filename, level = :normal source = Solargraph::Source.load(filename) - api_map = Solargraph::ApiMap.new + rules = Rules.new(level, {}) + api_map = Solargraph::ApiMap.new(loose_unions: + !rules.require_all_unique_types_support_call?) api_map.map(source) - new(filename, api_map: api_map, level: level) + new(filename, api_map: api_map, level: level, rules: rules) end # @param code [String] # @param filename [String, nil] # @param level [Symbol] + # @param api_map [Solargraph::ApiMap, nil] # @return [self] - def load_string code, filename = nil, level = :normal + def load_string code, filename = nil, level = :normal, api_map: nil source = Solargraph::Source.load_string(code, filename) - api_map = Solargraph::ApiMap.new + rules = Rules.new(level, {}) + api_map ||= Solargraph::ApiMap.new(loose_unions: + !rules.require_all_unique_types_support_call?) + # @sg-ignore flow sensitive typing needs better handling of ||= on lvars api_map.map(source) - new(filename, api_map: api_map, level: level) + new(filename, api_map: api_map, level: level, rules: rules) end end @@ -107,13 +144,18 @@ def method_return_type_problems_for pin result = [] declared = pin.typify(api_map).self_to_type(pin.full_context).qualify(api_map, *pin.gates) if declared.undefined? + # @sg-ignore Need to add nil check here if pin.return_type.undefined? && rules.require_type_tags? if pin.attribute? inferred = pin.probe(api_map).self_to_type(pin.full_context) - result.push Problem.new(pin.location, "Missing @return tag for #{pin.path}", pin: pin) unless inferred.defined? + unless inferred.defined? + result.push Problem.new(pin.location, "Missing @return tag for #{pin.path}", + pin: pin) + end else result.push Problem.new(pin.location, "Missing @return tag for #{pin.path}", pin: pin) end + # @sg-ignore Need to add nil check here elsif pin.return_type.defined? && !resolved_constant?(pin) result.push Problem.new(pin.location, "Unresolved return type #{pin.return_type} for #{pin.path}", pin: pin) elsif rules.must_tag_or_infer? && pin.probe(api_map).undefined? @@ -127,8 +169,9 @@ def method_return_type_problems_for pin result.push Problem.new(pin.location, "#{pin.path} return type could not be inferred", pin: pin) end else - unless (rules.require_all_return_types_match_inferred? ? all_types_match?(api_map, inferred, declared) : any_types_match?(api_map, declared, inferred)) - result.push Problem.new(pin.location, "Declared return type #{declared.rooted_tags} does not match inferred type #{inferred.rooted_tags} for #{pin.path}", pin: pin) + unless return_type_conforms_to?(inferred, declared) + result.push Problem.new(pin.location, + "Declared return type #{declared.rooted_tags} does not match inferred type #{inferred.rooted_tags} for #{pin.path}", pin: pin) end end end @@ -144,7 +187,7 @@ def method_return_type_problems_for pin def resolved_constant? pin return true if pin.typify(api_map).defined? constant_pins = api_map.get_constants('', *pin.closure.gates) - .select { |p| p.name == pin.return_type.namespace } + .select { |p| p.name == pin.return_type.namespace } return true if constant_pins.find { |p| p.typify(api_map).defined? } # will need to probe when a constant name is assigned to a # class/module (alias) @@ -154,6 +197,7 @@ def resolved_constant? pin # @param pin [Pin::Base] def virtual_pin? pin + # @sg-ignore Need to add nil check here pin.location && source.comment_at?(pin.location.range.ending) end @@ -165,19 +209,19 @@ def method_param_type_problems_for pin pin.signatures.each do |sig| params = param_details_from_stack(sig, stack) if rules.require_type_tags? - sig.parameters.each do |par| - break if par.decl == :restarg || par.decl == :kwrestarg || par.decl == :blockarg - unless params[par.name] - if pin.attribute? - inferred = pin.probe(api_map).self_to_type(pin.full_context) - if inferred.undefined? - result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin) - end - else + sig.parameters.each do |par| + break if %i[restarg kwrestarg blockarg].include?(par.decl) + unless params[par.name] + if pin.attribute? + inferred = pin.probe(api_map).self_to_type(pin.full_context) + if inferred.undefined? result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin) end + else + result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin) end end + end end # @param name [String] # @param data [Hash{Symbol => BasicObject}] @@ -185,7 +229,8 @@ def method_param_type_problems_for pin # @type [ComplexType] type = data[:qualified] if type.undefined? - result.push Problem.new(pin.location, "Unresolved type #{data[:tagged]} for #{name} param on #{pin.path}", pin: pin) + result.push Problem.new(pin.location, "Unresolved type #{data[:tagged]} for #{name} param on #{pin.path}", + pin: pin) end end end @@ -201,6 +246,7 @@ def ignored_pins def variable_type_tag_problems result = [] all_variables.each do |pin| + # @sg-ignore Need to add nil check here if pin.return_type.defined? declared = pin.typify(api_map) next if declared.duck_type? @@ -215,21 +261,21 @@ def variable_type_tag_problems result.push Problem.new(pin.location, "Variable type could not be inferred for #{pin.name}", pin: pin) end else - unless any_types_match?(api_map, declared, inferred) - result.push Problem.new(pin.location, "Declared type #{declared} does not match inferred type #{inferred} for variable #{pin.name}", pin: pin) + unless assignment_conforms_to?(inferred, declared) + result.push Problem.new(pin.location, + "Declared type #{declared} does not match inferred type #{inferred} for variable #{pin.name}", pin: pin) end end elsif declared_externally?(pin) ignored_pins.push pin end 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) + result.push Problem.new(pin.location, "Unresolved type #{pin.return_type} for variable #{pin.name}", + pin: pin) end elsif pin.assignment inferred = pin.probe(api_map) - if inferred.undefined? && declared_externally?(pin) - ignored_pins.push pin - end + ignored_pins.push pin if inferred.undefined? && declared_externally?(pin) end end result @@ -247,8 +293,10 @@ def const_problems Solargraph::Parser::NodeMethods.const_nodes_from(source.node).each do |const| rng = Solargraph::Range.from_node(const) chain = Solargraph::Parser.chain(const, filename) + # @sg-ignore Need to add nil check here closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) closure_pin.rebind(api_map) + # @sg-ignore Need to add nil check here location = Location.new(filename, rng) locals = source_map.locals_at(location) pins = chain.define(api_map, closure_pin, locals) @@ -265,44 +313,55 @@ def call_problems result = [] Solargraph::Parser::NodeMethods.call_nodes_from(source.node).each do |call| rng = Solargraph::Range.from_node(call) + # @sg-ignore Need to add nil check here next if @marked_ranges.any? { |d| d.contain?(rng.start) } chain = Solargraph::Parser.chain(call, filename) + # @sg-ignore Need to add nil check here closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) - namespace_pin = closure_pin if call.type == :block # blocks in the AST include the method call as well, so the # node returned by #call_nodes_from needs to be backed out # one closure + # @todo Need to add nil check here + # @todo Should warn on nil deference here closure_pin = closure_pin.closure end + # @sg-ignore Need to add nil check here closure_pin.rebind(api_map) + # @sg-ignore Need to add nil check here location = Location.new(filename, rng) locals = source_map.locals_at(location) + # @sg-ignore Need to add nil check here type = chain.infer(api_map, closure_pin, locals) if type.undefined? && !rules.ignore_all_undefined? base = chain missing = chain + # @type [Solargraph::Pin::Base, nil] found = nil - closest = ComplexType::UNDEFINED + # @type [Array] + all_found = [] until base.links.first.undefined? - found = base.define(api_map, closure_pin, locals).first + # @sg-ignore Need to add nil check here + all_found = base.define(api_map, closure_pin, locals) + found = all_found.first break if found missing = base base = base.base end - closest = found.typify(api_map) if found + all_closest = all_found.map { |pin| pin.typify(api_map) } + closest = ComplexType.new(all_closest.flat_map(&:items).uniq) # @todo remove the internal_or_core? check at a higher-than-strict level - if !found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found)) - unless closest.generic? || ignored_pins.include?(found) - if closest.defined? - result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}") - else - result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}") - end - @marked_ranges.push rng + # @sg-ignore Need to add nil check here + if (!found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found))) && !(closest.generic? || ignored_pins.include?(found)) + if closest.defined? + result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}") + else + result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}") end + @marked_ranges.push rng end end + # @sg-ignore Need to add nil check here result.concat argument_problems_for(chain, api_map, closure_pin, locals, location) end result @@ -311,13 +370,12 @@ def call_problems # @param chain [Solargraph::Source::Chain] # @param api_map [Solargraph::ApiMap] # @param closure_pin [Solargraph::Pin::Closure] - # @param locals [Array] + # @param locals [Array] # @param location [Solargraph::Location] # @return [Array] def argument_problems_for chain, api_map, closure_pin, locals, location result = [] base = chain - # @type last_base_link [Solargraph::Source::Chain::Call] last_base_link = base.links.last return [] unless last_base_link.is_a?(Solargraph::Source::Chain::Call) @@ -340,6 +398,8 @@ def argument_problems_for chain, api_map, closure_pin, locals, location base.base.infer(api_map, closure_pin, locals).namespace end init = api_map.get_method_stack(fqns, 'initialize').first + + # @type [::Array] init ? arity_problems_for(init, arguments, location) : [] else arity_problems_for(pin, arguments, location) @@ -348,7 +408,7 @@ def argument_problems_for chain, api_map, closure_pin, locals, location return [] if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper) all_errors = [] - pin.signatures.sort { |sig| sig.parameters.length }.each do |sig| + pin.signatures.sort_by { |sig| sig.parameters.length }.each do |sig| params = param_details_from_stack(sig, pins) signature_errors = signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin @@ -368,11 +428,10 @@ def argument_problems_for chain, api_map, closure_pin, locals, location # @param location [Location] # @param locals [Array] # @param closure_pin [Pin::Closure] - # @param params [Hash{String => Hash{Symbol => String, Solargraph::ComplexType}}] + # @param params [Hash{String => undefined}] # @param arguments [Array] # @param sig [Pin::Signature] # @param pin [Pin::Method] - # @param pins [Array] # # @return [Array] def signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin @@ -382,31 +441,25 @@ def signature_argument_problems_for location, locals, closure_pin, params, argum # when possible, and when not, ensure provably # incorrect situations are detected. sig.parameters.each_with_index do |par, idx| - return errors if par.decl == :restarg # bail out and assume the rest is valid pending better arg processing + return errors if par.decl == :restarg # bail out and assume the rest is valid pending better arg processing argchain = arguments[idx] if argchain.nil? + final_arg = arguments.last if par.decl == :arg - final_arg = arguments.last if final_arg && final_arg.node.type == :splat argchain = final_arg return errors else errors.push Problem.new(location, "Not enough arguments to #{pin.path}") end - else - final_arg = arguments.last - argchain = final_arg if final_arg && [:kwsplat, :hash].include?(final_arg.node.type) + elsif final_arg && %i[kwsplat hash].include?(final_arg.node.type) + argchain = final_arg end end if argchain - if par.decl != :arg - errors.concat kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx - next - else - if argchain.node.type == :splat && argchain == arguments.last - final_arg = argchain - end - if (final_arg && final_arg.node.type == :splat) + if par.decl == :arg + final_arg = argchain if argchain.node.type == :splat && argchain == arguments.last + if final_arg && final_arg.node.type == :splat # The final argument given has been seen and was a # splat, which doesn't give us useful types or # arities against positional parameters, so let's @@ -430,11 +483,15 @@ def signature_argument_problems_for location, locals, closure_pin, params, argum 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}") + if argtype.defined? && ptype.defined? && !arg_conforms_to?(argtype, ptype) + errors.push Problem.new(location, + "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") return errors end end + else + errors.concat kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx + next end elsif par.decl == :kwarg errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}") @@ -445,13 +502,13 @@ def signature_argument_problems_for location, locals, closure_pin, params, argum end # @param sig [Pin::Signature] - # @param argchain [Source::Chain] + # @param argchain [Solargraph::Source::Chain] # @param api_map [ApiMap] # @param closure_pin [Pin::Closure] # @param locals [Array] # @param location [Location] # @param pin [Pin::Method] - # @param params [Hash{String => Hash{Symbol => String, Solargraph::ComplexType}}] + # @param params [Hash{String => Hash{Symbol => undefined}}] # @param idx [Integer] # # @return [Array] @@ -459,29 +516,30 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi result = [] kwargs = convert_hash(argchain.node) par = sig.parameters[idx] + # @type [Solargraph::Source::Chain] argchain = kwargs[par.name.to_sym] if par.decl == :kwrestarg || (par.decl == :optarg && idx == pin.parameters.length - 1 && par.asgn_code == '{}') result.concat kwrestarg_problems_for(api_map, closure_pin, locals, location, pin, params, kwargs) - else - if argchain - data = params[par.name] - if data.nil? - # @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? - # @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 + elsif argchain + data = params[par.name] + if data.nil? + # @todo Some level (strong, I guess) should require the param here + else + # @type [ComplexType, ComplexType::UniqueType] + ptype = data[:qualified] + ptype = ptype.self_to_type(pin.context) + unless ptype.undefined? + # @type [ComplexType] + argtype = argchain.infer(api_map, closure_pin, locals).self_to_type(closure_pin.context) + # @todo Unresolved call to defined? + if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype) + result.push Problem.new(location, + "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") end end - elsif par.decl == :kwarg - result.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}") end + elsif par.decl == :kwarg + result.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}") end result end @@ -491,19 +549,22 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi # @param locals [Array] # @param location [Location] # @param pin [Pin::Method] - # @param params [Hash{String => [nil, Hash]}] + # @param params [Hash{String => nil, Hash}] # @param kwargs [Hash{Symbol => Source::Chain}] # @return [Array] - def kwrestarg_problems_for(api_map, closure_pin, locals, location, pin, params, kwargs) + def kwrestarg_problems_for api_map, closure_pin, locals, location, pin, params, kwargs result = [] 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) + # @sg-ignore + # @type [ComplexType] + raw_ptype = params[pname.to_s][:qualified] + ptype = raw_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}") + if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype) + result.push Problem.new(location, + "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}") end end result @@ -513,7 +574,7 @@ def kwrestarg_problems_for(api_map, closure_pin, locals, location, pin, params, # @param pin [Pin::Method, Pin::Signature] # @param relevant_pin [Pin::Method, Pin::Signature] the pin which is under inspection # @return [void] - def add_restkwarg_param_tag_details(param_details, pin, relevant_pin) + def add_restkwarg_param_tag_details param_details, pin, relevant_pin # see if we have additional tags to pay attention to from YARD - # e.g., kwargs in a **restkwargs splat tags = pin.docstring.tags(:param) @@ -534,7 +595,7 @@ def add_restkwarg_param_tag_details(param_details, pin, relevant_pin) # @param pin [Pin::Signature] # @return [Hash{String => Hash{Symbol => String, ComplexType}}] - def signature_param_details(pin) + def signature_param_details pin # @type [Hash{String => Hash{Symbol => String, ComplexType}}] result = {} pin.parameters.each do |param| @@ -553,6 +614,7 @@ def signature_param_details(pin) next if tag.types.nil? result[tag.name.to_s] = { tagged: tag.types.join(', '), + # @sg-ignore need to add a nil check here qualified: Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, *pin.closure.gates) } end @@ -567,7 +629,7 @@ def signature_param_details(pin) # @param new_param_details [Hash{String => Hash{Symbol => String, ComplexType}}] # # @return [void] - def add_to_param_details(param_details, param_names, new_param_details) + def add_to_param_details param_details, param_names, new_param_details new_param_details.each do |param_name, details| next unless param_names.include?(param_name) @@ -580,7 +642,7 @@ def add_to_param_details(param_details, param_names, new_param_details) # @param signature [Pin::Signature] # @param method_pin_stack [Array] # @return [Hash{String => Hash{Symbol => String, ComplexType}}] - def param_details_from_stack(signature, method_pin_stack) + def param_details_from_stack signature, method_pin_stack signature_type = signature.typify(api_map) signature = signature.proxy signature_type param_details = signature_param_details(signature) @@ -602,6 +664,7 @@ def param_details_from_stack(signature, method_pin_stack) # @param pin [Pin::Base] def internal? pin return false if pin.nil? + # @sg-ignore flow sensitive typing needs to handle attrs pin.location && api_map.bundled?(pin.location.filename) end @@ -619,29 +682,32 @@ def external? pin # @param pin [Pin::BaseVariable] def declared_externally? pin - raise "No assignment found" if pin.assignment.nil? + raise 'No assignment found' if pin.assignment.nil? chain = Solargraph::Parser.chain(pin.assignment, filename) + # @sg-ignore flow sensitive typing needs to handle attrs rng = Solargraph::Range.from_node(pin.assignment) + # @sg-ignore Need to add nil check here closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) + # @sg-ignore flow sensitive typing needs to handle "if foo.nil?" location = Location.new(filename, Range.from_node(pin.assignment)) locals = source_map.locals_at(location) type = chain.infer(api_map, closure_pin, locals) if type.undefined? && !rules.ignore_all_undefined? base = chain - missing = chain + # @type [Solargraph::Pin::Base, nil] found = nil - closest = ComplexType::UNDEFINED + # @type [Array] + all_found = [] until base.links.first.undefined? - found = base.define(api_map, closure_pin, locals).first + all_found = base.define(api_map, closure_pin, locals) + found = all_found.first break if found - missing = base base = base.base end - closest = found.typify(api_map) if found - if !found || closest.defined? || internal?(found) - return false - end + all_closest = all_found.map { |pin| pin.typify(api_map) } + closest = ComplexType.new(all_closest.flat_map(&:items).uniq) + return false if !found || closest.defined? || internal?(found) end true end @@ -664,7 +730,7 @@ def arity_problems_for pin, arguments, location # @param arguments [Array] # @param location [Location] # @return [Array] - def parameterized_arity_problems_for(pin, parameters, arguments, location) + def parameterized_arity_problems_for pin, parameters, arguments, location return [] unless pin.explicit? return [] if parameters.empty? && arguments.empty? return [] if pin.anon_splat? @@ -679,7 +745,7 @@ def parameterized_arity_problems_for(pin, parameters, arguments, location) settled_kwargs = parameters.count(&:keyword?) else kwargs = convert_hash(unchecked.last.node) - if parameters.any? { |param| [:kwarg, :kwoptarg].include?(param.decl) || param.kwrestarg? } + if parameters.any? { |param| %i[kwarg kwoptarg].include?(param.decl) || param.kwrestarg? } if kwargs.empty? add_params += 1 else @@ -708,10 +774,12 @@ def parameterized_arity_problems_for(pin, parameters, arguments, location) return [] if parameters.any?(&:rest?) opt = optional_param_count(parameters) return [] if unchecked.length <= req + opt - if req + add_params + 1 == unchecked.length && any_splatted_call?(unchecked.map(&:node)) && (parameters.map(&:decl) & [:kwarg, :kwoptarg, :kwrestarg]).any? + if req + add_params + 1 == unchecked.length && any_splatted_call?(unchecked.map(&:node)) && (parameters.map(&:decl) & %i[ + kwarg kwoptarg kwrestarg + ]).any? return [] end - return [] if arguments.length - req == parameters.select { |p| [:optarg, :kwoptarg].include?(p.decl) }.length + return [] if arguments.length - req == parameters.select { |p| %i[optarg kwoptarg].include?(p.decl) }.length return [Problem.new(location, "Too many arguments to #{pin.path}")] elsif unchecked.length < req - settled_kwargs && (arguments.empty? || (!arguments.last.splat? && !arguments.last.links.last.is_a?(Solargraph::Source::Chain::Hash))) # HACK: Kernel#raise signature is incorrect in Ruby 2.7 core docs. @@ -727,42 +795,48 @@ def parameterized_arity_problems_for(pin, parameters, arguments, location) # @todo need to use generic types in method to choose correct # signature and generate Integer as return type # @return [Integer] - def required_param_count(parameters) + def required_param_count parameters parameters.sum { |param| %i[arg kwarg].include?(param.decl) ? 1 : 0 } end # @param parameters [Enumerable] - # @param pin [Pin::Method] + # # @return [Integer] - def optional_param_count(parameters) + def optional_param_count parameters parameters.select { |p| p.decl == :optarg }.length end # @param pin [Pin::Method] def abstract? pin pin.docstring.has_tag?('abstract') || - (pin.closure && pin.closure.docstring.has_tag?('abstract')) + pin.closure&.docstring&.has_tag?('abstract') end # @param pin [Pin::Method] # @return [Array] - def fake_args_for(pin) + def fake_args_for pin args = [] with_opts = false with_block = false + # @param pin [Pin::Parameter] pin.parameters.each do |pin| - if [:kwarg, :kwoptarg, :kwrestarg].include?(pin.decl) + # @sg-ignore flow sensitive typing should be able to handle redefinition + if %i[kwarg kwoptarg kwrestarg].include?(pin.decl) with_opts = true + # @sg-ignore flow sensitive typing should be able to handle redefinition elsif pin.decl == :block with_block = true + # @sg-ignore flow sensitive typing should be able to handle redefinition elsif pin.decl == :restarg args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)], nil, true) else args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)]) end end - args.push Solargraph::Parser.chain_string('{}') if with_opts - args.push Solargraph::Parser.chain_string('&') if with_block + pin_location = pin.location + starting_line = pin_location ? pin_location.range.start.line : 0 + args.push Solargraph::Parser.chain_string('{}', filename, starting_line) if with_opts + args.push Solargraph::Parser.chain_string('&', filename, starting_line) if with_block args end @@ -774,6 +848,7 @@ def sg_ignore_lines_processed # @return [Set] def all_sg_ignore_lines source.associated_comments.select do |_line, text| + # @sg-ignore Need to add nil check here text.include?('@sg-ignore') end.keys.to_set end diff --git a/lib/solargraph/type_checker/checks.rb b/lib/solargraph/type_checker/checks.rb deleted file mode 100644 index de402978b..000000000 --- a/lib/solargraph/type_checker/checks.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - class TypeChecker - # Helper methods for performing type checks - # - module Checks - module_function - - # Compare an expected type with an inferred type. Common usage is to - # check if the type declared in a method's @return tag matches the type - # inferred from static analysis of the code. - # - # @param api_map [ApiMap] - # @param expected [ComplexType] - # @param inferred [ComplexType] - # @return [Boolean] - def types_match? api_map, expected, inferred - return true if expected.to_s == inferred.to_s - matches = [] - expected.each do |exp| - found = false - inferred.each do |inf| - # if api_map.super_and_sub?(fuzz(inf), fuzz(exp)) - if either_way?(api_map, inf, exp) - found = true - matches.push inf - break - end - end - return false unless found - end - inferred.each do |inf| - next if matches.include?(inf) - found = false - expected.each do |exp| - # if api_map.super_and_sub?(fuzz(inf), fuzz(exp)) - if either_way?(api_map, inf, exp) - found = true - break - end - end - return false unless found - end - true - end - - # @param api_map [ApiMap] - # @param expected [ComplexType] - # @param inferred [ComplexType] - # @return [Boolean] - def any_types_match? api_map, expected, inferred - expected = expected.downcast_to_literal_if_possible - inferred = inferred.downcast_to_literal_if_possible - return duck_types_match?(api_map, expected, inferred) if expected.duck_type? - # walk through the union expected type and see if any members - # of the union match the inferred type - expected.each do |exp| - next if exp.duck_type? - # @todo: there should be a level of typechecking where all - # unique types in the inferred must match one of the - # expected unique types - inferred.each do |inf| - # return true if exp == inf || api_map.super_and_sub?(fuzz(inf), fuzz(exp)) - return true if exp == inf || either_way?(api_map, inf, exp) - end - end - false - end - - # @param api_map [ApiMap] - # @param inferred [ComplexType] - # @param expected [ComplexType] - # @return [Boolean] - def all_types_match? api_map, inferred, expected - expected = expected.downcast_to_literal_if_possible - inferred = inferred.downcast_to_literal_if_possible - return duck_types_match?(api_map, expected, inferred) if expected.duck_type? - inferred.each do |inf| - next if inf.duck_type? - return false unless expected.any? { |exp| exp == inf || either_way?(api_map, inf, exp) } - end - true - end - - # @param api_map [ApiMap] - # @param expected [ComplexType] - # @param inferred [ComplexType] - # @return [Boolean] - def duck_types_match? api_map, expected, inferred - raise ArgumentError, 'Expected type must be duck type' unless expected.duck_type? - expected.each do |exp| - next unless exp.duck_type? - quack = exp.to_s[1..-1] - return false if api_map.get_method_stack(inferred.namespace, quack, scope: inferred.scope).empty? - end - true - end - - # @param type [ComplexType::UniqueType] - # @return [String] - def fuzz type - if type.parameters? - type.name - else - type.tag - end - end - - # @param api_map [ApiMap] - # @param cls1 [ComplexType::UniqueType] - # @param cls2 [ComplexType::UniqueType] - # @return [Boolean] - def either_way?(api_map, cls1, cls2) - # @todo there should be a level of typechecking which uses the - # full tag with parameters to determine compatibility - f1 = cls1.name - f2 = cls2.name - api_map.type_include?(f1, f2) || api_map.super_and_sub?(f1, f2) || api_map.super_and_sub?(f2, f1) - # api_map.type_include?(f1, f2) || api_map.super_and_sub?(f1, f2) || api_map.super_and_sub?(f2, f1) - end - end - end -end diff --git a/lib/solargraph/type_checker/param_def.rb b/lib/solargraph/type_checker/param_def.rb deleted file mode 100644 index dcab5f4a8..000000000 --- a/lib/solargraph/type_checker/param_def.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - class TypeChecker - # Data about a method parameter definition. This is the information from - # the args list in the def call, not the `@param` tags. - # - class ParamDef - # @return [String] - attr_reader :name - - # @return [Symbol] - attr_reader :type - - # @param name [String] - # @param type [Symbol] The type of parameter, such as :req, :opt, :rest, etc. - def initialize name, type - @name = name - @type = type - end - - class << self - # Get an array of ParamDefs from a method pin. - # - # @param pin [Solargraph::Pin::Method] - # @return [Array] - def from pin - result = [] - pin.parameters.each do |par| - result.push ParamDef.new(par.name, par.decl) - end - result - end - end - end - end -end diff --git a/lib/solargraph/type_checker/problem.rb b/lib/solargraph/type_checker/problem.rb index 1ae65571a..8375a58ff 100644 --- a/lib/solargraph/type_checker/problem.rb +++ b/lib/solargraph/type_checker/problem.rb @@ -5,19 +5,21 @@ class TypeChecker # A problem reported by TypeChecker. # class Problem + # @todo Missed nil violation # @return [Solargraph::Location] attr_reader :location # @return [String] attr_reader :message + # @todo Missed nil violation # @return [Pin::Base] attr_reader :pin # @return [String, nil] attr_reader :suggestion - # @param location [Solargraph::Location] + # @param location [Solargraph::Location, nil] # @param message [String] # @param pin [Solargraph::Pin::Base, nil] # @param suggestion [String, nil] diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 81a2d4aa3..6ce414a93 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -23,11 +23,11 @@ class Rules # @param overrides [Hash{Symbol => Symbol}] def initialize level, overrides @rank = if LEVELS.key?(level) - LEVELS[level] - else - Solargraph.logger.warn "Unrecognized TypeChecker level #{level}, assuming normal" - 0 - end + LEVELS[level] + else + Solargraph.logger.warn "Unrecognized TypeChecker level #{level}, assuming normal" + 0 + end @level = LEVELS[LEVELS.values.index(@rank)] @overrides = overrides end @@ -60,8 +60,81 @@ def validate_tags? report?(:validate_tags, :typed) end - def require_all_return_types_match_inferred? - report?(:require_all_return_types_match_inferred, :alpha) + def require_inferred_type_params? + report?(:require_inferred_type_params, :alpha) + end + + # + # False negatives: + # + # @todo 4: Missed nil violation + # + # pending code fixes (277): + # + # @todo 281: Need to add nil check here + # @todo 22: Translate to something flow sensitive typing understands + # @todo 3: Need a downcast here + # + # flow sensitive typing could handle (96): + # + # @todo 36: flow sensitive typing needs to handle attrs + # @todo 29: flow sensitive typing should be able to handle redefinition + # @todo 19: flow sensitive typing needs to narrow down type with an if is_a? check + # @todo 13: Need to validate config + # @todo 8: flow sensitive typing should support .class == .class + # @todo 6: need boolish support for ? methods + # @todo 6: flow sensitive typing needs better handling of ||= on lvars + # @todo 5: literal arrays in this module turn into ::Solargraph::Source::Chain::Array + # @todo 5: flow sensitive typing needs to handle 'raise if' + # @todo 4: flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo) + # @todo 4: nil? support in flow sensitive typing + # @todo 3: flow sensitive typing ought to be able to handle 'when ClassName' + # @todo 2: downcast output of Enumerable#select + # @todo 2: flow sensitive typing should handle return nil if location&.name.nil? + # @todo 2: flow sensitive typing should handle is_a? and next + # @todo 2: Need to look at Tuple#include? handling + # @todo 2: Should better support meaning of '&' in RBS + # @todo 2: (*) flow sensitive typing needs to handle "if foo = bar" + # @todo 2: flow sensitive typing needs to handle "if foo = bar" + # @todo 2: Need to handle duck-typed method calls on union types + # @todo 2: Need better handling of #compact + # @todo 2: flow sensitive typing should allow shadowing of Kernel#caller + # @todo 1: flow sensitive typing not smart enough to handle this case + # @todo 1: flow sensitive typing needs to handle if foo = bar + # @todo 1: flow sensitive typing needs to handle "if foo.nil?" + # @todo 1: flow sensitive typing should support case/when + # @todo 1: flow sensitive typing should support ivars + # @todo 1: Need to support this in flow sensitive typing + # @todo 1: flow sensitive typing needs to handle self.class == other.class + # @todo 1: flow sensitive typing needs to remove literal with + # @todo 1: flow sensitive typing needs to understand reassignment + # @todo 1: flow sensitive typing should be able to identify more blocks that always return + # @todo 1: should warn on nil dereference below + # @todo 1: flow sensitive typing needs to create separate ranges for postfix if + # @todo 1: flow sensitive typing needs to handle constants + # @todo 1: flow sensitive typing needs to eliminate literal from union with return if foo == :bar + def require_all_unique_types_match_expected? + report?(:require_all_unique_types_match_expected, :strong) + end + + def require_all_unique_types_support_call? + report?(:require_all_unique_types_support_call, :strong) + end + + def require_no_undefined_args? + report?(:require_no_undefined_args, :alpha) + end + + def require_generics_resolved? + report?(:require_generics_resolved, :alpha) + end + + def require_interfaces_resolved? + report?(:require_interfaces_resolved, :alpha) + end + + def require_downcasts? + report?(:require_downcasts, :alpha) end # We keep this at strong because if you added an @ sg-ignore to @@ -76,7 +149,7 @@ def validate_sg_ignores? # @param type [Symbol] # @param level [Symbol] - def report?(type, level) + def report? type, level rank >= LEVELS[@overrides.fetch(type, level)] end end diff --git a/lib/solargraph/version.rb b/lib/solargraph/version.rb index 11dd7e1ff..00cc77b02 100755 --- a/lib/solargraph/version.rb +++ b/lib/solargraph/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Solargraph - VERSION = '0.58.2' + VERSION = ENV.fetch('SOLARGRAPH_FORCE_VERSION', '0.59.0.dev.2') end diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 06980e6d0..351ee28a5 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -2,6 +2,7 @@ require 'open3' require 'json' +require 'yaml' module Solargraph # A workspace consists of the files in a project's directory and the @@ -9,7 +10,10 @@ module Solargraph # in an associated Library or ApiMap. # class Workspace + include Logging + autoload :Config, 'solargraph/workspace/config' + autoload :Gemspecs, 'solargraph/workspace/gemspecs' autoload :RequirePaths, 'solargraph/workspace/require_paths' # @return [String] @@ -19,7 +23,8 @@ class Workspace attr_reader :gemnames alias source_gems gemnames - # @param directory [String] TODO: Remove '' and '*' special cases + # @todo Remove '' and '*' special cases + # @param directory [String] # @param config [Config, nil] # @param server [Hash] def initialize directory = '', config = nil, server = {} @@ -50,9 +55,71 @@ def config @config ||= Solargraph::Workspace::Config.new(directory) end + # @param stdlib_name [String] + # + # @return [Array] + def stdlib_dependencies stdlib_name + gemspecs.stdlib_dependencies(stdlib_name) + end + + # @param out [IO, nil] output stream for logging + # @param gemspec [Gem::Specification] + # @return [Array] + def fetch_dependencies gemspec, out: $stderr + gemspecs.fetch_dependencies(gemspec, out: out) + end + + # @param require [String] The string sent to 'require' in the code to resolve, e.g. 'rails', 'bundler/require' + # @return [Array, nil] + def resolve_require require + gemspecs.resolve_require(require) + end + + # @return [Solargraph::PinCache] + def pin_cache + @pin_cache ||= fresh_pincache + end + + # @return [Environ] + def global_environ + # empty docmap, since the result needs to work in any possible + # context here + @global_environ ||= Convention.for_global(DocMap.new([], self, out: nil)) + end + + # @param gemspec [Gem::Specification] + # @param out [StringIO, IO, nil] output stream for logging + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # + # @return [void] + def cache_gem gemspec, out: nil, rebuild: false + pin_cache.cache_gem(gemspec: gemspec, out: out, rebuild: rebuild) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [StringIO, IO, nil] output stream for logging + # + # @return [void] + def uncache_gem gemspec, out: nil + pin_cache.uncache_gem(gemspec, out: out) + end + + # @return [Solargraph::PinCache] + def fresh_pincache + PinCache.new(rbs_collection_path: rbs_collection_path, + rbs_collection_config_path: rbs_collection_config_path, + yard_plugins: yard_plugins, + directory: directory) + end + + # @return [Array] + def yard_plugins + @yard_plugins ||= global_environ.yard_plugins.sort.uniq + end + # @param level [Symbol] # @return [TypeChecker::Rules] - def rules(level) + def rules level @rules ||= TypeChecker::Rules.new(level, config.type_checker_rules) end @@ -63,6 +130,7 @@ def rules(level) # @param sources [Array] # @return [Boolean] True if the source was added to the workspace def merge *sources + # @sg-ignore Need to add nil check here unless directory == '*' || sources.all? { |source| source_hash.key?(source.filename) } # Reload the config to determine if a new source should be included @config = Solargraph::Workspace::Config.new(directory) @@ -70,10 +138,12 @@ def merge *sources includes_any = false sources.each do |source| - if directory == "*" || config.calculated.include?(source.filename) - source_hash[source.filename] = source - includes_any = true - end + # @sg-ignore Need to add nil check here + next unless directory == '*' || config.calculated.include?(source.filename) + + # @sg-ignore Need to add nil check here + source_hash[source.filename] = source + includes_any = true end includes_any @@ -121,24 +191,76 @@ def source filename def would_require? path require_paths.each do |rp| full = File.join rp, path - return true if File.file?(full) || File.file?(full << ".rb") + return true if File.file?(full) || File.file?(full << '.rb') end false end + # True if the workspace contains at least one gemspec file. + # + # @return [Boolean] + def gemspec? + !gemspec_files.empty? + end + + # Get an array of all gemspec files in the workspace. + # + # @return [Array] + def gemspec_files + return [] if directory.empty? || directory == '*' + @gemspec_files ||= Dir[File.join(directory, '**/*.gemspec')].select do |gs| + config.allow? gs + end + end + # @return [String, nil] def rbs_collection_path - @gem_rbs_collection ||= read_rbs_collection_path + @rbs_collection_path ||= read_rbs_collection_path end # @return [String, nil] def rbs_collection_config_path - @rbs_collection_config_path ||= begin + @rbs_collection_config_path ||= unless directory.empty? || directory == '*' yaml_file = File.join(directory, 'rbs_collection.yaml') yaml_file if File.file?(yaml_file) end + end + + # @param name [String] + # @param version [String, nil] + # @param out [IO, nil] + # + # @return [Gem::Specification, nil] + def find_gem name, version = nil, out: nil + gemspecs.find_gem(name, version, out: out) + end + + # @return [Array] + def all_gemspecs_from_bundle + gemspecs.all_gemspecs_from_bundle + end + + # @param out [StringIO, IO, nil] output stream for logging + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # @return [void] + def cache_all_for_workspace! out, rebuild: false + PinCache.cache_core(out: out) unless PinCache.core? && !rebuild + + gem_specs = all_gemspecs_from_bundle + # try any possible standard libraries, but be quiet about it + stdlib_specs = pin_cache.possible_stdlibs.map { |stdlib| find_gem(stdlib, out: nil) }.compact + specs = (gem_specs + stdlib_specs) + specs.each do |spec| + pin_cache.cache_gem(gemspec: spec, rebuild: rebuild, out: out) unless pin_cache.cached?(spec) end + out&.puts "Documentation cached for all #{specs.length} gems." + + # do this after so that we prefer stdlib requires from gems, + # which are likely to be newer and have more pins + pin_cache.cache_all_stdlibs(out: out, rebuild: rebuild) + + out&.puts 'Documentation cached for core, standard library and gems.' end # Synchronize the workspace from the provided updater. @@ -149,7 +271,9 @@ def synchronize! updater source_hash[updater.filename] = source_hash[updater.filename].synchronize(updater) end + # @sg-ignore Need to validate config # @return [String] + # @sg-ignore Need to validate config def command_path server['commandPath'] || 'solargraph' end @@ -160,12 +284,9 @@ def directory_or_nil directory end - # True if the workspace has a root Gemfile. - # - # @todo Handle projects with custom Bundler/Gemfile setups (see DocMap#gemspecs_required_from_bundler) - # - def gemfile? - directory && File.file?(File.join(directory, 'Gemfile')) + # @return [Solargraph::Workspace::Gemspecs] + def gemspecs + @gemspecs ||= Solargraph::Workspace::Gemspecs.new(directory_or_nil) end private @@ -184,27 +305,25 @@ def source_hash # @return [void] def load_sources source_hash.clear - unless directory.empty? || directory == '*' - size = config.calculated.length - raise WorkspaceTooLargeError, "The workspace is too large to index (#{size} files, #{config.max_files} max)" if config.max_files > 0 and size > config.max_files - config.calculated.each do |filename| - begin - source_hash[filename] = Solargraph::Source.load(filename) - rescue Errno::ENOENT => e - Solargraph.logger.warn("Error loading #{filename}: [#{e.class}] #{e.message}") - end - end + return if directory.empty? || directory == '*' + size = config.calculated.length + if config.max_files.positive? && (size > config.max_files) + raise WorkspaceTooLargeError, + "The workspace is too large to index (#{size} files, #{config.max_files} max)" + end + config.calculated.each do |filename| + source_hash[filename] = Solargraph::Source.load(filename) + rescue Errno::ENOENT => e + Solargraph.logger.warn("Error loading #{filename}: [#{e.class}] #{e.message}") end end # @return [void] def require_plugins config.plugins.each do |plugin| - begin - require plugin - rescue LoadError - Solargraph.logger.warn "Failed to load plugin '#{plugin}'" - end + require plugin + rescue LoadError + Solargraph.logger.warn "Failed to load plugin '#{plugin}'" end end diff --git a/lib/solargraph/workspace/config.rb b/lib/solargraph/workspace/config.rb index ac9f36739..bd494b380 100644 --- a/lib/solargraph/workspace/config.rb +++ b/lib/solargraph/workspace/config.rb @@ -14,7 +14,7 @@ class Config # @return [String] attr_reader :directory - # @todo To make JSON strongly typed we'll need a record syntax + # @todo Need to validate config # @return [Hash{String => undefined, nil}] attr_reader :raw_data @@ -54,7 +54,9 @@ def allow? filename # # @return [Array] def calculated - Solargraph.logger.info "Indexing workspace files in #{directory}" unless @calculated || directory.empty? || directory == '*' + unless @calculated || directory.empty? || directory == '*' + Solargraph.logger.info "Indexing workspace files in #{directory}" + end @calculated ||= included - excluded end @@ -78,7 +80,9 @@ def required # An array of load paths for required paths. # + # @sg-ignore Need to validate config # @return [Array] + # @sg-ignore Need to validate config def require_paths raw_data['require_paths'] || [] end @@ -144,7 +148,7 @@ def config_data global_config = read_config(global_config_path) defaults = default_config - defaults.merge({'exclude' => []}) unless workspace_config.nil? + defaults.merge({ 'exclude' => [] }) unless workspace_config.nil? defaults .merge(global_config || {}) @@ -158,7 +162,7 @@ def config_data def read_config config_path = '' return nil if config_path.empty? return nil unless File.file?(config_path) - YAML.safe_load(File.read(config_path)) + YAML.safe_load_file(config_path) end # @return [Hash{String => Array, Hash, Integer}] @@ -174,11 +178,11 @@ def default_config 'cops' => 'safe', 'except' => [], 'only' => [], - 'extra_args' =>[] + 'extra_args' => [] } }, 'type_checker' => { - 'rules' => { } + 'rules' => {} }, 'require_paths' => [], 'plugins' => [], @@ -191,12 +195,11 @@ def default_config # @param globs [Array] # @return [Array] def process_globs globs - result = globs.flat_map do |glob| + globs.flat_map do |glob| Dir[File.absolute_path(glob, directory)] - .map{ |f| f.gsub(/\\/, '/') } + .map { |f| f.gsub('\\', '/') } .select { |f| File.file?(f) } end - result end # Modify the included files based on excluded directories and get an @@ -239,7 +242,7 @@ def glob_is_directory? glob # @param glob [String] # @return [String] def glob_to_directory glob - glob.gsub(/(\/\*|\/\*\*\/\*\*?)$/, '') + glob.gsub(%r{(/\*|/\*\*/\*\*?)$}, '') end # @return [Array] diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb new file mode 100644 index 000000000..2389c2d76 --- /dev/null +++ b/lib/solargraph/workspace/gemspecs.rb @@ -0,0 +1,367 @@ +# frozen_string_literal: true + +require 'rubygems' +require 'bundler' + +module Solargraph + class Workspace + # Manages determining which gemspecs are available in a workspace + class Gemspecs + include Logging + + attr_reader :directory, :preferences + + # @param directory [String, nil] If nil, assume no bundle is present + # @param preferences [Array] + def initialize directory, preferences: [] + # @todo an issue with both external bundles and the potential + # preferences feature is that bundler gives you a 'clean' + # rubygems environment with only the specified versions + # installed. Possible alternatives: + # + # *) prompt the user to run solargraph outside of bundler + # and treat all bundles as external + # *) reinstall the needed gems dynamically each time + # *) manipulate the rubygems/bundler environment + @directory = directory && File.absolute_path(directory) + # @todo implement preferences as a config-exposed feature + @preferences = preferences + end + + # Take the path given to a 'require' statement in a source file + # and return the Gem::Specifications which will be brought into + # scope with it, so we can load pins for them. + # + # @param require [String] The string sent to 'require' in the code to resolve, e.g. 'rails', 'bundler/require' + # @return [::Array, nil] + def resolve_require require + return nil if require.empty? + + # This is added in the parser when it sees 'Bundler.require' - + # see https://bundler.io/guides/bundler_setup.html ' + # + # @todo handle different arguments to Bundler.require + return auto_required_gemspecs_from_bundler if require == 'bundler/require' + + # Determine gem name based on the require path + file = "lib/#{require}.rb" + spec_with_path = Gem::Specification.find_by_path(file) + + all_gemspecs = all_gemspecs_from_bundle + + gem_names_to_try = [ + spec_with_path&.name, + require.tr('/', '-'), + require.split('/').first + ].compact.uniq + # @param gem_name [String] + gem_names_to_try.each do |gem_name| + # @sg-ignore Unresolved call to == on Boolean + gemspec = all_gemspecs.find { |gemspec| gemspec.name == gem_name } + # @sg-ignore flow sensitive typing should be able to handle redefinition + return [gemspec_or_preference(gemspec)] if gemspec + + begin + gemspec = Gem::Specification.find_by_name(gem_name) + # @sg-ignore flow sensitive typing should be able to handle redefinition + return [gemspec_or_preference(gemspec)] if gemspec + rescue Gem::MissingSpecError + logger.debug do + "Require path #{require} could not be resolved to a gem via find_by_path or guess of #{gem_name}" + end + end + + # look ourselves just in case this is hanging out somewhere + # that find_by_path doesn't index + gemspec = all_gemspecs.find do |spec| + spec = to_gem_specification(spec) unless spec.respond_to?(:files) + + # @sg-ignore Translate to something flow sensitive typing understands + spec&.files&.any? { |gemspec_file| file == gemspec_file } + end + # @sg-ignore flow sensitive typing should be able to handle redefinition + return [gemspec_or_preference(gemspec)] if gemspec + end + + nil + end + + # @param stdlib_name [String] + # + # @return [Array] + def stdlib_dependencies stdlib_name + deps = RbsMap::StdlibMap.stdlib_dependencies(stdlib_name, nil) || [] + deps.map { |dep| dep['name'] }.compact + end + + # @param name [String] + # @param version [String, nil] + # @param out [IO, nil] output stream for logging + # + # @return [Gem::Specification, nil] + def find_gem name, version = nil, out: $stderr + # @sg-ignore flow sensitive typing should be able to handle redefinition + specish = all_gemspecs_from_bundle.find { |specish| specish.name == name && specish.version == version } + return to_gem_specification specish if specish + + # @sg-ignore flow sensitive typing should be able to handle redefinition + specish = all_gemspecs_from_bundle.find { |specish| specish.name == name } + # @sg-ignore flow sensitive typing needs to create separate ranges for postfix if + return to_gem_specification specish if specish + + resolve_gem_ignoring_local_bundle name, version, out: out + end + + # @param gemspec [Gem::Specification] + # @param out[IO, nil] output stream for logging + # + # @return [Array] + def fetch_dependencies gemspec, out: $stderr + all_gemspecs_from_bundle + + # @type [Hash{String => Gem::Specification}] + deps_so_far = {} + + # @param runtime_dep [Gem::Dependency] + # @param deps [Hash{String => Gem::Specification}] + gem_dep_gemspecs = only_runtime_dependencies(gemspec).each_with_object(deps_so_far) do |runtime_dep, deps| + dep = find_gem(runtime_dep.name, runtime_dep.requirement) + next unless dep + + fetch_dependencies(dep, out: out).each { |sub_dep| deps[sub_dep.name] ||= sub_dep } + + deps[dep.name] ||= dep + end + + # RBS tracks implicit dependencies, like how the YAML standard + # library implies pulling in the psych library. + stdlib_deps = RbsMap::StdlibMap.stdlib_dependencies(gemspec.name, gemspec.version) || [] + stdlib_dep_gemspecs = stdlib_deps.map { |dep| find_gem(dep['name'], dep['version']) }.compact + (gem_dep_gemspecs.values.compact + stdlib_dep_gemspecs).uniq(&:name) + end + + # Returns all gemspecs directly depended on by this workspace's + # bundle (does not include transitive dependencies). + # + # @return [Array] + def all_gemspecs_from_bundle + return [] unless directory + + @all_gemspecs_from_bundle ||= + if in_this_bundle? + all_gemspecs_from_this_bundle + else + all_gemspecs_from_external_bundle + end + end + + # @return [Hash{Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification => Gem::Specification}] + def self.gem_specification_cache + @gem_specification_cache ||= {} + end + + private + + # @param specish [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification] + # + # @return [Gem::Specification, nil] + # @sg-ignore flow sensitive typing needs better handling of ||= on lvars + def to_gem_specification specish + # print time including milliseconds + self.class.gem_specification_cache[specish] ||= case specish + when Gem::Specification + specish + when Bundler::LazySpecification + # materializing didn't work. Let's look in the local + # rubygems without bundler's help + resolve_gem_ignoring_local_bundle specish.name, + specish.version + when Bundler::StubSpecification + # turns a Bundler::StubSpecification into a + # Gem::StubSpecification if we can + if specish.respond_to?(:stub) + # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' + to_gem_specification specish.stub + else + # A Bundler::StubSpecification is a Bundler:: + # RemoteSpecification which ought to proxy a Gem:: + # Specification + specish + end + # @sg-ignore Unresolved constant Gem::StubSpecification + when Gem::StubSpecification + # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' + specish.to_spec + else + raise "Unexpected type while resolving gem: #{specish.class}" + end + end + + # @param command [String] The expression to evaluate in the external bundle + # @sg-ignore Need a JSON type + # @yield [undefined, nil] + def query_external_bundle command + Solargraph.with_clean_env do + cmd = [ + 'ruby', '-e', + "require 'bundler'; require 'json'; Dir.chdir('#{directory}') { puts begin; #{command}; end.to_json }" + ] + o, e, s = Open3.capture3(*cmd) + if s.success? + Solargraph.logger.debug "External bundle: #{o}" + o && !o.empty? ? JSON.parse(o.split("\n").last) : nil + else + Solargraph.logger.warn e + raise BundleNotFoundError, "Failed to load gems from bundle at #{directory}" + end + end + end + + # @sg-ignore need boolish support for ? methods + def in_this_bundle? + Bundler.definition&.lockfile&.to_s&.start_with?(directory) + end + + # @return [Array] + def all_gemspecs_from_this_bundle + # Find only the gems bundler is now using + specish_objects = Bundler.definition.locked_gems.specs + if specish_objects.first.respond_to?(:materialize_for_installation) + specish_objects = specish_objects.map(&:materialize_for_installation) + end + specish_objects.map do |specish| + if specish.respond_to?(:name) && specish.respond_to?(:version) && specish.respond_to?(:gem_dir) + # duck type is good enough for outside uses! + specish + else + to_gem_specification(specish) + end + end.compact + end + + # @return [Array] + def auto_required_gemspecs_from_bundler + return [] unless directory + + logger.info 'Fetching gemspecs autorequired from Bundler (bundler/require)' + @auto_required_gemspecs_from_bundler ||= + if in_this_bundle? + auto_required_gemspecs_from_this_bundle + else + auto_required_gemspecs_from_external_bundle + end + end + + # @return [Array] + def auto_required_gemspecs_from_this_bundle + # Adapted from require() in lib/bundler/runtime.rb + dep_names = Bundler.definition.dependencies.select do |dep| + dep.groups.include?(:default) && dep.should_include? + end.map(&:name) + + all_gemspecs_from_bundle.select { |gemspec| dep_names.include?(gemspec.name) } + end + + # @return [Array] + def auto_required_gemspecs_from_external_bundle + @auto_required_gemspecs_from_external_bundle ||= + begin + logger.info 'Fetching auto-required gemspecs from Bundler (bundler/require)' + command = + 'Bundler.definition.dependencies' \ + '.select { |dep| dep.groups.include?(:default) && dep.should_include? }' \ + '.map(&:name)' + # @sg-ignore + # @type [Array] + dep_names = query_external_bundle command + + all_gemspecs_from_bundle.select { |gemspec| dep_names.include?(gemspec.name) } + end + end + + # @param gemspec [Gem::Specification] + # @return [Array] + def only_runtime_dependencies gemspec + unless gemspec.respond_to?(:dependencies) && gemspec.respond_to?(:development_dependencies) + gemspec = to_gem_specification(gemspec) + end + return [] if gemspec.nil? + + gemspec.dependencies - gemspec.development_dependencies + end + + # @todo Should this be using Gem::SpecFetcher and pull them automatically? + # + # @param name [String] + # @param version_or_requirement [String, nil] + # @param out [IO, nil] output stream for logging + # + # @return [Gem::Specification, nil] + def resolve_gem_ignoring_local_bundle name, version_or_requirement = nil, out: $stderr + Gem::Specification.find_by_name(name, version_or_requirement) + rescue Gem::MissingSpecError + begin + Gem::Specification.find_by_name(name) + rescue Gem::MissingSpecError + stdlibmap = RbsMap::StdlibMap.new(name) + unless stdlibmap.resolved? + gem_desc = name + gem_desc += ":#{version_or_requirement}" if version_or_requirement + out&.puts "Please install the gem #{gem_desc} in Solargraph's Ruby environment" + end + nil # either not here or in stdlib + end + end + + # @sg-ignore flow sensitive typing needs better handling of ||= on lvars + # @return [Array] + def all_gemspecs_from_external_bundle + @all_gemspecs_from_external_bundle ||= + begin + logger.info 'Fetching gemspecs required from external bundle' + + command = 'specish_objects = Bundler.definition.locked_gems&.specs; ' \ + 'if specish_objects.first.respond_to?(:materialize_for_installation);' \ + 'specish_objects = specish_objects.map(&:materialize_for_installation);' \ + 'end;' \ + 'specish_objects.map { |specish| [specish.name, specish.version] }' + # @type [Array] + query_external_bundle(command).map do |name, version| + resolve_gem_ignoring_local_bundle(name, version) + end.compact + rescue Solargraph::BundleNotFoundError => e + Solargraph.logger.info e.message + # @sg-ignore Need to add nil check here + Solargraph.logger.debug e.backtrace.join("\n") + [] + end + end + + # @return [Hash{String => Gem::Specification}] + def preference_map + @preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] } + end + + # @param gemspec [Gem::Specification] + # + # @return [Gem::Specification] + def gemspec_or_preference gemspec + return gemspec unless preference_map.key?(gemspec.name) + return gemspec if gemspec.version == preference_map[gemspec.name].version + + change_gemspec_version gemspec, preference_map[gemspec.name].version + end + + # @param gemspec [Gem::Specification] + # @param version [String] + # @return [Gem::Specification] + def change_gemspec_version gemspec, version + Gem::Specification.find_by_name(gemspec.name, "= #{version}") + rescue Gem::MissingSpecError + Solargraph.logger.info "Gem #{gemspec.name} version #{version.inspect} not found. " \ + "Using #{gemspec.version} instead" + gemspec + end + end + end +end diff --git a/lib/solargraph/workspace/require_paths.rb b/lib/solargraph/workspace/require_paths.rb index c8eea161b..d12364b07 100644 --- a/lib/solargraph/workspace/require_paths.rb +++ b/lib/solargraph/workspace/require_paths.rb @@ -83,6 +83,7 @@ def require_path_from_gemspec_file gemspec_file_path return [] if hash.empty? hash['paths'].map { |path| File.join(base, path) } rescue StandardError => e + # @sg-ignore flow sensitive typing should be able to handle redefinition Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}" [] end diff --git a/lib/solargraph/yard_map/helpers.rb b/lib/solargraph/yard_map/helpers.rb index 96bc454b5..8c1747d9a 100644 --- a/lib/solargraph/yard_map/helpers.rb +++ b/lib/solargraph/yard_map/helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Solargraph class YardMap module Helpers @@ -5,7 +7,7 @@ module Helpers # @param code_object [YARD::CodeObjects::Base] # @param spec [Gem::Specification, nil] - # @return [Solargraph::Location, nil] + # @return [Solargraph::Location] def object_location code_object, spec if spec.nil? || code_object.nil? || code_object.file.nil? || code_object.line.nil? if code_object.namespace.is_a?(YARD::CodeObjects::NamespaceObject) @@ -14,6 +16,7 @@ def object_location code_object, spec end return Solargraph::Location.new(__FILE__, Solargraph::Range.from_to(__LINE__ - 1, 0, __LINE__ - 1, 0)) end + # @sg-ignore flow sensitive typing should be able to identify more blocks that always return file = File.join(spec.full_gem_path, code_object.file) Solargraph::Location.new(file, Solargraph::Range.from_to(code_object.line - 1, 0, code_object.line - 1, 0)) end @@ -21,10 +24,12 @@ def object_location code_object, spec # @param code_object [YARD::CodeObjects::Base] # @param spec [Gem::Specification, nil] # @return [Solargraph::Pin::Namespace] - def create_closure_namespace_for(code_object, spec) + def create_closure_namespace_for code_object, spec code_object_for_location = code_object # code_object.namespace is sometimes a YARD proxy object pointing to a method path ("Object#new") - code_object_for_location = code_object.namespace if code_object.namespace.is_a?(YARD::CodeObjects::NamespaceObject) + if code_object.namespace.is_a?(YARD::CodeObjects::NamespaceObject) + code_object_for_location = code_object.namespace + end namespace_location = object_location(code_object_for_location, spec) ns_name = code_object.namespace.to_s if ns_name.empty? diff --git a/lib/solargraph/yard_map/mapper.rb b/lib/solargraph/yard_map/mapper.rb index 592b3805e..a0109189b 100644 --- a/lib/solargraph/yard_map/mapper.rb +++ b/lib/solargraph/yard_map/mapper.rb @@ -24,6 +24,7 @@ def map end # Some yardocs contain documentation for dependencies that can be # ignored here. The YardMap will load dependencies separately. + # @sg-ignore Need to add nil check here @pins.keep_if { |pin| pin.location.nil? || File.file?(pin.location.filename) } if @spec @pins end @@ -34,24 +35,27 @@ def map # @return [Array] def generate_pins code_object result = [] - if code_object.is_a?(YARD::CodeObjects::NamespaceObject) + case code_object + when YARD::CodeObjects::NamespaceObject nspin = ToNamespace.make(code_object, @spec, @namespace_pins[code_object.namespace.to_s]) @namespace_pins[code_object.path] = nspin result.push nspin - if code_object.is_a?(YARD::CodeObjects::ClassObject) and !code_object.superclass.nil? + if code_object.is_a?(YARD::CodeObjects::ClassObject) && !code_object.superclass.nil? # This method of superclass detection is a bit of a hack. If # the superclass is a Proxy, it is assumed to be undefined in its # yardoc and converted to a fully qualified namespace. superclass = if code_object.superclass.is_a?(YARD::CodeObjects::Proxy) - "::#{code_object.superclass}" - else - code_object.superclass.to_s - end + "::#{code_object.superclass}" + else + code_object.superclass.to_s + end result.push Solargraph::Pin::Reference::Superclass.new(name: superclass, closure: nspin, source: :yard_map) end + # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' code_object.class_mixins.each do |m| result.push Solargraph::Pin::Reference::Extend.new(closure: nspin, name: m.path, source: :yard_map) end + # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' code_object.instance_mixins.each do |m| result.push Solargraph::Pin::Reference::Include.new( closure: nspin, # @todo Fix this @@ -59,8 +63,9 @@ def generate_pins code_object source: :yard_map ) end - elsif code_object.is_a?(YARD::CodeObjects::MethodObject) + when YARD::CodeObjects::MethodObject closure = @namespace_pins[code_object.namespace.to_s] + # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' if code_object.name == :initialize && code_object.scope == :instance # @todo Check the visibility of .new result.push ToMethod.make(code_object, 'new', :class, :public, closure, @spec) @@ -68,7 +73,7 @@ def generate_pins code_object else result.push ToMethod.make(code_object, nil, nil, nil, closure, @spec) end - elsif code_object.is_a?(YARD::CodeObjects::ConstantObject) + when YARD::CodeObjects::ConstantObject closure = @namespace_pins[code_object.namespace] result.push ToConstant.make(code_object, closure, @spec) end diff --git a/lib/solargraph/yard_map/mapper/to_method.rb b/lib/solargraph/yard_map/mapper/to_method.rb index 0838b9f4f..047f53957 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -6,10 +6,11 @@ class Mapper module ToMethod extend YardMap::Helpers + # @type [Hash{Array => Symbol}] VISIBILITY_OVERRIDE = { # YARD pays attention to 'private' statements prior to class methods but shouldn't - ["Rails::Engine", :class, "find_root_with_flag"] => :public - } + ['Rails::Engine', :class, 'find_root_with_flag'] => :public + }.freeze # @param code_object [YARD::CodeObjects::MethodObject] # @param name [String, nil] @@ -25,10 +26,15 @@ def self.make code_object, name = nil, scope = nil, visibility = nil, closure = return_type = ComplexType::SELF if name == 'new' comments = code_object.docstring ? code_object.docstring.all.to_s : '' final_scope = scope || code_object.scope + # @sg-ignore Need to add nil check here override_key = [closure.path, final_scope, name] final_visibility = VISIBILITY_OVERRIDE[override_key] + # @sg-ignore Need to add nil check here final_visibility ||= VISIBILITY_OVERRIDE[[closure.path, final_scope]] - final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name.to_sym) + # @sg-ignore Need to add nil check here + if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name.to_sym) + final_visibility ||= :private + end final_visibility ||= visibility final_visibility ||= :private if code_object.module_function? && final_scope == :instance final_visibility ||= :public if code_object.module_function? && final_scope == :class @@ -46,9 +52,10 @@ def self.make code_object, name = nil, scope = nil, visibility = nil, closure = explicit: code_object.is_explicit?, return_type: return_type, parameters: [], - source: :yardoc, + source: :yardoc ) else + # @sg-ignore Need to add nil check here pin = Pin::Method.new( location: location, closure: closure, @@ -61,7 +68,7 @@ def self.make code_object, name = nil, scope = nil, visibility = nil, closure = return_type: return_type, attribute: code_object.is_attribute?, parameters: [], - source: :yardoc, + source: :yardoc ) pin.parameters.concat get_parameters(code_object, location, comments, pin) pin.parameters.freeze @@ -85,7 +92,6 @@ 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, @@ -95,7 +101,7 @@ def get_parameters code_object, location, comments, pin presence: nil, decl: arg_type(a), asgn_code: a[1], - source: :yardoc, + source: :yardoc ) end end diff --git a/lib/solargraph/yard_map/mapper/to_namespace.rb b/lib/solargraph/yard_map/mapper/to_namespace.rb index f7063e3d6..3e0887ecd 100644 --- a/lib/solargraph/yard_map/mapper/to_namespace.rb +++ b/lib/solargraph/yard_map/mapper/to_namespace.rb @@ -21,8 +21,9 @@ def self.make code_object, spec, closure = nil type: code_object.is_a?(YARD::CodeObjects::ClassObject) ? :class : :module, visibility: code_object.visibility, closure: closure, + # @sg-ignore need to add a nil check here gates: closure.gates, - source: :yardoc, + source: :yardoc ) end end diff --git a/lib/solargraph/yard_map/to_method.rb b/lib/solargraph/yard_map/to_method.rb deleted file mode 100644 index 010db89a5..000000000 --- a/lib/solargraph/yard_map/to_method.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - class YardMap - class ToMethod - module InnerMethods - module_function - - # @param code_object [YARD::CodeObjects::Base] - # @param location [Solargraph::Location] - # @param comments [String] - # @return [Array] - def get_parameters code_object, location, comments - return [] unless code_object.is_a?(YARD::CodeObjects::MethodObject) - # 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, - closure: self, - comments: comments, - name: arg_name(a), - presence: nil, - decl: arg_type(a), - asgn_code: a[1], - source: :yard_map - ) - end - end - - # @param a [Array] - # @return [String] - def arg_name a - a[0].gsub(/[^a-z0-9_]/i, '') - end - - # @param a [Array] - # @return [::Symbol] - def arg_type a - if a[0].start_with?('**') - :kwrestarg - elsif a[0].start_with?('*') - :restarg - elsif a[0].start_with?('&') - :blockarg - elsif a[0].end_with?(':') - a[1] ? :kwoptarg : :kwarg - elsif a[1] - :optarg - else - :arg - end - end - end - private_constant :InnerMethods - - include Helpers - - # @param code_object [YARD::CodeObjects::MethodObject] - # @param name [String, nil] - # @param scope [Symbol, nil] - # @param visibility [Symbol, nil] - # @param closure [Solargraph::Pin::Base, nil] - # @param spec [Solargraph::Pin::Base, nil] - # @return [Solargraph::Pin::Method] - def make code_object, name = nil, scope = nil, visibility = nil, closure = nil, spec = nil - closure ||= Solargraph::Pin::Namespace.new( - name: code_object.namespace.to_s, - gates: [code_object.namespace.to_s] - ) - location = object_location(code_object, spec) - comments = code_object.docstring ? code_object.docstring.all.to_s : '' - Pin::Method.new( - location: location, - closure: closure, - name: name || code_object.name.to_s, - comments: comments, - scope: scope || code_object.scope, - visibility: visibility || code_object.visibility, - parameters: InnerMethods.get_parameters(code_object, location, comments), - explicit: code_object.is_explicit?, - source: :yard_map - ) - end - end - end -end diff --git a/lib/solargraph/yard_tags.rb b/lib/solargraph/yard_tags.rb index c34710b63..462138f9c 100644 --- a/lib/solargraph/yard_tags.rb +++ b/lib/solargraph/yard_tags.rb @@ -14,7 +14,7 @@ def call; end end # Define a @type tag for documenting variables -YARD::Tags::Library.define_tag("Type", :type, :with_types_and_name) +YARD::Tags::Library.define_tag('Type', :type, :with_types_and_name) # Define an @!override directive for overriding method tags -YARD::Tags::Library.define_directive("override", :with_name, Solargraph::DomainDirective) +YARD::Tags::Library.define_directive('override', :with_name, Solargraph::DomainDirective) diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 907afb2de..4accf9425 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -8,15 +8,15 @@ module Solargraph module Yardoc module_function - # Build and cache a gem's yardoc and return the path. If the cache already - # exists, do nothing and return the path. + # Build and save a gem's yardoc into a given path. # - # @param yard_plugins [Array] The names of YARD plugins to use. + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem + # @param yard_plugins [Array] # @param gemspec [Gem::Specification] - # @return [String] The path to the cached yardoc. - def cache(yard_plugins, gemspec) - path = PinCache.yardoc_path gemspec - return path if cached?(gemspec) + # + # @return [void] + def build_docs gem_yardoc_path, yard_plugins, gemspec + return if docs_built?(gem_yardoc_path) unless Dir.exist? gemspec.gem_dir # Can happen in at least some (old?) RubyGems versions when we @@ -24,35 +24,44 @@ def cache(yard_plugins, gemspec) # # https://github.com/apiology/solargraph/actions/runs/17650140201/job/50158676842?pr=10 Solargraph.logger.info { "Bad info from gemspec - #{gemspec.gem_dir} does not exist" } - return path + return end Solargraph.logger.info "Caching yardoc for #{gemspec.name} #{gemspec.version}" - cmd = "yardoc --db #{path} --no-output --plugin solargraph" + cmd = "yardoc --db #{gem_yardoc_path} --no-output --plugin solargraph" yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" } Solargraph.logger.debug { "Running: #{cmd}" } # @todo set these up to run in parallel + # @sg-ignore Our fill won't work properly due to an issue in + # Callable#arity_matches? - see comment there 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 - end - path + return if status.success? + Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } + Solargraph.logger.info stdout_and_stderr_str + end + + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [StringIO, IO, nil] where to log messages + # @return [Array] + def build_pins gem_yardoc_path, gemspec, out: $stderr + yardoc = load!(gem_yardoc_path) + YardMap::Mapper.new(yardoc, gemspec).map end # True if the gem yardoc is cached. # - # @param gemspec [Gem::Specification] - def cached?(gemspec) - yardoc = File.join(PinCache.yardoc_path(gemspec), 'complete') + # @param gem_yardoc_path [String] + def docs_built? gem_yardoc_path + yardoc = File.join(gem_yardoc_path, 'complete') File.exist?(yardoc) end # True if another process is currently building the yardoc cache. # - # @param gemspec [Gem::Specification] - def processing?(gemspec) - yardoc = File.join(PinCache.yardoc_path(gemspec), 'processing') + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem + def processing? gem_yardoc_path + yardoc = File.join(gem_yardoc_path, 'processing') File.exist?(yardoc) end @@ -60,10 +69,10 @@ def processing?(gemspec) # # @note This method modifies the global YARD registry. # - # @param gemspec [Gem::Specification] + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem # @return [Array] - def load!(gemspec) - YARD::Registry.load! PinCache.yardoc_path gemspec + def load! gem_yardoc_path + YARD::Registry.load! gem_yardoc_path YARD::Registry.all end @@ -78,6 +87,7 @@ def load!(gemspec) # @return [Hash{String => String}] a hash of environment variables to override def current_bundle_env_tweaks tweaks = {} + # @sg-ignore Translate to something flow sensitive typing understands if ENV['BUNDLE_GEMFILE'] && !ENV['BUNDLE_GEMFILE'].empty? tweaks['BUNDLE_GEMFILE'] = File.expand_path(ENV['BUNDLE_GEMFILE']) end diff --git a/rbs/fills/rubygems/0/dependency.rbs b/rbs/fills/rubygems/0/dependency.rbs new file mode 100644 index 000000000..db572b80d --- /dev/null +++ b/rbs/fills/rubygems/0/dependency.rbs @@ -0,0 +1,193 @@ +# +# The Dependency class holds a Gem name and a Gem::Requirement. +# +class Gem::Dependency + @name: untyped + + @requirement: untyped + + @type: untyped + + @prerelease: untyped + + @version_requirements: untyped + + @version_requirement: untyped + + # + # Valid dependency types. + # + TYPES: ::Array[:development | :runtime] + + # + # Dependency name or regular expression. + # + attr_accessor name: untyped + + # + # Allows you to force this dependency to be a prerelease. + # + attr_writer prerelease: untyped + + # + # Constructs a dependency with `name` and `requirements`. The last argument can + # optionally be the dependency type, which defaults to `:runtime`. + # + def initialize: (untyped name, *untyped requirements) -> void + + def hash: () -> untyped + + def inspect: () -> untyped + + # + # Does this dependency require a prerelease? + # + def prerelease?: () -> untyped + + # + # Is this dependency simply asking for the latest version of a gem? + # + def latest_version?: () -> untyped + + def pretty_print: (untyped q) -> untyped + + # + # What does this dependency require? + # + def requirement: () -> untyped + + # + # + def requirements_list: () -> untyped + + def to_s: () -> ::String + + # + # Dependency type. + # + def type: () -> untyped + + # + # + def runtime?: () -> untyped + + def ==: (untyped other) -> untyped + + # + # Dependencies are ordered by name. + # + def <=>: (untyped other) -> untyped + + # + # Uses this dependency as a pattern to compare to `other`. This dependency will + # match if the name matches the other's name, and other has only an equal + # version requirement that satisfies this dependency. + # + def =~: (untyped other) -> (nil | false | untyped) + + # + # + alias === =~ + + # + # Does this dependency match the specification described by `name` and `version` + # or match `spec`? + # + # NOTE: Unlike #matches_spec? this method does not return true when the version + # is a prerelease version unless this is a prerelease dependency. + # + def match?: (untyped obj, ?untyped? version, ?bool allow_prerelease) -> (false | true | untyped) + + # + # Does this dependency match `spec`? + # + # NOTE: This is not a convenience method. Unlike #match? this method returns + # true when `spec` is a prerelease version even if this dependency is not a + # prerelease dependency. + # + def matches_spec?: (untyped spec) -> (false | true | untyped) + + # + # Merges the requirements of `other` into this dependency + # + def merge: (untyped other) -> untyped + + # + # + def matching_specs: (?bool platform_only) -> untyped + + # + # True if the dependency will not always match the latest version. + # + def specific?: () -> untyped + + # + # + def to_specs: () -> untyped + + # + # + def to_spec: () -> untyped + + # + # + def identity: () -> (:complete | :abs_latest | :latest | :released) + + def encode_with: (untyped coder) -> untyped +end diff --git a/rbs/fills/tuple/tuple.rbs b/rbs/fills/tuple/tuple.rbs index f4e213355..c21f13e1a 100644 --- a/rbs/fills/tuple/tuple.rbs +++ b/rbs/fills/tuple/tuple.rbs @@ -144,6 +144,34 @@ module Solargraph | [T] (8 index) { (int index) -> T } -> (I | T) | [T] (9 index) { (int index) -> T } -> (J | T) | [T] (int index) { (int index) -> T } -> (A | B | C | D | E | F | G | H | I | J | T) + + # + # Returns elements from `self`, or `nil`; does not modify `self`. + # + # With no argument given, returns the first element (if available): + # + # a = [:foo, 'bar', 2] + # a.first # => :foo + # a # => [:foo, "bar", 2] + # + # If `self` is empty, returns `nil`. + # + # [].first # => nil + # + # With a non-negative integer argument `count` given, returns the first `count` + # elements (as available) in a new array: + # + # a.first(0) # => [] + # a.first(2) # => [:foo, "bar"] + # a.first(50) # => [:foo, "bar", 2] + # + # Related: see [Methods for Querying](rdoc-ref:Array@Methods+for+Querying). + # + def first: %a{implicitly-returns-nil} () -> A end end end \ No newline at end of file diff --git a/rbs/shims/ast/0/node.rbs b/rbs/shims/ast/0/node.rbs index fab1a4de0..a9d8e06e1 100644 --- a/rbs/shims/ast/0/node.rbs +++ b/rbs/shims/ast/0/node.rbs @@ -1,5 +1,5 @@ module ::AST class Node - def children: () -> [self, Integer, String, Symbol, nil] + def children: () -> Array[self | Integer | String | Symbol | nil] end end diff --git a/rbs/shims/diff-lcs/1.5/diff-lcs.rbs b/rbs/shims/diff-lcs/1.5/diff-lcs.rbs new file mode 100644 index 000000000..40d4fc7e4 --- /dev/null +++ b/rbs/shims/diff-lcs/1.5/diff-lcs.rbs @@ -0,0 +1,11 @@ +module Diff +end + +module Diff::LCS + def self.LCS: (Array[String], Array[String]) -> Array[String] + | (String, String) -> Array[String] + def self.diff: (Array[String], Array[String]) -> Array[Array[String]] + | (String, String) -> Array[Array[Diff::LCS::Change]] + + def self.patch!: (Array[String], Array[String]) -> String +end diff --git a/solargraph.gemspec b/solargraph.gemspec index 98f524d4d..06edbf19f 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -1,75 +1,78 @@ -$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 - s.date = Date.today.strftime("%Y-%m-%d") - s.summary = "A Ruby language server" - s.description = "IDE tools for code completion, inline documentation, and static analysis" - s.authors = ["Fred Snyder"] - s.email = 'admin@castwide.com' - s.files = Dir.chdir(File.expand_path('..', __FILE__)) do - # @sg-ignore Need backtick support - # @type [String] - all_files = `git ls-files -z` - all_files.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } - end - s.homepage = 'https://solargraph.org' - s.license = 'MIT' - s.executables = ['solargraph'] - s.metadata["funding_uri"] = "https://www.patreon.com/castwide" - s.metadata["bug_tracker_uri"] = "https://github.com/castwide/solargraph/issues" - s.metadata["changelog_uri"] = "https://github.com/castwide/solargraph/blob/master/CHANGELOG.md" - s.metadata["source_code_uri"] = "https://github.com/castwide/solargraph" - s.metadata["rubygems_mfa_required"] = "true" - - s.required_ruby_version = '>= 3.0' - - s.add_runtime_dependency 'ast', '~> 2.4.3' - s.add_runtime_dependency 'backport', '~> 1.2' - s.add_runtime_dependency 'benchmark', '~> 0.4' - s.add_runtime_dependency 'bundler', '>= 2.0' - s.add_runtime_dependency 'diff-lcs', '~> 1.4' - s.add_runtime_dependency 'jaro_winkler', '~> 1.6', '>= 1.6.1' - s.add_runtime_dependency 'kramdown', '~> 2.3' - s.add_runtime_dependency 'kramdown-parser-gfm', '~> 1.1' - s.add_runtime_dependency 'logger', '~> 1.6' - s.add_runtime_dependency 'observer', '~> 0.1' - s.add_runtime_dependency 'ostruct', '~> 0.6' - s.add_runtime_dependency 'open3', '~> 0.2.1' - s.add_runtime_dependency 'parser', '~> 3.0' - s.add_runtime_dependency 'prism', '~> 1.4' - s.add_runtime_dependency 'rbs', ['>= 3.6.1', '<= 4.0.0.dev.4'] - s.add_runtime_dependency 'reverse_markdown', '~> 3.0' - s.add_runtime_dependency 'rubocop', '~> 1.76' - s.add_runtime_dependency 'thor', '~> 1.0' - s.add_runtime_dependency 'tilt', '~> 2.0' - s.add_runtime_dependency 'yard', '~> 0.9', '>= 0.9.24' - s.add_runtime_dependency 'yard-solargraph', '~> 0.1' - s.add_runtime_dependency 'yard-activesupport-concern', '~> 0.0' - - s.add_development_dependency 'pry', '~> 0.15' - s.add_development_dependency 'public_suffix', '~> 3.1' - s.add_development_dependency 'rake', '~> 13.2' - s.add_development_dependency 'rspec', '~> 3.5' - # - # very specific development-time RuboCop version patterns for CI - # stability - feel free to update in an isolated PR - # - # even more specific on RuboCop itself, which is written into _todo - # file. - s.add_development_dependency 'rubocop', '~> 1.80.0.0' - s.add_development_dependency 'rubocop-rake', '~> 0.7.1' - s.add_development_dependency 'rubocop-rspec', '~> 3.6.0' - s.add_development_dependency 'rubocop-yard', '~> 1.0.0' - s.add_development_dependency 'simplecov', '~> 0.21' - s.add_development_dependency 'simplecov-lcov', '~> 0.8' - s.add_development_dependency 'undercover', '~> 0.7' - s.add_development_dependency 'overcommit', '~> 0.68.0' - s.add_development_dependency 'webmock', '~> 3.6' - # work around missing yard dependency needed as of Ruby 3.5 - s.add_development_dependency 'irb', '~> 1.15' -end +# frozen_string_literal: true + +# @sg-ignore Should better support meaning of '&' in RBS +$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 + s.summary = 'A Ruby language server' + s.description = 'IDE tools for code completion, inline documentation, and static analysis' + s.authors = ['Fred Snyder'] + s.email = 'admin@castwide.com' + s.files = Dir.chdir(File.expand_path(__dir__)) do + # @sg-ignore Need backtick support + # @type [String] + all_files = `git ls-files -z` + all_files.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + end + s.homepage = 'https://solargraph.org' + s.license = 'MIT' + s.executables = ['solargraph'] + s.metadata['funding_uri'] = 'https://www.patreon.com/castwide' + s.metadata['bug_tracker_uri'] = 'https://github.com/castwide/solargraph/issues' + s.metadata['changelog_uri'] = 'https://github.com/castwide/solargraph/blob/master/CHANGELOG.md' + s.metadata['source_code_uri'] = 'https://github.com/castwide/solargraph' + s.metadata['rubygems_mfa_required'] = 'true' + + s.required_ruby_version = '>= 3.0' + + s.add_dependency 'ast', '~> 2.4.3' + s.add_dependency 'backport', '~> 1.2' + s.add_dependency 'benchmark', '~> 0.4' + s.add_dependency 'bundler', '>= 2.0' + s.add_dependency 'diff-lcs', '~> 1.4' + s.add_dependency 'jaro_winkler', '~> 1.6', '>= 1.6.1' + s.add_dependency 'kramdown', '~> 2.3' + s.add_dependency 'kramdown-parser-gfm', '~> 1.1' + s.add_dependency 'logger', '~> 1.6' + s.add_dependency 'observer', '~> 0.1' + s.add_dependency 'open3', '~> 0.2.1' + s.add_dependency 'ostruct', '~> 0.6' + s.add_dependency 'parser', '~> 3.0' + s.add_dependency 'prism', '~> 1.4' + s.add_dependency 'rbs', ['>= 3.6.1', '<= 4.0.0.dev.5'] + s.add_dependency 'reverse_markdown', '~> 3.0' + s.add_dependency 'rubocop', '~> 1.76' + s.add_dependency 'thor', '~> 1.0' + s.add_dependency 'tilt', '~> 2.0' + s.add_dependency 'yard', '~> 0.9', '>= 0.9.24' + s.add_dependency 'yard-activesupport-concern', '~> 0.0' + s.add_dependency 'yard-solargraph', '~> 0.1' + + s.add_development_dependency 'pry', '~> 0.15' + s.add_development_dependency 'public_suffix', '~> 3.1' + s.add_development_dependency 'rake', '~> 13.2' + s.add_development_dependency 'rspec', '~> 3.5' + # + # very specific development-time RuboCop version patterns for CI + # stability - feel free to update in an isolated PR + # + # even more specific on RuboCop itself, which is written into _todo + # file. + s.add_development_dependency 'overcommit', '~> 0.68.0' + s.add_development_dependency 'rubocop', '~> 1.80.0.0' + s.add_development_dependency 'rubocop-rake', '~> 0.7.1' + s.add_development_dependency 'rubocop-rspec', '~> 3.6.0' + s.add_development_dependency 'rubocop-yard', '~> 1.0.0' + s.add_development_dependency 'simplecov', '~> 0.21' + s.add_development_dependency 'simplecov-lcov', '~> 0.8' + s.add_development_dependency 'undercover', '~> 0.7' + s.add_development_dependency 'vernier', '< 2' + s.add_development_dependency 'webmock', '~> 3.6' + # work around missing yard dependency needed as of Ruby 3.5 + s.add_development_dependency 'irb', '~> 1.15' +end diff --git a/spec/api_map/cache_spec.rb b/spec/api_map/cache_spec.rb index 20e5b9df1..392f04fe4 100644 --- a/spec/api_map/cache_spec.rb +++ b/spec/api_map/cache_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + describe Solargraph::ApiMap::Cache do - it "recognizes empty caches" do - cache = Solargraph::ApiMap::Cache.new + it 'recognizes empty caches' do + cache = described_class.new expect(cache).to be_empty cache.set_methods('', :class, [:public], true, []) expect(cache).not_to be_empty diff --git a/spec/api_map/config_spec.rb b/spec/api_map/config_spec.rb index 5790265cd..993cb9a97 100644 --- a/spec/api_map/config_spec.rb +++ b/spec/api_map/config_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'tmpdir' describe Solargraph::Workspace::Config do @@ -5,62 +7,62 @@ let(:workspace_path) { File.realpath(Dir.mktmpdir) } let(:global_path) { @global_path } - before(:each) do + before do @global_path = File.realpath(Dir.mktmpdir) - @orig_env = ENV['SOLARGRAPH_GLOBAL_CONFIG'] + @orig_env = ENV.fetch('SOLARGRAPH_GLOBAL_CONFIG', nil) ENV['SOLARGRAPH_GLOBAL_CONFIG'] = File.join(@global_path, '.solargraph.yml') end - after(:each) do + after do ENV['SOLARGRAPH_GLOBAL_CONFIG'] = @orig_env FileUtils.remove_entry(workspace_path) FileUtils.remove_entry(global_path) end - it "reads workspace files from config" do + it 'reads workspace files from config' do File.write(File.join(workspace_path, 'foo.rb'), 'test') File.write(File.join(workspace_path, 'bar.rb'), 'test') File.open(File.join(workspace_path, '.solargraph.yml'), 'w') do |file| - file.puts "include:" - file.puts " - foo.rb" - file.puts "exclude:" - file.puts " - bar.rb" + file.puts 'include:' + file.puts ' - foo.rb' + file.puts 'exclude:' + file.puts ' - bar.rb' end expect(config.included).to eq([File.join(workspace_path, 'foo.rb')]) expect(config.excluded).to eq([File.join(workspace_path, 'bar.rb')]) end - it "reads workspace files from global config" do + it 'reads workspace files from global config' do File.write(File.join(workspace_path, 'foo.rb'), 'test') File.write(File.join(workspace_path, 'bar.rb'), 'test') File.open(File.join(global_path, '.solargraph.yml'), 'w') do |file| - file.puts "include:" - file.puts " - foo.rb" - file.puts "exclude:" - file.puts " - bar.rb" + file.puts 'include:' + file.puts ' - foo.rb' + file.puts 'exclude:' + file.puts ' - bar.rb' end expect(config.included).to eq([File.join(workspace_path, 'foo.rb')]) expect(config.excluded).to eq([File.join(workspace_path, 'bar.rb')]) end - it "overrides global config with workspace config" do + it 'overrides global config with workspace config' do File.write(File.join(workspace_path, 'foo.rb'), 'test') File.write(File.join(workspace_path, 'bar.rb'), 'test') - + File.open(File.join(workspace_path, '.solargraph.yml'), 'w') do |file| - file.puts "include:" - file.puts " - foo.rb" - file.puts "max_files: 8000" + file.puts 'include:' + file.puts ' - foo.rb' + file.puts 'max_files: 8000' end File.open(File.join(global_path, '.solargraph.yml'), 'w') do |file| - file.puts "include:" - file.puts " - include.rb" - file.puts "exclude:" - file.puts " - bar.rb" - file.puts "max_files: 1000" + file.puts 'include:' + file.puts ' - include.rb' + file.puts 'exclude:' + file.puts ' - bar.rb' + file.puts 'max_files: 1000' end expect(config.included).to eq([File.join(workspace_path, 'foo.rb')]) diff --git a/spec/api_map/index_spec.rb b/spec/api_map/index_spec.rb new file mode 100644 index 000000000..8afb74759 --- /dev/null +++ b/spec/api_map/index_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +describe Solargraph::ApiMap::Index do + subject(:output_pins) { described_class.new(input_pins).pins } + + describe '#map_overrides' do + let(:foo_class) do + Solargraph::Pin::Namespace.new(name: 'Foo') + end + + let(:foo_initialize) do + init = Solargraph::Pin::Method.new(name: 'initialize', + scope: :instance, + parameters: [], + closure: foo_class) + # no return type specified + param = Solargraph::Pin::Parameter.new(name: 'bar', + closure: init) + init.parameters << param + init + end + + let(:foo_new) do + init = Solargraph::Pin::Method.new(name: 'new', + scope: :class, + parameters: [], + closure: foo_class) + # no return type specified + param = Solargraph::Pin::Parameter.new(name: 'bar', + closure: init) + init.parameters << param + init + end + + let(:foo_override) do + Solargraph::Pin::Reference::Override.from_comment('Foo#initialize', + '@param [String] bar') + end + + let(:input_pins) do + [ + foo_initialize, + foo_new, + foo_override + ] + end + + it 'has a docstring to process on override' do + expect(foo_override.docstring.tags).to be_empty + end + + it 'overrides .new method' do + method_pin = output_pins.find { |pin| pin.path == 'Foo.new' } + first_parameter = method_pin.parameters.first + expect(first_parameter.return_type.tag).to eq('String') + end + + it 'overrides #initialize method in signature' do + method_pin = output_pins.find { |pin| pin.path == 'Foo#initialize' } + first_parameter = method_pin.parameters.first + expect(first_parameter.return_type.tag).to eq('String') + end + end +end diff --git a/spec/api_map/source_to_yard_spec.rb b/spec/api_map/source_to_yard_spec.rb index 88be54c64..c232ca41d 100644 --- a/spec/api_map/source_to_yard_spec.rb +++ b/spec/api_map/source_to_yard_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe Solargraph::ApiMap::SourceToYard do - it "rakes sources" do + it 'rakes sources' do source = Solargraph::SourceMap.load_string(%( module Foo class Bar @@ -9,7 +11,7 @@ def baz end )) object = Object.new - object.extend Solargraph::ApiMap::SourceToYard + object.extend described_class object.rake_yard Solargraph::ApiMap::Store.new(source.pins) expect(object.code_object_paths.length).to eq(3) expect(object.code_object_paths).to include('Foo') @@ -17,7 +19,7 @@ def baz expect(object.code_object_paths).to include('Foo::Bar#baz') end - it "generates docstrings" do + it 'generates docstrings' do source = Solargraph::SourceMap.load_string(%( # My foo class 描述 class Foo @@ -30,7 +32,7 @@ def self.baz end )) object = Object.new - object.extend Solargraph::ApiMap::SourceToYard + object.extend described_class object.rake_yard Solargraph::ApiMap::Store.new(source.pins) class_object = object.code_object_at('Foo') expect(class_object.docstring).to eq('My foo class 描述') @@ -40,7 +42,7 @@ def self.baz expect(class_method_object.tag(:return).types).to eq(['Foo']) end - it "generates instance mixins" do + it 'generates instance mixins' do source = Solargraph::SourceMap.load_string(%( module Foo def bar @@ -51,14 +53,14 @@ class Baz end )) object = Object.new - object.extend Solargraph::ApiMap::SourceToYard + object.extend described_class object.rake_yard Solargraph::ApiMap::Store.new(source.pins) module_object = object.code_object_at('Foo') class_object = object.code_object_at('Baz') expect(class_object.instance_mixins).to include(module_object) end - it "generates class mixins" do + it 'generates class mixins' do source = Solargraph::SourceMap.load_string(%( module Foo def bar; end @@ -68,14 +70,14 @@ class Baz end )) object = Object.new - object.extend Solargraph::ApiMap::SourceToYard + object.extend described_class object.rake_yard Solargraph::ApiMap::Store.new(source.pins) module_object = object.code_object_at('Foo') class_object = object.code_object_at('Baz') expect(class_object.class_mixins).to include(module_object) end - it "generates methods for attributes" do + it 'generates methods for attributes' do source = Solargraph::SourceMap.load_string(%( class Foo attr_reader :bar @@ -84,17 +86,17 @@ class Foo end )) object = Object.new - object.extend Solargraph::ApiMap::SourceToYard + object.extend described_class object.rake_yard Solargraph::ApiMap::Store.new(source.pins) - expect(object.code_object_at('Foo#bar')).not_to be(nil) - expect(object.code_object_at('Foo#bar=')).to be(nil) - expect(object.code_object_at('Foo#baz')).to be(nil) - expect(object.code_object_at('Foo#baz=')).not_to be(nil) - expect(object.code_object_at('Foo#boo')).not_to be(nil) - expect(object.code_object_at('Foo#boo=')).not_to be(nil) + expect(object.code_object_at('Foo#bar')).not_to be_nil + expect(object.code_object_at('Foo#bar=')).to be_nil + expect(object.code_object_at('Foo#baz')).to be_nil + expect(object.code_object_at('Foo#baz=')).not_to be_nil + expect(object.code_object_at('Foo#boo')).not_to be_nil + expect(object.code_object_at('Foo#boo=')).not_to be_nil end - it "generates method parameters" do + it 'generates method parameters' do source = Solargraph::SourceMap.load_string(%( class Foo def bar baz, boo = 'boo' @@ -102,7 +104,7 @@ def bar baz, boo = 'boo' end )) object = Object.new - object.extend Solargraph::ApiMap::SourceToYard + object.extend described_class object.rake_yard Solargraph::ApiMap::Store.new(source.pins) method_object = object.code_object_at('Foo#bar') expect(method_object.parameters.length).to eq(2) @@ -110,7 +112,7 @@ def bar baz, boo = 'boo' expect(method_object.parameters[1]).to eq(['boo', "'boo'"]) end - it "generates method keyword parameters" do + it 'generates method keyword parameters' do source = Solargraph::SourceMap.load_string(%( class Foo def bar baz, boo: 'boo' @@ -118,7 +120,7 @@ def bar baz, boo: 'boo' end )) object = Object.new - object.extend Solargraph::ApiMap::SourceToYard + object.extend described_class object.rake_yard Solargraph::ApiMap::Store.new(source.pins) method_object = object.code_object_at('Foo#bar') expect(method_object.parameters.length).to eq(2) diff --git a/spec/api_map/store_spec.rb b/spec/api_map/store_spec.rb index dc8413de5..059c3eb1b 100644 --- a/spec/api_map/store_spec.rb +++ b/spec/api_map/store_spec.rb @@ -4,7 +4,7 @@ it 'indexes multiple pinsets' do foo_pin = Solargraph::Pin::Namespace.new(name: 'Foo') bar_pin = Solargraph::Pin::Namespace.new(name: 'Bar') - store = Solargraph::ApiMap::Store.new([foo_pin], [bar_pin]) + store = described_class.new([foo_pin], [bar_pin]) expect(store.get_path_pins('Foo')).to eq([foo_pin]) expect(store.get_path_pins('Bar')).to eq([bar_pin]) @@ -13,7 +13,7 @@ it 'indexes empty pinsets' do foo_pin = Solargraph::Pin::Namespace.new(name: 'Foo') - store = Solargraph::ApiMap::Store.new([], [foo_pin]) + store = described_class.new([], [foo_pin]) expect(store.get_path_pins('Foo')).to eq([foo_pin]) end @@ -21,7 +21,7 @@ foo_pin = Solargraph::Pin::Namespace.new(name: 'Foo') bar_pin = Solargraph::Pin::Namespace.new(name: 'Bar') baz_pin = Solargraph::Pin::Namespace.new(name: 'Baz') - store = Solargraph::ApiMap::Store.new([foo_pin], [bar_pin]) + store = described_class.new([foo_pin], [bar_pin]) store.update([foo_pin], [baz_pin]) expect(store.get_path_pins('Foo')).to eq([foo_pin]) @@ -32,7 +32,7 @@ it 'updates new pinsets' do foo_pin = Solargraph::Pin::Namespace.new(name: 'Foo') bar_pin = Solargraph::Pin::Namespace.new(name: 'Bar') - store = Solargraph::ApiMap::Store.new([foo_pin]) + store = described_class.new([foo_pin]) store.update([foo_pin], [bar_pin]) expect(store.get_path_pins('Foo')).to eq([foo_pin]) @@ -42,7 +42,7 @@ it 'updates empty stores' do foo_pin = Solargraph::Pin::Namespace.new(name: 'Foo') bar_pin = Solargraph::Pin::Namespace.new(name: 'Bar') - store = Solargraph::ApiMap::Store.new + store = described_class.new store.update([foo_pin, bar_pin]) expect(store.get_path_pins('Foo')).to eq([foo_pin]) @@ -56,20 +56,20 @@ class Foo; end class Bar < Foo; end ), 'test.rb') - store = Solargraph::ApiMap::Store.new(map.pins) + store = described_class.new(map.pins) ref = store.get_superclass('Bar') expect(ref.name).to eq('Foo') end it 'returns Boolean superclass' do - store = Solargraph::ApiMap::Store.new + store = described_class.new ref = store.get_superclass('TrueClass') expect(ref.name).to eq('Boolean') end it 'maps core Errno classes' do map = Solargraph::RbsMap::CoreMap.new - store = Solargraph::ApiMap::Store.new(map.pins) + store = described_class.new(map.pins) Errno.constants.each do |const| pin = store.get_path_pins("Errno::#{const}").first expect(pin).to be_a(Solargraph::Pin::Namespace) diff --git a/spec/api_map_method_spec.rb b/spec/api_map_method_spec.rb index 9d4e4f553..87469562b 100644 --- a/spec/api_map_method_spec.rb +++ b/spec/api_map_method_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -describe 'Solargraph::ApiMap methods' do - let(:api_map) { Solargraph::ApiMap.new } +describe Solargraph::ApiMap do + let(:api_map) { described_class.new } let(:bench) do Solargraph::Bench.new(external_requires: external_requires, workspace: Solargraph::Workspace.new('.')) end @@ -11,6 +11,13 @@ api_map.catalog bench end + describe '#resolve_method_alias' do + it 'resolves the IO.for_fd alias to IO.new' do + stack = api_map.get_method_stack('IO', 'for_fd', scope: :class) + expect(stack.map(&:class).uniq).to eq([Solargraph::Pin::Method]) + end + end + describe '#qualify' do let(:external_requires) { ['yaml'] } @@ -30,7 +37,7 @@ module Bar Bar::Baz ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) + api_map = described_class.new.map(source) clip = api_map.clip_at('test.rb', [11, 8]) expect(clip.infer.to_s).to eq('Symbol') @@ -53,7 +60,7 @@ module Bar a ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) + api_map = described_class.new.map(source) clip = api_map.clip_at('test.rb', [13, 8]) expect(clip.infer.to_s).to eq('Symbol') @@ -78,7 +85,7 @@ module Bar a ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) + api_map = described_class.new.map(source) clip = api_map.clip_at('test.rb', [15, 8]) expect(clip.infer.to_s).to eq('Symbol') @@ -103,7 +110,7 @@ class B a ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) + api_map = described_class.new.map(source) clip = api_map.clip_at('test.rb', [15, 8]) expect(clip.infer.to_s).to eq('Symbol') @@ -112,7 +119,7 @@ class B describe '#get_method_stack' do let(:out) { StringIO.new } - let(:api_map) { Solargraph::ApiMap.load_with_cache(Dir.pwd, out) } + let(:api_map) { described_class.load_with_cache(Dir.pwd, out) } context 'with stdlib that has vital dependencies' do let(:external_requires) { ['yaml'] } @@ -133,6 +140,30 @@ class B end end + describe '#cache_all_for_doc_map!' do + it 'can cache gems without a bench' do + api_map = described_class.new + doc_map = instance_double(Solargraph::DocMap, cache_doc_map_gems!: true) + allow(Solargraph::DocMap).to receive(:new).and_return(doc_map) + api_map.cache_all_for_doc_map!(out: $stderr) + expect(doc_map).to have_received(:cache_doc_map_gems!).with($stderr, rebuild: false) + end + end + + describe '#workspace' do + it 'can get a default workspace without a bench' do + api_map = described_class.new + expect(api_map.workspace).not_to be_nil + end + end + + describe '#uncached_gemspecs' do + it 'can get uncached gemspecs workspace without a bench' do + api_map = described_class.new + expect(api_map.uncached_gemspecs).not_to be_nil + end + end + describe '#get_methods' do it 'recognizes mixin references from context' do source = Solargraph::Source.load_string(%( @@ -147,7 +178,7 @@ class Includer end ), 'test.rb') - api_map = Solargraph::ApiMap.new + api_map = described_class.new api_map.map source pins = api_map.get_methods('Foo::Includer') expect(pins.map(&:path)).to include('Foo::Bar#baz') diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index 1e0130c14..facf9489c 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + require 'tmpdir' describe Solargraph::ApiMap do before :all do - @api_map = Solargraph::ApiMap.new + @api_map = described_class.new end it 'returns core methods' do @@ -114,8 +116,9 @@ class Baz expect(paths).to include('Foo::Baz') end - # @todo Working on context resolution - xit 'finds nested namespaces within a context' do + it 'finds nested namespaces within a context' do + pending('better context resolution') + map = Solargraph::SourceMap.load_string(%( module Foo class Bar @@ -130,8 +133,9 @@ class Baz expect(pins.map(&:path)).to include('Foo::Bar::BAR_CONSTANT') end - # @todo This might be invalid now - xit 'checks constant visibility' do + it 'checks constant visibility' do + pending('This might be invalid now') + map = Solargraph::SourceMap.load_string(%( module Foo FOO_CONSTANT = 'foo' @@ -158,13 +162,6 @@ module Foo expect(pins.map(&:path)).to include('String#upcase') end - it 'gets class methods for complex types' do - @api_map.index [] - type = Solargraph::ComplexType.parse('Class') - pins = @api_map.get_complex_type_methods(type) - expect(pins.map(&:path)).to include('String.try_convert') - end - it 'checks visibility of complex type methods' do map = Solargraph::SourceMap.load_string(%( class Foo @@ -195,7 +192,7 @@ def prot end it 'adds Object instance methods to duck types' do - api_map = Solargraph::ApiMap.new + api_map = described_class.new type = Solargraph::ComplexType.parse('#foo') pins = api_map.get_complex_type_methods(type) expect(pins.any? { |p| p.namespace == 'BasicObject' }).to be(true) @@ -432,18 +429,19 @@ class Sup expect(pins.map(&:path)).to include('Mixin#bar') end - # pending https://github.com/apiology/solargraph/pull/4 - xit 'understands tuples inherit from regular arrays' do + it 'understands tuples inherit from regular arrays' do + pending('Fix to remove trailing generic<> after resolution') + method_pins = @api_map.get_method_stack("Array(1, 2, 'a')", 'include?') method_pin = method_pins.first - expect(method_pin).to_not be_nil + expect(method_pin).not_to be_nil expect(method_pin.path).to eq('Array#include?') parameter_type = method_pin.signatures.first.parameters.first.return_type expect(parameter_type.rooted_tags).to eq("1, 2, 'a'") end it 'loads workspaces from directories' do - api_map = Solargraph::ApiMap.load('spec/fixtures/workspace') + api_map = described_class.load('spec/fixtures/workspace') expect(api_map.source_map(File.absolute_path('spec/fixtures/workspace/app.rb'))).to be_a(Solargraph::SourceMap) end @@ -461,8 +459,7 @@ class Container expect(pins.map(&:path)).to include('Mixin::FOO') end - # @todo This test needs changed - xit 'sorts constants by name' do + it 'sorts constants by name' do source = Solargraph::Source.load_string(%( module Foo AAB = 'aaa' @@ -535,15 +532,11 @@ module Includer end # @todo Qualify methods might not accept parametrized types anymore - xit 'handles multiple type parameters without losing cache coherence' do + it 'handles multiple type parameters without losing cache coherence' do tag = @api_map.qualify('Array') expect(tag).to eq('Array') tag = @api_map.qualify('Array') expect(tag).to eq('Array') - end - - # @todo Qualify methods might not accept parametrized types anymore - xit 'handles multiple type parameters without losing cache coherence' do tag = @api_map.qualify('Hash{Integer => String}') expect(tag).to eq('Hash{Integer => String}') end @@ -678,8 +671,9 @@ class Container expect(paths).to eq(['Prepended::PRE_CONST']) end - # @todo This test fails with lazy dynamic rebinding - xit 'finds instance variables in yieldreceiver blocks' do + it 'finds instance variables in yieldreceiver blocks' do + pending('lazy dynamic rebinding fixes') + source = Solargraph::Source.load_string(%( module Container # @yieldreceiver [Container] @@ -760,18 +754,18 @@ def bar; end end it 'can qualify "Boolean"' do - api_map = Solargraph::ApiMap.new + api_map = described_class.new expect(api_map.qualify('Boolean')).to eq('Boolean') end it 'knows that true is a "subtype" of Boolean' do - api_map = Solargraph::ApiMap.new + api_map = described_class.new expect(api_map.super_and_sub?('Boolean', 'true')).to be(true) end it 'knows that false is a "subtype" of Boolean' do - api_map = Solargraph::ApiMap.new - expect(api_map.super_and_sub?('Boolean', 'true')).to be(true) + api_map = described_class.new + expect(api_map.super_and_sub?('Boolean', 'false')).to be(true) end it 'resolves aliases for YARD methods' do @@ -798,7 +792,7 @@ class Foo mixin = Solargraph::Pin::Reference::Include.new( name: 'defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable)', closure: closure ) - api_map = Solargraph::ApiMap.new(pins: [closure, mixin]) + api_map = described_class.new(pins: [closure, mixin]) expect(api_map.get_method_stack('Foo', 'foo')).to be_empty end @@ -819,7 +813,7 @@ def baz end ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) + api_map = described_class.new.map(source) clip = api_map.clip_at('test.rb', [11, 10]) expect(clip.infer.to_s).to eq('Symbol') diff --git a/spec/complex_type/conforms_to_spec.rb b/spec/complex_type/conforms_to_spec.rb new file mode 100644 index 000000000..f8a623bf0 --- /dev/null +++ b/spec/complex_type/conforms_to_spec.rb @@ -0,0 +1,257 @@ +# frozen_string_literal: true + +describe Solargraph::ComplexType do + let(:api_map) do + Solargraph::ApiMap.new + end + + it 'validates simple core types' do + exp = described_class.parse('String') + inf = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'invalidates simple core types' do + exp = described_class.parse('String') + inf = described_class.parse('Integer') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(false) + end + + it 'allows subtype skew if told' do + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call, [:allow_subtype_skew]) + expect(match).to be(true) + end + + it 'allows covariant behavior in parameters of Array' do + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'does not allow contravariant behavior in parameters of Array' do + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(false) + end + + it 'allows covariant behavior in key types of Hash' do + exp = described_class.parse('Hash{Object => String}') + inf = described_class.parse('Hash{Integer => String}') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'accepts valid tuple conformance' do + exp = described_class.parse('Array(Integer, Integer)') + inf = described_class.parse('Array(Integer, Integer)') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'rejects invalid tuple conformance' do + exp = described_class.parse('Array(Integer, Integer)') + inf = described_class.parse('Array(Integer, String)') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(false) + end + + it 'allows empty params when specified' do + exp = described_class.parse('Array(Integer, Integer)') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) + expect(match).to be(true) + end + + it 'validates expected superclasses' do + source = Solargraph::Source.load_string(%( + class Sup; end + class Sub < Sup; end + )) + api_map.map source + sup = described_class.parse('Sup') + sub = described_class.parse('Sub') + match = sub.conforms_to?(api_map, sup, :method_call) + expect(match).to be(true) + end + + it 'handles singleton types compared against their literals' do + exp = Solargraph::ComplexType::UniqueType.new('nil', rooted: true) + inf = Solargraph::ComplexType::UniqueType.new('NilClass', rooted: true) + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + # it 'invalidates inferred superclasses (expected must be super)' do + # # @todo This test might be invalid. There are use cases where inheritance + # # between inferred and expected classes should be acceptable in either + # # direction. + # # source = Solargraph::Source.load_string(%( + # # class Sup; end + # # class Sub < Sup; end + # # )) + # # api_map.map source + # # sup = described_class.parse('Sup') + # # sub = described_class.parse('Sub') + # # match = Solargraph::TypeChecker::Checks.types_match?(api_map, sub, sup) + # # expect(match).to be(false) + # end + + it 'fuzzy matches arrays with parameters' do + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'fuzzy matches sets with parameters' do + source = Solargraph::Source.load_string("require 'set'") + source_map = Solargraph::SourceMap.map(source) + api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['set']) + exp = described_class.parse('Set') + inf = described_class.parse('Set') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'fuzzy matches hashes with parameters' do + exp = described_class.parse('Hash{ Symbol => String}') + inf = described_class.parse('Hash') + match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) + expect(match).to be(true) + end + + it 'matches multiple types' do + exp = described_class.parse('String, Integer') + inf = described_class.parse('String, Integer') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'matches multiple types out of order' do + exp = described_class.parse('String, Integer') + inf = described_class.parse('Integer, String') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'invalidates inferred types missing from expected' do + exp = described_class.parse('String') + inf = described_class.parse('String, Integer') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(false) + end + + it 'matches nil' do + exp = described_class.parse('nil') + inf = described_class.parse('nil') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'validates classes with expected superclasses' do + exp = described_class.parse('Class') + inf = described_class.parse('Class') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'validates generic classes with expected Class' do + inf = described_class.parse('Class') + exp = described_class.parse('Class') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + context 'with invariant matching' do + it 'rejects String matching an Object' do + inf = described_class.parse('String') + exp = described_class.parse('Object') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :invariant) + expect(match).to be(false) + end + + it 'rejects Object matching an String' do + inf = described_class.parse('Object') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :invariant) + expect(match).to be(false) + end + + it 'accepts String matching a String' do + inf = described_class.parse('String') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :invariant) + expect(match).to be(true) + end + end + + context 'with contravariant matching' do + it 'rejects String matching an Objet' do + inf = described_class.parse('String') + exp = described_class.parse('Object') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :contravariant) + expect(match).to be(false) + end + + it 'accepts Object matching an String' do + inf = described_class.parse('Object') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :contravariant) + expect(match).to be(true) + end + + it 'accepts String matching a String' do + inf = described_class.parse('String') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :contravariant) + expect(match).to be(true) + end + end + + context 'with an inheritence relationship' do + let(:source) do + Solargraph::Source.load_string(%( + class Sup; end + class Sub < Sup; end + )) + end + let(:sup) { described_class.parse('Sup') } + let(:sub) { described_class.parse('Sub') } + + before do + api_map.map source + end + + it 'validates inheritance in one way' do + match = sub.conforms_to?(api_map, sup, :method_call, [:allow_reverse_match]) + expect(match).to be(true) + end + + it 'validates inheritance the other way' do + match = sup.conforms_to?(api_map, sub, :method_call, [:allow_reverse_match]) + expect(match).to be(true) + end + end + + context 'with inheritance relationship in allow_reverse_match mode' do + let(:api_map) { Solargraph::ApiMap.new } + let(:sup) { described_class.parse('String') } + let(:sub) { described_class.parse('Array') } + + it 'conforms one way' do + match = sub.conforms_to?(api_map, sup, :method_call, [:allow_reverse_match]) + expect(match).to be(false) + end + + it 'conforms the other way' do + match = sup.conforms_to?(api_map, sub, :method_call, [:allow_reverse_match]) + expect(match).to be(false) + end + end +end diff --git a/spec/complex_type/unique_type_spec.rb b/spec/complex_type/unique_type_spec.rb new file mode 100644 index 000000000..2d9812600 --- /dev/null +++ b/spec/complex_type/unique_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +describe Solargraph::ComplexType::UniqueType do + describe '#any?' do + let(:type) { described_class.parse('String') } + + it 'yields one and only one type, itself' do + types_encountered = [] + type.any? { |t| types_encountered << t } + expect(types_encountered).to eq([type]) + end + end +end diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index ba2a1ac7c..3075aaaaf 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe 'YARD type specifier list parsing' do - context 'in compliance with https://www.rubydoc.info/gems/yard/file/docs/Tags.md#type-list-conventions' do + context 'with https://www.rubydoc.info/gems/yard/file/docs/Tags.md#type-list-conventions compliance' do # Types Specifier List # # In some cases, a tag will allow for a "types specifier list"; this @@ -13,7 +15,8 @@ expect(types.length).to eq(0) end - xit 'parses zero types as a string' do + it 'parses zero types as a string' do + pending('special case being added') types = Solargraph::ComplexType.parse '' expect(types.length).to eq(0) end @@ -120,7 +123,7 @@ # types. This type does not exist in Ruby, however. it 'typifies Booleans' do - api_map = double(Solargraph::ApiMap, qualify: nil) + api_map = instance_double(Solargraph::ApiMap, qualify: nil) type = Solargraph::ComplexType.parse('::Boolean') qualified = type.qualify(api_map) expect(qualified.tag).to eq('Boolean') @@ -159,7 +162,6 @@ expect(types.to_rbs).to eq('Array[Symbol, String]') end - # Note that parametrized types are typically not order-dependent, in # other words, a list of parametrized types can occur in any order # inside of a type. An array specified as Array can @@ -215,7 +217,7 @@ types = Solargraph::ComplexType.parse('Hash{String, Symbol => Integer, BigDecimal}') expect(types.length).to eq(1) type = types.first - expect(type.hash_parameters?).to eq(true) + expect(type.hash_parameters?).to be(true) expect(type.key_types.map(&:name)).to eq(%w[String Symbol]) expect(type.value_types.map(&:name)).to eq(%w[Integer BigDecimal]) expect(type.to_rbs).to eq('Hash[(String | Symbol), (Integer | BigDecimal)]') @@ -265,14 +267,18 @@ # See literal details at # https://github.com/ruby/rbs/blob/master/docs/syntax.md and # https://yardoc.org/types.html - xit 'understands literal strings with double quotes' do + it 'understands literal strings with double quotes' do + pending('string escaping support being added') + type = Solargraph::ComplexType.parse('"foo"') expect(type.tag).to eq('"foo"') expect(type.to_rbs).to eq('"foo"') expect(type.to_s).to eq('String') end - xit 'understands literal strings with single quotes' do + it 'understands literal strings with single quotes' do + pending('string escaping support being added') + type = Solargraph::ComplexType.parse("'foo'") expect(type.tag).to eq("'foo'") expect(type.to_rbs).to eq("'foo'") @@ -335,7 +341,7 @@ xit 'understands reference tags' end - context 'offers machine users error messages given non-sensical types' do + context 'when given non-sensical types by machine users' do it 'raises ComplexTypeError for unmatched brackets' do expect do Solargraph::ComplexType.parse('Array and Module<> from type' do + context 'when offering type queries orthogonal to YARD spec' do + context 'when defining namespace concept which strips Class<> and Module<> from type' do # # Solargraph extensions and library features # @@ -404,7 +410,7 @@ end end - context 'simplifies type representation on output' do + context 'when simplifying type representation on output' do it 'throws away other types when in union with an undefined' do type = Solargraph::ComplexType.parse('Symbol, String, Array(Integer, Integer), undefined') expect(type.to_s).to eq('undefined') @@ -442,7 +448,7 @@ end end - context 'defines rooted and unrooted concept' do + context 'when defining rooted and unrooted concept' do it 'identify rooted types' do types = Solargraph::ComplexType.parse '::Array' expect(types.map(&:rooted?)).to eq([true]) @@ -462,7 +468,7 @@ end end - context 'allows users to define their own generic types' do + context 'when allowing users to define their own generic types' do it 'recognizes param types' do type = Solargraph::ComplexType.parse('generic') expect(type).to be_generic @@ -531,20 +537,20 @@ ['generic', 'Array>', { 'B' => 'Integer' }, 'Array', { 'B' => 'Integer', 'A' => 'Array' }], ['Array>', 'Array', {}, 'Array', { 'A' => 'String' }] - ] + ].freeze UNIQUE_METHOD_GENERIC_TESTS.each do |tag, context_type_tag, unfrozen_input_map, expected_tag, expected_output_map| - context "resolves #{tag} with context #{context_type_tag} and existing resolved generics #{unfrozen_input_map}" do + context "when resolveing #{tag} with context #{context_type_tag} and existing resolved generics #{unfrozen_input_map}" do let(:complex_type) { Solargraph::ComplexType.parse(tag) } let(:unique_type) { complex_type.first } - it '#{tag} is a unique type' do + let(:context_type) { Solargraph::ComplexType.parse(context_type_tag) } + let(:generic_value) { unfrozen_input_map.transform_values! { |tag| Solargraph::ComplexType.parse(tag) } } + + it "#{tag} is a unique type" do expect(complex_type.length).to eq(1) end - let(:generic_value) { unfrozen_input_map.transform_values! { |tag| Solargraph::ComplexType.parse(tag) } } - let(:context_type) { Solargraph::ComplexType.parse(context_type_tag) } - it "resolves to #{expected_tag} with updated map #{expected_output_map}" do resolved_generic_values = unfrozen_input_map.transform_values { |tag| Solargraph::ComplexType.parse(tag) } resolved_type = unique_type.resolve_generics_from_context(expected_output_map.keys, context_type, @@ -557,7 +563,7 @@ end end - context 'identifies type of parameter syntax used' do + context 'when identifying type of parameter syntax used' do it 'raises NoMethodError for missing methods' do type = Solargraph::ComplexType.parse('String') expect { type.undefined_method }.to raise_error(NoMethodError) @@ -584,9 +590,9 @@ end end - context "'qualifies' types by resolving relative references to types to absolute references (fully qualified types)" do + context "when 'qualifying' types by resolving relative references to types to absolute references (fully qualified types)" do it 'returns undefined for unqualified types' do - api_map = double(Solargraph::ApiMap, qualify: nil) + api_map = instance_double(Solargraph::ApiMap, qualify: nil) type = Solargraph::ComplexType.parse('UndefinedClass') qualified = type.qualify(api_map) expect(qualified).to be_undefined @@ -594,7 +600,7 @@ end end - context 'allows list-of-types to be destructively cast down to a single type' do + context 'when allowing list-of-types to be destructively cast down to a single type' do it 'returns the first type when multiple were parsed with #tag' do type = Solargraph::ComplexType.parse('String, Array') expect(type.tag).to eq('String') @@ -602,7 +608,22 @@ end end - context "supports arbitrary combinations of the above syntax and features" do + context 'when supporting arbitrary combinations of the above syntax and features' do + let(:foo_bar_api_map) do + api_map = Solargraph::ApiMap.new + source = Solargraph::Source.load_string(%( + module Foo + class Bar + # @return [Bar] + def make_bar + end + end + end + )) + api_map.map source + api_map + end + it 'returns string representations of the entire type array' do type = Solargraph::ComplexType.parse('String', 'Array') expect(type.to_s).to eq('String, Array') @@ -630,21 +651,6 @@ expect(types.to_rbs).to eq('(Array[String] | Hash[String, Symbol] | [String, Integer])') end - let(:foo_bar_api_map) do - api_map = Solargraph::ApiMap.new - source = Solargraph::Source.load_string(%( - module Foo - class Bar - # @return [Bar] - def make_bar - end - end - end - )) - api_map.map source - api_map - end - it 'qualifies types with list parameters' do original = Solargraph::ComplexType.parse('Class').first expect(original).not_to be_rooted @@ -725,7 +731,9 @@ def make_bar expect(result.to_rbs).to eq('::Array[::String]') end - xit 'stops parsing when the first character indicates a string literal' do + it 'stops parsing when the first character indicates a string literal' do + pending('string escaping support being added') + api_map = Solargraph::ApiMap.new type = Solargraph::ComplexType.parse('"Array(Symbol, String, Array(Integer, Integer)"') type = type.qualify(api_map) @@ -733,5 +741,33 @@ def make_bar expect(type.to_rbs).to eq('[Symbol, String, [Integer, Integer]]') expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') end + + it 'recognizes String conforms with itself' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('String') + atype = Solargraph::ComplexType.parse('String') + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) + end + + it 'recognizes an erased container type conforms with itself' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('Hash') + atype = Solargraph::ComplexType.parse('Hash') + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) + end + + it 'recognizes an unerased container type conforms with itself' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('Array') + atype = Solargraph::ComplexType.parse('Array') + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) + end + + it 'recognizes a literal conforms with its type' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('Symbol') + atype = Solargraph::ComplexType.parse(':foo') + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) + end end end diff --git a/spec/convention/activesupport_concern_spec.rb b/spec/convention/activesupport_concern_spec.rb index b58cd6584..2360e2a83 100644 --- a/spec/convention/activesupport_concern_spec.rb +++ b/spec/convention/activesupport_concern_spec.rb @@ -101,7 +101,7 @@ def self.my_method; end # create a temporary directory with the scope of the spec around do |example| require 'tmpdir' - Dir.mktmpdir("rspec-solargraph-") do |dir| + Dir.mktmpdir('rspec-solargraph-') do |dir| @temp_dir = dir example.run end @@ -149,13 +149,13 @@ class Base RBS end - it { should_not be_empty } + it { is_expected.not_to be_empty } - it "has one item" do + it 'has one item' do expect(method_pins.size).to eq(1) end - it "is a Pin::Method" do + it 'is a Pin::Method' do expect(method_pins.first).to be_a(Solargraph::Pin::Method) end end diff --git a/spec/convention/struct_definition_spec.rb b/spec/convention/struct_definition_spec.rb index 5c3fc5211..02786cfe6 100644 --- a/spec/convention/struct_definition_spec.rb +++ b/spec/convention/struct_definition_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + describe Solargraph::Convention::StructDefinition do describe 'parsing docs' do it 'supports keyword args' do @@ -103,7 +105,7 @@ expect(params.map(&:name)).to eql(%w[bar baz]) expect(params.map(&:return_type).map(&:tag)).to eql(%w[Integer String]) - expect(params[1].documentation).to eql("Some text") + expect(params[1].documentation).to eql('Some text') end [true, false].each do |kw_args| @@ -116,21 +118,21 @@ Foo = Struct.new(:bar, :baz, keyword_init: #{kw_args}) ), 'test.rb') - params_bar = source.pins.find { |p| p.path == "Foo#bar=" }.parameters + params_bar = source.pins.find { |p| p.path == 'Foo#bar=' }.parameters expect(params_bar.length).to be(1) - expect(params_bar.first.return_type.tag).to eql("String") + expect(params_bar.first.return_type.tag).to eql('String') expect(params_bar.first.arg?).to be(true) - params_baz = source.pins.find { |p| p.path == "Foo#baz=" }.parameters + params_baz = source.pins.find { |p| p.path == 'Foo#baz=' }.parameters expect(params_baz.length).to be(1) - expect(params_baz.first.return_type.tag).to eql("Integer") + expect(params_baz.first.return_type.tag).to eql('Integer') expect(params_baz.first.arg?).to be(true) - iv_bar = source.pins.find { |p| p.name == "@bar" } - expect(iv_bar.return_type.tag).to eql("String") + iv_bar = source.pins.find { |p| p.name == '@bar' } + expect(iv_bar.return_type.tag).to eql('String') - iv_baz = source.pins.find { |p| p.name == "@baz" } - expect(iv_baz.return_type.tag).to eql("Integer") + iv_baz = source.pins.find { |p| p.name == '@baz' } + expect(iv_baz.return_type.tag).to eql('Integer') end end end diff --git a/spec/convention_spec.rb b/spec/convention_spec.rb index b6f4fc52e..7785aef68 100644 --- a/spec/convention_spec.rb +++ b/spec/convention_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + describe Solargraph::Convention do it 'newly defined pins are resolved by ApiMap after file changes' do filename = 'test.rb' diff --git a/spec/diagnostics/base_spec.rb b/spec/diagnostics/base_spec.rb index d2068caf1..417797172 100644 --- a/spec/diagnostics/base_spec.rb +++ b/spec/diagnostics/base_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + describe Solargraph::Diagnostics::Base do - it "returns empty diagnostics" do - reporter = Solargraph::Diagnostics::Base.new + it 'returns empty diagnostics' do + reporter = described_class.new expect(reporter.diagnose(nil, nil)).to be_empty end end diff --git a/spec/diagnostics/require_not_found_spec.rb b/spec/diagnostics/require_not_found_spec.rb index 6ecfdcae9..1588a033b 100644 --- a/spec/diagnostics/require_not_found_spec.rb +++ b/spec/diagnostics/require_not_found_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe Solargraph::Diagnostics::RequireNotFound do - before :each do + before do @source = Solargraph::Source.new(%( require 'rexml/document' require 'not_valid' @@ -11,8 +13,8 @@ @api_map.catalog Solargraph::Bench.new(source_maps: [@source_map], external_requires: ['not_valid']) end - it "reports unresolved requires" do - reporter = Solargraph::Diagnostics::RequireNotFound.new + it 'reports unresolved requires' do + reporter = described_class.new result = reporter.diagnose(@source, @api_map) expect(result.length).to eq(1) end diff --git a/spec/diagnostics/rubocop_helpers_spec.rb b/spec/diagnostics/rubocop_helpers_spec.rb index d59dac4ae..7bf374d67 100644 --- a/spec/diagnostics/rubocop_helpers_spec.rb +++ b/spec/diagnostics/rubocop_helpers_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + describe Solargraph::Diagnostics::RubocopHelpers do - context do + context 'with custom version' do around do |example| old_gem_path = Gem.paths.path - custom_gem_path = File.absolute_path('spec/fixtures/rubocop-custom-version').gsub(/\\/, '/') + custom_gem_path = File.absolute_path('spec/fixtures/rubocop-custom-version').gsub('\\', '/') # Remove a post_reset hook set by bundler to restore cached specs # Source: https://github.com/ruby/ruby/blob/master/lib/bundler/rubygems_integration.rb#L487-L489 old_post_reset_hooks = Gem.post_reset_hooks.dup @@ -18,34 +20,34 @@ let(:custom_version) { '0.0.0' } - it "requires the specified version of rubocop" do + it 'requires the specified version of rubocop' do input = custom_version - Solargraph::Diagnostics::RubocopHelpers.require_rubocop(input) + described_class.require_rubocop(input) output = RuboCop::Version::STRING expect(output).to eq(custom_version) end end - context do + context 'with real version' do let(:default_version) { Gem::Specification.find_by_name('rubocop').full_gem_path[/[^-]+$/] } - it "requires the default version of rubocop" do + it 'requires the default version of rubocop' do input = nil - Solargraph::Diagnostics::RubocopHelpers.require_rubocop(input) + described_class.require_rubocop(input) output = RuboCop::Version::STRING expect(output).to eq(default_version) end end - it "converts lower-case drive letters to upper-case" do + it 'converts lower-case drive letters to upper-case' do input = 'c:/one/two' - output = Solargraph::Diagnostics::RubocopHelpers.fix_drive_letter(input) + output = described_class.fix_drive_letter(input) expect(output).to eq('C:/one/two') end - it "ignores paths without drive letters" do + it 'ignores paths without drive letters' do input = 'one/two' - output = Solargraph::Diagnostics::RubocopHelpers.fix_drive_letter(input) + output = described_class.fix_drive_letter(input) expect(output).to eq('one/two') end end diff --git a/spec/diagnostics/rubocop_spec.rb b/spec/diagnostics/rubocop_spec.rb index aa279f66c..7f6a9221d 100644 --- a/spec/diagnostics/rubocop_spec.rb +++ b/spec/diagnostics/rubocop_spec.rb @@ -10,41 +10,41 @@ def bar foo = Foo.new ), 'file.rb') - rubocop = Solargraph::Diagnostics::Rubocop.new + rubocop = described_class.new result = rubocop.diagnose(source, nil) expect(result).to be_a(Array) end - context "with validation error" do + context 'with validation error' do let(:fixture_path) do - File.absolute_path('spec/fixtures/rubocop-validation-error').gsub(/\\/, '/') + File.absolute_path('spec/fixtures/rubocop-validation-error').gsub('\\', '/') end around do |example| config_file = File.join(fixture_path, '.rubocop.yml') File.write(config_file, <<~YAML) - inherit_from: - - file_not_found.yml - YAML + inherit_from: + - file_not_found.yml + YAML example.run ensure - File.delete(config_file) if File.exist?(config_file) + FileUtils.rm_f(config_file) end it 'handles validation errors' do file = File.realpath(File.join(fixture_path, 'app.rb')) source = Solargraph::Source.load(file) - rubocop = Solargraph::Diagnostics::Rubocop.new - expect { + rubocop = described_class.new + expect do rubocop.diagnose(source, nil) - }.to raise_error(Solargraph::DiagnosticsError) + end.to raise_error(Solargraph::DiagnosticsError) end end it 'calculates ranges' do file = File.realpath(File.join('spec', 'fixtures', 'rubocop-unused-variable-error', 'app.rb')) source = Solargraph::Source.load(file) - rubocop = Solargraph::Diagnostics::Rubocop.new + rubocop = described_class.new results = rubocop.diagnose(source, nil) expect(results).to be_one diff --git a/spec/diagnostics/type_check_spec.rb b/spec/diagnostics/type_check_spec.rb index e343d6598..f28cc7b73 100644 --- a/spec/diagnostics/type_check_spec.rb +++ b/spec/diagnostics/type_check_spec.rb @@ -1,29 +1,31 @@ +# frozen_string_literal: true + describe Solargraph::Diagnostics::TypeCheck do let(:api_map) { Solargraph::ApiMap.new } - it "detects defined return types" do + it 'detects defined return types' do source = Solargraph::Source.load_string(%( # @return [String] def foo end )) api_map.map source - result = Solargraph::Diagnostics::TypeCheck.new('always').diagnose(source, api_map) + result = described_class.new('always').diagnose(source, api_map) expect(result).to be_empty end - it "detects missing return types" do + it 'detects missing return types' do source = Solargraph::Source.load_string(%( def foo end )) api_map.map source - result = Solargraph::Diagnostics::TypeCheck.new('always', 'strong').diagnose(source, api_map) + result = described_class.new('always', 'strong').diagnose(source, api_map) expect(result.length).to eq(1) expect(result[0][:message]).to include('foo') end - it "detects defined parameter types" do + it 'detects defined parameter types' do source = Solargraph::Source.load_string(%( # @param bar [String] # @return [String] @@ -31,11 +33,11 @@ def foo(bar) end )) api_map.map source - result = Solargraph::Diagnostics::TypeCheck.new('always').diagnose(source, api_map) + result = described_class.new('always').diagnose(source, api_map) expect(result).to be_empty end - it "detects missing parameter types" do + it 'detects missing parameter types' do source = Solargraph::Source.load_string(%( # @return [String] def foo(bar) @@ -43,12 +45,12 @@ def foo(bar) end )) api_map.map source - result = Solargraph::Diagnostics::TypeCheck.new('always', 'strong').diagnose(source, api_map) + result = described_class.new('always', 'strong').diagnose(source, api_map) expect(result.length).to eq(1) expect(result[0][:message]).to include('bar') end - it "detects return types from superclasses" do + it 'detects return types from superclasses' do source = Solargraph::Source.load_string(%( class First # @return [String] @@ -61,11 +63,11 @@ def foo end )) api_map.map source - result = Solargraph::Diagnostics::TypeCheck.new('always').diagnose(source, api_map) + result = described_class.new('always').diagnose(source, api_map) expect(result).to be_empty end - it "detects parameter types from superclasses" do + it 'detects parameter types from superclasses' do source = Solargraph::Source.load_string(%( class First # @param bar [String] @@ -79,11 +81,11 @@ def foo bar end )) api_map.map source - result = Solargraph::Diagnostics::TypeCheck.new('always').diagnose(source, api_map) + result = described_class.new('always').diagnose(source, api_map) expect(result).to be_empty end - it "works with optional and keyword arguments" do + it 'works with optional and keyword arguments' do source = Solargraph::Source.load_string(%( # @param bar [String] # @param baz [String] @@ -92,7 +94,7 @@ def foo(bar = 'bar', baz: 'baz') end )) api_map.map source - result = Solargraph::Diagnostics::TypeCheck.new('always').diagnose(source, api_map) + result = described_class.new('always').diagnose(source, api_map) expect(result).to be_empty end end diff --git a/spec/diagnostics/update_errors_spec.rb b/spec/diagnostics/update_errors_spec.rb index 11b8c6f8f..0bb013231 100644 --- a/spec/diagnostics/update_errors_spec.rb +++ b/spec/diagnostics/update_errors_spec.rb @@ -1,18 +1,20 @@ +# frozen_string_literal: true + describe Solargraph::Diagnostics::UpdateErrors do - it "detects repaired lines" do + it 'detects repaired lines' do api_map = Solargraph::ApiMap.new orig = Solargraph::Source.load_string('foo', 'test.rb') - diagnoser = Solargraph::Diagnostics::UpdateErrors.new + diagnoser = described_class.new result = diagnoser.diagnose(orig, api_map) expect(result.length).to eq(0) updater = Solargraph::Source::Updater.new('test.rb', 2, [ - Solargraph::Source::Change.new( - Solargraph::Range.from_to(0, 3, 0, 3), - '.' - ) - ]) + Solargraph::Source::Change.new( + Solargraph::Range.from_to(0, 3, 0, 3), + '.' + ) + ]) source = orig.synchronize(updater) - diagnoser = Solargraph::Diagnostics::UpdateErrors.new + diagnoser = described_class.new result = diagnoser.diagnose(source, api_map) expect(result.length).to eq(1) end diff --git a/spec/diagnostics_spec.rb b/spec/diagnostics_spec.rb index ba689692d..83e222e12 100644 --- a/spec/diagnostics_spec.rb +++ b/spec/diagnostics_spec.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + describe Solargraph::Diagnostics do - it "registers reporters" do - Solargraph::Diagnostics.register 'base', Solargraph::Diagnostics::Base - expect(Solargraph::Diagnostics.reporters).to include('base') - expect(Solargraph::Diagnostics.reporter('base')).to be(Solargraph::Diagnostics::Base) + it 'registers reporters' do + described_class.register 'base', Solargraph::Diagnostics::Base + expect(described_class.reporters).to include('base') + expect(described_class.reporter('base')).to be(Solargraph::Diagnostics::Base) end end diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index e82332161..8ff1e70b1 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -1,81 +1,187 @@ # frozen_string_literal: true +require 'bundler' +require 'benchmark' + describe Solargraph::DocMap do - before :all do - # We use ast here because it's a known dependency. - gemspec = Gem::Specification.find_by_name('ast') - yard_pins = Solargraph::GemPins.build_yard_pins([], gemspec) - Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) + subject(:doc_map) do + described_class.new(requires, workspace, out: out) end - it 'generates pins from gems' do - doc_map = Solargraph::DocMap.new(['ast'], []) - doc_map.cache_all!($stderr) - node_pin = doc_map.pins.find { |pin| pin.path == 'AST::Node' } - expect(node_pin).to be_a(Solargraph::Pin::Namespace) + let(:out) { StringIO.new } + let(:pre_cache) { true } + let(:requires) { [] } + + let(:workspace) do + Solargraph::Workspace.new(Dir.pwd) + end + + let(:plain_doc_map) { described_class.new([], workspace, out: nil) } + + before do + doc_map.cache_doc_map_gems!(nil) if pre_cache end - it 'tracks unresolved requires' do - doc_map = Solargraph::DocMap.new(['not_a_gem'], []) - expect(doc_map.unresolved_requires).to include('not_a_gem') + context 'with a require in solargraph test bundle' do + let(:requires) do + ['ast'] + end + + it 'generates pins from gems' do + node_pin = doc_map.pins.find { |pin| pin.path == 'AST::Node' } + expect(node_pin).to be_a(Solargraph::Pin::Namespace) + end end - it 'tracks uncached_gemspecs' do - gemspec = Gem::Specification.new do |spec| - spec.name = 'not_a_gem' - spec.version = '1.0.0' + context 'when understanding rspec + rspec-mocks require pattern' do + let(:requires) do + ['rspec-mocks'] + end + + it 'generates pins from gems' do + ns_pin = doc_map.pins.find { |pin| pin.path == 'RSpec::Mocks' } + expect(ns_pin).to be_a(Solargraph::Pin::Namespace) end - allow(Gem::Specification).to receive(:find_by_path).and_return(gemspec) - doc_map = Solargraph::DocMap.new(['not_a_gem'], [gemspec]) - expect(doc_map.uncached_yard_gemspecs).to eq([gemspec]) - expect(doc_map.uncached_rbs_collection_gemspecs).to eq([gemspec]) end - it 'imports all gems when bundler/require used' do - workspace = Solargraph::Workspace.new(Dir.pwd) - plain_doc_map = Solargraph::DocMap.new([], [], workspace) - doc_map_with_bundler_require = Solargraph::DocMap.new(['bundler/require'], [], workspace) + context 'with an invalid require' do + let(:requires) do + ['not_a_gem'] + end - expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive + it 'tracks unresolved requires' do + # These are auto-required by solargraph-rspec in case the bundle + # includes these gems. In our case, it doesn't! + unprovided_solargraph_rspec_requires = %w[ + rspec-rails + actionmailer + actionpack + activerecord + shoulda-matchers + rspec-sidekiq + airborne + activesupport + ] + expect(doc_map.unresolved_requires - unprovided_solargraph_rspec_requires) + .to eq(['not_a_gem']) + end end 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. allow(Solargraph.logger).to receive(:warn).and_call_original - Solargraph::DocMap.new(['set'], []) + described_class.new(['set'], workspace) expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) end - it 'ignores nil requires' do - expect { Solargraph::DocMap.new([nil], []) }.not_to raise_error + context 'when deserialization takes a while' do + let(:pre_cache) { false } + let(:requires) { ['backport'] } + + before do + # proxy this method to simulate a long-running deserialization + allow(Benchmark).to receive(:measure) do |&block| + block.call + 5.0 + end + end + + it 'logs timing' do + # force lazy evaluation + _pins = doc_map.pins + expect(out.string).to include('Deserialized ').and include(' gem pins ').and include(' ms') + end end - it 'ignores empty requires' do - expect { Solargraph::DocMap.new([''], []) }.not_to raise_error + context 'with an uncached but valid gemspec' do + let(:requires) { ['uncached_gem'] } + let(:pre_cache) { false } + let(:workspace) { instance_double(Solargraph::Workspace) } + + it 'tracks uncached_gemspecs' do + pincache = instance_double(Solargraph::PinCache, cache_stdlib_rbs_map: false) + uncached_gemspec = Gem::Specification.new('uncached_gem', '1.0.0') + allow(workspace).to receive(:fetch_dependencies).with(uncached_gemspec, out: out).and_return([]) + allow(workspace).to receive_messages(fresh_pincache: pincache, resolve_require: [uncached_gemspec], + stdlib_dependencies: [], global_environ: Solargraph::Environ.new) + allow(Gem::Specification).to receive(:find_by_path).with('uncached_gem').and_return(uncached_gemspec) + allow(workspace).to receive(:global_environ).and_return(Solargraph::Environ.new) + allow(pincache).to receive(:deserialize_combined_pin_cache).with(uncached_gemspec).and_return(nil) + + expect(doc_map.uncached_gemspecs).to eq([uncached_gemspec]) + end end - it 'collects dependencies' do - doc_map = Solargraph::DocMap.new(['rspec'], []) - expect(doc_map.dependencies.map(&:name)).to include('rspec-core') + context 'with require as bundle/require' do + it 'imports all gems when bundler/require used' do + doc_map_with_bundler_require = described_class.new(['bundler/require'], workspace, out: nil) + doc_map_with_bundler_require.cache_doc_map_gems!(nil) + expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive + end end - it 'includes convention requires from environ' do - dummy_convention = Class.new(Solargraph::Convention::Base) do - def global(doc_map) - Solargraph::Environ.new( - requires: ['convention_gem1', 'convention_gem2'] - ) - end + context 'with a require not needed by Ruby core' do + let(:requires) { ['set'] } + + it 'does not warn' 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. + allow(Solargraph.logger).to receive(:warn) + doc_map + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) + end + end + + context 'with a nil require' do + let(:requires) { [nil] } + + it 'does not raise error' do + expect { doc_map }.not_to raise_error end + end - Solargraph::Convention.register dummy_convention + context 'with an empty require' do + let(:requires) { [''] } - doc_map = Solargraph::DocMap.new(['original_gem'], []) + it 'does not raise error' do + expect { doc_map }.not_to raise_error + end + end - expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') + context 'with a require that has dependencies' do + let(:requires) { ['rspec'] } - # Clean up the registered convention - Solargraph::Convention.unregister dummy_convention + it 'collects dependencies' do + # we include doc_map.requires as solargraph-rspec will bring it + # in directly and we exclude it from dependencies + expect(doc_map.dependencies.map(&:name) + doc_map.requires).to include('rspec-core') + end + end + + context 'with convention' do + let(:pre_cache) { false } + + it 'includes convention requires from environ' do + dummy_convention = Class.new(Solargraph::Convention::Base) do + def global doc_map + Solargraph::Environ.new( + requires: %w[convention_gem1 convention_gem2] + ) + end + end + + Solargraph::Convention.register dummy_convention + + doc_map = described_class.new(['original_gem'], workspace) + + # @todo this should probably not be in requires, which is a + # path, and instead be in a new gem_names property on the + # Environ + expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') + ensure + # Clean up the registered convention + Solargraph::Convention.unregister dummy_convention + end end end diff --git a/spec/gem_pins_spec.rb b/spec/gem_pins_spec.rb index 8e3962341..944afd331 100644 --- a/spec/gem_pins_spec.rb +++ b/spec/gem_pins_spec.rb @@ -1,19 +1,49 @@ # frozen_string_literal: true describe Solargraph::GemPins do - it 'can merge YARD and RBS' do - gemspec = Gem::Specification.find_by_name('rbs') - yard_pins = Solargraph::GemPins.build_yard_pins([], gemspec) - rbs_map = Solargraph::RbsMap.from_gemspec(gemspec, nil, nil) - pins = Solargraph::GemPins.combine yard_pins, rbs_map.pins - - core_root = pins.find { |pin| pin.path == 'RBS::EnvironmentLoader#core_root' } - expect(core_root.return_type.to_s).to eq('Pathname, nil') - expect(core_root.location.filename).to end_with('environment_loader.rb') + let(:workspace) { Solargraph::Workspace.new(Dir.pwd) } + let(:doc_map) { Solargraph::DocMap.new(requires, workspace, out: nil) } + let(:pin) { doc_map.pins.find { |pin| pin.path == path } } + + before do + doc_map.cache_doc_map_gems!(STDERR) # rubocop:disable Style/GlobalStdStream + end + + context 'with a combined method pin' do + let(:path) { 'RBS::EnvironmentLoader#core_root' } + let(:requires) { ['rbs'] } + + it 'can merge YARD and RBS' do + expect(pin.source).to eq(:combined) + end + + it 'finds types from RBS' do + expect(pin.return_type.to_s).to eq('Pathname, nil') + end + + it 'finds locations from YARD' do + expect(pin.location.filename).to end_with('environment_loader.rb') + end end - it 'does not error out when handed incorrect gemspec' do - gemspec = instance_double(Gem::Specification, name: 'foo', version: '1.0', gem_dir: '/not-there') - expect { Solargraph::GemPins.build_yard_pins([], gemspec) }.not_to raise_error + context 'with a YARD-only pin' do + let(:requires) { ['rake'] } + let(:path) { 'Rake::Task#prerequisites' } + + it 'found a pin' do + expect(pin.source).not_to be_nil + end + + it 'can merge YARD and RBS' do + expect(pin.source).to eq(:yardoc) + end + + it 'does not find types from YARD in this case' do + expect(pin.return_type.to_s).to eq('undefined') + end + + it 'finds locations from YARD' do + expect(pin.location.filename).to end_with('task.rb') + end end end diff --git a/spec/language_server/host/diagnoser_spec.rb b/spec/language_server/host/diagnoser_spec.rb index 69ee0b866..9ce9df627 100644 --- a/spec/language_server/host/diagnoser_spec.rb +++ b/spec/language_server/host/diagnoser_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Host::Diagnoser do - it "diagnoses on ticks" do - host = double(Solargraph::LanguageServer::Host, options: { 'diagnostics' => true }, synchronizing?: false) - diagnoser = Solargraph::LanguageServer::Host::Diagnoser.new(host) - diagnoser.schedule 'file.rb' + it 'diagnoses on ticks' do + host = instance_double(Solargraph::LanguageServer::Host, options: { 'diagnostics' => true }, synchronizing?: false) allow(host).to receive(:diagnose) + diagnoser = described_class.new(host) + diagnoser.schedule 'file.rb' diagnoser.tick expect(host).to have_received(:diagnose).with('file.rb') end diff --git a/spec/language_server/host/dispatch_spec.rb b/spec/language_server/host/dispatch_spec.rb index 9b314c403..8db9c1246 100644 --- a/spec/language_server/host/dispatch_spec.rb +++ b/spec/language_server/host/dispatch_spec.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Host::Dispatch do before :all do # @dispatch = Solargraph::LanguageServer::Host::Dispatch @dispatch = Object.new - @dispatch.extend Solargraph::LanguageServer::Host::Dispatch + @dispatch.extend described_class end - after :each do + after do @dispatch.libraries.clear @dispatch.sources.clear end - it "finds an explicit library" do + it 'finds an explicit library' do @dispatch.libraries.push Solargraph::Library.load('*') src = @dispatch.sources.open('file:///file.rb', 'a=b', 0) @dispatch.libraries.first.merge src @@ -18,7 +20,7 @@ expect(lib).to be(@dispatch.libraries.first) end - it "finds an implicit library" do + it 'finds an implicit library' do dir = File.realpath(File.join('spec', 'fixtures', 'workspace')) file = File.join(dir, 'new.rb') uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) @@ -28,7 +30,7 @@ expect(lib).to be(@dispatch.libraries.first) end - it "finds a generic library" do + it 'finds a generic library' do dir = File.realpath(File.join('spec', 'fixtures', 'workspace')) file = '/external.rb' uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) diff --git a/spec/language_server/host/message_worker_spec.rb b/spec/language_server/host/message_worker_spec.rb index 5e5bef481..4b10f0265 100644 --- a/spec/language_server/host/message_worker_spec.rb +++ b/spec/language_server/host/message_worker_spec.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Host::MessageWorker do - it "handle requests on queue" do - host = double(Solargraph::LanguageServer::Host) - message = {'method' => '$/example'} + it 'handle requests on queue' do + host = instance_double(Solargraph::LanguageServer::Host) + message = { 'method' => '$/example' } allow(host).to receive(:receive).with(message).and_return(nil) - worker = Solargraph::LanguageServer::Host::MessageWorker.new(host) + worker = described_class.new(host) worker.queue(message) expect(worker.messages).to eq [message] worker.tick diff --git a/spec/language_server/host_spec.rb b/spec/language_server/host_spec.rb index 5635680e3..f0497b8f3 100644 --- a/spec/language_server/host_spec.rb +++ b/spec/language_server/host_spec.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true + require 'tmpdir' describe Solargraph::LanguageServer::Host do - it "prepares a workspace" do - host = Solargraph::LanguageServer::Host.new + it 'prepares a workspace' do + host = described_class.new Dir.mktmpdir do |dir| - host.prepare (dir) - expect(host.libraries.first).not_to be(nil) + host.prepare(dir) + expect(host.libraries.first).not_to be_nil end end - it "processes responses to message requests" do - host = Solargraph::LanguageServer::Host.new + it 'processes responses to message requests' do + host = described_class.new done_somethings = 0 host.send_request 'window/showMessageRequest', { 'message' => 'Message', @@ -20,15 +22,15 @@ end expect(host.pending_requests.length).to eq(1) host.receive({ - 'id' => host.pending_requests.first, - 'result' => 'Do something' - }) + 'id' => host.pending_requests.first, + 'result' => 'Do something' + }) expect(done_somethings).to eq(1) end - it "creates files from disk" do + it 'creates files from disk' do Dir.mktmpdir do |dir| - host = Solargraph::LanguageServer::Host.new + host = described_class.new host.prepare dir file = File.join(dir, 'test.rb') File.write(file, "foo = 'foo'") @@ -38,34 +40,34 @@ end end - it "deletes files" do + it 'deletes files' do Dir.mktmpdir do |dir| - expect { - host = Solargraph::LanguageServer::Host.new + expect do + host = described_class.new file = File.join(dir, 'test.rb') File.write(file, "foo = 'foo'") host.prepare dir uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) host.delete(uri) - }.not_to raise_error + end.not_to raise_error end end - it "cancels requests" do - host = Solargraph::LanguageServer::Host.new + it 'cancels requests' do + host = described_class.new host.cancel 1 expect(host.cancel?(1)).to be(true) end - it "runs diagnostics on opened files" do + it 'runs diagnostics on opened files' do Dir.mktmpdir do |dir| - host = Solargraph::LanguageServer::Host.new + host = described_class.new host.configure({ 'diagnostics' => true }) file = File.join(dir, 'test.rb') File.write(file, "foo = 'foo'") host.start host.prepare dir - uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) + Solargraph::LanguageServer::UriHelpers.file_to_uri(file) host.open(file, File.read(file), 1) buffer = host.flush times = 0 @@ -81,30 +83,28 @@ end end - it "handles DiagnosticsErrors" do - host = Solargraph::LanguageServer::Host.new - library = double(:Library) + it 'handles DiagnosticsErrors' do + host = described_class.new + library = instance_double(Solargraph::Library) allow(library).to receive(:diagnose).and_raise(Solargraph::DiagnosticsError) - allow(library).to receive(:contain?).and_return(true) - allow(library).to receive(:synchronized?).and_return(true) - allow(library).to receive(:mapped?).and_return(true) + allow(library).to receive_messages(contain?: true, synchronized?: true, mapped?: true) allow(library).to receive(:attach) allow(library).to receive(:merge) allow(library).to receive(:catalog) # @todo Smelly instance variable access host.instance_variable_set(:@libraries, [library]) host.open('file:///test.rb', '', 0) - expect { + expect do host.diagnose 'file:///test.rb' - }.not_to raise_error + end.not_to raise_error result = host.flush expect(result).to include('Error in diagnostics') end - it "opens multiple folders" do - host = Solargraph::LanguageServer::Host.new - app1_folder = File.absolute_path('spec/fixtures/workspace_folders/folder1').gsub(/\\/, '/') - app2_folder = File.absolute_path('spec/fixtures/workspace_folders/folder2').gsub(/\\/, '/') + it 'opens multiple folders' do + host = described_class.new + app1_folder = File.absolute_path('spec/fixtures/workspace_folders/folder1').gsub('\\', '/') + app2_folder = File.absolute_path('spec/fixtures/workspace_folders/folder2').gsub('\\', '/') host.prepare(app1_folder) host.prepare(app2_folder) file1_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri("#{app1_folder}/app.rb") @@ -119,27 +119,27 @@ expect(app2_map).not_to include('Folder1App') end - it "stops" do - host = Solargraph::LanguageServer::Host.new + it 'stops' do + host = described_class.new host.stop expect(host.stopped?).to be(true) end - it "retains orphaned sources" do + it 'retains orphaned sources' do dir = File.absolute_path('spec/fixtures/workspace') file = File.join(dir, 'lib', 'thing.rb') file_uri = Solargraph::LanguageServer::UriHelpers.uri_to_file(file) - host = Solargraph::LanguageServer::Host.new + host = described_class.new host.prepare(dir) host.open(file_uri, File.read(file), 1) host.remove(dir) - expect{ + expect do host.document_symbols(file_uri) - }.not_to raise_error + end.not_to raise_error end - it "responds with empty diagnostics for unopened files" do - host = Solargraph::LanguageServer::Host.new + it 'responds with empty diagnostics for unopened files' do + host = described_class.new host.diagnose 'file:///file.rb' response = host.flush json = JSON.parse(response.lines.last) @@ -147,48 +147,48 @@ expect(json['params']['diagnostics']).to be_empty end - it "rescues runtime errors from messages" do - host = Solargraph::LanguageServer::Host.new + it 'rescues runtime errors from messages' do + host = described_class.new message_class = Class.new(Solargraph::LanguageServer::Message::Base) do def process - raise RuntimeError, 'Always raise an error from this message' + raise 'Always raise an error from this message' end end Solargraph::LanguageServer::Message.register('raiseRuntimeError', message_class) - expect { + expect do host.receive({ - 'id' => 1, - 'method' => 'raiseRuntimeError', - 'params' => {} - }) - }.not_to raise_error + 'id' => 1, + 'method' => 'raiseRuntimeError', + 'params' => {} + }) + end.not_to raise_error end - it "ignores invalid messages" do - host = Solargraph::LanguageServer::Host.new - expect { + it 'ignores invalid messages' do + host = described_class.new + expect do host.receive({ 'bad' => 'message' }) - }.not_to raise_error + end.not_to raise_error end it 'repairs simple breaking changes without incremental sync' do file = '/test.rb' uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) - host = Solargraph::LanguageServer::Host.new + host = described_class.new host.prepare '' host.open uri, 'Foo::Bar', 1 sleep 0.1 until host.libraries.all?(&:mapped?) host.change({ - "textDocument" => { - "uri" => uri, - 'version' => 2 - }, - "contentChanges" => [ - { - "text" => "Foo::Bar." - } - ] - }) + 'textDocument' => { + 'uri' => uri, + 'version' => 2 + }, + 'contentChanges' => [ + { + 'text' => 'Foo::Bar.' + } + ] + }) source = host.sources.find(uri) # @todo Smelly private method access expect(source.send(:repaired)).to eq('Foo::Bar ') @@ -206,76 +206,76 @@ def initialize(foo); end file = '/test.rb' uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) - host = Solargraph::LanguageServer::Host.new + host = described_class.new host.prepare '' host.open uri, code, 1 sleep 0.1 until host.libraries.all?(&:mapped?) result = host.locate_pins({ - "data" => { - "uri" => uri, - "location" => { - "range" => { - "start" => { - "line" => 5, - "character" => 12 - }, - "end" => { - "line" => 5, - "character" => 15 - } - } - }, - "path" => "Example.new" - } - }) + 'data' => { + 'uri' => uri, + 'location' => { + 'range' => { + 'start' => { + 'line' => 5, + 'character' => 12 + }, + 'end' => { + 'line' => 5, + 'character' => 15 + } + } + }, + 'path' => 'Example.new' + } + }) expect(result.map(&:path)).to include('Example.new') end end describe '#references_from' do it 'rescues FileNotFound errors' do - host = Solargraph::LanguageServer::Host.new + host = described_class.new expect { host.references_from('file:///not_a_file.rb', 1, 1) }.not_to raise_error end it 'logs FileNotFound errors' do allow(Solargraph.logger).to receive(:warn) - host = Solargraph::LanguageServer::Host.new + host = described_class.new host.references_from('file:///not_a_file.rb', 1, 1) expect(Solargraph.logger).to have_received(:warn).with(/FileNotFoundError/) end it 'rescues InvalidOffset errors' do - host = Solargraph::LanguageServer::Host.new + host = described_class.new host.open('file:///file.rb', 'class Foo; end', 1) expect { host.references_from('file:///file.rb', 0, 100) }.not_to raise_error end it 'logs InvalidOffset errors' do allow(Solargraph.logger).to receive(:warn) - host = Solargraph::LanguageServer::Host.new + host = described_class.new host.open('file:///file.rb', 'class Foo; end', 1) host.references_from('file:///file.rb', 0, 100) expect(Solargraph.logger).to have_received(:warn).with(/InvalidOffsetError/) end end - describe "Workspace variations" do - before :each do - @host = Solargraph::LanguageServer::Host.new + describe 'Workspace variations' do + before do + @host = described_class.new end - after :each do + after do @host.stop end - it "creates a library for a file without a workspace" do + it 'creates a library for a file without a workspace' do @host.open('file:///file.rb', 'class Foo; end', 1) symbols = @host.document_symbols('file:///file.rb') expect(symbols).not_to be_empty end - it "opens a file outside of prepared libraries" do + it 'opens a file outside of prepared libraries' do @host.prepare(File.absolute_path(File.join('spec', 'fixtures', 'workspace'))) @host.open('file:///file.rb', 'class Foo; end', 1) symbols = @host.document_symbols('file:///file.rb') diff --git a/spec/language_server/message/completion_item/resolve_spec.rb b/spec/language_server/message/completion_item/resolve_spec.rb index 3ae66dbda..9e270b4e9 100644 --- a/spec/language_server/message/completion_item/resolve_spec.rb +++ b/spec/language_server/message/completion_item/resolve_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Message::CompletionItem::Resolve do - it "returns MarkupContent for documentation" do + it 'returns MarkupContent for documentation' do pin = Solargraph::Pin::Method.new( location: nil, closure: Solargraph::Pin::Namespace.new(name: 'Foo'), @@ -9,26 +11,26 @@ visibility: :public, parameters: [] ) - host = double(Solargraph::LanguageServer::Host, locate_pins: [pin], probe: pin, detail: nil, options: { 'enablePages' => true }) - resolve = Solargraph::LanguageServer::Message::CompletionItem::Resolve.new(host, { - 'params' => pin.completion_item - }) + host = instance_double(Solargraph::LanguageServer::Host, locate_pins: [pin], options: { 'enablePages' => true }) + resolve = described_class.new(host, { + 'params' => pin.completion_item + }) resolve.process expect(resolve.result[:documentation][:kind]).to eq('markdown') expect(resolve.result[:documentation][:value]).to include('A method') end - it "returns nil documentation for empty strings" do + it 'returns nil documentation for empty strings' do pin = Solargraph::Pin::InstanceVariable.new( location: nil, closure: Solargraph::Pin::Namespace.new(name: 'Foo'), name: '@bar', comments: '' ) - host = double(Solargraph::LanguageServer::Host, locate_pins: [pin], probe: pin, detail: nil) - resolve = Solargraph::LanguageServer::Message::CompletionItem::Resolve.new(host, { - 'params' => pin.completion_item - }) + host = instance_double(Solargraph::LanguageServer::Host, locate_pins: [pin]) + resolve = described_class.new(host, { + 'params' => pin.completion_item + }) resolve.process expect(resolve.result[:documentation]).to be_nil end diff --git a/spec/language_server/message/extended/check_gem_version_spec.rb b/spec/language_server/message/extended/check_gem_version_spec.rb index 935917442..26023f505 100644 --- a/spec/language_server/message/extended/check_gem_version_spec.rb +++ b/spec/language_server/message/extended/check_gem_version_spec.rb @@ -1,40 +1,43 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Message::Extended::CheckGemVersion do - before :each do - version = double(:GemVersion, version: Gem::Version.new('1.0.0')) - Solargraph::LanguageServer::Message::Extended::CheckGemVersion.fetcher = double(:fetcher, search_for_dependency: [version]) + before do + version = instance_double(Gem::Version, version: Gem::Version.new('1.0.0')) + described_class.fetcher = + instance_double(Gem::SpecFetcher, search_for_dependency: [version]) end - after :each do - Solargraph::LanguageServer::Message::Extended::CheckGemVersion.fetcher = nil + after do + described_class.fetcher = nil end - it "checks the gem source" do + it 'checks the gem source' do host = Solargraph::LanguageServer::Host.new message = described_class.new(host, {}) expect { message.process }.not_to raise_error end - it "performs a verbose check" do + it 'performs a verbose check' do host = Solargraph::LanguageServer::Host.new message = described_class.new(host, { 'params' => { 'verbose' => true } }) expect { message.process }.not_to raise_error end - it "detects available updates" do + it 'detects available updates' do host = Solargraph::LanguageServer::Host.new message = described_class.new(host, {}, current: Gem::Version.new('0.0.1')) expect { message.process }.not_to raise_error end - it "performs a verbose check with an available update" do + it 'performs a verbose check with an available update' do host = Solargraph::LanguageServer::Host.new message = described_class.new(host, { 'params' => { 'verbose' => true } }, current: Gem::Version.new('0.0.1')) expect { message.process }.not_to raise_error end - it "responds to update actions" do + it 'responds to update actions' do host = Solargraph::LanguageServer::Host.new - message = Solargraph::LanguageServer::Message::Extended::CheckGemVersion.new(host, {}, current: Gem::Version.new('0.0.1')) + message = described_class.new(host, {}, current: Gem::Version.new('0.0.1')) message.process response = nil reader = Solargraph::LanguageServer::Transport::DataReader.new @@ -42,19 +45,19 @@ response = data end reader.receive host.flush - expect { + expect do action = { - "id" => response['id'], - "result" => response['params']['actions'].first + 'id' => response['id'], + 'result' => response['params']['actions'].first } host.receive action - }.not_to raise_error + end.not_to raise_error end it 'uses bundler' do host = Solargraph::LanguageServer::Host.new - host.configure({'useBundler' => true}) - message = Solargraph::LanguageServer::Message::Extended::CheckGemVersion.new(host, {}, current: Gem::Version.new('0.0.1')) + host.configure({ 'useBundler' => true }) + message = described_class.new(host, {}, current: Gem::Version.new('0.0.1')) message.process response = nil reader = Solargraph::LanguageServer::Transport::DataReader.new @@ -62,12 +65,12 @@ response = data end reader.receive host.flush - expect { + expect do action = { - "id" => response['id'], - "result" => response['params']['actions'].first + 'id' => response['id'], + 'result' => response['params']['actions'].first } host.receive action - }.not_to raise_error + end.not_to raise_error end end diff --git a/spec/language_server/message/initialize_spec.rb b/spec/language_server/message/initialize_spec.rb index 3ab54e2de..aae61cff7 100644 --- a/spec/language_server/message/initialize_spec.rb +++ b/spec/language_server/message/initialize_spec.rb @@ -1,94 +1,98 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Message::Initialize do - it "prepares workspace folders" do + it 'prepares workspace folders' do host = Solargraph::LanguageServer::Host.new dir = File.realpath(File.join('spec', 'fixtures', 'workspace')) - init = Solargraph::LanguageServer::Message::Initialize.new(host, { - 'params' => { - 'capabilities' => { - 'workspace' => { - 'workspaceFolders' => true - } - }, - 'workspaceFolders' => [ - { - 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(dir), - 'name' => 'workspace' - } - ] - } - }) + init = described_class.new(host, { + 'params' => { + 'capabilities' => { + 'workspace' => { + 'workspaceFolders' => true + } + }, + 'workspaceFolders' => [ + { + 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(dir), + 'name' => 'workspace' + } + ] + } + }) init.process expect(host.folders.length).to eq(1) end - it "prepares rootUri as a workspace" do + it 'prepares rootUri as a workspace' do host = Solargraph::LanguageServer::Host.new dir = File.realpath(File.join('spec', 'fixtures', 'workspace')) - init = Solargraph::LanguageServer::Message::Initialize.new(host, { - 'params' => { - 'capabilities' => { - 'workspace' => { - 'workspaceFolders' => true - } - }, - 'rootUri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(dir) - } - }) + init = described_class.new(host, { + 'params' => { + 'capabilities' => { + 'workspace' => { + 'workspaceFolders' => true + } + }, + 'rootUri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(dir) + } + }) init.process expect(host.folders.length).to eq(1) end - it "prepares rootPath as a workspace" do + it 'prepares rootPath as a workspace' do host = Solargraph::LanguageServer::Host.new dir = File.realpath(File.join('spec', 'fixtures', 'workspace')) - init = Solargraph::LanguageServer::Message::Initialize.new(host, { - 'params' => { - 'capabilities' => { - 'workspace' => { - 'workspaceFolders' => true - } - }, - 'rootPath' => dir - } - }) + init = described_class.new(host, { + 'params' => { + 'capabilities' => { + 'workspace' => { + 'workspaceFolders' => true + } + }, + 'rootPath' => dir + } + }) init.process expect(host.folders.length).to eq(1) end it 'returns the default capabilities' do host = Solargraph::LanguageServer::Host.new - init = Solargraph::LanguageServer::Message::Initialize.new(host, {}) + init = described_class.new(host, {}) init.process result = init.result expect(result).to include(:capabilities) expect(result[:capabilities]).to eq({ - textDocumentSync: 2, - workspace: { workspaceFolders: { supported: true, changeNotifications: true } }, - completionProvider: { resolveProvider: true, triggerCharacters: ['.', ':', '@'] }, - signatureHelpProvider: { triggerCharacters: ['(', ','] }, - hoverProvider: true, - documentSymbolProvider: true, - definitionProvider: true, - typeDefinitionProvider: true, - renameProvider: { prepareProvider: true }, - referencesProvider: true, - workspaceSymbolProvider: true, - foldingRangeProvider: true, - documentHighlightProvider: true - }) + textDocumentSync: 2, + workspace: { workspaceFolders: { supported: true, + changeNotifications: true } }, + completionProvider: { resolveProvider: true, + triggerCharacters: ['.', ':', '@'] }, + signatureHelpProvider: { triggerCharacters: ['(', ','] }, + hoverProvider: true, + documentSymbolProvider: true, + definitionProvider: true, + typeDefinitionProvider: true, + renameProvider: { prepareProvider: true }, + referencesProvider: true, + workspaceSymbolProvider: true, + foldingRangeProvider: true, + documentHighlightProvider: true + }) end it 'returns all capabilities when all options are enabled' do host = Solargraph::LanguageServer::Host.new - init = Solargraph::LanguageServer::Message::Initialize.new(host, { - 'params' => { - 'initializationOptions' => { - 'completion' => true, - 'autoformat' => true, - 'formatting' => true - } - } - }) + init = described_class.new(host, { + 'params' => { + 'initializationOptions' => { + 'completion' => true, + 'autoformat' => true, + 'formatting' => true + } + } + }) init.process result = init.result diff --git a/spec/language_server/message/text_document/definition_spec.rb b/spec/language_server/message/text_document/definition_spec.rb index 72ff77f1e..d84d23cbe 100644 --- a/spec/language_server/message/text_document/definition_spec.rb +++ b/spec/language_server/message/text_document/definition_spec.rb @@ -1,4 +1,35 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Message::TextDocument::Definition do + it 'prepares empty directory' do + Dir.mktmpdir do |dir| + host = Solargraph::LanguageServer::Host.new + test_rb_path = File.join(dir, 'test.rb') + thing_rb_path = File.join(dir, 'thing.rb') + FileUtils.cp('spec/fixtures/workspace/lib/other.rb', test_rb_path) + FileUtils.cp('spec/fixtures/workspace/lib/thing.rb', thing_rb_path) + host.prepare(dir) + sleep 0.1 until host.libraries.all?(&:mapped?) + host.catalog + file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(test_rb_path) + other_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(thing_rb_path) + message = described_class + .new(host, { + 'params' => { + 'textDocument' => { + 'uri' => file_uri + }, + 'position' => { + 'line' => 4, + 'character' => 10 + } + } + }) + message.process + expect(message.result.first[:uri]).to eq(other_uri) + end + end + it 'finds definitions of methods' do host = Solargraph::LanguageServer::Host.new host.prepare('spec/fixtures/workspace') @@ -6,39 +37,42 @@ host.catalog file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(File.absolute_path('spec/fixtures/workspace/lib/other.rb')) other_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(File.absolute_path('spec/fixtures/workspace/lib/thing.rb')) - message = Solargraph::LanguageServer::Message::TextDocument::Definition.new(host, { - 'params' => { - 'textDocument' => { - 'uri' => file_uri - }, - 'position' => { - 'line' => 4, - 'character' => 10 - } - } - }) + message = described_class.new(host, { + 'params' => { + 'textDocument' => { + 'uri' => file_uri + }, + 'position' => { + 'line' => 4, + 'character' => 10 + } + } + }) message.process expect(message.result.first[:uri]).to eq(other_uri) end - it 'finds definitions of require paths' do + it 'finds definitions of require paths', time_limit_seconds: 120 do path = File.absolute_path('spec/fixtures/workspace') host = Solargraph::LanguageServer::Host.new host.prepare(path) sleep 0.1 until host.libraries.all?(&:mapped?) host.catalog - message = Solargraph::LanguageServer::Message::TextDocument::Definition.new(host, { - 'params' => { - 'textDocument' => { - 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(File.join(path, 'lib', 'other.rb')) - }, - 'position' => { - 'line' => 0, - 'character' => 10 - } - } - }) + message = described_class.new(host, { + 'params' => { + 'textDocument' => { + 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(File.join( + path, 'lib', 'other.rb' + )) + }, + 'position' => { + 'line' => 0, + 'character' => 10 + } + } + }) message.process - expect(message.result.first[:uri]).to eq(Solargraph::LanguageServer::UriHelpers.file_to_uri(File.join(path, 'lib', 'thing.rb'))) + expect(message.result.first[:uri]).to eq(Solargraph::LanguageServer::UriHelpers.file_to_uri(File.join(path, 'lib', + 'thing.rb'))) end end diff --git a/spec/language_server/message/text_document/formatting_spec.rb b/spec/language_server/message/text_document/formatting_spec.rb index 10797e8db..65c0841d7 100644 --- a/spec/language_server/message/text_document/formatting_spec.rb +++ b/spec/language_server/message/text_document/formatting_spec.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Message::TextDocument::Formatting do it 'gracefully handles empty files' do - host = double(:Host, read_text: '', formatter_config: {}) + host = instance_double(Solargraph::LanguageServer::Host, read_text: '', formatter_config: {}) request = { 'params' => { - 'textDocument' => { + 'textDocument' => { 'uri' => 'test.rb' } } } - message = Solargraph::LanguageServer::Message::TextDocument::Formatting.new(host, request) + message = described_class.new(host, request) message.process expect(message.process.first[:newText]).to be_empty end diff --git a/spec/language_server/message/text_document/hover_spec.rb b/spec/language_server/message/text_document/hover_spec.rb index 74eaee597..76b3c9082 100644 --- a/spec/language_server/message/text_document/hover_spec.rb +++ b/spec/language_server/message/text_document/hover_spec.rb @@ -1,20 +1,22 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Message::TextDocument::Hover do it 'returns nil for empty documentation' do host = Solargraph::LanguageServer::Host.new host.prepare('spec/fixtures/workspace') sleep 0.1 until host.libraries.all?(&:mapped?) host.catalog - message = Solargraph::LanguageServer::Message::TextDocument::Hover.new(host, { - 'params' => { - 'textDocument' => { - 'uri' => 'file://spec/fixtures/workspace/lib/other.rb' - }, - 'position' => { - 'line' => 5, - 'character' => 0 - } - } - }) + message = described_class.new(host, { + 'params' => { + 'textDocument' => { + 'uri' => 'file://spec/fixtures/workspace/lib/other.rb' + }, + 'position' => { + 'line' => 5, + 'character' => 0 + } + } + }) message.process expect(message.result).to be_nil end @@ -29,17 +31,17 @@ def foo host = Solargraph::LanguageServer::Host.new host.open('file:///test.rb', code, 1) host.catalog - message = Solargraph::LanguageServer::Message::TextDocument::Hover.new(host, { - 'params' => { - 'textDocument' => { - 'uri' => 'file:///test.rb' - }, - 'position' => { - 'line' => 4, - 'character' => 6 - } - } - }) + message = described_class.new(host, { + 'params' => { + 'textDocument' => { + 'uri' => 'file:///test.rb' + }, + 'position' => { + 'line' => 4, + 'character' => 6 + } + } + }) message.process expect(message.result[:contents][:value]).to eq("x\n\n`=~ String`") end diff --git a/spec/language_server/message/text_document/rename_spec.rb b/spec/language_server/message/text_document/rename_spec.rb index b16110455..19903eaf9 100644 --- a/spec/language_server/message/text_document/rename_spec.rb +++ b/spec/language_server/message/text_document/rename_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe Solargraph::LanguageServer::Message::TextDocument::Rename do - it "renames a symbol" do + it 'renames a symbol' do host = Solargraph::LanguageServer::Host.new host.start host.open('file:///file.rb', %( @@ -10,25 +10,25 @@ class Foo foo = Foo.new ), 1) sleep 0.01 until host.libraries.all?(&:mapped?) - rename = Solargraph::LanguageServer::Message::TextDocument::Rename.new(host, { - 'id' => 1, - 'method' => 'textDocument/rename', - 'params' => { - 'textDocument' => { - 'uri' => 'file:///file.rb' - }, - 'position' => { - 'line' => 1, - 'character' => 12 - }, - 'newName' => 'Bar' - } - }) + rename = described_class.new(host, { + 'id' => 1, + 'method' => 'textDocument/rename', + 'params' => { + 'textDocument' => { + 'uri' => 'file:///file.rb' + }, + 'position' => { + 'line' => 1, + 'character' => 12 + }, + 'newName' => 'Bar' + } + }) rename.process expect(rename.result[:changes]['file:///file.rb'].length).to eq(2) end - it "renames an argument symbol from method signature" do + it 'renames an argument symbol from method signature' do host = Solargraph::LanguageServer::Host.new host.start host.open('file:///file.rb', %( @@ -40,25 +40,25 @@ def foo(bar) end ), 1) - rename = Solargraph::LanguageServer::Message::TextDocument::Rename.new(host, { - 'id' => 1, - 'method' => 'textDocument/rename', - 'params' => { - 'textDocument' => { - 'uri' => 'file:///file.rb' - }, - 'position' => { - 'line' => 2, - 'character' => 14 - }, - 'newName' => 'baz' - } - }) + rename = described_class.new(host, { + 'id' => 1, + 'method' => 'textDocument/rename', + 'params' => { + 'textDocument' => { + 'uri' => 'file:///file.rb' + }, + 'position' => { + 'line' => 2, + 'character' => 14 + }, + 'newName' => 'baz' + } + }) rename.process expect(rename.result[:changes]['file:///file.rb'].length).to eq(3) end - it "renames an argument symbol from method body" do + it 'renames an argument symbol from method body' do host = Solargraph::LanguageServer::Host.new host.start host.open('file:///file.rb', %( @@ -69,25 +69,25 @@ def foo(bar) end end ), 1) - rename = Solargraph::LanguageServer::Message::TextDocument::Rename.new(host, { - 'id' => 1, - 'method' => 'textDocument/rename', - 'params' => { - 'textDocument' => { - 'uri' => 'file:///file.rb' - }, - 'position' => { - 'line' => 3, - 'character' => 6 - }, - 'newName' => 'baz' - } - }) + rename = described_class.new(host, { + 'id' => 1, + 'method' => 'textDocument/rename', + 'params' => { + 'textDocument' => { + 'uri' => 'file:///file.rb' + }, + 'position' => { + 'line' => 3, + 'character' => 6 + }, + 'newName' => 'baz' + } + }) rename.process expect(rename.result[:changes]['file:///file.rb'].length).to eq(3) end - it "renames namespace symbol with proper range" do + it 'renames namespace symbol with proper range' do host = Solargraph::LanguageServer::Host.new host.start host.open('file:///file.rb', %( @@ -96,20 +96,20 @@ class Namespace::ExampleClass end obj = Namespace::ExampleClass.new ), 1) - rename = Solargraph::LanguageServer::Message::TextDocument::Rename.new(host, { - 'id' => 1, - 'method' => 'textDocument/rename', - 'params' => { - 'textDocument' => { - 'uri' => 'file:///file.rb' - }, - 'position' => { - 'line' => 2, - 'character' => 12 - }, - 'newName' => 'Nameplace' - } - }) + rename = described_class.new(host, { + 'id' => 1, + 'method' => 'textDocument/rename', + 'params' => { + 'textDocument' => { + 'uri' => 'file:///file.rb' + }, + 'position' => { + 'line' => 2, + 'character' => 12 + }, + 'newName' => 'Nameplace' + } + }) rename.process changes = rename.result[:changes]['file:///file.rb'] expect(changes.length).to eq(3) diff --git a/spec/language_server/message/text_document/type_definition_spec.rb b/spec/language_server/message/text_document/type_definition_spec.rb index 2f7ec3668..16f7f3006 100644 --- a/spec/language_server/message/text_document/type_definition_spec.rb +++ b/spec/language_server/message/text_document/type_definition_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Message::TextDocument::TypeDefinition do it 'finds definitions of methods' do host = Solargraph::LanguageServer::Host.new @@ -6,17 +8,17 @@ host.catalog file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(File.absolute_path('spec/fixtures/workspace/lib/other.rb')) something_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(File.absolute_path('spec/fixtures/workspace/lib/something.rb')) - message = Solargraph::LanguageServer::Message::TextDocument::TypeDefinition.new(host, { - 'params' => { - 'textDocument' => { - 'uri' => file_uri - }, - 'position' => { - 'line' => 4, - 'character' => 10 - } - } - }) + message = described_class.new(host, { + 'params' => { + 'textDocument' => { + 'uri' => file_uri + }, + 'position' => { + 'line' => 4, + 'character' => 10 + } + } + }) message.process expect(message.result.first[:uri]).to eq(something_uri) end diff --git a/spec/language_server/message/workspace/did_change_configuration_spec.rb b/spec/language_server/message/workspace/did_change_configuration_spec.rb index 0a082875d..a352a5c85 100644 --- a/spec/language_server/message/workspace/did_change_configuration_spec.rb +++ b/spec/language_server/message/workspace/did_change_configuration_spec.rb @@ -71,8 +71,8 @@ described_class.new(host, message).process expect( - host.registered?('textDocument/completion') - ).to be_truthy, 'Expected textDocument/completion to be registered' + host + ).to be_registered('textDocument/completion'), 'Expected textDocument/completion to be registered' end end @@ -83,8 +83,8 @@ described_class.new(host, message).process expect( - host.registered?('textDocument/completion') - ).to be_falsy, 'Expected textDocument/completion to not be registered' + host + ).not_to be_registered('textDocument/completion'), 'Expected textDocument/completion to not be registered' end end end diff --git a/spec/language_server/message/workspace/did_change_watched_files_spec.rb b/spec/language_server/message/workspace/did_change_watched_files_spec.rb index b78aa06d3..ebe76fc50 100644 --- a/spec/language_server/message/workspace/did_change_watched_files_spec.rb +++ b/spec/language_server/message/workspace/did_change_watched_files_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'tmpdir' describe Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles do @@ -8,17 +10,17 @@ file = File.join(dir, 'foo.rb') File.write file, 'class Foo; end' uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) - changed = Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles.new(host, { - 'method' => 'workspace/didChangeWatchedFiles', - 'params' => { - 'changes' => [ - { - 'type' => Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles::CREATED, - 'uri' => uri - } - ] - } - }) + changed = described_class.new(host, { + 'method' => 'workspace/didChangeWatchedFiles', + 'params' => { + 'changes' => [ + { + 'type' => Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles::CREATED, + 'uri' => uri + } + ] + } + }) changed.process expect(host.synchronizing?).to be(false) expect(host.library_for(uri)).to be_a(Solargraph::Library) @@ -33,22 +35,22 @@ uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) host = Solargraph::LanguageServer::Host.new host.prepare dir - changed = Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles.new(host, { - 'method' => 'workspace/didChangeWatchedFiles', - 'params' => { - 'changes' => [ - { - 'type' => Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles::DELETED, - 'uri' => uri - } - ] - } - }) + changed = described_class.new(host, { + 'method' => 'workspace/didChangeWatchedFiles', + 'params' => { + 'changes' => [ + { + 'type' => Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles::DELETED, + 'uri' => uri + } + ] + } + }) changed.process expect(host.synchronizing?).to be(false) - expect { + expect do host.library_for(uri) - }.to raise_error(Solargraph::FileNotFoundError) + end.to raise_error(Solargraph::FileNotFoundError) end end @@ -60,17 +62,17 @@ host = Solargraph::LanguageServer::Host.new host.prepare dir File.write file, 'class FooBar; end' - changed = Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles.new(host, { - 'method' => 'workspace/didChangeWatchedFiles', - 'params' => { - 'changes' => [ - { - 'type' => Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles::CHANGED, - 'uri' => uri - } - ] - } - }) + changed = described_class.new(host, { + 'method' => 'workspace/didChangeWatchedFiles', + 'params' => { + 'changes' => [ + { + 'type' => Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles::CHANGED, + 'uri' => uri + } + ] + } + }) changed.process expect(host.synchronizing?).to be(false) library = host.library_for(uri) @@ -81,20 +83,20 @@ end it 'sets errors for invalid change types' do - host = double(Solargraph::LanguageServer::Host, catalog: nil) + host = instance_double(Solargraph::LanguageServer::Host, catalog: nil) allow(host).to receive(:create) allow(host).to receive(:delete) - changed = Solargraph::LanguageServer::Message::Workspace::DidChangeWatchedFiles.new(host, { - 'method' => 'workspace/didChangeWatchedFiles', - 'params' => { - 'changes' => [ - { - 'type' => -1, - 'uri' => 'file:///foo.rb' - } - ] - } - }) + changed = described_class.new(host, { + 'method' => 'workspace/didChangeWatchedFiles', + 'params' => { + 'changes' => [ + { + 'type' => -1, + 'uri' => 'file:///foo.rb' + } + ] + } + }) changed.process expect(changed.error).not_to be_nil end diff --git a/spec/language_server/message_spec.rb b/spec/language_server/message_spec.rb index 9a8c26328..504e66d07 100644 --- a/spec/language_server/message_spec.rb +++ b/spec/language_server/message_spec.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Message do - it "returns MethodNotFound for unregistered methods" do - msg = Solargraph::LanguageServer::Message.select 'notARealMethod' + it 'returns MethodNotFound for unregistered methods' do + msg = described_class.select 'notARealMethod' expect(msg).to be(Solargraph::LanguageServer::Message::MethodNotFound) end - it "returns MethodNotImplemented for unregistered $ methods" do - msg = Solargraph::LanguageServer::Message.select '$/notARealMethod' + it 'returns MethodNotImplemented for unregistered $ methods' do + msg = described_class.select '$/notARealMethod' expect(msg).to be(Solargraph::LanguageServer::Message::MethodNotImplemented) end end diff --git a/spec/language_server/protocol_spec.rb b/spec/language_server/protocol_spec.rb index e88fb9c05..25764e6eb 100644 --- a/spec/language_server/protocol_spec.rb +++ b/spec/language_server/protocol_spec.rb @@ -1,4 +1,4 @@ -require 'thread' +# frozen_string_literal: true class Protocol attr_reader :response @@ -36,23 +36,24 @@ def stop describe Protocol do before :all do - @protocol = Protocol.new(Solargraph::LanguageServer::Host.new) + @protocol = described_class.new(Solargraph::LanguageServer::Host.new) end after :all do @protocol.stop end - before :each do - version = double(:GemVersion, version: Gem::Version.new('1.0.0')) - Solargraph::LanguageServer::Message::Extended::CheckGemVersion.fetcher = double(:fetcher, search_for_dependency: [version]) + before do + version = instance_double(Gem::Version, version: Gem::Version.new('1.0.0')) + Solargraph::LanguageServer::Message::Extended::CheckGemVersion.fetcher = + instance_double(Gem::SpecFetcher, search_for_dependency: [version]) end - after :each do + after do Solargraph::LanguageServer::Message::Extended::CheckGemVersion.fetcher = nil end - it "handles initialize" do + it 'handles initialize' do @protocol.request 'initialize', { 'capabilities' => { 'textDocument' => { @@ -69,27 +70,27 @@ def stop expect(response['result'].keys).to include('capabilities') end - it "is not stopped after initialization" do + it 'is not stopped after initialization' do expect(@protocol.host.stopped?).to be(false) end - it "configured dynamic registration capabilities from initialize" do + it 'configured dynamic registration capabilities from initialize' do expect(@protocol.host.can_register?('textDocument/completion')).to be(true) expect(@protocol.host.can_register?('textDocument/hover')).to be(false) expect(@protocol.host.can_register?('workspace/symbol')).to be(false) end - it "handles initialized" do + it 'handles initialized' do @protocol.request 'initialized', nil response = @protocol.response expect(response['error']).to be_nil end - it "configured default dynamic registration capabilities from initialized" do + it 'configured default dynamic registration capabilities from initialized' do expect(@protocol.host.registered?('textDocument/completion')).to be(true) end - it "handles textDocument/didOpen" do + it 'handles textDocument/didOpen' do @protocol.request 'textDocument/didOpen', { 'textDocument' => { 'uri' => 'file:///file.rb', @@ -107,7 +108,7 @@ def bar baz 'version' => 0 } } - response = @protocol.response + @protocol.response expect(@protocol.host.open?('file:///file.rb')).to be(true) end @@ -126,7 +127,7 @@ def bar baz expect(response['result'].length).to eq(2) end - it "handles textDocument/didChange" do + it 'handles textDocument/didChange' do @protocol.request 'textDocument/didChange', { 'textDocument' => { 'uri' => 'file:///file.rb', @@ -149,10 +150,10 @@ def bar baz ] } response = @protocol.response - # @todo What to expect? + expect(response).not_to be_nil end - it "handles textDocument/completion" do + it 'handles textDocument/completion' do @protocol.request 'textDocument/completion', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -164,13 +165,13 @@ def bar baz } response = @protocol.response expect(response['error']).to be_nil - expect(response['result']['items'].length > 0).to be(true) + expect(!response['result']['items'].empty?).to be(true) end - it "handles completionItem/resolve" do + it 'handles completionItem/resolve' do # Reuse the response from textDocument/completion response = @protocol.response - item = response['result']['items'].select{|h| h['label'] == 'bar'}.first + item = response['result']['items'].select { |h| h['label'] == 'bar' }.first @protocol.request 'completionItem/resolve', item response = @protocol.response expect(response['result']['documentation']['value']).to include('bar method') @@ -189,7 +190,7 @@ def bar baz expect(@protocol.response['error']).to be_nil end - it "documents YARD pins" do + it 'documents YARD pins' do @protocol.request 'textDocument/completion', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -200,22 +201,14 @@ def bar baz } } response = @protocol.response - item = response['result']['items'].select{|i| i['data']['path'] == 'File.absolute_path'}.first + item = response['result']['items'].select { |i| i['data']['path'] == 'File.absolute_path' }.first expect(item).not_to be_nil @protocol.request 'completionItem/resolve', item response = @protocol.response expect(response['result']['documentation']).not_to be_empty end - it "handles workspace/symbol" do - @protocol.request 'workspace/symbol', { - 'query' => 'test' - } - response = @protocol.response - expect(response['error']).to be_nil - end - - it "handles textDocument/definition" do + it 'handles textDocument/definition' do sleep 0.5 # HACK: Give the Host::Sources thread time to work @protocol.request 'textDocument/definition', { 'textDocument' => { @@ -231,7 +224,7 @@ def bar baz expect(response['result']).not_to be_empty end - it "handles textDocument/definition on undefined symbols" do + it 'handles textDocument/definition on undefined symbols' do @protocol.request 'textDocument/definition', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -246,7 +239,7 @@ def bar baz expect(response['result']).to be_empty end - it "handles textDocument/documentSymbol" do + it 'handles textDocument/documentSymbol' do @protocol.request 'textDocument/documentSymbol', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -256,7 +249,7 @@ def bar baz expect(response['error']).to be_nil end - it "handles textDocument/hover" do + it 'handles textDocument/hover' do @protocol.request 'textDocument/hover', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -285,7 +278,7 @@ def bar baz expect(@protocol.response['error']).to be_nil end - it "handles textDocument/signatureHelp" do + it 'handles textDocument/signatureHelp' do @protocol.request 'textDocument/signatureHelp', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -300,7 +293,7 @@ def bar baz expect(response['result']['signatures']).not_to be_empty end - it "handles workspace/symbol" do + it 'handles workspace/symbol' do @protocol.request 'workspace/symbol', { 'query' => 'Foo' } @@ -309,7 +302,7 @@ def bar baz expect(response['result']).not_to be_empty end - it "handles textDocument/references for namespaces" do + it 'handles textDocument/references for namespaces' do @protocol.request 'textDocument/references', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -324,7 +317,7 @@ def bar baz expect(response['result']).not_to be_empty end - it "handles textDocument/references for methods" do + it 'handles textDocument/references for methods' do @protocol.request 'textDocument/references', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -339,7 +332,7 @@ def bar baz expect(response['result']).not_to be_empty end - it "handles textDocument/rename" do + it 'handles textDocument/rename' do @protocol.request 'textDocument/rename', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -355,7 +348,7 @@ def bar baz expect(response['result']['changes']['file:///file.rb']).to be_a(Array) end - it "handles textDocument/prepareRename" do + it 'handles textDocument/prepareRename' do @protocol.request 'textDocument/prepareRename', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -371,7 +364,7 @@ def bar baz expect(response['result']).to be_a(Hash) end - it "handles textDocument/foldingRange" do + it 'handles textDocument/foldingRange' do @protocol.request 'textDocument/foldingRange', { 'textDocument' => { 'uri' => 'file:///file.rb' @@ -382,17 +375,17 @@ def bar baz expect(response['result'].length).not_to be_zero end - it "handles textDocument/didClose" do + it 'handles textDocument/didClose' do @protocol.request 'textDocument/didClose', { 'textDocument' => { 'uri' => 'file:///file.rb' } } - response = @protocol.response + @protocol.response expect(@protocol.host.open?('file:///file.rb')).to be(false) end - it "handles $/solargraph/search" do + it 'handles $/solargraph/search' do @protocol.request '$/solargraph/search', { 'query' => 'Foo#bar' } @@ -401,7 +394,7 @@ def bar baz expect(response['result']['content']).not_to be_empty end - it "handles $/solargraph/document" do + it 'handles $/solargraph/document' do @protocol.request '$/solargraph/document', { 'query' => 'String' } @@ -410,7 +403,7 @@ def bar baz expect(response['result']['content']).not_to be_empty end - it "handles workspace/didChangeConfiguration" do + it 'handles workspace/didChangeConfiguration' do @protocol.request 'workspace/didChangeConfiguration', { 'settings' => { 'solargraph' => { @@ -423,7 +416,7 @@ def bar baz expect(@protocol.host.registered?('textDocument/completion')).to be(false) end - it "handles $/solargraph/checkGemVersion" do + it 'handles $/solargraph/checkGemVersion' do @protocol.request '$/solargraph/checkGemVersion', { verbose: false } response = @protocol.response expect(response['error']).to be_nil @@ -431,13 +424,13 @@ def bar baz expect(response['result']['available']).to be_a(String) end - it "handles $/solargraph/documentGems" do + it 'handles $/solargraph/documentGems' do @protocol.request '$/solargraph/documentGems', {} response = @protocol.response expect(response['error']).to be_nil end - it "handles textDocument/formatting" do + it 'handles textDocument/formatting' do @protocol.request 'textDocument/didOpen', { 'textDocument' => { 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(File.realpath('spec/fixtures/formattable.rb')), @@ -455,7 +448,7 @@ def bar baz expect(response['result'].first['newText']).to be_a(String) end - it "can format file without file extension" do + it 'can format file without file extension' do @protocol.request 'textDocument/didOpen', { 'textDocument' => { 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(File.realpath('spec/fixtures/formattable')), @@ -474,13 +467,13 @@ def bar baz # expect(response['result'].first['newText']).to include('def barbaz(parameter); end') end - it "handles MethodNotFound errors" do + it 'handles MethodNotFound errors' do @protocol.request 'notamethod', {} response = @protocol.response expect(response['error']['code']).to be(Solargraph::LanguageServer::ErrorCodes::METHOD_NOT_FOUND) end - it "handles didChangeWatchedFiles for created files" do + it 'handles didChangeWatchedFiles for created files' do @protocol.request 'workspace/didChangeWatchedFiles', { 'changes' => [ { @@ -493,7 +486,7 @@ def bar baz expect(response['error']).to be_nil end - it "handles didChangeWatchedFiles for changed files" do + it 'handles didChangeWatchedFiles for changed files' do @protocol.request 'workspace/didChangeWatchedFiles', { 'changes' => [ { @@ -506,7 +499,7 @@ def bar baz expect(response['error']).to be_nil end - it "handles didChangeWatchedFiles for deleted files" do + it 'handles didChangeWatchedFiles for deleted files' do @protocol.request 'workspace/didChangeWatchedFiles', { 'changes' => [ { @@ -519,11 +512,11 @@ def bar baz expect(response['error']).to be_nil end - it "handles didChangeWatchedFiles for invalid change types" do + it 'handles didChangeWatchedFiles for invalid change types' do @protocol.request 'workspace/didChangeWatchedFiles', { 'changes' => [ { - 'type' => -99999, + 'type' => -99_999, 'uri' => 'file:///watched-file.rb' } ] @@ -532,7 +525,7 @@ def bar baz expect(response['error']).not_to be_nil end - it "adds folders to the workspace" do + it 'adds folders to the workspace' do dir = File.absolute_path('spec/fixtures/workspace_folders/folder1') uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(dir) @protocol.request 'workspace/didChangeWorkspaceFolders', { @@ -549,7 +542,7 @@ def bar baz expect(@protocol.host.folders).to include(dir) end - it "removes folders from the workspace" do + it 'removes folders from the workspace' do dir = File.absolute_path('spec/fixtures/workspace_folders/folder1') uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(dir) @protocol.request 'workspace/didChangeWorkspaceFolders', { @@ -566,27 +559,27 @@ def bar baz expect(@protocol.host.folders).not_to include(dir) end - it "handles $/cancelRequest" do - expect { + it 'handles $/cancelRequest' do + expect do @protocol.request '$/cancelRequest', { 'id' => 0 } - }.not_to raise_error + end.not_to raise_error end - it "handles $/solargraph/environment" do + it 'handles $/solargraph/environment' do @protocol.request '$/solargraph/environment', {} response = @protocol.response expect(response['result']['content']).not_to be_nil end - it "handles shutdown" do + it 'handles shutdown' do @protocol.request 'shutdown', {} response = @protocol.response expect(response['error']).to be_nil end - it "handles exit" do + it 'handles exit' do @protocol.request 'exit', {} response = @protocol.response expect(response['error']).to be_nil diff --git a/spec/language_server/transport/adapter_spec.rb b/spec/language_server/transport/adapter_spec.rb index 3d30dd4e6..23d6ac123 100644 --- a/spec/language_server/transport/adapter_spec.rb +++ b/spec/language_server/transport/adapter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AdapterTester include Solargraph::LanguageServer::Transport::Adapter @@ -15,21 +17,21 @@ def flush end describe Solargraph::LanguageServer::Transport::Adapter do - it "creates a host on open" do + it 'creates a host on open' do tester = AdapterTester.new tester.opening expect(tester.host).to be_a(Solargraph::LanguageServer::Host) expect(tester.host).not_to be_stopped end - it "stops a host on close" do + it 'stops a host on close' do tester = AdapterTester.new tester.opening tester.closing expect(tester.host).to be_stopped end - it "stops Backport when the host stops" do + it 'stops Backport when the host stops' do tester = AdapterTester.new Backport.run do tester.opening @@ -40,13 +42,13 @@ def flush expect(tester.host).to be_stopped end - it "processes sent data" do + it 'processes sent data' do tester = AdapterTester.new tester.opening message = '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}' - expect { + expect do tester.receiving "Content-Length: #{message.length}\r\n\r\n#{message}" - }.not_to raise_error + end.not_to raise_error tester.closing end end diff --git a/spec/language_server/transport/data_reader_spec.rb b/spec/language_server/transport/data_reader_spec.rb index c035d2b2c..179f14a24 100644 --- a/spec/language_server/transport/data_reader_spec.rb +++ b/spec/language_server/transport/data_reader_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::Transport::DataReader do - it "rescues exceptions for invalid JSON" do - reader = Solargraph::LanguageServer::Transport::DataReader.new + it 'rescues exceptions for invalid JSON' do + reader = described_class.new handled = 0 - reader.set_message_handler do |msg| + reader.set_message_handler do |_msg| handled += 1 end msg = { @@ -10,9 +12,9 @@ method: 'test' }.to_json msg += '}' - expect { + expect do reader.receive "Content-Length:#{msg.bytesize}\r\n\r\n#{msg}" - }.not_to raise_error + end.not_to raise_error expect(handled).to eq(0) end end diff --git a/spec/language_server/uri_helpers_spec.rb b/spec/language_server/uri_helpers_spec.rb index 002a10d1c..038dca0bc 100644 --- a/spec/language_server/uri_helpers_spec.rb +++ b/spec/language_server/uri_helpers_spec.rb @@ -1,37 +1,39 @@ +# frozen_string_literal: true + describe Solargraph::LanguageServer::UriHelpers do it "doesn't escapes colons in file paths" do file = 'c:/one/two' - uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) + uri = described_class.file_to_uri(file) expect(uri).to start_with('file:///c:') end it 'uses %20 for spaces' do file = '/path/to/a file' - uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) + uri = described_class.file_to_uri(file) expect(uri).to end_with('a%20file') end it 'removes file:// prefix' do uri = 'file:///dev_tools/' - file = Solargraph::LanguageServer::UriHelpers.uri_to_file(uri) + file = described_class.uri_to_file(uri) expect(file).to eq('/dev_tools/') end it 'removes file: prefix' do uri = 'file:/dev_tools/' - file = Solargraph::LanguageServer::UriHelpers.uri_to_file(uri) + file = described_class.uri_to_file(uri) expect(file).to eq('/dev_tools/') end it 'removes file:/// prefix when a drive is specified' do uri = 'file:///Z:/dev_tools/' - file = Solargraph::LanguageServer::UriHelpers.uri_to_file(uri) + file = described_class.uri_to_file(uri) expect(file).to eq('Z:/dev_tools/') end it 'removes file:/ prefix when a drive is specified' do uri = 'file:/Z:/dev_tools/' - file = Solargraph::LanguageServer::UriHelpers.uri_to_file(uri) + file = described_class.uri_to_file(uri) expect(file).to eq('Z:/dev_tools/') end end diff --git a/spec/library_spec.rb b/spec/library_spec.rb index 34de9e1f0..9f9ab87dc 100644 --- a/spec/library_spec.rb +++ b/spec/library_spec.rb @@ -1,22 +1,24 @@ +# frozen_string_literal: true + require 'tmpdir' require 'yard' describe Solargraph::Library do - it "does not open created files in the workspace" do + it 'does not open created files in the workspace' do Dir.mktmpdir do |temp_dir_path| # Ensure we resolve any symlinks to their real path workspace_path = File.realpath(temp_dir_path) file_path = File.join(workspace_path, 'file.rb') File.write(file_path, 'a = b') - library = Solargraph::Library.load(workspace_path) + library = described_class.load(workspace_path) result = library.create(file_path, File.read(file_path)) expect(result).to be(true) expect(library.open?(file_path)).to be(false) end end - it "returns a Completion" do - library = Solargraph::Library.new + it 'returns a Completion' do + library = described_class.new library.attach Solargraph::Source.load_string(%( x = 1 x @@ -26,14 +28,39 @@ expect(completion.pins.map(&:name)).to include('x') end + context 'with a require from a not-yet-cached external gem' do + before do + Solargraph::Shell.new.uncache('backport') + end + + it 'returns a Completion', time_limit_seconds: 50 do + library = described_class.new(Solargraph::Workspace.new(Dir.pwd, + Solargraph::Workspace::Config.new)) + library.attach Solargraph::Source.load_string(%( + require 'backport' + + # @param adapter [Backport::Adapter] + def foo(adapter) + adapter.remo + end + ), 'file.rb', 0) + # give Solargraph time to cache the gem + while (completion = library.completions_at('file.rb', 5, 19)).pins.empty? + sleep 0.25 + end + expect(completion).to be_a(Solargraph::SourceMap::Completion) + expect(completion.pins.map(&:name)).to include('remote') + end + end + context 'with a require from an already-cached external gem' do before do Solargraph::Shell.new.gems('backport') end - it "returns a Completion" do - library = Solargraph::Library.new(Solargraph::Workspace.new(Dir.pwd, - Solargraph::Workspace::Config.new)) + it 'returns a Completion' do + library = described_class.new(Solargraph::Workspace.new(Dir.pwd, + Solargraph::Workspace::Config.new)) library.attach Solargraph::Source.load_string(%( require 'backport' @@ -48,8 +75,8 @@ def foo(adapter) end end - it "gets definitions from a file" do - library = Solargraph::Library.new + it 'gets definitions from a file' do + library = described_class.new src = Solargraph::Source.load_string %( class Foo def bar @@ -61,8 +88,8 @@ def bar expect(paths).to include('Foo#bar') end - it "gets type definitions from a file" do - library = Solargraph::Library.new + it 'gets type definitions from a file' do + library = described_class.new src = Solargraph::Source.load_string %( class Bar; end class Foo @@ -77,8 +104,8 @@ def self.bar expect(paths).to include('Bar') end - it "signifies method arguments" do - library = Solargraph::Library.new + it 'signifies method arguments' do + library = described_class.new src = Solargraph::Source.load_string %( class Foo def bar baz, key: '' @@ -92,18 +119,18 @@ def bar baz, key: '' expect(pins.first.path).to eq('Foo#bar') end - it "ignores invalid filenames in create_from_disk" do - library = Solargraph::Library.new + it 'ignores invalid filenames in create_from_disk' do + library = described_class.new filename = 'not_a_real_file.rb' expect(library.create_from_disk(filename)).to be(false) expect(library.contain?(filename)).to be(false) end - it "adds mergeable files to the workspace in create_from_disk" do + it 'adds mergeable files to the workspace in create_from_disk' do Dir.mktmpdir do |temp_dir_path| # Ensure we resolve any symlinks to their real path workspace_path = File.realpath(temp_dir_path) - library = Solargraph::Library.load(workspace_path) + library = described_class.load(workspace_path) file_path = File.join(workspace_path, 'created.rb') File.write(file_path, "puts 'hello'") expect(library.create_from_disk(file_path)).to be(true) @@ -111,9 +138,9 @@ def bar baz, key: '' end end - it "ignores non-mergeable files in create_from_disk" do + it 'ignores non-mergeable files in create_from_disk' do Dir.mktmpdir do |dir| - library = Solargraph::Library.load(dir) + library = described_class.load(dir) filename = File.join(dir, 'created.txt') File.write(filename, "puts 'hello'") expect(library.create_from_disk(filename)).to be(false) @@ -121,8 +148,8 @@ def bar baz, key: '' end end - it "diagnoses files" do - library = Solargraph::Library.new + it 'diagnoses files' do + library = described_class.new src = Solargraph::Source.load_string(%( puts 'hello' ), 'file.rb', 0) @@ -137,7 +164,7 @@ def bar baz, key: '' config = instance_double(Solargraph::Workspace::Config) allow(config).to receive_messages(plugins: [], required: [], reporters: ['all!']) workspace = Solargraph::Workspace.new directory, config - library = Solargraph::Library.new workspace + library = described_class.new workspace src = Solargraph::Source.load_string(%( puts 'hello' ), 'file.rb', 0) @@ -146,8 +173,8 @@ def bar baz, key: '' expect(result.to_s).to include('rubocop') end - it "documents symbols" do - library = Solargraph::Library.new + it 'documents symbols' do + library = described_class.new src = Solargraph::Source.load_string(%( class Foo def bar @@ -161,10 +188,47 @@ def bar expect(pins.map(&:path)).to include('Foo#bar') end - it "collects references to an instance method symbol" do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - src1 = Solargraph::Source.load_string(%( + describe '#references_from' do + it 'collects references to a new method on a constant from assignment of Class.new' do + workspace = Solargraph::Workspace.new('*') + library = described_class.new(workspace) + src1 = Solargraph::Source.load_string(%( + Foo.new + ), 'file1.rb', 0) + library.merge src1 + src2 = Solargraph::Source.load_string(%( + Foo = Class.new + ), 'file2.rb', 0) + library.merge src2 + library.catalog + locs = library.references_from('file1.rb', 1, 12) + expect(locs.map { |l| [l.filename, l.range.start.line] }) + .to eq([['file1.rb', 1]]) + end + + it 'collects references to a new method to a constant from assignment' do + workspace = Solargraph::Workspace.new('*') + library = described_class.new(workspace) + src1 = Solargraph::Source.load_string(%( + Foo.new + ), 'file1.rb', 0) + library.merge src1 + src2 = Solargraph::Source.load_string(%( + class Foo + end + blah = Foo.new + ), 'file2.rb', 0) + library.merge src2 + library.catalog + locs = library.references_from('file2.rb', 3, 21) + expect(locs.map { |l| [l.filename, l.range.start.line] }) + .to eq([['file1.rb', 1], ['file2.rb', 3]]) + end + + it 'collects references to an instance method symbol' do + workspace = Solargraph::Workspace.new('*') + library = described_class.new(workspace) + src1 = Solargraph::Source.load_string(%( class Foo def bar end @@ -172,8 +236,8 @@ def bar Foo.new.bar ), 'file1.rb', 0) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( foo = Foo.new foo.bar class Other @@ -181,17 +245,17 @@ def bar; end end Other.new.bar ), 'file2.rb', 0) - library.merge src2 - library.catalog - locs = library.references_from('file2.rb', 2, 11) - expect(locs.length).to eq(3) - expect(locs.select{|l| l.filename == 'file2.rb' && l.range.start.line == 6}).to be_empty - end + library.merge src2 + library.catalog + locs = library.references_from('file2.rb', 2, 11) + expect(locs.length).to eq(3) + expect(locs.select { |l| l.filename == 'file2.rb' && l.range.start.line == 6 }).to be_empty + end - it "collects references to a class method symbol" do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - src1 = Solargraph::Source.load_string(%( + it 'collects references to a class method symbol' do + workspace = Solargraph::Workspace.new('*') + library = described_class.new(workspace) + src1 = Solargraph::Source.load_string(%( class Foo def self.bar end @@ -203,8 +267,8 @@ def bar Foo.bar Foo.new.bar ), 'file1.rb', 0) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( Foo.bar Foo.new.bar class Other @@ -214,48 +278,48 @@ def bar; end Other.bar Other.new.bar ), 'file2.rb', 0) - library.merge src2 - library.catalog - locs = library.references_from('file2.rb', 1, 11) - expect(locs.length).to eq(3) - expect(locs.select{|l| l.filename == 'file1.rb' && l.range.start.line == 2}).not_to be_empty - expect(locs.select{|l| l.filename == 'file1.rb' && l.range.start.line == 9}).not_to be_empty - expect(locs.select{|l| l.filename == 'file2.rb' && l.range.start.line == 1}).not_to be_empty - end + library.merge src2 + library.catalog + locs = library.references_from('file2.rb', 1, 11) + expect(locs.length).to eq(3) + expect(locs.select { |l| l.filename == 'file1.rb' && l.range.start.line == 2 }).not_to be_empty + expect(locs.select { |l| l.filename == 'file1.rb' && l.range.start.line == 9 }).not_to be_empty + expect(locs.select { |l| l.filename == 'file2.rb' && l.range.start.line == 1 }).not_to be_empty + end - it "collects stripped references to constant symbols" do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - src1 = Solargraph::Source.load_string(%( + it 'collects stripped references to constant symbols' do + workspace = Solargraph::Workspace.new('*') + library = described_class.new(workspace) + src1 = Solargraph::Source.load_string(%( class Foo def bar end end Foo.new.bar ), 'file1.rb', 0) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( class Other foo = Foo.new foo.bar end ), 'file2.rb', 0) - library.merge src2 - library.catalog - locs = library.references_from('file1.rb', 1, 12, strip: true) - expect(locs.length).to eq(3) - locs.each do |l| - code = library.read_text(l.filename) - o1 = Solargraph::Position.to_offset(code, l.range.start) - o2 = Solargraph::Position.to_offset(code, l.range.ending) - expect(code[o1..o2-1]).to eq('Foo') + library.merge src2 + library.catalog + locs = library.references_from('file1.rb', 1, 12, strip: true) + expect(locs.length).to eq(3) + locs.each do |l| + code = library.read_text(l.filename) + o1 = Solargraph::Position.to_offset(code, l.range.start) + o2 = Solargraph::Position.to_offset(code, l.range.ending) + expect(code[o1..(o2 - 1)]).to eq('Foo') + end end - end - it 'rejects new references from different classes' do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - source = Solargraph::Source.load_string(%( + it 'rejects new references from different classes' do + workspace = Solargraph::Workspace.new('*') + library = described_class.new(workspace) + source = Solargraph::Source.load_string(%( class Foo def bar end @@ -263,110 +327,135 @@ def bar Foo.new Array.new ), 'test.rb') - library.merge source - library.catalog - foo_new_locs = library.references_from('test.rb', 5, 10) - expect(foo_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 10, 5, 13))]) - obj_new_locs = library.references_from('test.rb', 6, 12) - expect(obj_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(6, 12, 6, 15))]) - end + library.merge source + library.catalog + foo_new_locs = library.references_from('test.rb', 5, 10) + expect(foo_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 10, 5, 13))]) + obj_new_locs = library.references_from('test.rb', 6, 12) + expect(obj_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(6, 12, 6, 15))]) + end - it "searches the core for queries" do - library = Solargraph::Library.new - result = library.search('String') - expect(result).not_to be_empty - end + it 'searches the core for queries' do + library = described_class.new + result = library.search('String') + expect(result).not_to be_empty + end - it "returns YARD documentation from the core" do - library = Solargraph::Library.new - api_map, result = library.document('String') - expect(result).not_to be_empty - expect(result.first).to be_a(Solargraph::Pin::Base) - end + it 'returns YARD documentation from the core' do + library = described_class.new + _, result = library.document('String') + expect(result).not_to be_empty + expect(result.first).to be_a(Solargraph::Pin::Base) + end - it "returns YARD documentation from sources" do - library = Solargraph::Library.new - src = Solargraph::Source.load_string(%( + it 'returns YARD documentation from sources' do + library = described_class.new + src = Solargraph::Source.load_string(%( class Foo # My bar method def bar; end end ), 'test.rb', 0) - library.attach src - api_map, result = library.document('Foo#bar') - expect(result).not_to be_empty - expect(result.first).to be_a(Solargraph::Pin::Base) - end + library.attach src + _, result = library.document('Foo#bar') + expect(result).not_to be_empty + expect(result.first).to be_a(Solargraph::Pin::Base) + end - it "synchronizes sources from updaters" do - library = Solargraph::Library.new - src = Solargraph::Source.load_string(%( + it 'synchronizes sources from updaters' do + library = described_class.new + src = Solargraph::Source.load_string(%( class Foo end ), 'test.rb', 1) - library.attach src - repl = %( + library.attach src + repl = %( class Foo def bar; end end ) - updater = Solargraph::Source::Updater.new( - 'test.rb', - 2, - [Solargraph::Source::Change.new(nil, repl)] - ) - library.attach src.synchronize(updater) - expect(library.current.code).to eq(repl) - end + updater = Solargraph::Source::Updater.new( + 'test.rb', + 2, + [Solargraph::Source::Change.new(nil, repl)] + ) + library.attach src.synchronize(updater) + expect(library.current.code).to eq(repl) + end - it "finds unique references" do - library = Solargraph::Library.new(Solargraph::Workspace.new('*')) - src1 = Solargraph::Source.load_string(%( + it 'finds unique references' do + library = described_class.new(Solargraph::Workspace.new('*')) + src1 = Solargraph::Source.load_string(%( class Foo end ), 'src1.rb', 1) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( foo = Foo.new ), 'src2.rb', 1) - library.merge src2 - library.catalog - refs = library.references_from('src2.rb', 1, 12) - expect(refs.length).to eq(2) - end + library.merge src2 + library.catalog + refs = library.references_from('src2.rb', 1, 12) + expect(refs.length).to eq(2) + end - it "includes method parameters in references" do - library = Solargraph::Library.new(Solargraph::Workspace.new('*')) - source = Solargraph::Source.load_string(%( + it 'includes method parameters in references' do + library = described_class.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( class Foo def bar(baz) baz.upcase end end ), 'test.rb', 1) - library.attach source - from_def = library.references_from('test.rb', 2, 16) - expect(from_def.length).to eq(2) - from_ref = library.references_from('test.rb', 3, 10) - expect(from_ref.length).to eq(2) - end + library.attach source + from_def = library.references_from('test.rb', 2, 16) + expect(from_def.length).to eq(2) + from_ref = library.references_from('test.rb', 3, 10) + expect(from_ref.length).to eq(2) + end - it "includes block parameters in references" do - library = Solargraph::Library.new(Solargraph::Workspace.new('*')) - source = Solargraph::Source.load_string(%( + it "lies about names when client can't handle the truth" do + library = described_class.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( + class Foo + def 🤦🏻foo♀️; 123; end + end + ), 'test.rb', 1) + library.attach source + from_def = library.references_from('test.rb', 2, 16, strip: true) + expect(from_def.first.range.start.column).to eq(14) + end + + it 'tells the truth about names when client can handle the truth' do + library = described_class.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( + class Foo + def 🤦🏻foo♀️; 123; end + end + ), 'test.rb', 1) + library.attach source + from_def = library.references_from('test.rb', 2, 16, strip: false) + expect(from_def.first.range.start.column).to eq(12) + end + + it 'includes block parameters in references' do + library = described_class.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( 100.times do |foo| puts foo end ), 'test.rb', 1) - library.attach source - from_def = library.references_from('test.rb', 1, 20) - expect(from_def.length).to eq(2) - from_ref = library.references_from('test.rb', 2, 13) - expect(from_ref.length).to eq(2) + library.attach source + from_def = library.references_from('test.rb', 1, 20) + expect(from_def.length).to eq(2) + from_ref = library.references_from('test.rb', 2, 13) + expect(from_ref.length).to eq(2) + end end it 'defines YARD tags' do - library = Solargraph::Library.new + library = described_class.new source = Solargraph::Source.load_string(%( class TaggedExample end @@ -388,7 +477,7 @@ def foo; end end it 'defines YARD tags with nested namespaces' do - library = Solargraph::Library.new + library = described_class.new source = Solargraph::Source.load_string(%( class Tagged class Example; end @@ -408,7 +497,7 @@ def foo; end end it 'defines generic YARD tags' do - library = Solargraph::Library.new + library = described_class.new source = Solargraph::Source.load_string(%( class TaggedExample; end class CallerExample @@ -422,7 +511,7 @@ def foo; end end it 'defines multiple YARD tags' do - library = Solargraph::Library.new + library = described_class.new source = Solargraph::Source.load_string(%( class TaggedExample; end class CallerExample @@ -436,7 +525,7 @@ def foo; end end it 'skips comment text outside of tags' do - library = Solargraph::Library.new + library = described_class.new source = Solargraph::Source.load_string(%( # String def foo; end @@ -447,7 +536,7 @@ def foo; end end it 'marks aliases as methods or attributes in completion items' do - library = Solargraph::Library.new + library = described_class.new source = Solargraph::Source.load_string(%( class Example attr_reader :foo @@ -470,7 +559,7 @@ def baz end it 'marks aliases as methods or attributes in definitions' do - library = Solargraph::Library.new + library = described_class.new source = Solargraph::Source.load_string(%( class Example attr_reader :foo @@ -489,7 +578,7 @@ def bar; end end it 'detaches current source with nil' do - library = Solargraph::Library.new + library = described_class.new source = Solargraph::Source.load_string(%( class Example attr_reader :foo @@ -507,7 +596,7 @@ def bar; end describe '#locate_ref' do it 'returns nil without a matching reference location' do workspace = File.absolute_path(File.join('spec', 'fixtures', 'workspace')) - library = Solargraph::Library.load(workspace) + library = described_class.load(workspace) library.map! location = Solargraph::Location.new(File.join(workspace, 'app.rb'), Solargraph::Range.from_to(0, 8, 0, 8)) found = library.locate_ref(location) @@ -518,7 +607,7 @@ def bar; end describe '#delete' do it 'removes files from Library#source_map_hash' do workspace = File.absolute_path(File.join('spec', 'fixtures', 'workspace')) - library = Solargraph::Library.load(workspace) + library = described_class.load(workspace) library.map! library.catalog other_file = File.absolute_path(File.join('spec', 'fixtures', 'workspace', 'lib', 'other.rb')) @@ -535,36 +624,36 @@ def bar; end end end - context 'unsynchronized' do - let(:library) { Solargraph::Library.load File.absolute_path(File.join('spec', 'fixtures', 'workspace')) } + context 'when unsynchronized' do + let(:library) { described_class.load File.absolute_path(File.join('spec', 'fixtures', 'workspace')) } let(:good_file) { File.join(library.workspace.directory, 'lib', 'thing.rb') } let(:bad_file) { File.join(library.workspace.directory, 'lib', 'not_a_thing.rb') } describe 'Library#completions_at' do it 'gracefully handles unmapped sources' do - expect { + expect do library.completions_at(good_file, 0, 0) - }.not_to raise_error + end.not_to raise_error end it 'raises errors for nonexistent sources' do - expect { + expect do library.completions_at(bad_file, 0, 0) - }.to raise_error(Solargraph::FileNotFoundError) + end.to raise_error(Solargraph::FileNotFoundError) end end describe 'Library#definitions_at' do it 'gracefully handles unmapped sources' do - expect { + expect do library.definitions_at(good_file, 0, 0) - }.not_to raise_error + end.not_to raise_error end it 'raises errors for nonexistent sources' do - expect { + expect do library.definitions_at(bad_file, 0, 0) - }.to raise_error(Solargraph::FileNotFoundError) + end.to raise_error(Solargraph::FileNotFoundError) end end end diff --git a/spec/logging_spec.rb b/spec/logging_spec.rb index eee59e606..7dcd52000 100644 --- a/spec/logging_spec.rb +++ b/spec/logging_spec.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + require 'tempfile' describe Solargraph::Logging do - it "logs messages with levels" do + it 'logs messages with levels' do file = Tempfile.new('log') - Solargraph::Logging.logger.reopen file - Solargraph::Logging.logger.warn "Test" + described_class.logger.reopen file + described_class.logger.warn 'Test' file.rewind msg = file.read file.close file.unlink - Solargraph::Logging.logger.reopen STDERR + described_class.logger.reopen File::NULL expect(msg).to include('WARN') end end diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index bf747fc76..cee6afef1 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -3,7 +3,7 @@ # @todo These tests depend on `Clip`, but we're putting the tests here to # avoid overloading clip_spec.rb. describe Solargraph::Parser::FlowSensitiveTyping do - it 'uses is_a? in a simple if() to refine types on a simple class' do + it 'uses is_a? in a simple if() to refine types' do source = Solargraph::Source.load_string(%( class ReproBase; end class Repro < ReproBase; end @@ -24,6 +24,28 @@ def verify_repro(repr) expect(clip.infer.to_s).to eq('ReproBase') end + it 'uses is_a? in a simple if() with a union to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro1 < ReproBase; end + class Repro2 < ReproBase; end + # @param repr [Repro1, Repro2] + def verify_repro(repr) + if repr.is_a?(Repro1) + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 10]) + expect(clip.infer.to_s).to eq('Repro1') + + clip = api_map.clip_at('test.rb', [9, 10]) + expect(clip.infer.to_s).to eq('Repro2') + end + it 'uses is_a? in a simple if() to refine types on a module-scoped class' do source = Solargraph::Source.load_string(%( class ReproBase; end @@ -72,7 +94,7 @@ def verify_repro(repr) expect(clip.infer.to_s).to eq('ReproBase') end - it 'uses is_a? in a simple unless statement to refine types on a simple class' do + it 'uses is_a? in a simple unless statement to refine types' do source = Solargraph::Source.load_string(%( class ReproBase; end class Repro < ReproBase; end @@ -201,6 +223,7 @@ class Repro < ReproBase; end value end ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [3, 6]) expect(clip.infer.to_s).to eq('Array') @@ -212,6 +235,65 @@ class Repro < ReproBase; end expect(clip.infer.to_s).to eq('Float') end + it 'uses varname in a simple if()' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr + repr + else + repr + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses varname in a "break unless" statement in a while to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + # @type [ReproBase, nil] + value = bar + while !is_done() + break unless value + value + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 8]) + expect(clip.infer.to_s).to eq('ReproBase') + end + + it 'uses varname in a "break if" statement in a while to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + # @type [ReproBase, nil] + value = bar + while !is_done() + break if value.nil? + value + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 8]) + expect(clip.infer.to_s).to eq('ReproBase') + end + it 'understands compatible reassignments' do source = Solargraph::Source.load_string(%( class Foo @@ -223,6 +305,7 @@ def baz; end bar = Foo.new bar ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [6, 6]) expect(clip.infer.to_s).to eq('Foo') @@ -253,4 +336,692 @@ def baz; end clip = api_map.clip_at('test.rb', [3, 6]) expect { clip.infer.to_s }.not_to raise_error end + + it 'uses nil? in a simple if() to refine nilness' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + def verify_repro(repr) + repr = 10 if floop + repr + if repr.nil? + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + end + + it 'uses nil? and && in a simple if() to refine nilness - nil? first' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr.nil? && throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses nil? and && in a simple if() to refine nilness - nil? second' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice && repr.nil? + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses nil? and || in a simple if() - nil? first' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr.nil? || throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + end + + it 'uses nil? and || in a simple if() - nil? second' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice || repr.nil? + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + end + + it 'uses varname and || in a simple if() - varname first' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr || throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses varname and || in a simple if() - varname second' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice || repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses .nil? and or in an unless' do + source = Solargraph::Source.load_string(%( + # @param repr [String, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr) + repr unless repr.nil? || repr.downcase + repr + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 33]) + expect(clip.infer.rooted_tags).to eq('::String') + + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::String') + + clip = api_map.clip_at('test.rb', [5, 8]) + expect(clip.infer.rooted_tags).to eq('::String, nil') + end + + it 'uses varname and && in a simple if() - varname first' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr && throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses varname and && in a simple if() - varname second' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice && repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses variable in a simple if() to refine types' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + def verify_repro(repr) + repr = 10 if floop + repr + if repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses variable in a simple if() to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + def verify_repro(repr = nil) + repr = 10 if floop + repr + if repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [3, 8]) + expect(clip.infer.rooted_tags).to eq('nil, 10') + + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('10') + + clip = api_map.clip_at('test.rb', [7, 10]) + expect(clip.infer.rooted_tags).to eq('nil, false') + end + + it 'uses .nil? in a return if() in an if to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + if rand + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + # https://cse.buffalo.edu/~regan/cse305/RubyBNF.pdf + # https://ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html + it 'uses .nil? in a return if() in a method to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + return if baz.nil? + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + it 'uses .nil? in a return if() in a block to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @param arr [Array] + # @return [void] + def bar(arr, baz: nil) + baz + arr.each do |item| + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [9, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [11, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in an unless to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + unless rand + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a while to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + while rand do + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [9, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses foo in a a while to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @param other [::Boolean, nil] + # @return [void] + def bar(baz: nil, other: nil) + baz + while baz do + baz + baz = other + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [11, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in an until to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + until rand do + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [9, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a switch/case/else to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + case rand + when 0..0.5 + return if baz.nil? + baz + else + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [12, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a ternary operator to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + rand > 0.5 ? (return if baz.nil?; baz) : baz + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [6, 44]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [6, 51]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [7, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a begin/end to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + begin + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + it 'uses .nil? in a return if() in a ||= to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + baz ||= begin + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + it 'uses .nil? in a return if() in a try / rescue / ensure to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + begin + return if baz.nil? + baz + rescue StandardError + baz + ensure + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + pending('better scoping of return if in begin/rescue/ensure') + + clip = api_map.clip_at('test.rb', [12, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [14, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'provides a useful pin after a return if .nil?' do + source = Solargraph::Source.load_string(%( + class A + # @param b [Hash{String => String}] + # @return [void] + def a b + c = b["123"] + c + return c if c.nil? + c + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.to_s).to eq('String') + + clip = api_map.clip_at('test.rb', [7, 17]) + expect(clip.infer.to_s).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.to_s).to eq('String') + end + + it 'uses ! to detect nilness' do + source = Solargraph::Source.load_string(%( + class A + # @param a [Integer, nil] + # @return [Integer] + def foo a + return a unless !a + 123 + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 17]) + expect(clip.infer.to_s).to eq('Integer') + end + + it 'supports !@x.nil && @x.y' do + source = Solargraph::Source.load_string(%( + class Bar + # @param foo [String, nil] + def initialize(foo) + @foo = foo + end + + def foo? + out = !@foo.nil? && @foo.upcase == 'FOO' + out + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [9, 10]) + expect(clip.infer.to_s).to eq('Boolean') + end + + it 'uses is_a? with instance variables to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + class Example + # @param value [ReproBase] + def initialize(value) + @value = value + end + + def check + if @value.is_a?(Repro) + @value + else + @value + end + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [11, 12]) + expect(clip.infer.to_s).to eq('Repro') + + clip = api_map.clip_at('test.rb', [13, 12]) + expect(clip.infer.to_s).to eq('ReproBase') + end end diff --git a/spec/parser/node_chainer_spec.rb b/spec/parser/node_chainer_spec.rb index e92431aae..8f24eb335 100644 --- a/spec/parser/node_chainer_spec.rb +++ b/spec/parser/node_chainer_spec.rb @@ -1,55 +1,61 @@ +# frozen_string_literal: true + describe 'NodeChainer' do - it "recognizes self keywords" do - chain = Solargraph::Parser.chain_string('self.foo') + def chain_string str + Solargraph::Parser.chain_string(str, 'file.rb', 0) + end + + it 'recognizes self keywords' do + chain = chain_string('self.foo') expect(chain.links.first.word).to eq('self') expect(chain.links.first).to be_a(Solargraph::Source::Chain::Head) end - it "recognizes super keywords" do - chain = Solargraph::Parser.chain_string('super.foo') + it 'recognizes super keywords' do + chain = chain_string('super.foo') expect(chain.links.first.word).to eq('super') expect(chain.links.first).to be_a(Solargraph::Source::Chain::ZSuper) end - it "recognizes constants" do - chain = Solargraph::Parser.chain_string('Foo::Bar') + it 'recognizes constants' do + chain = chain_string('Foo::Bar') expect(chain.links.length).to eq(1) expect(chain.links.first).to be_a(Solargraph::Source::Chain::Constant) expect(chain.links.map(&:word)).to eq(['Foo::Bar']) end - it "splits method calls with arguments and blocks" do - chain = Solargraph::Parser.chain_string('var.meth1(1, 2).meth2 do; end') - expect(chain.links.map(&:word)).to eq(['var', 'meth1', 'meth2']) + it 'splits method calls with arguments and blocks' do + chain = chain_string('var.meth1(1, 2).meth2 do; end') + expect(chain.links.map(&:word)).to eq(%w[var meth1 meth2]) end - it "recognizes literals" do - chain = Solargraph::Parser.chain_string('"string"') + it 'recognizes literals' do + chain = chain_string('"string"') expect(chain).to be_literal - chain = Solargraph::Parser.chain_string('100') + chain = chain_string('100') expect(chain).to be_literal - chain = Solargraph::Parser.chain_string('[1, 2, 3]') + chain = chain_string('[1, 2, 3]') expect(chain).to be_literal - chain = Solargraph::Parser.chain_string('{ foo: "bar" }') + chain = chain_string('{ foo: "bar" }') expect(chain).to be_literal end - it "recognizes instance variables" do - chain = Solargraph::Parser.chain_string('@foo') + it 'recognizes instance variables' do + chain = chain_string('@foo') expect(chain.links.first).to be_a(Solargraph::Source::Chain::InstanceVariable) end - it "recognizes class variables" do - chain = Solargraph::Parser.chain_string('@@foo') + it 'recognizes class variables' do + chain = chain_string('@@foo') expect(chain.links.first).to be_a(Solargraph::Source::Chain::ClassVariable) end - it "recognizes global variables" do - chain = Solargraph::Parser.chain_string('$foo') + it 'recognizes global variables' do + chain = chain_string('$foo') expect(chain.links.first).to be_a(Solargraph::Source::Chain::GlobalVariable) end - it "operates on nodes" do + it 'operates on nodes' do source = Solargraph::Source.load_string(%( class Foo Bar.meth1(1, 2).meth2{} @@ -57,7 +63,7 @@ class Foo )) node = source.node_at(2, 26) chain = Solargraph::Parser.chain(node) - expect(chain.links.map(&:word)).to eq(['Bar', 'meth1', 'meth2']) + expect(chain.links.map(&:word)).to eq(%w[Bar meth1 meth2]) end it 'chains and/or nodes' do @@ -141,7 +147,9 @@ class Foo expect(chain.links.first).to be_with_block end - xit 'tracks complex multiple assignment' do + it 'tracks complex multiple assignment' do + pending('complex multiple assignment support') + source = Solargraph::Source.load_string(%( foo.baz, bar = [1, 2] )) diff --git a/spec/parser/node_methods_spec.rb b/spec/parser/node_methods_spec.rb index f9504b584..1ead0a6b6 100644 --- a/spec/parser/node_methods_spec.rb +++ b/spec/parser/node_methods_spec.rb @@ -1,96 +1,104 @@ +# frozen_string_literal: true + # These tests are deliberately generic because they apply to both the Legacy # and Rubyvm node methods. describe Solargraph::Parser::NodeMethods do - it "unpacks constant nodes into strings" do - ast = Solargraph::Parser.parse("Foo::Bar") - expect(Solargraph::Parser::NodeMethods.unpack_name(ast)).to eq "Foo::Bar" + def parse source + Solargraph::Parser.parse(source, 'test.rb', 0) + end + + it 'unpacks constant nodes into strings' do + ast = parse('Foo::Bar') + expect(described_class.unpack_name(ast)).to eq 'Foo::Bar' end - it "infers literal strings" do - ast = Solargraph::Parser.parse("x = 'string'") - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast.children[1])).to eq '::String' + it 'infers literal strings' do + ast = parse("x = 'string'") + expect(described_class.infer_literal_node_type(ast.children[1])).to eq '::String' end - it "infers literal hashes" do - ast = Solargraph::Parser.parse("x = {}") - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast.children[1])).to eq '::Hash' + it 'infers literal hashes' do + ast = parse('x = {}') + expect(described_class.infer_literal_node_type(ast.children[1])).to eq '::Hash' end - it "infers literal arrays" do - ast = Solargraph::Parser.parse("x = []") - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast.children[1])).to eq '::Array' + it 'infers literal arrays' do + ast = parse('x = []') + expect(described_class.infer_literal_node_type(ast.children[1])).to eq '::Array' end - it "infers literal integers" do - ast = Solargraph::Parser.parse("x = 100") - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast.children[1])).to eq '::Integer' + it 'infers literal integers' do + ast = parse('x = 100') + expect(described_class.infer_literal_node_type(ast.children[1])).to eq '::Integer' end - it "infers literal floats" do - ast = Solargraph::Parser.parse("x = 10.1") - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast.children[1])).to eq '::Float' + it 'infers literal floats' do + ast = parse('x = 10.1') + expect(described_class.infer_literal_node_type(ast.children[1])).to eq '::Float' end - it "infers literal symbols" do - ast = Solargraph::Parser.parse(":symbol") - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast)).to eq '::Symbol' + it 'infers literal symbols' do + ast = parse(':symbol') + expect(described_class.infer_literal_node_type(ast)).to eq '::Symbol' end - it "infers double quoted symbols" do - ast = Solargraph::Parser.parse(':"symbol"') - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast)).to eq '::Symbol' + it 'infers double quoted symbols' do + ast = parse(':"symbol"') + expect(described_class.infer_literal_node_type(ast)).to eq '::Symbol' end - it "infers interpolated double quoted symbols" do - ast = Solargraph::Parser.parse(':"#{Object}"') - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast)).to eq '::Symbol' + it 'infers interpolated double quoted symbols' do + ast = parse(%(:"#{Object}")) + expect(described_class.infer_literal_node_type(ast)).to eq '::Symbol' end - it "infers single quoted symbols" do - ast = Solargraph::Parser.parse(":'symbol'") - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast)).to eq '::Symbol' + it 'infers single quoted symbols' do + ast = parse(":'symbol'") + expect(described_class.infer_literal_node_type(ast)).to eq '::Symbol' end it 'infers literal booleans' do - true_ast = Solargraph::Parser.parse("true") - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(true_ast)).to eq '::Boolean' - false_ast = Solargraph::Parser.parse("false") - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(false_ast)).to eq '::Boolean' + true_ast = parse('true') + expect(described_class.infer_literal_node_type(true_ast)).to eq '::Boolean' + false_ast = parse('false') + expect(described_class.infer_literal_node_type(false_ast)).to eq '::Boolean' end - it "handles return nodes with implicit nil values" do - node = Solargraph::Parser.parse(%( + it 'handles empty return nodes with implicit nil values' do + node = parse(%( return if true )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + rets = described_class.returns_from_method_body(node) # @todo Should there be two returns, the second being nil? expect(rets.map(&:to_s)).to eq(['(nil)', '(nil)']) + # The expectation is changing from previous versions. If conditions + # have an implicit else branch, so this node should return [nil, nil]. expect(rets.length).to eq(2) end - it "handles return nodes with implicit nil values" do - node = Solargraph::Parser.parse(%( + it 'handles local return nodes with implicit nil values' do + node = parse(%( return bla if true )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + rets = described_class.returns_from_method_body(node) # Two returns, the second being implicit nil expect(rets.length).to eq(2) end - it 'handles return nodes from case statements' do - node = Solargraph::Parser.parse(%( + it 'handles boolean return nodes from case statements without else' do + node = parse(%( case x when 100 true end )) - returns = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + returns = described_class.returns_from_method_body(node) # Include an implicit `nil` for missing else expect(returns.map(&:to_s)).to eq(['(true)', '(nil)']) end it 'handles return nodes from case statements with else' do - node = Solargraph::Parser.parse(%( + node = parse(%( case x when 100, 125 true @@ -108,13 +116,13 @@ end end )) - returns = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + returns = described_class.returns_from_method_body(node) expect(returns.length).to eq(6) expect(returns.map(&:to_s)).to eq(['(true)', '(int 73)', '(false)', '(nil)', '(false)', '(true)']) end it 'handles return nodes from case statements with boolean conditions' do - node = Solargraph::Parser.parse(%( + node = parse(%( case true when x true @@ -122,114 +130,70 @@ false end )) - returns = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + returns = described_class.returns_from_method_body(node) expect(returns.length).to eq(2) end - it "handles return nodes in reduceable (begin) nodes" do - # @todo Temporarily disabled. Result is 3 nodes instead of 2. - # node = Solargraph::Parser.parse(%( - # begin - # return if true - # end - # )) - # rets = Solargraph::Parser::NodeMethods.returns_from(node) - # expect(rets.length).to eq(2) - end - - it "handles return nodes after other nodes" do - node = Solargraph::Parser.parse(%( - x = 1 - return x - )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.length).to eq(1) - end + it 'handles return nodes in reduceable (begin) nodes' do + pending('Temporarily disabled. Result is 3 nodes instead of 2.') - it "handles return nodes with unreachable code" do - node = Solargraph::Parser.parse(%( - x = 1 - return x - y + node = parse(%( + begin + return if true + end )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.length).to eq(1) + rets = described_class.returns_from_method_body(node) + expect(rets.length).to eq(2) end - it "handles conditional returns with following code" do - node = Solargraph::Parser.parse(%( + it 'handles conditional returns with following code' do + node = parse(%( x = 1 return x if foo y )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.length).to eq(2) - end - - it "handles return nodes with reduceable code" do - node = Solargraph::Parser.parse(%( - return begin - x if foo - y - end - )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.length).to eq(1) - end - - it "handles top 'and' nodes" do - node = Solargraph::Parser.parse('1 && "2"') - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.length).to eq(1) - expect(rets[0].type.to_s.downcase).to eq('and') - end - - it "handles top 'or' nodes" do - node = Solargraph::Parser.parse('1 || "2"') - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + rets = described_class.returns_from_method_body(node) expect(rets.length).to eq(2) - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(rets[0])).to eq('::Integer') - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(rets[1])).to eq('::String') end it "handles nested 'and' nodes" do - node = Solargraph::Parser.parse('return 1 && "2"') - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + node = parse('return 1 && "2"') + rets = described_class.returns_from_method_body(node) expect(rets.length).to eq(1) expect(rets[0].type.to_s.downcase).to eq('and') end it "handles nested 'or' nodes" do - node = Solargraph::Parser.parse('return 1 || "2"') - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + node = parse('return 1 || "2"') + rets = described_class.returns_from_method_body(node) expect(rets.length).to eq(2) - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(rets[0])).to eq('::Integer') - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(rets[1])).to eq('::String') + expect(described_class.infer_literal_node_type(rets[0])).to eq('::Integer') + expect(described_class.infer_literal_node_type(rets[1])).to eq('::String') end it 'finds return nodes in blocks' do - node = Solargraph::Parser.parse(%( + node = parse(%( array.each do |item| return item if foo end )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:block, :lvar]) + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq(%i[block lvar]) end it 'finds correct return node line in begin expressions' do - node = Solargraph::Parser.parse(%( + node = parse(%( begin 123 '123' end )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + rets = described_class.returns_from_method_body(node) expect(rets.map(&:type)).to eq([:str]) end it 'returns nested return blocks' do - node = Solargraph::Parser.parse(%( + node = parse(%( if foo array.each do |item| return item if foo @@ -237,172 +201,128 @@ end nil )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:lvar, :nil]) - end - - it "handles return nodes with implicit nil values" do - node = Solargraph::Parser.parse(%( - return if true - )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - # The expectation is changing from previous versions. If conditions - # have an implicit else branch, so this node should return [nil, nil]. - expect(rets.length).to eq(2) + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq(%i[lvar nil]) end - it "handles return nodes with implicit nil values" do - node = Solargraph::Parser.parse(%( + it 'handles return nodes with implicit nil values' do + node = parse(%( return bla if true )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:send, :nil]) + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq(%i[send nil]) end - it "handles return nodes in reduceable (begin) nodes" do - # @todo Temporarily disabled. Result is 3 nodes instead of 2 in legacy. - # node = Solargraph::Parser.parse(%( - # begin - # return if true - # end - # )) - # rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - # expect(rets.length).to eq(2) - end - - it "handles return nodes after other nodes" do - node = Solargraph::Parser.parse(%( + it 'handles return nodes after other nodes' do + node = parse(%( x = 1 return x )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + rets = described_class.returns_from_method_body(node) expect(rets.map(&:type)).to eq([:lvar]) end - it "handles return nodes with unreachable code" do - node = Solargraph::Parser.parse(%( + it 'handles return nodes with unreachable code' do + node = parse(%( x = 1 return x y )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + rets = described_class.returns_from_method_body(node) expect(rets.length).to eq(1) end - xit "short-circuits return node finding after a raise statement in a begin expressiona" do - node = Solargraph::Parser.parse(%( + it 'short-circuits return node finding after a raise statement in a begin expression' do + pending('case being handled') + + node = parse(%( raise "Error" y )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + rets = described_class.returns_from_method_body(node) expect(rets.length).to eq(0) end - it "does not short circuit return node finding after a raise statement in a conditional" do - node = Solargraph::Parser.parse(%( + it 'does not short circuit return node finding after a raise statement in a conditional' do + node = parse(%( x = 1 raise "Error" if foo y )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + rets = described_class.returns_from_method_body(node) expect(rets.length).to eq(1) end - it "does not short circuit return node finding after a return statement in a conditional" do - node = Solargraph::Parser.parse(%( + it 'does not short circuit return node finding after a return statement in a conditional' do + node = parse(%( x = 1 return "Error" if foo y )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + rets = described_class.returns_from_method_body(node) expect(rets.length).to eq(2) end - it "handles return nodes with reduceable code" do - node = Solargraph::Parser.parse(%( + it 'handles return nodes with reduceable code' do + node = parse(%( return begin x if foo y end )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + rets = described_class.returns_from_method_body(node) expect(rets.length).to eq(1) end it "handles top 'and' nodes" do - node = Solargraph::Parser.parse('1 && "2"') - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + node = parse('1 && "2"') + rets = described_class.returns_from_method_body(node) expect(rets.map(&:type)).to eq([:and]) end it "handles top 'or' nodes" do - node = Solargraph::Parser.parse('1 || "2"') - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:int, :str]) + node = parse('1 || "2"') + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq([:or]) end it "handles nested 'and' nodes from return" do - node = Solargraph::Parser.parse('return 1 && "2"') - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) + node = parse('return 1 && "2"') + rets = described_class.returns_from_method_body(node) expect(rets.map(&:type)).to eq([:and]) end it "handles nested 'or' nodes from return" do - node = Solargraph::Parser.parse('return 1 || "2"') - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:int, :str]) - end - - it 'finds return nodes in blocks' do - node = Solargraph::Parser.parse(%( - array.each do |item| - return item if foo - end - )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:block, :lvar]) - # expect(rets[1].type).to eq(:DVAR) - end - - it 'returns nested return blocks' do - node = Solargraph::Parser.parse(%( - if foo - array.each do |item| - return item if foo - end - end - nil - )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:lvar, :nil]) - # expect(rets[0].type).to eq(:DVAR) + node = parse('return 1 || "2"') + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq(%i[int str]) end it 'handles return nodes from case statements' do - node = Solargraph::Parser.parse(%( + node = parse(%( case 1 when 1 then "" else "" end )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:str, :str]) + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq(%i[str str]) end - it 'handles return nodes from case statements without else' do - node = Solargraph::Parser.parse(%( + it 'handles String return nodes from case statements without else' do + node = parse(%( case 1 when 1 "" end )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:str, :nil]) + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq(%i[str nil]) end it 'handles return nodes from case statements with super' do - node = Solargraph::Parser.parse(%( + node = parse(%( case other when Docstring Docstring.new([all, other.all].join("\n"), object) @@ -410,20 +330,20 @@ super end )) - rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:send, :zsuper]) + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq(%i[send zsuper]) end describe 'convert_hash' do it 'converts literal hash arguments' do - node = Solargraph::Parser.parse('{foo: :bar}') - hash = Solargraph::Parser::NodeMethods.convert_hash(node) + node = parse('{foo: :bar}') + hash = described_class.convert_hash(node) expect(hash.keys).to eq([:foo]) end it 'ignores call arguments' do - node = Solargraph::Parser.parse('some_call') - hash = Solargraph::Parser::NodeMethods.convert_hash(node) + node = parse('some_call') + hash = described_class.convert_hash(node) expect(hash).to eq({}) end end @@ -437,7 +357,7 @@ def super_with_block end end )) - calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + calls = described_class.call_nodes_from(source.node) expect(calls).to be_one end @@ -445,7 +365,7 @@ def super_with_block source = Solargraph::Source.load_string(%( Foo.new.bar('string') )) - calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + calls = described_class.call_nodes_from(source.node) expect(calls.length).to eq(2) end @@ -453,7 +373,7 @@ def super_with_block source = Solargraph::Source.load_string(%( [ Foo.new.bar('string') ] )) - calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + calls = described_class.call_nodes_from(source.node) expect(calls.length).to eq(2) end @@ -461,7 +381,7 @@ def super_with_block source = Solargraph::Source.load_string(%( [ Foo.new.bar('string') ].compact )) - calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + calls = described_class.call_nodes_from(source.node) expect(calls.length).to eq(3) end @@ -477,7 +397,7 @@ def something end end )) - calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + calls = described_class.call_nodes_from(source.node) expect(calls.length).to eq(2) end end diff --git a/spec/parser/node_processor_spec.rb b/spec/parser/node_processor_spec.rb index 5b8d7cd40..da7031779 100644 --- a/spec/parser/node_processor_spec.rb +++ b/spec/parser/node_processor_spec.rb @@ -1,32 +1,38 @@ +# frozen_string_literal: true + describe Solargraph::Parser::NodeProcessor do + def parse source + Solargraph::Parser.parse(source, 'file.rb', 0) + end + it 'ignores bare private_constant calls' do - node = Solargraph::Parser.parse(%( + node = parse(%( class Foo private_constant end )) - expect { - Solargraph::Parser::NodeProcessor.process(node) - }.not_to raise_error + expect do + described_class.process(node) + end.not_to raise_error end it 'orders optional args correctly' do - node = Solargraph::Parser.parse(%( + node = parse(%( def foo(bar = nil, baz = nil); end )) - pins, = Solargraph::Parser::NodeProcessor.process(node) + pins, = described_class.process(node) # Method pin is first pin after default namespace pin = pins[1] expect(pin.parameters.map(&:name)).to eq(%w[bar baz]) end it 'understands +=' do - node = Solargraph::Parser.parse(%( + node = parse(%( detail = '' detail += "foo" detail.strip! )) - _, vars = Solargraph::Parser::NodeProcessor.process(node) + _, vars = described_class.process(node) # ensure we parsed the += correctly and won't report an unexpected # nil assignment @@ -51,17 +57,17 @@ def process end end - Solargraph::Parser::NodeProcessor.register(:def, dummy_processor1) - Solargraph::Parser::NodeProcessor.register(:def, dummy_processor2) - node = Solargraph::Parser.parse(%( + described_class.register(:def, dummy_processor1) + described_class.register(:def, dummy_processor2) + node = parse(%( def some_method; end )) - pins, = Solargraph::Parser::NodeProcessor.process(node) + pins, = described_class.process(node) # empty namespace pin is root namespace expect(pins.map(&:name)).to contain_exactly('', 'foo', 'bar', 'some_method') # Clean up the registered processors - Solargraph::Parser::NodeProcessor.deregister(:def, dummy_processor1) - Solargraph::Parser::NodeProcessor.deregister(:def, dummy_processor2) + described_class.deregister(:def, dummy_processor1) + described_class.deregister(:def, dummy_processor2) end end diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb index 267f412f4..62fe7d955 100644 --- a/spec/parser_spec.rb +++ b/spec/parser_spec.rb @@ -1,11 +1,17 @@ +# frozen_string_literal: true + describe Solargraph::Parser do - it "parses nodes" do - node = Solargraph::Parser.parse('class Foo; end', 'test.rb') - expect(Solargraph::Parser.is_ast_node?(node)).to be(true) + def parse source + Solargraph::Parser.parse(source, 'file.rb', 0) + end + + it 'parses nodes' do + node = parse('class Foo; end') + expect(described_class.is_ast_node?(node)).to be(true) end it 'raises repairable SyntaxError for unknown encoding errors' do code = "# encoding: utf-\nx = 'y'" - expect { Solargraph::Parser.parse(code) }.to raise_error(Solargraph::Parser::SyntaxError) + expect { parse(code) }.to raise_error(Solargraph::Parser::SyntaxError) end end diff --git a/spec/pin/base_spec.rb b/spec/pin/base_spec.rb index 1a6cfd1e8..4d4315040 100644 --- a/spec/pin/base_spec.rb +++ b/spec/pin/base_spec.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + describe Solargraph::Pin::Base do let(:zero_location) { Solargraph::Location.new('test.rb', Solargraph::Range.from_to(0, 0, 0, 0)) } let(:one_location) { Solargraph::Location.new('test.rb', Solargraph::Range.from_to(0, 0, 1, 0)) } - it "will not combine pins with directive changes" do - pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: 'A Foo class', - source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) - pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro', - source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) + it 'does not combine pins with directive changes' do + pin1 = described_class.new(location: zero_location, name: 'Foo', comments: 'A Foo class', + source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) + pin2 = described_class.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro', + source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) expect(pin1.nearly?(pin2)).to be(false) # enable asserts with_env_var('SOLARGRAPH_ASSERTS', 'on') do @@ -14,38 +16,38 @@ end end - it "will not combine pins with different directives" do - pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro', - source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) - pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro other', - source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) + it 'does not combine pins with different directives' do + pin1 = described_class.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro', + source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) + pin2 = described_class.new(location: zero_location, name: 'Foo', comments: '@!macro other', + source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) expect(pin1.nearly?(pin2)).to be(false) with_env_var('SOLARGRAPH_ASSERTS', 'on') do expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :macros values/) end end - it "sees tag differences as not near or equal" do - pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@return [Foo]') - pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@return [Bar]') + it 'sees tag differences as not near or equal' do + pin1 = described_class.new(location: zero_location, name: 'Foo', comments: '@return [Foo]') + pin2 = described_class.new(location: zero_location, name: 'Foo', comments: '@return [Bar]') expect(pin1.nearly?(pin2)).to be(false) expect(pin1 == pin2).to be(false) end - it "sees comment differences as nearly but not equal" do - pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: 'A Foo class') - pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: 'A different Foo') + it 'sees comment differences as nearly but not equal' do + pin1 = described_class.new(location: zero_location, name: 'Foo', comments: 'A Foo class') + pin2 = described_class.new(location: zero_location, name: 'Foo', comments: 'A different Foo') expect(pin1.nearly?(pin2)).to be(true) expect(pin1 == pin2).to be(false) end - it "recognizes deprecated tags" do - pin = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@deprecated Use Bar instead.') + it 'recognizes deprecated tags' do + pin = described_class.new(location: zero_location, name: 'Foo', comments: '@deprecated Use Bar instead.') expect(pin).to be_deprecated end - it "does not link documentation for undefined return types" do - pin = Solargraph::Pin::Base.new(name: 'Foo', comments: '@return [undefined]') + it 'does not link documentation for undefined return types' do + pin = described_class.new(name: 'Foo', comments: '@return [undefined]') expect(pin.link_documentation).to eq('Foo') end @@ -56,8 +58,8 @@ expect(pins.length).to eq(1) parser_method_pin = pins.first return_type = parser_method_pin.typify(api_map) - expect(parser_method_pin.closure.name).to eq("Docstring") - expect(parser_method_pin.closure.gates).to eq(["YARD::Docstring", "YARD", '']) + expect(parser_method_pin.closure.name).to eq('Docstring') + expect(parser_method_pin.closure.gates).to eq(['YARD::Docstring', 'YARD', '']) expect(return_type).to be_defined expect(parser_method_pin.typify(api_map).rooted_tags).to eq('::YARD::DocstringParser') end diff --git a/spec/pin/base_variable_spec.rb b/spec/pin/base_variable_spec.rb index 8c462bff3..9ca63f3dd 100644 --- a/spec/pin/base_variable_spec.rb +++ b/spec/pin/base_variable_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe Solargraph::Pin::BaseVariable do - it "checks assignments for equality" do + it 'checks assignments for equality' do smap = Solargraph::SourceMap.load_string('foo = "foo"') pin1 = smap.locals.first smap = Solargraph::SourceMap.load_string('foo = "foo"') @@ -44,4 +46,19 @@ def bar expect(type.to_rbs).to eq('(1 | nil)') expect(type.simplify_literals.to_rbs).to eq('(::Integer | ::NilClass)') end + + it "understands proc kwarg parameters aren't affected by @type" do + code = %( + # @return [Proc] + def foo + # @type [Proc] + # @param layout [Boolean] + @render_method = proc { |layout = false| + 123 if layout + } + end + ) + checker = Solargraph::TypeChecker.load_string(code, 'test.rb', :alpha) + expect(checker.problems.map(&:message)).to eq([]) + end end diff --git a/spec/pin/block_spec.rb b/spec/pin/block_spec.rb index 0ca36e950..7d7d1b028 100644 --- a/spec/pin/block_spec.rb +++ b/spec/pin/block_spec.rb @@ -1,7 +1,12 @@ +# frozen_string_literal: true + describe Solargraph::Pin::Block do + let(:foo) { instance_double(Solargraph::Pin::Parameter, name: 'foo') } + let(:bar) { instance_double(Solargraph::Pin::Parameter, name: 'bar') } + let(:block) { instance_double(Solargraph::Pin::Parameter, name: 'block') } + it 'strips prefixes from parameter names' do - # @todo Method parameters are pins now - # pin = Solargraph::Pin::Block.new(args: ['foo', '*bar', '&block']) - # expect(pin.parameter_names).to eq(['foo', 'bar', 'block']) + pin = described_class.new(args: [foo, bar, block]) + expect(pin.parameter_names).to eq(%w[foo bar block]) end end diff --git a/spec/pin/class_variable_spec.rb b/spec/pin/class_variable_spec.rb index c327f01a1..686b13177 100644 --- a/spec/pin/class_variable_spec.rb +++ b/spec/pin/class_variable_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # describe Solargraph::Pin::ClassVariable do # it "always has class scope" do # source = Solargraph::Source.load_string(%( diff --git a/spec/pin/combine_with_spec.rb b/spec/pin/combine_with_spec.rb index 38d45a3e1..cc80d76d5 100644 --- a/spec/pin/combine_with_spec.rb +++ b/spec/pin/combine_with_spec.rb @@ -9,7 +9,6 @@ end it 'combines return types with another method without type parameters' do - pending('logic being added to handle this case') pin1 = Solargraph::Pin::Method.new(name: 'foo', parameters: [], comments: '@return [Array]') pin2 = Solargraph::Pin::Method.new(name: 'foo', parameters: [], comments: '@return [Array]') combined = pin1.combine_with(pin2) diff --git a/spec/pin/constant_spec.rb b/spec/pin/constant_spec.rb index 7a7fdb0c9..116c72b2a 100644 --- a/spec/pin/constant_spec.rb +++ b/spec/pin/constant_spec.rb @@ -1,23 +1,25 @@ +# frozen_string_literal: true + describe Solargraph::Pin::Constant do - it "resolves constant paths" do + it 'resolves constant paths' do source = Solargraph::Source.new(%( class Foo BAR = 'bar' end )) map = Solargraph::SourceMap.map(source) - pin = map.pins.select{|pin| pin.name == 'BAR'}.first + pin = map.pins.select { |pin| pin.name == 'BAR' }.first expect(pin.path).to eq('Foo::BAR') end - it "is a constant kind" do + it 'is a constant kind' do source = Solargraph::Source.new(%( class Foo BAR = 'bar' end )) map = Solargraph::SourceMap.map(source) - pin = map.pins.select{|pin| pin.name == 'BAR'}.first + pin = map.pins.select { |pin| pin.name == 'BAR' }.first expect(pin.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::CONSTANT) expect(pin.symbol_kind).to eq(Solargraph::LanguageServer::SymbolKinds::CONSTANT) end diff --git a/spec/pin/delegated_method_spec.rb b/spec/pin/delegated_method_spec.rb index a20fc26d4..7063d42c0 100644 --- a/spec/pin/delegated_method_spec.rb +++ b/spec/pin/delegated_method_spec.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + require 'pry' describe Solargraph::Pin::DelegatedMethod do it 'can be constructed from a Method pin' do method_pin = Solargraph::Pin::Method.new(comments: '@return [Hash{String => String}]') - delegation_pin = Solargraph::Pin::DelegatedMethod.new(method: method_pin, scope: :instance) + delegation_pin = described_class.new(method: method_pin, scope: :instance) expect(delegation_pin.return_type.to_s).to eq('Hash{String => String}') end @@ -26,7 +28,7 @@ def collaborator; end class2 = api_map.get_path_pins('Class2').first chain = Solargraph::Source::Chain.new([Solargraph::Source::Chain::Call.new('collaborator', nil)]) - pin = Solargraph::Pin::DelegatedMethod.new( + pin = described_class.new( closure: class2, scope: :instance, name: 'name', diff --git a/spec/pin/documenting_spec.rb b/spec/pin/documenting_spec.rb index 1fdf9b8de..cfecb0468 100644 --- a/spec/pin/documenting_spec.rb +++ b/spec/pin/documenting_spec.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + describe Solargraph::Pin::Documenting do - let(:object) { + let(:object) do Class.new do include Solargraph::Pin::Documenting attr_accessor :docstring end.new - } + end it 'parses indented code blocks' do object.docstring = YARD::Docstring.new(%(Method overview diff --git a/spec/pin/instance_variable_spec.rb b/spec/pin/instance_variable_spec.rb index fd6203398..e1b08e028 100644 --- a/spec/pin/instance_variable_spec.rb +++ b/spec/pin/instance_variable_spec.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + describe Solargraph::Pin::InstanceVariable do - it "is a kind of variable" do + it 'is a kind of variable' do source = Solargraph::Source.load_string("@foo = 'foo'", 'file.rb') map = Solargraph::SourceMap.map(source) - pin = map.pins.select{ |p| p.is_a?(Solargraph::Pin::InstanceVariable) }.first + pin = map.pins.select { |p| p.is_a?(described_class) }.first expect(pin.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::VARIABLE) expect(pin.symbol_kind).to eq(Solargraph::LanguageServer::SymbolKinds::VARIABLE) end - it "does not link documentation for undefined return types" do - pin = Solargraph::Pin::InstanceVariable.new(name: '@bar') + it 'does not link documentation for undefined return types' do + pin = described_class.new(name: '@bar') expect(pin.link_documentation).to eq('@bar') end end diff --git a/spec/pin/keyword_spec.rb b/spec/pin/keyword_spec.rb index 82bb373ef..4ffb9d34c 100644 --- a/spec/pin/keyword_spec.rb +++ b/spec/pin/keyword_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + describe Solargraph::Pin::Keyword do - it "is a kind of keyword" do - pin = Solargraph::Pin::Keyword.new('foo') + it 'is a kind of keyword' do + pin = described_class.new('foo') expect(pin.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::KEYWORD) end end diff --git a/spec/pin/local_variable_spec.rb b/spec/pin/local_variable_spec.rb index 88075efb9..d9e8590f9 100644 --- a/spec/pin/local_variable_spec.rb +++ b/spec/pin/local_variable_spec.rb @@ -1,5 +1,9 @@ +# frozen_string_literal: true + describe Solargraph::Pin::LocalVariable do - xit "merges presence changes so that [not currently used]" do + it 'merges presence changes so that [not currently used]' do + pending 'but not sure why' + map1 = Solargraph::SourceMap.load_string(%( class Foo foo = 'foo' @@ -21,33 +25,171 @@ class Foo expect(pin2.presence.start.to_hash).to eq({ line: 3, character: 8 }) expect(pin2.presence.ending.to_hash).to eq({ line: 5, character: 9 }) - combined = pin1.combine_with(pin2) - expect(combined).to be_a(Solargraph::Pin::LocalVariable) - + with_env_var('SOLARGRAPH_ASSERTS', 'on') do + expect(Solargraph.asserts_on?).to be true + expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :closure name/) + end expect(combined.source).to eq(:combined) # no choice behavior defined yet - if/when this is to be used, we # should indicate which one should override in the range situation end - it "asserts on attempt to merge namespace changes" do - map1 = Solargraph::SourceMap.load_string(%( - class Foo - foo = 'foo' + describe '#visible_at?' do + it 'detects scoped methods in rebound blocks' do + source = Solargraph::Source.load_string(%( + object = MyClass.new + object + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + api_map.clip_at('test.rb', [2, 0]) + object_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'object' } + expect(object_pin).not_to be_nil + location = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(2, 0, 2, 0)) + expect(object_pin.visible_at?(Solargraph::Pin::ROOT_PIN, location)).to be true + end + + it 'does not allow access to top-level locals from top-level methods' do + map = Solargraph::SourceMap.load_string(%( + x = 'string' + def foo + x + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map map.source + x_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'x' } + expect(x_pin).not_to be_nil + foo_pin = api_map.get_path_pins('#foo').first + expect(foo_pin).not_to be_nil + location = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(3, 9, 3, 9)) + expect(x_pin.visible_at?(foo_pin, location)).to be false + end + + it 'scopes local variables correctly in class_eval blocks' do + map = Solargraph::SourceMap.load_string(%( + class Foo; end + x = 'y' + Foo.class_eval do + foo = :bar + etc + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map map.source + block_pin = api_map.get_block_pins.find do |b| + b.location.range.start.line == 3 end - )) - pin1 = map1.locals.first - map2 = Solargraph::SourceMap.load_string(%( - class Bar - foo = 'foo' + expect(block_pin).not_to be_nil + x_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'x' } + expect(x_pin).not_to be_nil + location = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 10, 5, 10)) + expect(x_pin.visible_at?(block_pin, location)).to be true + end + + it 'understands local lookup in root scope' do + api_map = Solargraph::ApiMap.new + source = Solargraph::Source.load_string(%( + # @type [Array] + arr = [] + + + ), 'test.rb') + api_map.map source + arr_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'arr' } + expect(arr_pin).not_to be_nil + location = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(3, 0, 3, 0)) + expect(arr_pin.visible_at?(Solargraph::Pin::ROOT_PIN, location)).to be true + end + + it 'selects local variables using gated scopes' do + source = Solargraph::Source.load_string(%( + lvar1 = 'lvar1' + module MyModule + lvar2 = 'lvar2' + + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + lvar1_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'lvar1' } + expect(lvar1_pin).not_to be_nil + my_module_pin = api_map.get_namespace_pins('MyModule', 'Class<>').first + expect(my_module_pin).not_to be_nil + location = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(4, 0, 4, 0)) + expect(lvar1_pin.visible_at?(my_module_pin, location)).to be false + + lvar2_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'lvar2' } + expect(lvar2_pin).not_to be_nil + expect(lvar2_pin.visible_at?(my_module_pin, location)).to be true + end + + it 'is visible within same method' do + source = Solargraph::Source.load_string(%( + class Foo + def bar + x = 1 + puts x + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.source_map('test.rb').locals.first + bar_method = api_map.get_path_pins('Foo#bar').first + expect(bar_method).not_to be_nil + range = Solargraph::Range.from_to(4, 16, 4, 17) + location = Solargraph::Location.new('test.rb', range) + expect(pin.visible_at?(bar_method, location)).to be true + end + + it 'is visible within each block scope inside function' do + source = Solargraph::Source.load_string(%( + class Foo + def bar + x = 1 + [2,3,4].each do |i| + puts x + i + end + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + x = api_map.source_map('test.rb').locals.find { |p| p.name == 'x' } + api_map.get_path_pins('Foo#bar').first + each_block_pin = api_map.get_block_pins.find do |b| + b.location.range.start.line == 4 end - )) - pin2 = map2.locals.first - # set env variable 'FOO' to 'true' in block + expect(each_block_pin).not_to be_nil + range = Solargraph::Range.from_to(5, 24, 5, 25) + location = Solargraph::Location.new('test.rb', range) + expect(x.visible_at?(each_block_pin, location)).to be true + end - with_env_var('SOLARGRAPH_ASSERTS', 'on') do - expect(Solargraph.asserts_on?(:combine_with_closure_name)).to be true - expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :closure name/) + it 'sees block parameter inside block' do + source = Solargraph::Source.load_string(%( + class Foo + def bar + [1,2,3].each do |i| + puts i + end + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + i = api_map.source_map('test.rb').locals.find { |p| p.name == 'i' } + bar_method = api_map.get_path_pins('Foo#bar').first + expect(bar_method).not_to be_nil + each_block_pin = api_map.get_block_pins.find do |b| + b.location.range.start.line == 3 + end + expect(each_block_pin).not_to be_nil + range = Solargraph::Range.from_to(4, 24, 4, 25) + location = Solargraph::Location.new('test.rb', range) + expect(i.visible_at?(each_block_pin, location)).to be true end end end diff --git a/spec/pin/method_alias_spec.rb b/spec/pin/method_alias_spec.rb new file mode 100644 index 000000000..d3b408273 --- /dev/null +++ b/spec/pin/method_alias_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +describe Solargraph::Pin::MethodAlias do + describe '#to_rbs' do + it 'generates RBS from simple alias' do + method_alias = described_class.new(name: 'name', original: 'original_name') + + expect(method_alias.to_rbs).to eq('alias name original_name') + end + + it 'generates RBS from static alias' do + method_alias = described_class.new(name: 'name', original: 'original_name', scope: :class) + + expect(method_alias.to_rbs).to eq('alias self.name self.original_name') + end + end +end diff --git a/spec/pin/method_spec.rb b/spec/pin/method_spec.rb index 283ef6d51..c109746af 100644 --- a/spec/pin/method_spec.rb +++ b/spec/pin/method_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + describe Solargraph::Pin::Method do it 'tracks code parameters' do source = Solargraph::Source.new(%( @@ -33,7 +35,7 @@ def foo bar:, baz: MyClass.new )) map = Solargraph::SourceMap.map(source) pin = map.pins.select { |pin| pin.path == '#foo' }.first - expect(pin.class).to eq(Solargraph::Pin::Method) + expect(pin.class).to eq(described_class) method_pin = pin expect(method_pin.signatures.length).to eq(1) method_signature = method_pin.signatures.first @@ -53,7 +55,7 @@ def foo bar:, baz: MyClass.new @param two [Second] description2 COMMENTS # pin = source.pins.select{|pin| pin.path == 'Foo#bar'}.first - pin = Solargraph::Pin::Method.new(comments: comments) + pin = described_class.new(comments: comments) expect(pin.documentation).to include('one') expect(pin.documentation).to include('[First]') expect(pin.documentation).to include('description1') @@ -77,9 +79,9 @@ def bazzle; end COMMENTS map = Solargraph::SourceMap.map(source) bazzle = map.pins.select { |pin| pin.path == 'Bar::Baz#bazzle' }.first - expect(bazzle.return_type.rooted?).to eq(false) + expect(bazzle.return_type.rooted?).to be(false) bing = map.pins.select { |pin| pin.path == 'Bar::Baz#bing' }.first - expect(bing.return_type.rooted?).to eq(true) + expect(bing.return_type.rooted?).to be(true) end it 'includes yieldparam tags in documentation' do @@ -87,7 +89,7 @@ def bazzle; end @yieldparam one [First] description1 @yieldparam two [Second] description2 COMMENTS - pin = Solargraph::Pin::Method.new(comments: comments) + pin = described_class.new(comments: comments) expect(pin.documentation).to include('one') expect(pin.documentation).to include('[First]') expect(pin.documentation).to include('description1') @@ -101,37 +103,37 @@ def bazzle; end @yieldreturn [YRet] yretdescription @return [String] COMMENTS - pin = Solargraph::Pin::Method.new(comments: comments) + pin = described_class.new(comments: comments) expect(pin.documentation).to include('YRet') expect(pin.documentation).to include('yretdescription') end it 'detects return types from tags' do - pin = Solargraph::Pin::Method.new(comments: '@return [Hash]') + pin = described_class.new(comments: '@return [Hash]') expect(pin.return_type.tag).to eq('Hash') end it 'ignores malformed return tags' do - pin = Solargraph::Pin::Method.new(name: 'bar', comments: '@return [Array) @@ -425,7 +421,7 @@ def bar?; end end it 'supports multiple return tags' do - pin = Solargraph::Pin::Method.new( + pin = described_class.new( name: 'foo', comments: %( @return [String] @@ -436,7 +432,7 @@ def bar?; end end it 'includes @return text in documentation' do - pin = Solargraph::Pin::Method.new( + pin = described_class.new( name: 'foo', comments: %( @return [String] the foo text string @@ -446,7 +442,7 @@ def bar?; end end it 'includes @example text in documentation' do - pin = Solargraph::Pin::Method.new( + pin = described_class.new( name: 'foo', comments: %( @example @@ -458,7 +454,7 @@ def bar?; end end it 'includes @example names' do - pin = Solargraph::Pin::Method.new( + pin = described_class.new( name: 'foo', comments: %( @example Call foo @@ -485,8 +481,8 @@ def bar param1, param2 api_map.map source pin = api_map.get_path_pins('Example#bar').first pin.resolve_ref_tag(api_map) - expect(pin.docstring.tags(:param).map(&:name)).to eq(['param1', 'param2']) - expect(pin.docstring.tags(:param).map(&:type)).to eq(['String', 'Integer']) + expect(pin.docstring.tags(:param).map(&:name)).to eq(%w[param1 param2]) + expect(pin.docstring.tags(:param).map(&:type)).to eq(%w[String Integer]) end it 'resolves ref tags with namespaces' do @@ -508,11 +504,11 @@ def bar param1, param2 api_map.map source pin = api_map.get_path_pins('Example2#bar').first pin.resolve_ref_tag(api_map) - expect(pin.docstring.tags(:param).map(&:name)).to eq(['param1', 'param2']) - expect(pin.docstring.tags(:param).map(&:type)).to eq(['String', 'Integer']) + expect(pin.docstring.tags(:param).map(&:name)).to eq(%w[param1 param2]) + expect(pin.docstring.tags(:param).map(&:type)).to eq(%w[String Integer]) end - context 'as attribute' do + context 'when attr_reader is used' do it 'is a kind of attribute/property' do source = Solargraph::Source.load_string(%( class Foo @@ -520,35 +516,45 @@ class Foo end )) map = Solargraph::SourceMap.map(source) - pin = map.pins.select { |p| p.is_a?(Solargraph::Pin::Method) }.first + pin = map.pins.select { |p| p.is_a?(described_class) }.first expect(pin).to be_attribute expect(pin.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::PROPERTY) expect(pin.symbol_kind).to eq(Solargraph::LanguageServer::SymbolKinds::PROPERTY) end it 'uses return type tags' do - pin = Solargraph::Pin::Method.new(name: 'bar', comments: '@return [File]', attribute: true) + pin = described_class.new(name: 'bar', comments: '@return [File]', attribute: true) expect(pin.return_type.tag).to eq('File') end it 'detects undefined types' do - pin = Solargraph::Pin::Method.new(name: 'bar', attribute: true) + pin = described_class.new(name: 'bar', attribute: true) expect(pin.return_type).to be_undefined end it 'generates paths' do npin = Solargraph::Pin::Namespace.new(name: 'Foo', type: :class) - ipin = Solargraph::Pin::Method.new(closure: npin, name: 'bar', attribute: true, scope: :instance) + ipin = described_class.new(closure: npin, name: 'bar', attribute: true, scope: :instance) expect(ipin.path).to eq('Foo#bar') - cpin = Solargraph::Pin::Method.new(closure: npin, name: 'bar', attribute: true, scope: :class) + cpin = described_class.new(closure: npin, name: 'bar', attribute: true, scope: :class) expect(cpin.path).to eq('Foo.bar') end it 'handles invalid return type tags' do - pin = Solargraph::Pin::Method.new(name: 'bar', comments: '@return [Array<]', attribute: true) + pin = described_class.new(name: 'bar', comments: '@return [Array<]', attribute: true) expect(pin.return_type).to be_undefined end + it 'combines signatures by type' do + # Integer+ in RBS is a number of signatures that dispatch based + # on type. Let's make sure we combine those with anything else + # found (e.g., additions from the BigDecimal RBS collection) + # without collapsing signatures + api_map = Solargraph::ApiMap.load_with_cache(Dir.pwd, nil) + method = api_map.get_method_stack('Integer', '+', scope: :instance).first + expect(method.signatures.count).to be > 3 + end + it 'infers untagged types from instance variables' do source = Solargraph::Source.load_string(%( class Foo @@ -622,7 +628,7 @@ def bar end it 'ignores malformed overload tags' do - pin = Solargraph::Pin::Method.new(name: 'example', comments: "@overload\n @param") + pin = described_class.new(name: 'example', comments: "@overload\n @param") expect(pin.overloads).to be_empty end end diff --git a/spec/pin/namespace_spec.rb b/spec/pin/namespace_spec.rb index 3c94a526e..cafadcc58 100644 --- a/spec/pin/namespace_spec.rb +++ b/spec/pin/namespace_spec.rb @@ -1,34 +1,36 @@ +# frozen_string_literal: true + describe Solargraph::Pin::Namespace do - it "handles long namespaces" do - pin = Solargraph::Pin::Namespace.new(closure: Solargraph::Pin::Namespace.new(name: 'Foo'), name: 'Bar') + it 'handles long namespaces' do + pin = described_class.new(closure: described_class.new(name: 'Foo'), name: 'Bar') expect(pin.path).to eq('Foo::Bar') end - it "has class scope" do - source = Solargraph::Source.load_string(%( + it 'has class scope' do + Solargraph::Source.load_string(%( class Foo end )) - pin = Solargraph::Pin::Namespace.new(name: 'Foo') + pin = described_class.new(name: 'Foo') expect(pin.context.scope).to eq(:class) end - it "is a kind of namespace/class/module" do - pin1 = Solargraph::Pin::Namespace.new(name: 'Foo') + it 'is a kind of namespace/class/module' do + pin1 = described_class.new(name: 'Foo') expect(pin1.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::CLASS) - pin2 = Solargraph::Pin::Namespace.new(name: 'Foo', type: :module) + pin2 = described_class.new(name: 'Foo', type: :module) expect(pin2.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::MODULE) end it 'handles nested namespaces inside closures' do - pin = Solargraph::Pin::Namespace.new(closure: Solargraph::Pin::Namespace.new(name: 'Foo'), name: 'Bar::Baz') + pin = described_class.new(closure: described_class.new(name: 'Foo'), name: 'Bar::Baz') expect(pin.namespace).to eq('Foo::Bar') expect(pin.name).to eq('Baz') expect(pin.path).to eq('Foo::Bar::Baz') end it 'uses @param tags as generic type parameters' do - pin = Solargraph::Pin::Namespace.new(name: 'Foo', comments: '@generic GenericType') + pin = described_class.new(name: 'Foo', comments: '@generic GenericType') expect(pin.generics).to eq(['GenericType']) expect(pin.to_rbs).to eq('class ::Foo[GenericType]') end diff --git a/spec/pin/parameter_spec.rb b/spec/pin/parameter_spec.rb index 14c39f3fe..971acc10e 100644 --- a/spec/pin/parameter_spec.rb +++ b/spec/pin/parameter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + describe Solargraph::Pin::Parameter do it 'detects block parameter return types from @yieldparam tags' do api_map = Solargraph::ApiMap.new @@ -195,8 +197,8 @@ def baz; end it 'uses longer comment while combining compatible parameters' do loc = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(0, 0, 0, 0)) block = Solargraph::Pin::Block.new(location: loc, name: 'Foo') - pin1 = Solargraph::Pin::Parameter.new(closure: block, name: 'bar') - pin2 = Solargraph::Pin::Parameter.new(closure: block, name: 'bar', comments: 'a comment') + pin1 = described_class.new(closure: block, name: 'bar') + pin2 = described_class.new(closure: block, name: 'bar', comments: 'a comment') expect(pin1.combine_with(pin2).comments).to eq('a comment') end @@ -207,7 +209,7 @@ def baz; end ), 'test.rb') api_map = Solargraph::ApiMap.new api_map.map source - pin = api_map.source_map('test.rb').locals.select { |p| p.is_a?(Solargraph::Pin::Parameter) }.first + pin = api_map.source_map('test.rb').locals.select { |p| p.is_a?(described_class) }.first # expect(pin.infer(api_map)).to be_undefined expect(pin.typify(api_map)).to be_undefined expect(pin.probe(api_map)).to be_undefined @@ -367,7 +369,7 @@ def use_string str expect(type.tag).to eq('String') end - context 'for instance methods' do + context 'when used in an instance methods' do it 'infers types from optarg values' do source = Solargraph::Source.load_string(%( class Example @@ -397,7 +399,7 @@ def foo bar: 'bar' end end - context 'for class methods' do + context 'when used in an class method' do it 'infers types from optarg values' do source = Solargraph::Source.load_string(%( class Example @@ -441,7 +443,7 @@ def self.foo bar: Hash.new end end - context 'for singleton methods' do + context 'when used in a singleton method' do it 'infers types from optarg values' do source = Solargraph::Source.load_string(%( class Example diff --git a/spec/pin/search_spec.rb b/spec/pin/search_spec.rb index 28c302839..f505f45e9 100644 --- a/spec/pin/search_spec.rb +++ b/spec/pin/search_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + describe Solargraph::Pin::Search do it 'returns ordered matches on paths' do example_class = Solargraph::Pin::Namespace.new(name: 'Example') @@ -6,7 +8,7 @@ Solargraph::Pin::Method.new(name: 'foobar', closure: example_class), Solargraph::Pin::Method.new(name: 'foo_bar', closure: example_class) ] - search = Solargraph::Pin::Search.new(pins, 'example') + search = described_class.new(pins, 'example') expect(search.results).to eq(pins) end @@ -17,7 +19,7 @@ Solargraph::Pin::Method.new(name: 'foobar', closure: example_class), Solargraph::Pin::Method.new(name: 'foo_bar', closure: example_class) ] - search = Solargraph::Pin::Search.new(pins, 'foobar') + search = described_class.new(pins, 'foobar') expect(search.results.map(&:path)).to eq(['Example.foobar', 'Example.foo_bar']) end @@ -28,7 +30,7 @@ Solargraph::Pin::Method.new(name: 'foobar', closure: example_class), Solargraph::Pin::Method.new(name: 'bazquz', closure: example_class) ] - search = Solargraph::Pin::Search.new(pins, 'foobar') + search = described_class.new(pins, 'foobar') expect(search.results.map(&:path)).to eq(['Example.foobar']) end end diff --git a/spec/pin/symbol_spec.rb b/spec/pin/symbol_spec.rb index 16961cadc..585c8b5b1 100644 --- a/spec/pin/symbol_spec.rb +++ b/spec/pin/symbol_spec.rb @@ -1,54 +1,55 @@ +# frozen_string_literal: true + describe Solargraph::Pin::Symbol do - context "as an unquoted literal" do - it "is a kind of keyword to the LSP" do - pin = Solargraph::Pin::Symbol.new(nil, ':symbol') + context 'when an unquoted literal' do + it 'is a kind of keyword to the LSP' do + pin = described_class.new(nil, ':symbol') expect(pin.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::KEYWORD) end - it "has global closure" do - pin = Solargraph::Pin::Symbol.new(nil, ':symbol') + it 'has global closure' do + pin = described_class.new(nil, ':symbol') expect(pin.closure).to eq(Solargraph::Pin::ROOT_PIN) end - it "has a Symbol return type" do - pin = Solargraph::Pin::Symbol.new(nil, ':symbol') + it 'has a Symbol return type' do + pin = described_class.new(nil, ':symbol') expect(pin.return_type.tag).to eq('Symbol') end end - context "as a double quoted literal" do - it "is a kind of keyword" do - pin = Solargraph::Pin::Symbol.new(nil, ':"symbol"') + context 'when a double quoted literal' do + it 'is a kind of keyword' do + pin = described_class.new(nil, ':"symbol"') expect(pin.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::KEYWORD) end - it "has a Symbol return type" do - pin = Solargraph::Pin::Symbol.new(nil, ':"symbol"') + it 'has a Symbol return type' do + pin = described_class.new(nil, ':"symbol"') expect(pin.return_type.tag).to eq('Symbol') end end - context "as a double quoted interpolatd literal" do - it "is a kind of keyword" do - pin = Solargraph::Pin::Symbol.new(nil, ':"symbol #{variable}"') + context 'when a double quoted interpolated literal' do + it 'is a kind of keyword' do + pin = described_class.new(nil, ':"symbol #{variable}"') expect(pin.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::KEYWORD) end - it "has a Symbol return type" do - pin = Solargraph::Pin::Symbol.new(nil, ':"symbol #{variable}"') + it 'has a Symbol return type' do + pin = described_class.new(nil, ':"symbol #{variable}"') expect(pin.return_type.tag).to eq('Symbol') end end - - context "as a single quoted literal" do - it "is a kind of keyword" do - pin = Solargraph::Pin::Symbol.new(nil, ":'symbol'") + context 'when a single quoted literal' do + it 'is a kind of keyword' do + pin = described_class.new(nil, ":'symbol'") expect(pin.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::KEYWORD) end - it "has a Symbol return type" do - pin = Solargraph::Pin::Symbol.new(nil, ":'symbol'") + it 'has a Symbol return type' do + pin = described_class.new(nil, ":'symbol'") expect(pin.return_type.tag).to eq('Symbol') end end diff --git a/spec/pin_cache_spec.rb b/spec/pin_cache_spec.rb new file mode 100644 index 000000000..83cf7a3b7 --- /dev/null +++ b/spec/pin_cache_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'bundler' +require 'benchmark' + +describe Solargraph::PinCache do + subject(:pin_cache) do + described_class.new(rbs_collection_path: '.gem_rbs_collection', + rbs_collection_config_path: 'rbs_collection.yaml', + directory: Dir.pwd, + yard_plugins: ['activesupport-concern']) + end + + describe '#cached?' do + it 'returns true for a gem that is cached' do + allow(File).to receive(:file?).with(%r{.*stdlib/backport.ser$}).and_return(false) + allow(File).to receive(:file?).with(%r{.*combined/.*/backport-.*.ser$}).and_return(true) + + gemspec = Gem::Specification.find_by_name('backport') + expect(pin_cache.cached?(gemspec)).to be true + end + + it 'returns false for a gem that is not cached' do + gemspec = Gem::Specification.new.tap do |spec| + spec.name = 'nonexistent' + spec.version = '0.0.1' + end + expect(pin_cache.cached?(gemspec)).to be false + end + end + + describe '.core?' do + it 'returns true when core pins exist' do + allow(File).to receive(:file?).with(%r{.*/core.ser$}).and_return(true) + + expect(described_class.core?).to be true + end + + it "returns true when core pins don't" do + allow(File).to receive(:file?).with(%r{.*/core.ser$}).and_return(false) + + expect(described_class.core?).to be false + end + end + + describe '#possible_stdlibs' do + it 'is tolerant of less usual Ruby installations' do + stub_const('Gem::RUBYGEMS_DIR', nil) + + expect(pin_cache.possible_stdlibs).to eq([]) + end + end + + describe '#cache_all_stdlibs' do + it 'creates stdlibmaps' do + allow(Solargraph::RbsMap::StdlibMap).to receive(:new).and_return(instance_double(Solargraph::RbsMap::StdlibMap)) + + pin_cache.cache_all_stdlibs + + expect(Solargraph::RbsMap::StdlibMap).to have_received(:new).at_least(:once) + end + end + + describe '#cache_gem' do + context 'with an already in-memory gem' do + let(:backport_gemspec) { Gem::Specification.find_by_name('backport') } + + before do + pin_cache.cache_gem(gemspec: backport_gemspec, out: nil) + end + + it 'does not load the gem again' do + allow(Marshal).to receive(:load).and_call_original + + pin_cache.cache_gem(gemspec: backport_gemspec, out: nil) + + expect(Marshal).not_to have_received(:load).with(anything) + end + end + + context 'with the parser gem' do + before do + Solargraph::Shell.new.uncache('parser') + allow(Solargraph::Yardoc).to receive(:build_docs) + end + + it 'chooses not to use YARD' do + parser_gemspec = Gem::Specification.find_by_name('parser') + pin_cache.cache_gem(gemspec: parser_gemspec, out: nil) + # if this fails, you may not have run `bundle exec rbs collection update` + expect(Solargraph::Yardoc).not_to have_received(:build_docs).with(any_args) + end + end + + context 'with an installed gem' do + before do + Solargraph::Shell.new.gems('kramdown') + end + + it 'uncaches when asked' do + gemspec = Gem::Specification.find_by_name('kramdown') + expect do + pin_cache.uncache_gem(gemspec, out: nil) + end.not_to raise_error + end + end + + context 'with the rebuild flag' do + before do + allow(Solargraph::Yardoc).to receive(:build_docs) + end + + it 'chooses not to use YARD' do + parser_gemspec = Gem::Specification.find_by_name('parser') + pin_cache.cache_gem(gemspec: parser_gemspec, rebuild: true, out: nil) + # if this fails, you may not have run `bundle exec rbs collection update` + expect(Solargraph::Yardoc).not_to have_received(:build_docs).with(any_args) + end + end + + context 'with a stdlib gem' do + let(:gem_name) { 'logger' } + + before do + Solargraph::Shell.new.uncache(gem_name) + end + + it 'caches' do + yaml_gemspec = Gem::Specification.find_by_name(gem_name) + allow(File).to receive(:write).and_call_original + + pin_cache.cache_gem(gemspec: yaml_gemspec, out: nil) + + # match arguments with regexp using rspec-matchers syntax + expect(File).to have_received(:write).with(%r{combined/.*/logger-.*-stdlib.ser$}, any_args).once + end + end + + context 'with gem packaged with its own RBS' do + let(:gem_name) { 'rubocop-yard' } + + before do + Solargraph::Shell.new.uncache(gem_name) + end + + it 'caches' do + yaml_gemspec = Gem::Specification.find_by_name(gem_name) + allow(File).to receive(:write).and_call_original + + pin_cache.cache_gem(gemspec: yaml_gemspec, out: nil) + + # match arguments with regexp using rspec-matchers syntax + expect(File).to have_received(:write).with(%r{combined/.*/rubocop-yard-.*-export.ser$}, any_args, + mode: 'wb').once + end + end + end + + describe '#uncache_gem' do + subject(:call) { pin_cache.uncache_gem(gemspec, out: out) } + + let(:out) { StringIO.new } + + before do + allow(FileUtils).to receive(:rm_rf) + end + + context 'with an already cached gem' do + let(:gemspec) { Gem::Specification.find_by_name('backport') } + + it 'deletes files' do + call + + expect(FileUtils).to have_received(:rm_rf).at_least(:once) + end + end + + context 'with a non-existent gem' do + let(:gemspec) { instance_double(Gem::Specification, name: 'nonexistent', version: '0.0.1') } + + it 'does not raise an error' do + expect { call }.not_to raise_error + end + + it 'logs a message' do + call + + expect(out.string).to include('does not exist') + end + + it 'does not delete files' do + call + + expect(FileUtils).not_to have_received(:rm_rf) + end + end + end +end diff --git a/spec/position_spec.rb b/spec/position_spec.rb index e8dab1960..d61b05ce5 100644 --- a/spec/position_spec.rb +++ b/spec/position_spec.rb @@ -1,49 +1,51 @@ +# frozen_string_literal: true + describe Solargraph::Position do - it "normalizes arrays into positions" do - pos = Solargraph::Position.normalize([0, 1]) - expect(pos).to be_a(Solargraph::Position) + it 'normalizes arrays into positions' do + pos = described_class.normalize([0, 1]) + expect(pos).to be_a(described_class) expect(pos.line).to eq(0) expect(pos.column).to eq(1) end - it "returns original positions when normalizing" do - orig = Solargraph::Position.new(0, 1) - norm = Solargraph::Position.normalize(orig) + it 'returns original positions when normalizing' do + orig = described_class.new(0, 1) + norm = described_class.normalize(orig) expect(orig).to be(norm) end it 'finds offset from position' do text = "\n class Foo\n def bar baz, boo = 'boo'\n end\n end\n " - expect(Solargraph::Position.to_offset(text, Solargraph::Position.new(0, 0))).to eq(0) - expect(Solargraph::Position.to_offset(text, Solargraph::Position.new(0, 4))).to eq(4) - expect(Solargraph::Position.to_offset(text, Solargraph::Position.new(2, 12))).to eq(29) - expect(Solargraph::Position.to_offset(text, Solargraph::Position.new(2, 27))).to eq(44) - expect(Solargraph::Position.to_offset(text, Solargraph::Position.new(3, 8))).to eq(58) + expect(described_class.to_offset(text, described_class.new(0, 0))).to eq(0) + expect(described_class.to_offset(text, described_class.new(0, 4))).to eq(4) + expect(described_class.to_offset(text, described_class.new(2, 12))).to eq(29) + expect(described_class.to_offset(text, described_class.new(2, 27))).to eq(44) + expect(described_class.to_offset(text, described_class.new(3, 8))).to eq(58) end it 'constructs position from offset' do text = "\n class Foo\n def bar baz, boo = 'boo'\n end\n end\n " - expect(Solargraph::Position.from_offset(text, 0)).to eq(Solargraph::Position.new(0, 0)) - expect(Solargraph::Position.from_offset(text, 4)).to eq(Solargraph::Position.new(1, 3)) - expect(Solargraph::Position.from_offset(text, 29)).to eq(Solargraph::Position.new(2, 12)) - expect(Solargraph::Position.from_offset(text, 44)).to eq(Solargraph::Position.new(2, 27)) + expect(described_class.from_offset(text, 0)).to eq(described_class.new(0, 0)) + expect(described_class.from_offset(text, 4)).to eq(described_class.new(1, 3)) + expect(described_class.from_offset(text, 29)).to eq(described_class.new(2, 12)) + expect(described_class.from_offset(text, 44)).to eq(described_class.new(2, 27)) end - it "raises an error for objects that cannot be normalized" do - expect { - Solargraph::Position.normalize('0, 1') - }.to raise_error(ArgumentError) + it 'raises an error for objects that cannot be normalized' do + expect do + described_class.normalize('0, 1') + end.to raise_error(ArgumentError) end it 'avoids fencepost errors' do text = " class Foo\n def bar baz, boo = 'boo'\n end\n end\n " - offset = Solargraph::Position.to_offset(text, Solargraph::Position.new(3, 6)) + offset = described_class.to_offset(text, described_class.new(3, 6)) expect(offset).to eq(67) end it 'avoids fencepost errors with multiple blank lines' do text = " class Foo\n def bar baz, boo = 'boo'\n\n end\n end\n " - offset = Solargraph::Position.to_offset(text, Solargraph::Position.new(4, 6)) + offset = described_class.to_offset(text, described_class.new(4, 6)) expect(offset).to eq(68) end end diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index 896a55f37..e9dc1ccf8 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + describe Solargraph::RbsMap::Conversions do context 'with RBS to digest' do # create a temporary directory with the scope of the spec around do |example| require 'tmpdir' - Dir.mktmpdir("rspec-solargraph-") do |dir| + Dir.mktmpdir('rspec-solargraph-') do |dir| @temp_dir = dir example.run end @@ -12,7 +14,7 @@ let(:conversions) do loader = RBS::EnvironmentLoader.new(core_root: nil, repository: RBS::Repository.new(no_stdlib: false)) loader.add(path: Pathname(temp_dir)) - Solargraph::RbsMap::Conversions.new(loader: loader) + described_class.new(loader: loader) end let(:api_map) { Solargraph::ApiMap.new } @@ -25,7 +27,35 @@ attr_reader :temp_dir + context 'with overlapping module hierarchies and inheritance' do + subject(:method_pin) { api_map.get_method_stack('A::B::C', 'foo').first } + + let(:rbs) do + <<~RBS + module B + class C + def foo: () -> String + end + end + module A + module B + class C < ::B::C + end + end + end + RBS + end + + before do + api_map.index conversions.pins + end + + it { is_expected.to be_a(Solargraph::Pin::Method) } + end + context 'with self alias to self method' do + subject(:alias_pin) { api_map.get_method_stack('Foo', 'bar?', scope: :class).first } + let(:rbs) do <<~RBS class Foo @@ -35,13 +65,9 @@ def self.bar: () -> String RBS end - let(:method_pin) { api_map.get_method_stack('Foo', 'bar', scope: :class).first } - - subject(:alias_pin) { api_map.get_method_stack('Foo', 'bar?', scope: :class).first } - - it { should_not be_nil } + it { is_expected.not_to be_nil } - it { should be_instance_of(Solargraph::Pin::Method) } + it { is_expected.to be_instance_of(Solargraph::Pin::Method) } it 'finds the type' do expect(alias_pin.return_type.tag).to eq('String') @@ -49,6 +75,8 @@ def self.bar: () -> String end context 'with untyped response' do + subject(:method_pin) { conversions.pins.find { |pin| pin.path == 'Foo#bar' } } + let(:rbs) do <<~RBS class Foo @@ -57,16 +85,35 @@ def bar: () -> untyped RBS end - subject(:method_pin) { conversions.pins.find { |pin| pin.path == 'Foo#bar' } } - - it { should_not be_nil } + it { is_expected.not_to be_nil } - it { should be_a(Solargraph::Pin::Method) } + it { is_expected.to be_a(Solargraph::Pin::Method) } - it 'maps untyped in RBS to undefined in Solargraph 'do + it 'maps untyped in RBS to undefined in Solargraph' do expect(method_pin.return_type.tag).to eq('undefined') end end + end + + context 'with standard loads for solargraph project' do + before :all do # rubocop:disable RSpec/BeforeAfterAll + @api_map = Solargraph::ApiMap.load_with_cache('.') + end + + let(:api_map) { @api_map } + + context 'with superclass pin for Parser::AST::Node' do + let(:superclass_pin) do + api_map.pins.find do |pin| + pin.is_a?(Solargraph::Pin::Reference::Superclass) && pin.context.namespace == 'Parser::AST::Node' + end + end + + it 'generates a rooted pin' do + # rooted! + expect(superclass_pin&.name).to eq('::AST::Node') + end + end # https://github.com/castwide/solargraph/issues/1042 context 'with Hash superclass with untyped value and alias' do @@ -99,28 +146,6 @@ class Sub < Hash[Symbol, untyped] .uniq).to eq(['Symbol']) end end - - context 'with overlapping module hierarchies and inheritance' do - let(:rbs) do - <<~RBS - module B - class C - def foo: () -> String - end - end - module A - module B - class C < ::B::C - end - end - end - RBS - end - - subject(:method_pin) { api_map.get_method_stack('A::B::C', 'foo').first } - - it { should be_a(Solargraph::Pin::Method) } - end end if Gem::Version.new(RBS::VERSION) >= Gem::Version.new('3.9.1') diff --git a/spec/rbs_map/core_map_spec.rb b/spec/rbs_map/core_map_spec.rb index 7b5007529..79878c572 100644 --- a/spec/rbs_map/core_map_spec.rb +++ b/spec/rbs_map/core_map_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + describe Solargraph::RbsMap::CoreMap do it 'maps core Errno classes' do - map = Solargraph::RbsMap::CoreMap.new + map = described_class.new store = Solargraph::ApiMap::Store.new(map.pins) Errno.constants.each do |const| pin = store.get_path_pins("Errno::#{const}").first @@ -12,20 +14,20 @@ end it 'understands RBS class aliases' do - map = Solargraph::RbsMap::CoreMap.new + map = described_class.new store = Solargraph::ApiMap::Store.new(map.pins) # The core RBS contains: # class Mutex = Thread::Mutex - thread_mutex_pin = store.get_path_pins("Thread::Mutex").first + thread_mutex_pin = store.get_path_pins('Thread::Mutex').first expect(thread_mutex_pin).to be_a(Solargraph::Pin::Namespace) - mutex_pin = store.get_path_pins("Mutex").first + mutex_pin = store.get_path_pins('Mutex').first expect(mutex_pin).to be_a(Solargraph::Pin::Constant) - expect(mutex_pin.return_type.to_s).to eq("Class") + expect(mutex_pin.return_type.to_s).to eq('Class') end it 'understands RBS global variables' do - map = Solargraph::RbsMap::CoreMap.new + map = described_class.new store = Solargraph::ApiMap::Store.new(map.pins) global_variable_pins = store.pins_by_class(Solargraph::Pin::GlobalVariable) stderr_pins = global_variable_pins.select do |pin| @@ -39,10 +41,10 @@ it 'understands implied Enumerator#each method' do api_map = Solargraph::ApiMap.new methods = api_map.get_methods('Enumerable') - each_pins = methods.select{|pin| pin.path.end_with?('#each')} + each_pins = methods.select { |pin| pin.path.end_with?('#each') } # expect this to come from the _Each implied interface ("self # type") defined in the RBS - expect(each_pins.map(&:path)).to eq(["_Each#each"]) + expect(each_pins.map(&:path)).to eq(['_Each#each']) expect(each_pins.map(&:class)).to eq([Solargraph::Pin::Method]) each_pin = each_pins.first expect(each_pin.signatures.length).to eq(1) @@ -53,13 +55,15 @@ it 'populates types in block parameters from generics' do api_map = Solargraph::ApiMap.new methods = api_map.get_methods('Enumerable') - each_pins = methods.select{|pin| pin.path.end_with?('#each')} + each_pins = methods.select { |pin| pin.path.end_with?('#each') } each_pin = each_pins.first signature = each_pin.signatures.first expect(signature.block.parameters.map(&:return_type).map(&:to_s)).to eq(['String']) end - xit 'understands defaulted type parameters' do + it 'understands defaulted type parameters' do + pending('defaulted type parameter support') + # @todo Enumerable#each's' return type not yet supported as _Each<> # takes two type parameters, the second has a default value, # Enumerable specifies it, but Solargraph doesn't support type @@ -67,7 +71,7 @@ # api_map = Solargraph::ApiMap.new methods = api_map.get_methods('Enumerable') - each_pins = methods.select{|pin| pin.path.end_with?('#each')} + each_pins = methods.select { |pin| pin.path.end_with?('#each') } each_pin = each_pins.first signature = each_pin.signatures.first expect(signature.return_type.to_s).to eq('Enumerable') @@ -77,8 +81,8 @@ # @todo This is a simple smoke test to ensure that mixins are applied # correctly. It would be better to test RbsMap or RbsMap::Conversions # with an RBS fixture. - core_map = Solargraph::RbsMap::CoreMap.new - pins = core_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.name == 'Enumerable' } + core_map = described_class.new + pins = core_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.name == '::Enumerable' } expect(pins.map(&:closure).map(&:namespace)).to include('Enumerator') end @@ -95,16 +99,6 @@ class Foo expect(clip.infer.to_s).to eq('Foo') end - it "generates rooted pins from RBS for core" do - map = Solargraph::RbsMap::CoreMap.new - map.pins.each do |pin| - expect(pin).to be_all_rooted - unless pin.is_a?(Solargraph::Pin::Keyword) - expect(pin.closure).to_not be_nil, ->(){ "Pin #{pin.inspect} (#{pin.path}) has no closure" } - end - end - end - it 'renders string literals from RBS in a useful way' do source = Solargraph::Source.load_string(%( foo = nil diff --git a/spec/rbs_map/stdlib_map_spec.rb b/spec/rbs_map/stdlib_map_spec.rb index 03f0b547f..00afc6542 100644 --- a/spec/rbs_map/stdlib_map_spec.rb +++ b/spec/rbs_map/stdlib_map_spec.rb @@ -1,42 +1,20 @@ +# frozen_string_literal: true + describe Solargraph::RbsMap::StdlibMap do - it "finds stdlib require paths" do - rbs_map = Solargraph::RbsMap::StdlibMap.load('fileutils') + it 'finds stdlib require paths' do + rbs_map = described_class.load('fileutils') pin = rbs_map.path_pin('FileUtils#chdir') - expect(pin).to be + expect(pin).not_to be_nil end it 'maps YAML' do - rbs_map = Solargraph::RbsMap::StdlibMap.load('yaml') + rbs_map = described_class.load('yaml') pin = rbs_map.path_pin('YAML') expect(pin).to be_a(Solargraph::Pin::Base) end - it 'processes RBS class variables' do - map = Solargraph::RbsMap::StdlibMap.load('rbs') - store = Solargraph::ApiMap::Store.new(map.pins) - class_variable_pins = store.pins_by_class(Solargraph::Pin::ClassVariable) - count_pins = class_variable_pins.select do |pin| - pin.name.to_s == '@@count' && pin.context.to_s == 'Class' - end - expect(count_pins.length).to eq(1) - count_pin = count_pins.first - expect(count_pin.return_type.to_s).to eq('Integer') - end - - it 'processes RBS class instance variables' do - map = Solargraph::RbsMap::StdlibMap.load('rbs') - store = Solargraph::ApiMap::Store.new(map.pins) - instance_variable_pins = store.pins_by_class(Solargraph::Pin::InstanceVariable) - root_pins = instance_variable_pins.select do |pin| - pin.name.to_s == '@root' && pin.context.to_s == 'Class' && pin.scope == :class - end - expect(root_pins.length).to eq(1) - root_pin = root_pins.first - expect(root_pin.return_type.to_s).to eq('RBS::Namespace, nil') - end - it 'processes RBS module aliases' do - map = Solargraph::RbsMap::StdlibMap.load('yaml') + map = described_class.load('yaml') store = Solargraph::ApiMap::Store.new(map.pins) constant_pins = store.get_constants('') yaml_pins = constant_pins.select do |pin| @@ -49,7 +27,7 @@ end it 'pins are marked as coming from RBS parsing' do - map = Solargraph::RbsMap::StdlibMap.load('yaml') + map = described_class.load('yaml') store = Solargraph::ApiMap::Store.new(map.pins) constant_pins = store.get_constants('') pin = constant_pins.first diff --git a/spec/rbs_map_spec.rb b/spec/rbs_map_spec.rb index b06c975d1..09e7a1a80 100644 --- a/spec/rbs_map_spec.rb +++ b/spec/rbs_map_spec.rb @@ -1,17 +1,56 @@ +# frozen_string_literal: true + describe Solargraph::RbsMap do it 'loads from a gemspec' do spec = Gem::Specification.find_by_name('rbs') - rbs_map = Solargraph::RbsMap.from_gemspec(spec, nil, nil) + rbs_map = described_class.from_gemspec(spec, nil, nil) pin = rbs_map.path_pin('RBS::EnvironmentLoader#add_collection') - expect(pin).to be + expect(pin).not_to be_nil + end + + it 'fails if it does not find data from gemspec' do + spec = Gem::Specification.find_by_name('backport') + rbs_map = described_class.from_gemspec(spec, nil, nil) + expect(rbs_map).not_to be_resolved + end + + it 'fails if it does not find data from name' do + rbs_map = described_class.new('lskdflaksdfjl') + expect(rbs_map.pins).to be_empty end it 'converts constants and aliases to correct types' do spec = Gem::Specification.find_by_name('rbs') - rbs_map = Solargraph::RbsMap.from_gemspec(spec, nil, nil) + rbs_map = described_class.from_gemspec(spec, nil, nil) pin = rbs_map.path_pin('RBS::EnvironmentLoader::DEFAULT_CORE_ROOT') expect(pin.return_type.tag).to eq('Pathname') pin = rbs_map.path_pin('RBS::EnvironmentWalker::InstanceNode') expect(pin.return_type.tag).to eq('Class') end + + it 'processes RBS class variables' do + spec = Gem::Specification.find_by_name('rbs') + rbs_map = described_class.from_gemspec(spec, nil, nil) + store = Solargraph::ApiMap::Store.new(rbs_map.pins) + class_variable_pins = store.pins_by_class(Solargraph::Pin::ClassVariable) + count_pins = class_variable_pins.select do |pin| + pin.name.to_s == '@@count' && pin.context.to_s == 'Class' + end + expect(count_pins.length).to eq(1) + count_pin = count_pins.first + expect(count_pin.return_type.to_s).to eq('Integer') + end + + it 'processes RBS class instance variables' do + spec = Gem::Specification.find_by_name('rbs') + rbs_map = described_class.from_gemspec(spec, nil, nil) + store = Solargraph::ApiMap::Store.new(rbs_map.pins) + instance_variable_pins = store.pins_by_class(Solargraph::Pin::InstanceVariable) + root_pins = instance_variable_pins.select do |pin| + pin.name.to_s == '@root' && pin.context.to_s == 'Class' && pin.scope == :class + end + expect(root_pins.length).to eq(1) + root_pin = root_pins.first + expect(root_pin.return_type.to_s).to eq('RBS::Namespace, nil') + end end diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index 3b8dc0426..e37b3d9b6 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -10,13 +10,15 @@ before do File.open(File.join(temp_dir, 'Gemfile'), 'w') do |file| - file.puts "source 'https://rubygems.org'" - file.puts "gem 'solargraph', path: #{File.expand_path('..', __dir__)}" + file.puts "source 'https://rubygems.org'" + file.puts "gem 'solargraph', path: '#{File.expand_path('..', __dir__)}'" end - output, status = Open3.capture2e("bundle install", chdir: temp_dir) + output, status = Open3.capture2e('bundle install', chdir: temp_dir) raise "Failure installing bundle: #{output}" unless status.success? end + # @type cmd [Array] + # @return [String] def bundle_exec(*cmd) # run the command in the temporary directory with bundle exec output, status = Open3.capture2e("bundle exec #{cmd.join(' ')}", chdir: temp_dir) @@ -29,21 +31,148 @@ def bundle_exec(*cmd) FileUtils.rm_rf(temp_dir) end - describe "--version" do - it "returns a version when run" do - output = bundle_exec("solargraph", "--version") + describe '--version' do + let(:output) { bundle_exec('solargraph', '--version') } + it 'returns output' do expect(output).not_to be_empty + end + + it 'returns a version when run' do expect(output).to eq("#{Solargraph::VERSION}\n") end end - describe "uncache" do - it "uncaches without erroring out" do - output = bundle_exec("solargraph", "uncache", "solargraph") + describe 'uncache' do + it 'uncaches without erroring out' do + output = capture_stdout do + shell.uncache('backport') + end expect(output).to include('Clearing pin cache in') end + + it 'uncaches stdlib without erroring out' do + expect { shell.uncache('stdlib') }.not_to raise_error + end + + it 'uncaches core without erroring out' do + expect { shell.uncache('core') }.not_to raise_error + end + end + + describe 'scan' do + context 'with mocked dependencies' do + let(:api_map) { instance_double(Solargraph::ApiMap) } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + end + + it 'scans without erroring out' do + allow(api_map).to receive(:pins).and_return([]) + output = capture_stdout do + shell.options = { directory: 'spec/fixtures/workspace' } + shell.scan + end + + expect(output).to include('Scanned ').and include(' seconds.') + end + end + end + + describe 'typecheck' do + context 'with mocked dependencies' do + let(:type_checker) { instance_double(Solargraph::TypeChecker) } + let(:api_map) { instance_double(Solargraph::ApiMap) } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + allow(Solargraph::TypeChecker).to receive(:new).and_return(type_checker) + allow(type_checker).to receive(:problems).and_return([]) + end + + it 'typechecks without erroring out' do + output = capture_stdout do + shell.options = { level: 'normal', directory: '.' } + shell.typecheck('Gemfile') + end + + expect(output).to include('Typecheck finished in') + end + end + end + + describe 'gems' do + context 'without mocked ApiMap' do + it 'complains when gem does not exist' do + output = capture_both do + shell.gems('nonexistentgem') + end + + expect(output).to include("Gem 'nonexistentgem' not found") + end + + it 'caches core without erroring out' do + capture_both do + shell.uncache('core') + end + + expect { shell.cache('core') }.not_to raise_error + end + + it 'gives sensible error for gem that does not exist' do + output = capture_both do + shell.gems('solargraph123') + end + + expect(output).to include("Gem 'solargraph123' not found") + end + end + + context 'with mocked Workspace' do + let(:workspace) { instance_double(Solargraph::Workspace) } + let(:gemspec) { instance_double(Gem::Specification, name: 'backport') } + + before do + allow(Solargraph::Workspace).to receive(:new).and_return(workspace) + end + + it 'caches all without erroring out' do + allow(workspace).to receive(:cache_all_for_workspace!) + + _output = capture_both { shell.gems } + + expect(workspace).to have_received(:cache_all_for_workspace!) + end + + it 'caches single gem without erroring out' do + allow(workspace).to receive(:find_gem).with('backport').and_return(gemspec) + allow(workspace).to receive(:cache_gem) + + capture_both do + shell.options = { rebuild: false } + shell.gems('backport') + end + + expect(workspace).to have_received(:cache_gem).with(gemspec, out: an_instance_of(StringIO), rebuild: false) + end + end + end + + describe 'cache' do + it 'caches a stdlib gem without erroring out' do + expect { shell.cache('stringio') }.not_to raise_error + end + + context 'when gem does not exist' do + subject(:call) { shell.cache('nonexistentgem8675309') } + + it 'gives a good error message' do + # capture stderr output + expect { call }.to output(/not found/).to_stderr + end + end end # @type cmd [Array] diff --git a/spec/source/chain/array_spec.rb b/spec/source/chain/array_spec.rb index b8ef9db23..bb93820cd 100644 --- a/spec/source/chain/array_spec.rb +++ b/spec/source/chain/array_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe Solargraph::Source::Chain::Array do - it "resolves an instance of an array" do + it 'resolves an instance of an array' do literal = described_class.new([], nil) pin = literal.resolve(nil, nil, nil).first expect(pin.return_type.tag).to eq('Array') diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 3725686a7..122cc2ed7 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe Solargraph::Source::Chain::Call do - it "recognizes core methods that return subtypes" do + it 'recognizes core methods that return subtypes' do api_map = Solargraph::ApiMap.new source = Solargraph::Source.load_string(%( # @type [Array] @@ -12,7 +14,7 @@ expect(type.tag).to eq('String') end - it "recognizes core methods that return self" do + it 'recognizes core methods that return self' do api_map = Solargraph::ApiMap.new source = Solargraph::Source.load_string(%( arr = [] @@ -24,7 +26,7 @@ expect(type.tag).to eq('Array') end - it "handles super calls to same method" do + it 'handles super calls to same method' do api_map = Solargraph::ApiMap.new source = Solargraph::Source.load_string(%( class Foo @@ -44,7 +46,7 @@ def my_method expect(type.tag).to eq('Integer') end - it "infers return types based on yield call and @yieldreturn" do + it 'infers return types based on yield call and @yieldreturn' do api_map = Solargraph::ApiMap.new source = Solargraph::Source.load_string(%( class Foo @@ -61,7 +63,7 @@ def my_method(&block) expect(type.tag).to eq('Integer') end - it "infers return types based only on yield call and @yieldreturn" do + it 'infers return types based only on yield call and @yieldreturn' do api_map = Solargraph::ApiMap.new source = Solargraph::Source.load_string(%( class Foo @@ -78,10 +80,11 @@ def my_method(&block) expect(type.tag).to eq('Integer') end - it "adds virtual constructors for .new calls with conflicting return types" do + it 'adds virtual constructors for .new calls with conflicting return types' do api_map = Solargraph::ApiMap.new source = Solargraph::Source.load_string(%( class Foo + # @return [String] def self.new; end end Foo.new @@ -89,12 +92,10 @@ def self.new; end api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 11)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map(nil).locals) - # @todo This test looks invalid now. If `Foo.new` is an empty method, - # shouldn't it return `nil` or `undefined`? - # expect(type.tag).to eq('Foo') + expect(type.tag).to eq('String') end - it "infers types from macros" do + it 'infers types from macros' do source = Solargraph::Source.load_string(%( class Foo # @!macro @@ -110,7 +111,7 @@ def self.bar; end expect(type.tag).to eq('String') end - it 'infers generic types' do + it 'infers generic types from Array#reverse' do source = Solargraph::Source.load_string(%( # @type [Array] list = array_of_strings @@ -224,7 +225,8 @@ def self.bar type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) expect(type.tag).to eq('Set') chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 17)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + block_pin = api_map.source_map('test.rb').pins.find { |p| p.is_a?(Solargraph::Pin::Block) } + type = chain.infer(api_map, block_pin, api_map.source_map('test.rb').locals) expect(type.tag).to eq('Class') chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(7, 9)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) @@ -250,7 +252,9 @@ def baz expect(type.simple_tags).to eq('Integer') end - xit 'infers method return types based on method generic' do + it 'infers method return types based on method generic' do + pending('deeper inference support') + source = Solargraph::Source.load_string(%( class Foo # @Generic A @@ -289,7 +293,7 @@ def baz(&block) expect(type.simple_tags).to eq('Integer') end - it 'infers generic types' do + it 'infers generic types from @generic tag' do source = Solargraph::Source.load_string(%( # @generic GenericTypeParam class Foo @@ -315,7 +319,9 @@ def baz expect(type.tag).to eq('String') end - xit 'infers generic return types from block from yield being a return node' do + it 'infers generic return types from block from yield being a return node' do + pending('deeper inference support') + source = Solargraph::Source.load_string(%( def yielder(&blk) yield @@ -371,6 +377,21 @@ def yielder(&blk) expect(type.tag).to eq('Enumerator>') end + it 'allows calls off of nilable objects by default' do + source = Solargraph::Source.load_string(%( + # @type [String, nil] + f = foo + a = f.upcase + a + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + + chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 6)) + type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + expect(type.tag).to eq('String') + end + it 'calculates class return type based on class generic' do source = Solargraph::Source.load_string(%( # @generic A @@ -392,6 +413,21 @@ def bar; end expect(type.tag).to eq('String') end + it 'denies calls off of nilable objects when loose union mode is off' do + source = Solargraph::Source.load_string(%( + # @type [String, nil] + f = foo + a = f.upcase + a + ), 'test.rb') + api_map = Solargraph::ApiMap.new(loose_unions: false) + api_map.map source + + chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 6)) + type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + expect(type.tag).to eq('undefined') + end + it 'preserves unions in value position in Hash' do source = Solargraph::Source.load_string(%( # @param params [Hash{String => Array, Hash{String => undefined}, String, Integer}] @@ -406,8 +442,9 @@ def foo(params) api_map = Solargraph::ApiMap.new api_map.map source + foo_pin = api_map.source_map('test.rb').pins.find { |p| p.name == 'foo' } chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 8)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, foo_pin, api_map.source_map('test.rb').locals) expect(type.rooted_tags).to eq('::Array, ::Hash{::String => undefined}, ::String, ::Integer') end @@ -443,8 +480,9 @@ def foo api_map = Solargraph::ApiMap.new api_map.map source + foo_pin = api_map.source_map('test.rb').pins.find { |p| p.name == 'foo' } chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(5, 8)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, foo_pin, api_map.source_map('test.rb').locals) expect(type.rooted_tags).to eq('::Array<::String>') end @@ -464,8 +502,12 @@ def c api_map = Solargraph::ApiMap.new api_map.map source + closure_pin = api_map.source_map('test.rb').pins.find do |p| + p.is_a?(Solargraph::Pin::Block) && p.location.range.start.line == 4 + end + chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(5, 14)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, closure_pin, api_map.source_map('test.rb').locals) expect(type.tags).to eq('A::B') end @@ -485,8 +527,12 @@ def c api_map = Solargraph::ApiMap.new api_map.map source + closure_pin = api_map.source_map('test.rb').pins.find do |p| + p.is_a?(Solargraph::Pin::Block) && p.location.range.start.line == 4 + end + chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(5, 14)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, closure_pin, api_map.source_map('test.rb').locals) expect(type.rooted_tags).to eq('::A::B') end @@ -512,11 +558,17 @@ def d api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(6, 14)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + closure_pin = api_map.source_map('test.rb').pins.find do |p| + p.is_a?(Solargraph::Pin::Block) && p.location.range.start.line == 5 + end + type = chain.infer(api_map, closure_pin, api_map.source_map('test.rb').locals) expect(type.rooted_tags).to eq('::A::B').or eq('::A::B, ::A::C').or eq('::A::C, ::A::B') + closure_pin = api_map.source_map('test.rb').pins.find do |p| + p.is_a?(Solargraph::Pin::Block) && p.location.range.start.line == 10 + end chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(11, 14)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, closure_pin, api_map.source_map('test.rb').locals) # valid options here: # * emit type checker warning when adding [B.new] and type whole thing as '::A::B' # * type whole thing as '::A::B, A::C' @@ -525,7 +577,7 @@ def d expect(type.rooted_tags).not_to eq('::A::C') end - it 'qualifies types in a second Array#+ ' do + it 'qualifies types in a second Array#+' do source = Solargraph::Source.load_string(%( module A1 class B1 @@ -595,7 +647,7 @@ def k expect(clip.infer.rooted_tags).to eq('::Array<::A::D::E>') end - xit 'correctly looks up civars' do + it 'correctly looks up civars' do source = Solargraph::Source.load_string(%( class Foo BAZ = /aaa/ diff --git a/spec/source/chain/class_variable_spec.rb b/spec/source/chain/class_variable_spec.rb index 4121e9948..67a75e0c4 100644 --- a/spec/source/chain/class_variable_spec.rb +++ b/spec/source/chain/class_variable_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + describe Solargraph::Source::Chain::ClassVariable do - it "resolves class variable pins" do + it 'resolves class variable pins' do foo_pin = Solargraph::Pin::ClassVariable.new(name: '@@foo') bar_pin = Solargraph::Pin::ClassVariable.new(name: '@@bar') - api_map = double(Solargraph::ApiMap, :get_class_variable_pins => [foo_pin, bar_pin]) - link = Solargraph::Source::Chain::ClassVariable.new('@@bar') + api_map = instance_double(Solargraph::ApiMap, get_class_variable_pins: [foo_pin, bar_pin]) + link = described_class.new('@@bar') pins = link.resolve(api_map, Solargraph::Pin::ROOT_PIN, []) expect(pins.length).to eq(1) expect(pins.first.name).to eq('@@bar') diff --git a/spec/source/chain/constant_spec.rb b/spec/source/chain/constant_spec.rb index 4376650b3..6fbf54bff 100644 --- a/spec/source/chain/constant_spec.rb +++ b/spec/source/chain/constant_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe Solargraph::Source::Chain::Constant do - it "resolves constants in the current context" do + it 'resolves constants in the current context' do foo_pin = Solargraph::Pin::Constant.new(name: 'Foo', closure: Solargraph::Pin::ROOT_PIN) api_map = Solargraph::ApiMap.new api_map.index [foo_pin] diff --git a/spec/source/chain/global_variable_spec.rb b/spec/source/chain/global_variable_spec.rb index 4c5bd6bce..ec89fd599 100644 --- a/spec/source/chain/global_variable_spec.rb +++ b/spec/source/chain/global_variable_spec.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + describe Solargraph::Source::Chain::GlobalVariable do - it "resolves instance variable pins" do + it 'resolves instance variable pins' do closure = Solargraph::Pin::Namespace.new(name: 'Foo') foo_pin = Solargraph::Pin::GlobalVariable.new(closure: closure, name: '$foo') not_pin = Solargraph::Pin::InstanceVariable.new(closure: closure, name: '@bar') api_map = Solargraph::ApiMap.new api_map.index [foo_pin, not_pin] - link = Solargraph::Source::Chain::GlobalVariable.new('$foo') + link = described_class.new('$foo') pins = link.resolve(api_map, Solargraph::ComplexType.parse('Foo'), []) expect(pins.length).to eq(1) expect(pins.first.name).to eq('$foo') diff --git a/spec/source/chain/head_spec.rb b/spec/source/chain/head_spec.rb index cc63a54a3..6526d4747 100644 --- a/spec/source/chain/head_spec.rb +++ b/spec/source/chain/head_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + describe Solargraph::Source::Chain::Head do - it "returns self pins" do - head = Solargraph::Source::Chain::Head.new('self') + it 'returns self pins' do + head = described_class.new('self') npin = Solargraph::Pin::ProxyType.anonymous(Solargraph::ComplexType.parse('Foo')) ipin = head.resolve(nil, npin, []).first expect(ipin.return_type.namespace).to eq('Foo') diff --git a/spec/source/chain/instance_variable_spec.rb b/spec/source/chain/instance_variable_spec.rb index 8326a66d2..29ef714ee 100644 --- a/spec/source/chain/instance_variable_spec.rb +++ b/spec/source/chain/instance_variable_spec.rb @@ -1,17 +1,39 @@ +# frozen_string_literal: true + describe Solargraph::Source::Chain::InstanceVariable do - it "resolves instance variable pins" do - closure = Solargraph::Pin::Namespace.new(name: 'Foo') - methpin = Solargraph::Pin::Method.new(closure: closure, name: 'imeth', scope: :instance) - foo_pin = Solargraph::Pin::InstanceVariable.new(closure: methpin, name: '@foo') - bar_pin = Solargraph::Pin::InstanceVariable.new(closure: closure, name: '@foo') + it 'resolves instance variable pins' do + closure = Solargraph::Pin::Namespace.new(name: 'Foo', + location: Solargraph::Location.new('test.rb', + Solargraph::Range.from_to(1, 1, 9, 0)), + source: :closure) + methpin = Solargraph::Pin::Method.new(closure: closure, name: 'imeth', scope: :instance, + location: Solargraph::Location.new('test.rb', Solargraph::Range.from_to(1, 1, 3, 0)), + source: :methpin) + foo_pin = Solargraph::Pin::InstanceVariable.new(closure: methpin, name: '@foo', + location: Solargraph::Location.new('test.rb', Solargraph::Range.from_to(2, 0, 2, 0)), + presence: Solargraph::Range.from_to(2, 0, 2, 4), + source: :foo_pin) + bar_pin = Solargraph::Pin::InstanceVariable.new(closure: closure, name: '@foo', + location: Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 0, 5, 0)), + presence: Solargraph::Range.from_to(5, 1, 5, 4), + source: :bar_pin) api_map = Solargraph::ApiMap.new api_map.index [closure, methpin, foo_pin, bar_pin] - link = Solargraph::Source::Chain::InstanceVariable.new('@foo') + + link = described_class.new('@foo', nil, + Solargraph::Location.new('test.rb', Solargraph::Range.from_to(2, 2, 2, 3))) pins = link.resolve(api_map, methpin, []) expect(pins.length).to eq(1) + expect(pins.first.name).to eq('@foo') expect(pins.first.context.scope).to eq(:instance) - pins = link.resolve(api_map, closure, []) + # Lookup context is Class to find the civar + name_pin = Solargraph::Pin::ProxyType.anonymous(closure.binder, + # Closure is the class + closure: closure) + link = described_class.new('@foo', nil, + Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 1, 5, 2))) + pins = link.resolve(api_map, name_pin, []) expect(pins.length).to eq(1) expect(pins.first.name).to eq('@foo') expect(pins.first.context.scope).to eq(:class) diff --git a/spec/source/chain/link_spec.rb b/spec/source/chain/link_spec.rb index 39143dbbd..b303ee5b8 100644 --- a/spec/source/chain/link_spec.rb +++ b/spec/source/chain/link_spec.rb @@ -1,26 +1,28 @@ +# frozen_string_literal: true + describe Solargraph::Source::Chain::Link do - it "is undefined by default" do + it 'is undefined by default' do link = described_class.new expect(link).to be_undefined end - it "is not a constant by default" do + it 'is not a constant by default' do link = described_class.new expect(link).not_to be_constant end - it "resolves empty arrays by default" do + it 'resolves empty arrays by default' do link = described_class.new expect(link.resolve(nil, nil, nil)).to be_empty end - it "recognizes equivalent links" do + it 'recognizes equivalent links' do l1 = described_class.new('foo') l2 = described_class.new('foo') expect(l1).to eq(l2) end - it "recognizes inequivalent links" do + it 'recognizes inequivalent links' do l1 = described_class.new('foo') l2 = described_class.new('bar') expect(l1).not_to eq(l2) diff --git a/spec/source/chain/literal_spec.rb b/spec/source/chain/literal_spec.rb index a1431ae07..ce75772c5 100644 --- a/spec/source/chain/literal_spec.rb +++ b/spec/source/chain/literal_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe Solargraph::Source::Chain::Literal do - it "resolves an instance of a literal" do + it 'resolves an instance of a literal' do literal = described_class.new('String', nil) api_map = Solargraph::ApiMap.new pin = literal.resolve(api_map, nil, nil).first diff --git a/spec/source/chain/or_spec.rb b/spec/source/chain/or_spec.rb new file mode 100644 index 000000000..4a36dfe7c --- /dev/null +++ b/spec/source/chain/or_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +describe Solargraph::Source::Chain::Or do + it 'handles simple nil-removal' do + source = Solargraph::Source.load_string(%( + # @param a [Integer, nil] + def foo a + b = a || 10 + b + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.simplify_literals.rooted_tags).to eq('::Integer') + end + + it 'removes nil from more complex cases' do + source = Solargraph::Source.load_string(%( + def foo + out = ENV['BAR'] || + File.join(Dir.home, '.config', 'solargraph', 'config.yml') + out + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [3, 8]) + expect(clip.infer.simplify_literals.rooted_tags).to eq('::String') + end +end diff --git a/spec/source/chain/q_call_spec.rb b/spec/source/chain/q_call_spec.rb new file mode 100644 index 000000000..21994fb2d --- /dev/null +++ b/spec/source/chain/q_call_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +describe Solargraph::Source::Chain::QCall do + it 'understands &. in chains' do + source = Solargraph::Source.load_string(%( + # @param a [String, nil] + # @return [String, nil] + def foo a + b = a&.upcase + b + end + + b = foo 123 + b + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [5, 8]) + expect(clip.infer.to_s).to eq('String, nil') + + clip = api_map.clip_at('test.rb', [9, 6]) + expect(clip.infer.to_s).to eq('String, nil') + end +end diff --git a/spec/source/chain/z_super_spec.rb b/spec/source/chain/z_super_spec.rb index aa412dda6..fc6f5a554 100644 --- a/spec/source/chain/z_super_spec.rb +++ b/spec/source/chain/z_super_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + describe Solargraph::Source::Chain::ZSuper do - it "resolves super" do - head = Solargraph::Source::Chain::ZSuper.new('super') + it 'resolves super' do + head = described_class.new('super') npin = Solargraph::Pin::Namespace.new(name: 'Substring') scpin = Solargraph::Pin::Reference::Superclass.new(closure: npin, name: 'String') mpin = Solargraph::Pin::Method.new(closure: npin, name: 'upcase', scope: :instance, visibility: :public) diff --git a/spec/source/chain_spec.rb b/spec/source/chain_spec.rb index abc8c2b05..4cccd285c 100644 --- a/spec/source/chain_spec.rb +++ b/spec/source/chain_spec.rb @@ -1,12 +1,12 @@ describe Solargraph::Source::Chain do it "gets empty definitions for undefined links" do chain = described_class.new([Solargraph::Source::Chain::Link.new]) - expect(chain.define(nil, nil, nil)).to be_empty + expect(chain.define(nil, nil, [])).to be_empty end it "infers undefined types for undefined links" do chain = described_class.new([Solargraph::Source::Chain::Link.new]) - expect(chain.infer(nil, nil, nil)).to be_undefined + expect(chain.infer(nil, nil, [])).to be_undefined end it "calls itself undefined if any of its links are undefined" do @@ -362,7 +362,9 @@ class Bar; end expect(chain.links[1]).to be_with_block end - xit 'infers instance variables from multiple assignments' do + it 'infers instance variables from sequential assignments' do + pending('sequential assignment support') + source = Solargraph::Source.load_string(%( def foo @foo = nil @@ -428,8 +430,9 @@ def obj(foo); end str = obj.stringify ), 'test.rb') api_map = Solargraph::ApiMap.new.map(source) + obj_fn_pin = api_map.get_path_pins('Example.obj').first chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(12, 6)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, obj_fn_pin, api_map.source_map('test.rb').locals) expect(type.to_s).to eq('String') end end diff --git a/spec/source/change_spec.rb b/spec/source/change_spec.rb index 247c1e204..b9e019cfd 100644 --- a/spec/source/change_spec.rb +++ b/spec/source/change_spec.rb @@ -1,78 +1,80 @@ +# frozen_string_literal: true + describe Solargraph::Source::Change do - it "inserts a character" do + it 'inserts a character' do text = 'var' range = Solargraph::Range.from_to(0, 3, 0, 3) new_text = '.' - change = Solargraph::Source::Change.new(range, new_text) + change = described_class.new(range, new_text) updated = change.write(text) expect(updated).to eq('var.') end - it "repairs nullable characters" do + it 'repairs nullable characters' do text = 'var' range = Solargraph::Range.from_to(0, 3, 0, 3) new_text = '.' - change = Solargraph::Source::Change.new(range, new_text) + change = described_class.new(range, new_text) updated = change.write(text, true) expect(updated).to eq('var ') end - it "repairs entire changes" do + it 'repairs entire changes' do text = 'var' range = Solargraph::Range.from_to(0, 3, 0, 3) new_text = '._(!' - change = Solargraph::Source::Change.new(range, new_text) + change = described_class.new(range, new_text) updated = change.repair(text) expect(updated).to eq('var ') end - it "repairs nil ranges" do + it 'repairs nil ranges' do text = 'original' - change = Solargraph::Source::Change.new(nil, '...') + change = described_class.new(nil, '...') updated = change.repair(text) expect(updated).to eq(' ') end - it "overwrites nil ranges" do + it 'overwrites nil ranges' do text = 'foo' new_text = 'bar' - change = Solargraph::Source::Change.new(nil, new_text) + change = described_class.new(nil, new_text) updated = change.write(text) expect(updated).to eq('bar') end - it "blanks single colons in nullable changes" do + it 'blanks single colons in nullable changes' do text = 'bar' new_text = ':' range = Solargraph::Range.from_to(0, 3, 0, 3) - change = Solargraph::Source::Change.new(range, new_text) + change = described_class.new(range, new_text) updated = change.write(text, true) expect(updated).to eq('bar ') end - it "blanks double colons in nullable changes" do + it 'blanks double colons in nullable changes' do text = 'bar:' new_text = ':' range = Solargraph::Range.from_to(0, 4, 0, 4) - change = Solargraph::Source::Change.new(range, new_text) + change = described_class.new(range, new_text) updated = change.write(text, true) expect(updated).to eq('bar ') end - it "repairs preceding periods" do + it 'repairs preceding periods' do text = 'bar.' new_text = ' ' range = Solargraph::Range.from_to(0, 4, 0, 4) - change = Solargraph::Source::Change.new(range, new_text) + change = described_class.new(range, new_text) updated = change.repair(text) expect(updated).to eq('bar ') end - it "repairs preceding colons" do + it 'repairs preceding colons' do text = 'bar:' new_text = 'x' range = Solargraph::Range.from_to(0, 4, 0, 4) - change = Solargraph::Source::Change.new(range, new_text) + change = described_class.new(range, new_text) updated = change.repair(text) expect(updated).to eq('bar ') end diff --git a/spec/source/cursor_spec.rb b/spec/source/cursor_spec.rb index 150e99449..b9539c57a 100644 --- a/spec/source/cursor_spec.rb +++ b/spec/source/cursor_spec.rb @@ -1,5 +1,5 @@ describe Solargraph::Source::Cursor do - it "detects cursors in strings" do + it 'detects cursors in strings' do source = Solargraph::Source.load_string('str = "string"') cursor = described_class.new(source, Solargraph::Position.new(0, 6)) expect(cursor).not_to be_string @@ -7,7 +7,7 @@ expect(cursor).to be_string end - it "detects cursors in comments" do + it 'detects cursors in comments' do source = Solargraph::Source.load_string(%( # @type [String] var = make_a_string @@ -20,48 +20,48 @@ expect(cursor).not_to be_comment end - it "detects arguments inside parentheses" do + it 'detects arguments inside parentheses' do source = Solargraph::Source.load_string('a(1); b') - cur = described_class.new(source, Solargraph::Position.new(0,2)) + cur = described_class.new(source, Solargraph::Position.new(0, 2)) expect(cur).to be_argument - cur = described_class.new(source, Solargraph::Position.new(0,3)) + cur = described_class.new(source, Solargraph::Position.new(0, 3)) expect(cur).to be_argument - cur = described_class.new(source, Solargraph::Position.new(0,4)) + cur = described_class.new(source, Solargraph::Position.new(0, 4)) expect(cur).not_to be_argument - cur = described_class.new(source, Solargraph::Position.new(0,5)) + cur = described_class.new(source, Solargraph::Position.new(0, 5)) expect(cur).not_to be_argument - cur = described_class.new(source, Solargraph::Position.new(0,7)) + cur = described_class.new(source, Solargraph::Position.new(0, 7)) expect(cur).not_to be_argument end it 'detects arguments at opening parentheses' do source = Solargraph::Source.load_string('String.new', 'test.rb') - change = Solargraph::Source::Change.new(Solargraph::Range.from_to(0, 10, 0, 10) ,'(') + change = Solargraph::Source::Change.new(Solargraph::Range.from_to(0, 10, 0, 10), '(') updater = Solargraph::Source::Updater.new('test.rb', 1, [change]) source = source.synchronize(updater) cursor = source.cursor_at([0, 11]) expect(cursor).to be_argument end - it "detects class variables" do - source = double(:Source, :code => '@@foo') + it 'detects class variables' do + source = instance_double(Solargraph::Source, code: '@@foo') cur = described_class.new(source, Solargraph::Position.new(0, 2)) expect(cur.word).to eq('@@foo') end - it "detects instance variables" do - source = double(:Source, :code => '@foo') + it 'detects instance variables' do + source = instance_double(Solargraph::Source, code: '@foo') cur = described_class.new(source, Solargraph::Position.new(0, 1)) expect(cur.word).to eq('@foo') end - it "detects global variables" do - source = double(:Source, :code => '@foo') + it 'detects global variables' do + source = instance_double(Solargraph::Source, code: '$foo') cur = described_class.new(source, Solargraph::Position.new(0, 1)) - expect(cur.word).to eq('@foo') + expect(cur.word).to eq('$foo') end - it "generates word ranges" do + it 'generates word ranges' do source = Solargraph::Source.load_string(%( foo = bar )) @@ -69,26 +69,26 @@ expect(source.at(cur.range)).to eq('bar') end - it "generates chains" do + it 'generates chains' do source = Solargraph::Source.load_string('foo.bar(1,2).baz{}') cur = described_class.new(source, Solargraph::Position.new(0, 18)) expect(cur.chain).to be_a(Solargraph::Source::Chain) - expect(cur.chain.links.map(&:word)).to eq(['foo', 'bar', 'baz']) + expect(cur.chain.links.map(&:word)).to eq(%w[foo bar baz]) end - it "detects constant words" do - source = double(:Source, :code => 'Foo::Bar') + it 'detects constant words' do + source = instance_double(Solargraph::Source, code: 'Foo::Bar') cur = described_class.new(source, Solargraph::Position.new(0, 5)) expect(cur.word).to eq('Bar') end - it "detects cursors in dynamic strings" do + it 'detects cursors in dynamic strings' do source = Solargraph::Source.load_string('"#{100}"') cursor = source.cursor_at(Solargraph::Position.new(0, 7)) expect(cursor).to be_string end - it "detects cursors in embedded strings" do + it 'detects cursors in embedded strings' do source = Solargraph::Source.load_string('"#{100}..."') cursor = source.cursor_at(Solargraph::Position.new(0, 10)) expect(cursor).to be_string @@ -118,8 +118,8 @@ class Foo; end "#{[]}" ', 'test.rb') updater = Solargraph::Source::Updater.new('test.rb', 1, [ - Solargraph::Source::Change.new(Solargraph::Range.from_to(1, 12, 1, 12), '.') - ]) + Solargraph::Source::Change.new(Solargraph::Range.from_to(1, 12, 1, 12), '.') + ]) updated = source.synchronize(updater) cursor = updated.cursor_at(Solargraph::Position.new(1, 13)) expect(cursor).to be_string diff --git a/spec/source/source_chainer_spec.rb b/spec/source/source_chainer_spec.rb index 7a8eb9fb8..272cbb6ef 100644 --- a/spec/source/source_chainer_spec.rb +++ b/spec/source/source_chainer_spec.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + describe Solargraph::Source::SourceChainer do - it "handles trailing colons that are not namespace separators" do + it 'handles trailing colons that are not namespace separators' do source = Solargraph::Source.load_string('Foo:') map = Solargraph::SourceMap.map(source) cursor = map.cursor_at(Solargraph::Position.new(0, 4)) expect(cursor.chain.links.first).to be_undefined end - it "recognizes literal strings" do + it 'recognizes literal strings' do map = Solargraph::SourceMap.load_string("'string'") cursor = map.cursor_at(Solargraph::Position.new(0, 0)) expect(cursor.chain).not_to be_a(Solargraph::Source::Chain::Literal) @@ -15,8 +17,8 @@ expect(cursor.chain.links.first.word).to eq('<::String>') end - it "recognizes literal integers" do - map = Solargraph::SourceMap.load_string("100") + it 'recognizes literal integers' do + map = Solargraph::SourceMap.load_string('100') cursor = map.cursor_at(Solargraph::Position.new(0, 0)) expect(cursor.chain).not_to be_a(Solargraph::Source::Chain::Literal) cursor = map.cursor_at(Solargraph::Position.new(0, 1)) @@ -24,42 +26,42 @@ expect(cursor.chain.links.first.word).to eq('<::Integer>') end - it "recognizes literal regexps" do - map = Solargraph::SourceMap.load_string("/[a-z]/") + it 'recognizes literal regexps' do + map = Solargraph::SourceMap.load_string('/[a-z]/') cursor = map.cursor_at(Solargraph::Position.new(0, 0)) expect(cursor.chain.links.first).to be_a(Solargraph::Source::Chain::Literal) expect(cursor.chain.links.first.word).to eq('<::Regexp>') end - it "recognizes class variables" do + it 'recognizes class variables' do map = Solargraph::SourceMap.load_string('@@foo') cursor = map.cursor_at(Solargraph::Position.new(0, 0)) expect(cursor.chain.links.first).to be_a(Solargraph::Source::Chain::ClassVariable) expect(cursor.chain.links.first.word).to eq('@@foo') end - it "recognizes instance variables" do + it 'recognizes instance variables' do map = Solargraph::SourceMap.load_string('@foo') cursor = map.cursor_at(Solargraph::Position.new(0, 0)) expect(cursor.chain.links.first).to be_a(Solargraph::Source::Chain::InstanceVariable) expect(cursor.chain.links.first.word).to eq('@foo') end - it "recognizes global variables" do + it 'recognizes global variables' do map = Solargraph::SourceMap.load_string('$foo') cursor = map.cursor_at(Solargraph::Position.new(0, 0)) expect(cursor.chain.links.first).to be_a(Solargraph::Source::Chain::GlobalVariable) expect(cursor.chain.links.first.word).to eq('$foo') end - it "recognizes constants" do + it 'recognizes constants' do map = Solargraph::SourceMap.load_string('Foo::Bar') cursor = map.cursor_at(Solargraph::Position.new(0, 6)) expect(cursor.chain).to be_constant expect(cursor.chain.links.map(&:word)).to eq(['Foo::Bar']) end - it "recognizes unfinished constants" do + it 'recognizes unfinished constants' do map = Solargraph::SourceMap.load_string('Foo:: $something') cursor = map.cursor_at(Solargraph::Position.new(0, 5)) expect(cursor.chain).to be_constant @@ -67,11 +69,11 @@ expect(cursor.chain).to be_undefined end - it "recognizes unfinished calls" do + it 'recognizes unfinished calls' do orig = Solargraph::Source.load_string('foo.bar') updater = Solargraph::Source::Updater.new(nil, 1, [ - Solargraph::Source::Change.new(Solargraph::Range.from_to(0, 7, 0, 7), '.') - ]) + Solargraph::Source::Change.new(Solargraph::Range.from_to(0, 7, 0, 7), '.') + ]) source = orig.synchronize(updater) map = Solargraph::SourceMap.map(source) cursor = map.cursor_at(Solargraph::Position.new(0, 8)) @@ -80,25 +82,25 @@ expect(cursor.chain).to be_undefined end - it "chains signatures with square brackets" do + it 'chains signatures with square brackets' do map = Solargraph::SourceMap.load_string('foo[0].bar') cursor = map.cursor_at(Solargraph::Position.new(0, 8)) expect(cursor.chain.links.map(&:word)).to eq(['foo', '[]', 'bar']) end - it "chains signatures with curly brackets" do + it 'chains signatures with curly brackets' do map = Solargraph::SourceMap.load_string('foo{|x| x == y}.bar') cursor = map.cursor_at(Solargraph::Position.new(0, 16)) - expect(cursor.chain.links.map(&:word)).to eq(['foo', 'bar']) + expect(cursor.chain.links.map(&:word)).to eq(%w[foo bar]) end - it "chains signatures with parentheses" do + it 'chains signatures with parentheses' do map = Solargraph::SourceMap.load_string('foo(x, y).bar') cursor = map.cursor_at(Solargraph::Position.new(0, 10)) - expect(cursor.chain.links.map(&:word)).to eq(['foo', 'bar']) + expect(cursor.chain.links.map(&:word)).to eq(%w[foo bar]) end - it "chains from repaired sources with literal strings" do + it 'chains from repaired sources with literal strings' do orig = Solargraph::Source.load_string("''") updater = Solargraph::Source::Updater.new( nil, @@ -111,118 +113,116 @@ ] ) source = orig.synchronize(updater) - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(0, 3)) + chain = described_class.chain(source, Solargraph::Position.new(0, 3)) expect(chain.links.first).to be_a(Solargraph::Source::Chain::Literal) expect(chain.links.length).to eq(2) end - it "chains incomplete constants" do - source = Solargraph::Source.load_string("Foo::") - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(0, 5)) + it 'chains incomplete constants' do + source = Solargraph::Source.load_string('Foo::') + chain = described_class.chain(source, Solargraph::Position.new(0, 5)) expect(chain.links.length).to eq(2) expect(chain.links.first).to be_a(Solargraph::Source::Chain::Constant) expect(chain.links.last).to be_a(Solargraph::Source::Chain::Constant) expect(chain.links.last).to be_undefined end - it "works when source error ranges contain a nil range" do + it 'works when source error ranges contain a nil range' do orig = Solargraph::Source.load_string("msg = 'msg'\nmsg", 'test.rb') updater = Solargraph::Source::Updater.new('test.rb', 1, [ - Solargraph::Source::Change.new(nil, "msg = 'msg'\nmsg.") - ]) + Solargraph::Source::Change.new(nil, "msg = 'msg'\nmsg.") + ]) source = orig.synchronize(updater) - expect { - Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(1, 4)) - }.not_to raise_error + expect do + described_class.chain(source, Solargraph::Position.new(1, 4)) + end.not_to raise_error end - it "stops phrases at opening brackets" do + it 'stops phrases at opening brackets' do source = Solargraph::Source.load_string(%( (aa1, 2, 3) [bb2, 2, 3] {cc3, 2, 3} )) - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(1, 10)) + chain = described_class.chain(source, Solargraph::Position.new(1, 10)) expect(chain.links.first.word).to eq('aa1') - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(2, 10)) + chain = described_class.chain(source, Solargraph::Position.new(2, 10)) expect(chain.links.first.word).to eq('bb2') - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(3, 10)) + chain = described_class.chain(source, Solargraph::Position.new(3, 10)) expect(chain.links.first.word).to eq('cc3') end - it "chains instance variables from unsynchronized sources" do - source = double(Solargraph::Source, - :synchronized? => false, - :code => '@foo.', - :filename => 'test.rb', - :string_at? => false, - :comment_at? => false, - :repaired? => false, - :parsed? => true, - :error_ranges => [], - :node_at => nil, - :tree_at => [] - ) - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(0, 5)) + it 'chains instance variables from unsynchronized sources' do + source = instance_double(Solargraph::Source, + synchronized?: false, + code: '@foo.', + filename: 'test.rb', + string_at?: false, + comment_at?: false, + repaired?: false, + parsed?: true, + error_ranges: [], + node_at: nil, + tree_at: []) + chain = described_class.chain(source, Solargraph::Position.new(0, 5)) expect(chain.links.first.word).to eq('@foo') expect(chain.links.last.word).to eq('') end - it "chains class variables from unsynchronized sources" do - source = double(Solargraph::Source, - :synchronized? => false, - :code => '@@foo.', - :filename => 'test.rb', - :string_at? => false, - :comment_at? => false, - :repaired? => false, - :parsed? => true, - :error_ranges => [], - :node_at => nil, - :tree_at => [] - ) - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(0, 6)) + it 'chains class variables from unsynchronized sources' do + source = instance_double(Solargraph::Source, + synchronized?: false, + code: '@@foo.', + filename: 'test.rb', + string_at?: false, + comment_at?: false, + repaired?: false, + parsed?: true, + error_ranges: [], + node_at: nil, + tree_at: []) + chain = described_class.chain(source, Solargraph::Position.new(0, 6)) expect(chain.links.first.word).to eq('@@foo') expect(chain.links.last.word).to eq('') end - it "detects literals from chains in unsynchronized sources" do + it 'detects literals from chains in unsynchronized sources' do source1 = Solargraph::Source.load_string(%( '' )) source2 = source1.synchronize(Solargraph::Source::Updater.new( - nil, - 2, - [ - Solargraph::Source::Change.new( - Solargraph::Range.from_to(1, 8, 1, 8), - '.' - ) - ] - )) - chain = Solargraph::Source::SourceChainer.chain(source2, Solargraph::Position.new(1, 9)) + nil, + 2, + [ + Solargraph::Source::Change.new( + Solargraph::Range.from_to(1, 8, 1, 8), + '.' + ) + ] + )) + chain = described_class.chain(source2, Solargraph::Position.new(1, 9)) expect(chain.links.first).to be_a(Solargraph::Source::Chain::Literal) expect(chain.links.first.word).to eq('<::String>') expect(chain.links.last.word).to eq('') end - it "ignores ? and ! that are not method suffixes" do + it 'ignores ? and ! that are not method suffixes' do source = Solargraph::Source.load_string(%( if !t ), 'test.rb') - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(1, 11)) + chain = described_class.chain(source, Solargraph::Position.new(1, 11)) expect(chain.links.length).to eq(1) expect(chain.links.first.word).to eq('t') end - it "chains from fixed phrases in repaired sources with missing nodes" do + it 'chains from fixed phrases in repaired sources with missing nodes' do source = Solargraph::Source.load_string(%( x = [] ), 'test.rb') updater = Solargraph::Source::Updater.new('test.rb', 1, [ - Solargraph::Source::Change.new(Solargraph::Range.from_to(2, 6, 2, 6), 'x.') - ]) + Solargraph::Source::Change.new(Solargraph::Range.from_to(2, 6, 2, 6), 'x.') + ]) updated = source.synchronize(updater) cursor = updated.cursor_at(Solargraph::Position.new(2, 8)) expect(cursor.chain.links.first.word).to eq('x') @@ -230,13 +230,13 @@ it 'handles integers with dots at end of file' do source = Solargraph::Source.load_string('100.') - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(0, 4)) + chain = described_class.chain(source, Solargraph::Position.new(0, 4)) expect(chain.links.length).to eq(2) expect(chain.links.first).to be_a(Solargraph::Source::Chain::Literal) expect(chain.links.last).to be_undefined end - it 'detects whole constant with cursor at double colon' do + it 'detects whole constant with cursor at double colon - double level' do source = Solargraph::Source.load_string(%( class Outer class Inner @@ -244,11 +244,11 @@ class Inner end Outer::Inner.new ), 'test.rb') - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(5, 13)) + chain = described_class.chain(source, Solargraph::Position.new(5, 13)) expect(chain.links.last.word).to eq('Outer::Inner') end - it 'detects whole constant with cursor at double colon' do + it 'detects whole constant with cursor at double colon - triple level' do source = Solargraph::Source.load_string(%( class Outer class Inner1 @@ -258,7 +258,7 @@ class Inner2 end Outer::Inner1::Inner2.new ), 'test.rb') - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(7, 21)) + chain = described_class.chain(source, Solargraph::Position.new(7, 21)) expect(chain.links.last.word).to eq('Outer::Inner1::Inner2') end @@ -266,7 +266,7 @@ class Inner2 source = Solargraph::Source.load_string(%( foo(*optargs, **kwargs) ), 'test.rb') - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(1, 7)) + chain = described_class.chain(source, Solargraph::Position.new(1, 7)) expect(chain.links.last.arguments.length).to eq(2) end @@ -278,7 +278,7 @@ class Inner2 api_map = Solargraph::ApiMap.new api_map.map source - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(2, 9)) + chain = described_class.chain(source, Solargraph::Position.new(2, 9)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) expect(type.tag).to eq('Array') end @@ -290,12 +290,12 @@ class Inner2 api_map = Solargraph::ApiMap.new api_map.map source - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(1, 16)) + chain = described_class.chain(source, Solargraph::Position.new(1, 16)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) expect(type.tag).to eq('Array(String, Integer)') end - it 'infers tuple type when types in literal differ' do + it 'allows Array methods when tuple type in literal inferred' do source = Solargraph::Source.load_string(%( b = ['a', 'b', 123] c = b.include?('a') @@ -304,7 +304,7 @@ class Inner2 api_map = Solargraph::ApiMap.new api_map.map source - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(3, 6)) + chain = described_class.chain(source, Solargraph::Position.new(3, 6)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) expect(type.rooted_tag).to eq('::Boolean') end @@ -316,7 +316,7 @@ class Inner2 api_map = Solargraph::ApiMap.new api_map.map source - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(1, 16)) + chain = described_class.chain(source, Solargraph::Position.new(1, 16)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) expect(type.tag).to eq('Array') end @@ -331,12 +331,12 @@ def strings; end end ), 'test.rb') updater = Solargraph::Source::Updater.new('test.rb', 1, [ - Solargraph::Source::Change.new(Solargraph::Range.from_to(5, 10, 5, 10), 'if s') - ]) + Solargraph::Source::Change.new(Solargraph::Range.from_to(5, 10, 5, 10), 'if s') + ]) updated = source.synchronize(updater) api_map = Solargraph::ApiMap.new api_map.map updated - chain = Solargraph::Source::SourceChainer.chain(updated, Solargraph::Position.new(5, 14)) + chain = described_class.chain(updated, Solargraph::Position.new(5, 14)) expect(chain.node.type).to be(:send) expect(chain.node.children[1]).to be(:s) end @@ -347,8 +347,9 @@ def strings; end z end )) - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(1, 9)) - expect(chain.links.map(&:class)).to be + chain = described_class.chain(source, Solargraph::Position.new(1, 9)) + expect(chain.links.map(&:class)) + .to eq([Solargraph::Source::Chain::Call, Solargraph::Source::Chain::Call]) end it 'infers specific array type from block sent to Array#map' do @@ -358,7 +359,7 @@ def strings; end api_map = Solargraph::ApiMap.new api_map.map source - chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(1, 20)) + chain = described_class.chain(source, Solargraph::Position.new(1, 20)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) expect(type.tag).to eq('Array') end diff --git a/spec/source/updater_spec.rb b/spec/source/updater_spec.rb index 0c5f1f4c6..373072e0d 100644 --- a/spec/source/updater_spec.rb +++ b/spec/source/updater_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe Solargraph::Source::Updater do - it "applies changes" do + it 'applies changes' do text = 'foo' changes = [] range = Solargraph::Range.from_to(0, 3, 0, 3) @@ -8,12 +10,12 @@ range = Solargraph::Range.from_to(0, 4, 0, 4) new_text = 'bar' changes.push Solargraph::Source::Change.new(range, new_text) - updater = Solargraph::Source::Updater.new('file.rb', 0, changes) + updater = described_class.new('file.rb', 0, changes) updated = updater.write(text) expect(updated).to eq('foo.bar') end - it "applies repairs" do + it 'applies repairs' do text = 'foo' changes = [] range = Solargraph::Range.from_to(0, 3, 0, 3) @@ -22,18 +24,18 @@ range = Solargraph::Range.from_to(0, 4, 0, 4) new_text = 'bar' changes.push Solargraph::Source::Change.new(range, new_text) - updater = Solargraph::Source::Updater.new('file.rb', 0, changes) + updater = described_class.new('file.rb', 0, changes) updated = updater.repair(text) expect(updated).to eq('foo ') end - it "handles nil ranges" do + it 'handles nil ranges' do text = 'foo' changes = [] range = nil new_text = 'bar' changes.push Solargraph::Source::Change.new(range, new_text) - updater = Solargraph::Source::Updater.new('file.rb', 0, changes) + updater = described_class.new('file.rb', 0, changes) updated = updater.write(text) expect(updated).to eq('bar') end diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index ee7e4bcfa..67b3085b2 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + describe Solargraph::SourceMap::Clip do let(:api_map) { Solargraph::ApiMap.new } @@ -93,7 +95,7 @@ class Bar api_map = Solargraph::ApiMap.new api_map.map source clip = api_map.clip_at('test.rb', [6, 12]) - expect(clip.complete.pins.map(&:name)).to eq (['@foo']) + expect(clip.complete.pins.map(&:name)).to eq(['@foo']) end it 'completes instance variables' do @@ -288,7 +290,7 @@ def bar klass expect(clip.infer.tag).to eq('String') end - it 'infers method types from return nodes' do + it 'infers method types from return nodes - initialization' do source = Solargraph::Source.load_string(%( def foo String.new(from_object) @@ -302,6 +304,23 @@ def foo expect(type.tag).to eq('String') end + it 'infers method types from return nodes - method return type' do + source = Solargraph::Source.load_string(%( + class Foo + # @return [self] + def foo + bar + end + end + Foo.new.foo + ), 'test.rb') + map = Solargraph::ApiMap.new + map.map source + clip = map.clip_at('test.rb', Solargraph::Position.new(7, 10)) + type = clip.infer + expect(type.tag).to eq('Foo') + end + it 'infers multiple method types from return nodes' do source = Solargraph::Source.load_string(%( def foo @@ -320,7 +339,9 @@ def foo expect(type.simple_tags).to eq('String, Integer') end - xit 'uses flow-sensitive typing to infer non-nil method return type' do + it 'uses flow sensitive typing to infer non-nil method return type' do + pending('if x.nil? support in flow sensitive typing') + source = Solargraph::Source.load_string(%( # @return [Gem::Specification,nil] def find_by_name; end @@ -337,7 +358,7 @@ def spec_for_require path map.map source clip = map.clip_at('test.rb', Solargraph::Position.new(10, 10)) type = clip.infer - expect(type.to_s.split(',').map(&:strip).to_set).to eq(Set.new(['Gem::Specification'])) + expect(type.to_s.split(',').to_set(&:strip)).to eq(Set.new(['Gem::Specification'])) end it 'infers return types from method calls' do @@ -648,7 +669,7 @@ def initialize clip = api_map.clip_at('test.rb', [7, 8]) # @todo expect(clip.infer.tags).to eq('""') expect(clip.infer.tags).to eq('String') - expect(clip.infer.simple_tags).to eq("String") + expect(clip.infer.simple_tags).to eq('String') end it 'completes instance variable methods in rebound blocks' do @@ -679,17 +700,13 @@ def initialize @foo._ end end - Foo.define_method(:test2) do - @foo._ - define_method(:test4) { @foo._ } # only handle Module#define_method, other pin is ignored.. - end Foo.class_eval do define_method(:test5) { @foo._ } end ), 'test.rb') api_map = Solargraph::ApiMap.new api_map.map source - [[4, 39], [7, 15], [11, 13], [12, 37], [15, 37]].each do |loc| + [[4, 39], [7, 15], [11, 37]].each do |loc| clip = api_map.clip_at('test.rb', loc) paths = clip.complete.pins.map(&:path) expect(paths).to include('String#upcase'), -> { %(expected #{paths} at #{loc} to include "String#upcase") } @@ -1230,7 +1247,7 @@ def one updated = source.synchronize(updater) api_map.map updated clip = api_map.clip_at('test.rb', [2, 8]) - expect(clip.complete.pins.first.path).to start_with('Array#') + expect(clip.complete.pins.first&.path).to start_with('Array#') end it 'selects local variables using gated scopes' do @@ -1645,10 +1662,12 @@ def foo; end api_map = Solargraph::ApiMap.new.map(source) array_names = api_map.clip_at('test.rb', [5, 22]).complete.pins.map(&:name) - expect(array_names).to eq(["byteindex", "byterindex", "bytes", "bytesize", "byteslice", "bytesplice"]) + expect(array_names).to eq(%w[byteindex byterindex bytes bytesize byteslice bytesplice]) string_names = api_map.clip_at('test.rb', [6, 22]).complete.pins.map(&:name) - expect(string_names).to eq(['upcase', 'upcase!', 'upto']) + # can be brought in by solargraph-rails + activesupport_completions = ['upcase_first'] + expect(string_names - activesupport_completions).to eq(['upcase', 'upcase!', 'upto']) end it 'completes global methods defined in top level scope inside class when referenced inside a namespace' do @@ -1866,17 +1885,6 @@ def bad_passthrough; yield; end expect(type.to_s).to eq('undefined') end - it 'infers block-pass symbols from generics' do - source = Solargraph::Source.load_string(%( - array = [0, 1, 2] - array.max_by(&:abs) - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [2, 13]) - type = clip.infer - expect(type.to_s).to eq('Integer, nil') - end - it 'infers block-pass symbols with variant yields' do source = Solargraph::Source.load_string(%( array = [0] @@ -2007,7 +2015,7 @@ def foo ), 'test.rb') api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [8, 6]) + clip = api_map.clip_at('test.rb', [9, 6]) type = clip.infer expect(type.tags).to eq('Integer') @@ -2284,17 +2292,6 @@ def meth arg, arg2 expect(type.to_s).to eq('Array(String, Integer)') end - it 'infers array of identical diverse arrays into tuples' do - source = Solargraph::Source.load_string(%( - h = [['foo', 1], ['bar', 2]] - h - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [2, 6]) - type = clip.infer - expect(type.to_s).to eq('Array') - end - it 'infers literal diverse array of diverse arrays into tuple of tuples' do source = Solargraph::Source.load_string(%( h = [['foo', 1], ['bar', :baz]] @@ -2473,101 +2470,6 @@ def [](index); end expect(clip.infer.to_s).to eq('Float') end - it 'can use strings and symbols to choose a signature' do - source = Solargraph::Source.load_string(%( - # @generic A - # @generic B - class Foo - # @overload find(index) - # @param [String] index - # @return [generic] - # @overload find(index) - # @param [Symbol] index - # @return [generic] - def find(index); end - end - - # @type [Foo(String, Integer)] - m = blah - mb = m.find('foo') - mb - mc = m.find(:bar) - mc -), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [16, 6]) - expect(clip.infer.to_s).to eq('String') - - clip = api_map.clip_at('test.rb', [18, 6]) - expect(clip.infer.to_s).to eq('Integer') - end - - it 'uses types to determine overload of [] to match' do - source = Solargraph::Source.load_string(%( - # @generic A - # @generic B - class Foo - # @overload [](index) - # @param [String] index - # @return [generic] - # @overload [](index) - # @param [Symbol] index - # @return [generic] - def [](index); end - end - - # @type [Foo(String, Integer)] - m = blah - mb = m['foo'] - mb - mc = m[:bar] - mc -), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [16, 6]) - expect(clip.infer.to_s).to eq('String') - - clip = api_map.clip_at('test.rb', [18, 6]) - expect(clip.infer.to_s).to eq('Integer') - end - - it 'uses literal types to determine overload of [] to match' do - source = Solargraph::Source.load_string(%( - # @generic A - # @generic B - class Foo - # @overload [](index) - # @param [1] index - # @return [generic] - # @overload [](index) - # @param [2] index - # @return [generic] - # @overload [](index) - # @param [Integer] index - # @return [Float] - def [](index); end - end - - # @type [Foo(String, Integer)] - m = blah - mb = m[1] - mb - mc = m[2] - mc - md = m[3] - md -), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [19, 6]) - expect(clip.infer.to_s).to eq('String') - - clip = api_map.clip_at('test.rb', [21, 6]) - expect(clip.infer.to_s).to eq('Integer') - - clip = api_map.clip_at('test.rb', [23, 6]) - expect(clip.infer.to_s).to eq('Float') - end - it 'interprets self type in superclass method return type' do source = Solargraph::Source.load_string(%( class Foo @@ -2627,7 +2529,9 @@ def bar; end expect(clip.infer.to_s).to eq('Foo') end - xit 'replaces nil with reassignments' do + it 'replaces nil with reassignments' do + pending 'sequential assignment support' + source = Solargraph::Source.load_string(%( bar = nil bar @@ -2642,7 +2546,9 @@ def bar; end expect(clip.infer.to_s).to eq('Integer') end - xit 'replaces type with reassignments' do + it 'replaces type with reassignments' do + pending 'sequential assignment support' + source = Solargraph::Source.load_string(%( bar = 'a' bar @@ -2667,10 +2573,12 @@ def bar; end ), 'test.rb') api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [5, 6]) - expect(clip.infer.to_s).to eq('String, nil') + expect(clip.infer.to_s).to eq('nil, String') end - xit 'replaces nil with alternate reassignments' do + it 'replaces nil with alternate reassignments' do + pending 'conditional assignment support' + source = Solargraph::Source.load_string(%( bar = nil if baz @@ -2685,7 +2593,9 @@ def bar; end expect(clip.infer.to_s).to eq('Symbol, Integer') end - xit 'replaces type with alternate reassignments' do + it 'replaces type with alternate reassignments' do + pending 'conditional assignment support' + source = Solargraph::Source.load_string(%( bar = 'a' if baz @@ -2712,7 +2622,7 @@ def bar; end ), 'test.rb') api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [7, 6]) - expect(clip.infer.to_s).to eq(':foo, 123, nil') + expect(clip.infer.to_s).to eq('nil, 123, :foo') end it 'expands type with conditional reassignments' do @@ -2728,7 +2638,7 @@ def bar; end api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [7, 6]) # The order of the types can vary between platforms - expect(clip.infer.items.map(&:to_s).sort).to eq(["123", ":foo", "String"]) + expect(clip.infer.items.map(&:to_s).sort).to eq(['123', ':foo', 'String']) end it 'does not map Module methods into an Object' do @@ -2950,7 +2860,7 @@ def foo expect(clip.infer.to_s).to eq('Array, Hash, Integer, nil') end - xit 'infers that type of argument has been overridden' do + it 'infers that type of argument has been overridden' do source = Solargraph::Source.load_string(%( def foo a a = 'foo' @@ -2963,7 +2873,9 @@ def foo a expect(clip.infer.to_s).to eq('String') end - xit 'preserves hash value when it is a union with brackets' do + it 'preserves hash value when it is a union with brackets' do + pending 'union in bracket support' + source = Solargraph::Source.load_string(%( # @type [Hash{String => [Array, Hash, Integer, nil]}] raw_data = {} @@ -2989,19 +2901,6 @@ def foo a expect(clip.infer.to_s).to eq('Array') end - xit 'preserves hash value when it is a union with brackets' do - source = Solargraph::Source.load_string(%( - # @type [Hash{String => [Array, Hash, Integer, nil]}] - raw_data = {} - a = raw_data['domains'] - a - ), 'test.rb') - - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 6]) - expect(clip.infer.to_s).to eq('Array, Hash, Integer, nil') - end - it 'handles block method super scenarios' do source = Solargraph::Source.load_string(%( class Foo @@ -3283,7 +3182,7 @@ def foo expect(names).not_to include('bar=', 'baz=') end - it 'completes Struct methods via const assignment without a block' do + it 'completes Data methods via const assignment without a block' do source = Solargraph::Source.load_string(%( # @param bar [String] # @param baz [Integer] diff --git a/spec/source_map/mapper_spec.rb b/spec/source_map/mapper_spec.rb index 96d2bdab5..02cd17e29 100644 --- a/spec/source_map/mapper_spec.rb +++ b/spec/source_map/mapper_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + describe Solargraph::SourceMap::Mapper do - it "ignores include calls that are not attached to the current namespace" do + it 'ignores include calls that are not attached to the current namespace' do source = Solargraph::Source.new(%( class Foo include Direct @@ -8,14 +10,14 @@ class Foo end )) map = Solargraph::SourceMap.map(source) - pins = map.pins.select{|pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.namespace == 'Foo'} + pins = map.pins.select { |pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.namespace == 'Foo' } names = pins.map(&:name) expect(names).to include('Direct') expect(names).not_to include('Indirect') expect(names).to include('Interior') end - it "ignores prepend calls that are not attached to the current namespace" do + it 'ignores prepend calls that are not attached to the current namespace' do source = Solargraph::Source.new(%( class Foo prepend Direct @@ -24,14 +26,14 @@ class Foo end )) map = Solargraph::SourceMap.map(source) - pins = map.pins.select{|pin| pin.is_a?(Solargraph::Pin::Reference::Prepend) && pin.namespace == 'Foo'} + pins = map.pins.select { |pin| pin.is_a?(Solargraph::Pin::Reference::Prepend) && pin.namespace == 'Foo' } names = pins.map(&:name) expect(names).to include('Direct') expect(names).not_to include('Indirect') expect(names).to include('Interior') end - it "ignores extend calls that are not attached to the current namespace" do + it 'ignores extend calls that are not attached to the current namespace' do source = Solargraph::Source.new(%( class Foo extend Direct @@ -40,17 +42,17 @@ class Foo end )) map = Solargraph::SourceMap.map(source) - foo_pin = map.pins.select{|pin| pin.path == 'Foo'}.first + map.pins.select { |pin| pin.path == 'Foo' }.first # expect(foo_pin.extend_references.map(&:name)).to include('Direct') # expect(foo_pin.extend_references.map(&:name)).not_to include('Indirect') - pins = map.pins.select{|pin| pin.is_a?(Solargraph::Pin::Reference::Extend) && pin.namespace == 'Foo'} + pins = map.pins.select { |pin| pin.is_a?(Solargraph::Pin::Reference::Extend) && pin.namespace == 'Foo' } names = pins.map(&:name) expect(names).to include('Direct') expect(names).not_to include('Indirect') expect(names).to include('Interior') end - it "sets scopes for attributes" do + it 'sets scopes for attributes' do source = Solargraph::Source.new(%( module Foo attr_reader :bar1 @@ -60,13 +62,13 @@ class << self end )) map = Solargraph::SourceMap.map(source) - bar1 = map.pins.select{|pin| pin.name == 'bar1'}.first + bar1 = map.pins.select { |pin| pin.name == 'bar1' }.first expect(bar1.scope).to eq(:instance) - bar2 = map.pins.select{|pin| pin.name == 'bar2'}.first + bar2 = map.pins.select { |pin| pin.name == 'bar2' }.first expect(bar2.scope).to eq(:class) end - it "sets attribute visibility" do + it 'sets attribute visibility' do map = Solargraph::SourceMap.load_string(%( module Foo attr_reader :default_public_method @@ -84,7 +86,7 @@ module Foo expect(map.first_pin('Foo#explicit_public_method').visibility).to eq(:public) end - it "processes method directives" do + it 'processes method directives' do map = Solargraph::SourceMap.load_string(%( class Foo # @!method bar(baz) @@ -119,7 +121,7 @@ class Foo expect(pin.scope).to eq(:class) end - it "processes attribute reader directives" do + it 'processes attribute reader directives' do map = Solargraph::SourceMap.load_string(%( class Foo # @!attribute [r] bar @@ -131,7 +133,7 @@ class Foo expect(pin.return_type.tag).to eq('String') end - it "processes attribute writer directives" do + it 'processes attribute writer directives' do map = Solargraph::SourceMap.load_string(%( class Foo # @!attribute [w] bar @@ -143,7 +145,7 @@ class Foo expect(pin.return_type.tag).to eq('String') end - it "processes attribute accessor directives" do + it 'processes attribute accessor directives' do map = Solargraph::SourceMap.load_string(%( class Foo # @!attribute [r,w] bar @@ -157,7 +159,7 @@ class Foo expect(pin.return_type.tag).to eq('String') end - it "processes default attribute directives" do + it 'processes default attribute directives' do map = Solargraph::SourceMap.load_string(%( class Foo # @!attribute bar @@ -171,7 +173,7 @@ class Foo expect(pin.return_type.tag).to eq('String') end - it "processes attribute directives attached to methods" do + it 'processes attribute directives attached to methods' do map = Solargraph::SourceMap.load_string(%( class Foo # @!attribute [r] bar @@ -184,7 +186,7 @@ def make_bar_attr expect(pin.return_type.tag).to eq('String') end - it "processes private visibility directives attached to methods" do + it 'processes private visibility directives attached to methods' do map = Solargraph::SourceMap.load_string(%( class Foo # @!visibility private @@ -195,7 +197,7 @@ def bar expect(map.first_pin('Foo#bar').visibility).to be(:private) end - it "processes protected visibility directives attached to methods" do + it 'processes protected visibility directives attached to methods' do map = Solargraph::SourceMap.load_string(%( class Foo # @!visibility protected @@ -206,7 +208,7 @@ def bar expect(map.first_pin('Foo#bar').visibility).to be(:protected) end - it "processes public visibility directives attached to methods" do + it 'processes public visibility directives attached to methods' do map = Solargraph::SourceMap.load_string(%( class Foo # @!visibility public @@ -217,7 +219,7 @@ def bar expect(map.first_pin('Foo#bar').visibility).to be(:public) end - it "does not process attached visibility directives on other methods" do + it 'does not process attached visibility directives on other methods' do map = Solargraph::SourceMap.load_string(%( class Example # @!visibility private @@ -232,7 +234,7 @@ def method2; end expect(method2.visibility).to be(:public) end - it "processes class-wide private visibility directives" do + it 'processes class-wide private visibility directives' do map = Solargraph::SourceMap.load_string(%( class Example # @!visibility private @@ -253,7 +255,7 @@ def method3; end expect(method3.visibility).to be(:public) end - it "processes attribute directives at class endings" do + it 'processes attribute directives at class endings' do map = Solargraph::SourceMap.load_string(%( class Foo # @!attribute [r] bar @@ -264,43 +266,43 @@ class Foo expect(pin.return_type.tag).to eq('String') end - it "finds assignment nodes for local variables using nil guards" do + it 'finds assignment nodes for local variables using nil guards' do map = Solargraph::SourceMap.load_string(%( x ||= [] )) pin = map.locals.first # @todo Dirty test - expect([:ZLIST, :ZARRAY, :array]).to include(pin.assignment.type) + expect(%i[ZLIST ZARRAY array]).to include(pin.assignment.type) end - it "finds assignment nodes for instance variables using nil guards" do + it 'finds assignment nodes for instance variables using nil guards' do map = Solargraph::SourceMap.load_string(%( @x ||= [] )) pin = map.pins.last # @todo Dirty test - expect([:ZLIST, :ZARRAY, :array]).to include(pin.assignment.type) + expect(%i[ZLIST ZARRAY array]).to include(pin.assignment.type) end - it "finds assignment nodes for class variables using nil guards" do + it 'finds assignment nodes for class variables using nil guards' do map = Solargraph::SourceMap.load_string(%( @@x ||= [] )) pin = map.pins.last # @todo Dirty test - expect([:ZLIST, :ZARRAY, :array]).to include(pin.assignment.type) + expect(%i[ZLIST ZARRAY array]).to include(pin.assignment.type) end - it "finds assignment nodes for global variables using nil guards" do + it 'finds assignment nodes for global variables using nil guards' do map = Solargraph::SourceMap.load_string(%( $x ||= [] )) pin = map.pins.last # @todo Dirty test - expect([:ZLIST, :ZARRAY, :array]).to include(pin.assignment.type) + expect(%i[ZLIST ZARRAY array]).to include(pin.assignment.type) end - it "requalifies namespace definitions with leading colons" do + it 'requalifies namespace definitions with leading colons' do map = Solargraph::SourceMap.load_string(%( class Foo class ::Bar; end @@ -311,7 +313,7 @@ class ::Bar; end expect(map.pins.map(&:path)).not_to include('Foo::Bar') end - it "maps method parameters" do + it 'maps method parameters' do map = Solargraph::SourceMap.load_string(%( class Foo def bar baz, boo = 'boo', key: 'value' @@ -319,16 +321,16 @@ def bar baz, boo = 'boo', key: 'value' end )) pin = map.first_pin('Foo#bar') - expect(pin.parameter_names).to eq(['baz', 'boo', 'key']) - pin = map.locals.select{|p| p.name == 'baz'}.first + expect(pin.parameter_names).to eq(%w[baz boo key]) + pin = map.locals.select { |p| p.name == 'baz' }.first expect(pin).to be_a(Solargraph::Pin::Parameter) - pin = map.locals.select{|p| p.name == 'boo'}.first + pin = map.locals.select { |p| p.name == 'boo' }.first expect(pin).to be_a(Solargraph::Pin::Parameter) - pin = map.locals.select{|p| p.name == 'key'}.first + pin = map.locals.select { |p| p.name == 'key' }.first expect(pin).to be_a(Solargraph::Pin::Parameter) end - it "maps method splat parameters" do + it 'maps method splat parameters' do map = Solargraph::SourceMap.load_string(%( class Foo def bar *baz @@ -340,7 +342,7 @@ def bar *baz expect(pin.parameters.first.name).to eq('baz') end - it "maps method block parameters" do + it 'maps method block parameters' do map = Solargraph::SourceMap.load_string(%( class Foo def bar &block @@ -352,18 +354,18 @@ def bar &block expect(pin.parameters.first.name).to eq('block') end - it "adds superclasses to class pins" do + it 'adds superclasses to class pins' do map = Solargraph::SourceMap.load_string(%( class Sub < Sup; end )) # pin = map.first_pin('Sub') # expect(pin.superclass_reference.name).to eq('Sup') - pin = map.pins.select{|p| p.is_a?(Solargraph::Pin::Reference::Superclass)}.first + pin = map.pins.select { |p| p.is_a?(Solargraph::Pin::Reference::Superclass) }.first expect(pin.namespace).to eq('Sub') expect(pin.name).to eq('Sup') end - it "modifies scope and visibility for module functions" do + it 'modifies scope and visibility for module functions' do map = Solargraph::SourceMap.load_string(%( module Functions module_function @@ -376,7 +378,7 @@ def foo; end expect(pin.visibility).to eq(:private) end - it "recognizes single module functions" do + it 'recognizes single module functions' do map = Solargraph::SourceMap.load_string(%( module Functions module_function def foo; end @@ -391,7 +393,7 @@ def bar; end expect(pin.visibility).to eq(:public) end - it "remaps methods for module_function symbol arguments" do + it 'remaps methods for module_function symbol arguments' do map = Solargraph::SourceMap.load_string(%( module Functions def foo @@ -409,13 +411,13 @@ def bar expect(pin.visibility).to eq(:private) pin = map.first_pin('Functions#bar') expect(pin.visibility).to eq(:public) - pin = map.pins.select{|p| p.name == '@foo' and p.context.scope == :class}.first + pin = map.pins.select { |p| p.name == '@foo' and p.context.scope == :class }.first expect(pin).to be_a(Solargraph::Pin::InstanceVariable) - pin = map.pins.select{|p| p.name == '@foo' and p.context.scope == :instance}.first + pin = map.pins.select { |p| p.name == '@foo' and p.context.scope == :instance }.first expect(pin).to be_a(Solargraph::Pin::InstanceVariable) end - it "modifies instance variables in module functions" do + it 'modifies instance variables in module functions' do map = Solargraph::SourceMap.load_string(%( module Functions module_function @@ -425,46 +427,46 @@ def foo end end )) - pin = map.pins.select{|p| p.name == '@foo' and p.context.scope == :class}.first + pin = map.pins.select { |p| p.name == '@foo' and p.context.scope == :class }.first expect(pin).to be_a(Solargraph::Pin::InstanceVariable) - pin = map.pins.select{|p| p.name == '@foo' and p.context.scope == :instance}.first + pin = map.pins.select { |p| p.name == '@foo' and p.context.scope == :instance }.first expect(pin).to be_a(Solargraph::Pin::InstanceVariable) - pin = map.pins.select{|p| p.name == '@bar' and p.context.scope == :class}.first + pin = map.pins.select { |p| p.name == '@bar' and p.context.scope == :class }.first expect(pin).to be_a(Solargraph::Pin::InstanceVariable) - pin = map.pins.select{|p| p.name == '@bar' and p.context.scope == :instance}.first + pin = map.pins.select { |p| p.name == '@bar' and p.context.scope == :instance }.first expect(pin).to be_a(Solargraph::Pin::InstanceVariable) end - it "maps class variables" do + it 'maps class variables' do map = Solargraph::SourceMap.load_string(%( class Foo @@bar = 'bar' @@baz ||= 'baz' end )) - pin = map.pins.select{|p| p.name == '@@bar'}.first + pin = map.pins.select { |p| p.name == '@@bar' }.first expect(pin).to be_a(Solargraph::Pin::ClassVariable) - pin = map.pins.select{|p| p.name == '@@baz'}.first + pin = map.pins.select { |p| p.name == '@@baz' }.first expect(pin).to be_a(Solargraph::Pin::ClassVariable) end - it "maps local variables" do + it 'maps local variables' do map = Solargraph::SourceMap.load_string(%( x = y )) - pin = map.locals.select{|p| p.name == 'x'}.first + pin = map.locals.select { |p| p.name == 'x' }.first expect(pin).to be_a(Solargraph::Pin::LocalVariable) end - it "maps global variables" do + it 'maps global variables' do map = Solargraph::SourceMap.load_string(%( $x = y )) - pin = map.pins.select{|p| p.name == '$x'}.first + pin = map.pins.select { |p| p.name == '$x' }.first expect(pin).to be_a(Solargraph::Pin::GlobalVariable) end - it "maps constants" do + it 'maps constants' do map = Solargraph::SourceMap.load_string(%( module Foo BAR = 'bar' @@ -474,7 +476,7 @@ module Foo expect(pin).to be_a(Solargraph::Pin::Constant) end - it "maps singleton methods" do + it 'maps singleton methods' do map = Solargraph::SourceMap.load_string(%( class Foo def self.bar; end @@ -485,7 +487,7 @@ def self.bar; end expect(pin.context.scope).to be(:class) end - it "maps requalified singleton methods" do + it 'maps requalified singleton methods' do map = Solargraph::SourceMap.load_string(%( class Foo; end class Bar @@ -505,7 +507,7 @@ def boo; end expect(pin.context.scope).to be(:instance) end - it "maps private class methods" do + it 'maps private class methods' do map = Solargraph::SourceMap.load_string(%( class Foo def self.bar; end @@ -517,7 +519,7 @@ def self.bar; end expect(pin.visibility).to be(:private) end - it "maps singly defined private class methods" do + it 'maps singly defined private class methods' do map = Solargraph::SourceMap.load_string(%( class Foo private_class_method def bar; end @@ -528,7 +530,7 @@ class Foo expect(pin.visibility).to be(:private) end - it "maps private constants" do + it 'maps private constants' do map = Solargraph::SourceMap.load_string(%( class Foo BAR = 'bar' @@ -540,7 +542,7 @@ class Foo expect(pin.visibility).to be(:private) end - it "maps private namespaces" do + it 'maps private namespaces' do map = Solargraph::SourceMap.load_string(%( class Foo class Bar; end @@ -552,7 +554,7 @@ class Bar; end expect(pin.visibility).to be(:private) end - it "maps attribute writers" do + it 'maps attribute writers' do map = Solargraph::SourceMap.load_string(%( class Foo attr_writer :bar @@ -562,7 +564,7 @@ class Foo expect(map.pins.map(&:path)).not_to include('Foo#bar') end - it "maps attribute accessors" do + it 'maps attribute accessors' do map = Solargraph::SourceMap.load_string(%( class Foo attr_accessor :bar @@ -572,29 +574,29 @@ class Foo expect(map.pins.map(&:path)).to include('Foo#bar') end - it "maps extend self" do + it 'maps extend self' do map = Solargraph::SourceMap.load_string(%( class Foo extend self def bar; end end )) - pin = map.first_pin('Foo') + map.first_pin('Foo') # expect(pin.extend_references.map(&:name)).to include('Foo') - pin = map.pins.select{|p| p.is_a?(Solargraph::Pin::Reference::Extend)}.first + pin = map.pins.select { |p| p.is_a?(Solargraph::Pin::Reference::Extend) }.first expect(pin.namespace).to eq('Foo') expect(pin.name).to eq('Foo') end - it "maps require calls" do + it 'maps require calls' do map = Solargraph::SourceMap.load_string(%( require 'set' )) - pin = map.pins.select{|p| p.is_a?(Solargraph::Pin::Reference::Require)}.first + pin = map.pins.select { |p| p.is_a?(Solargraph::Pin::Reference::Require) }.first expect(pin.name).to eq('set') end - it "ignores dynamic require calls" do + it 'ignores dynamic require calls' do map = Solargraph::SourceMap.load_string(%( path = 'solargraph' require path @@ -602,16 +604,16 @@ def bar; end expect(map.requires.length).to eq(0) end - it "maps block parameters" do + it 'maps block parameters' do map = Solargraph::SourceMap.load_string(%( x.each do |y| end )) - pin = map.locals.select{|p| p.name == 'y'}.first + pin = map.locals.select { |p| p.name == 'y' }.first expect(pin).to be_a(Solargraph::Pin::Parameter) end - it "forces initialize methods to be private" do + it 'forces initialize methods to be private' do map = Solargraph::SourceMap.load_string(' class Foo def initialize name @@ -622,7 +624,7 @@ def initialize name expect(pin.visibility).to be(:private) end - it "maps top-level methods" do + it 'maps top-level methods' do map = Solargraph::SourceMap.load_string(%( def foo(bar, baz) end @@ -632,18 +634,18 @@ def foo(bar, baz) expect(pin).to be_a(Solargraph::Pin::Method) end - it "maps root blocks to class scope" do + it 'maps root blocks to class scope' do smap = Solargraph::SourceMap.load_string(%( @a = some_array @a.each do |b| b end ), 'test.rb') - pin = smap.pins.select{|p| p.is_a?(Solargraph::Pin::Block)}.first + pin = smap.pins.select { |p| p.is_a?(Solargraph::Pin::Block) }.first expect(pin.context.scope).to eq(:class) end - it "maps class method blocks to class scope" do + it 'maps class method blocks to class scope' do smap = Solargraph::SourceMap.load_string(%( class Foo def self.bar @@ -654,11 +656,11 @@ def self.bar end end )) - pin = smap.pins.select{|p| p.is_a?(Solargraph::Pin::Block)}.first + pin = smap.pins.select { |p| p.is_a?(Solargraph::Pin::Block) }.first expect(pin.context.scope).to eq(:class) end - it "maps instance method blocks to instance scope" do + it 'maps instance method blocks to instance scope' do smap = Solargraph::SourceMap.load_string(%( class Foo def bar @@ -669,11 +671,11 @@ def bar end end )) - pin = smap.pins.select{|p| p.is_a?(Solargraph::Pin::Block)}.first + pin = smap.pins.select { |p| p.is_a?(Solargraph::Pin::Block) }.first expect(pin.context.scope).to eq(:instance) end - it "maps rebased namespaces without leading colons" do + it 'maps rebased namespaces without leading colons' do smap = Solargraph::SourceMap.load_string(%( class Foo class ::Bar @@ -686,79 +688,79 @@ def baz; end expect(smap.first_pin('Bar#baz')).to be_a(Solargraph::Pin::Method) end - it "maps contexts of constants" do + it 'maps contexts of constants' do var = 'BAR' smap = Solargraph::SourceMap.load_string("#{var} = nil") - pin = smap.pins.select{|p| p.name == var}.first + pin = smap.pins.select { |p| p.name == var }.first expect(pin.context).to be_a(Solargraph::ComplexType) smap = Solargraph::SourceMap.load_string("#{var} ||= nil") - pin = smap.pins.select{|p| p.name == var}.first + pin = smap.pins.select { |p| p.name == var }.first expect(pin.context).to be_a(Solargraph::ComplexType) end - it "maps contexts of instance variables" do + it 'maps contexts of instance variables' do var = '@bar' smap = Solargraph::SourceMap.load_string("#{var} = nil") - pin = smap.pins.select{|p| p.name == var}.first + pin = smap.pins.select { |p| p.name == var }.first expect(pin.context).to be_a(Solargraph::ComplexType) smap = Solargraph::SourceMap.load_string("#{var} ||= nil") - pin = smap.pins.select{|p| p.name == var}.first + pin = smap.pins.select { |p| p.name == var }.first expect(pin.context).to be_a(Solargraph::ComplexType) end - it "maps contexts of class variables" do + it 'maps contexts of class variables' do var = '@@bar' smap = Solargraph::SourceMap.load_string("#{var} = nil") - pin = smap.pins.select{|p| p.name == var}.first + pin = smap.pins.select { |p| p.name == var }.first expect(pin.context).to be_a(Solargraph::ComplexType) smap = Solargraph::SourceMap.load_string("#{var} ||= nil") - pin = smap.pins.select{|p| p.name == var}.first + pin = smap.pins.select { |p| p.name == var }.first expect(pin.context).to be_a(Solargraph::ComplexType) end - it "maps contexts of global variables" do + it 'maps contexts of global variables' do var = '$bar' smap = Solargraph::SourceMap.load_string("#{var} = nil") - pin = smap.pins.select{|p| p.name == var}.first + pin = smap.pins.select { |p| p.name == var }.first expect(pin.context).to be_a(Solargraph::ComplexType) smap = Solargraph::SourceMap.load_string("#{var} ||= nil") - pin = smap.pins.select{|p| p.name == var}.first + pin = smap.pins.select { |p| p.name == var }.first expect(pin.context).to be_a(Solargraph::ComplexType) end - it "maps contexts of local variables" do + it 'maps contexts of local variables' do var = 'bar' smap = Solargraph::SourceMap.load_string("#{var} = nil") - pin = smap.locals.select{|p| p.name == var}.first + pin = smap.locals.select { |p| p.name == var }.first expect(pin.context).to be_a(Solargraph::ComplexType) smap = Solargraph::SourceMap.load_string("#{var} ||= nil") - pin = smap.locals.select{|p| p.name == var}.first + pin = smap.locals.select { |p| p.name == var }.first expect(pin.context).to be_a(Solargraph::ComplexType) end - it "maps method aliases" do + it 'maps method aliases' do smap = Solargraph::SourceMap.load_string(%( class Foo def bar; end alias baz bar end )) - pin = smap.pins.select{|p| p.path == 'Foo#baz'}.first + pin = smap.pins.select { |p| p.path == 'Foo#baz' }.first expect(pin).to be_a(Solargraph::Pin::MethodAlias) end - it "maps attribute aliases" do + it 'maps attribute aliases' do smap = Solargraph::SourceMap.load_string(%( class Foo attr_accessor :bar alias baz bar end )) - pin = smap.pins.select{|p| p.path == 'Foo#baz'}.first + pin = smap.pins.select { |p| p.path == 'Foo#baz' }.first expect(pin).to be_a(Solargraph::Pin::MethodAlias) end - it "maps class method aliases" do + it 'maps class method aliases' do smap = Solargraph::SourceMap.load_string(%( class Foo class << self @@ -767,12 +769,12 @@ def bar; end end end )) - pin = smap.pins.select{|p| p.path == 'Foo.baz'}.first + pin = smap.pins.select { |p| p.path == 'Foo.baz' }.first expect(pin).to be_a(Solargraph::Pin::MethodAlias) expect(pin.location.range.start.line).to eq(4) end - it "maps method macros" do + it 'maps method macros' do smap = Solargraph::SourceMap.load_string(%( class Foo # @!macro @@ -780,23 +782,23 @@ class Foo def make klass; end end ), 'test.rb') - pin = smap.pins.select{|p| p.path == 'Foo#make'}.first + pin = smap.pins.select { |p| p.path == 'Foo#make' }.first expect(pin.macros).not_to be_empty end - it "maps method directives" do + it 'maps method directives' do smap = Solargraph::SourceMap.load_string(%( class Foo # @!method bar(baz) # @return [String] end ), 'test.rb') - pin = smap.pins.select{|p| p.path == 'Foo#bar'}.first + pin = smap.pins.select { |p| p.path == 'Foo#bar' }.first expect(pin.return_type.tag).to eq('String') expect(pin.location.filename).to eq('test.rb') end - it "maps aliases from alias_method" do + it 'maps aliases from alias_method' do smap = Solargraph::SourceMap.load_string(%( class Foo class << self @@ -805,22 +807,22 @@ def bar; end end end )) - pin = smap.pins.select{|p| p.path == 'Foo.baz'}.first + pin = smap.pins.select { |p| p.path == 'Foo.baz' }.first expect(pin).to be_a(Solargraph::Pin::MethodAlias) expect(pin.location.range.start.line).to eq(4) end - it "maps aliases with unknown bases" do + it 'maps aliases with unknown bases' do smap = Solargraph::SourceMap.load_string(%( class Foo alias bar baz end )) - pin = smap.pins.select{|p| p.path == 'Foo#bar'}.first + pin = smap.pins.select { |p| p.path == 'Foo#bar' }.first expect(pin).to be_a(Solargraph::Pin::MethodAlias) end - it "maps aliases to superclass methods" do + it 'maps aliases to superclass methods' do smap = Solargraph::SourceMap.load_string(%( class Sup # My foo method @@ -830,33 +832,33 @@ class Sub < Sup alias bar foo end )) - pin = smap.pins.select{|p| p.path == 'Sub#bar'}.first + pin = smap.pins.select { |p| p.path == 'Sub#bar' }.first expect(pin).to be_a(Solargraph::Pin::MethodAlias) end - it "uses nodes for method parameter assignments" do + it 'uses nodes for method parameter assignments' do smap = Solargraph::SourceMap.load_string(%( class Foo def bar(baz = quz) end end )) - pin = smap.locals.select{|p| p.name == 'baz'}.first + pin = smap.locals.select { |p| p.name == 'baz' }.first # expect(pin.assignment).to be_a(Parser::AST::Node) expect(Solargraph::Parser.is_ast_node?(pin.assignment)).to be(true) end - it "defers resolution of distant alias_method aliases" do + it 'defers resolution of distant alias_method aliases' do smap = Solargraph::SourceMap.load_string(%( class MyClass alias_method :foo, :bar end )) - pin = smap.pins.select{|p| p.is_a?(Solargraph::Pin::MethodAlias)}.first + pin = smap.pins.select { |p| p.is_a?(Solargraph::Pin::MethodAlias) }.first expect(pin).not_to be_nil end - it "maps explicit begin nodes" do + it 'maps explicit begin nodes' do smap = Solargraph::SourceMap.load_string(%( def foo begin @@ -864,11 +866,11 @@ def foo end end )) - pin = smap.pins.select{|p| p.name == '@x'}.first + pin = smap.pins.select { |p| p.name == '@x' }.first expect(pin).not_to be_nil end - it "maps rescue nodes" do + it 'maps rescue nodes' do smap = Solargraph::SourceMap.load_string(%( def foo @x = make_x @@ -876,13 +878,13 @@ def foo @y = y end )) - err_pin = smap.locals{|p| p.name == 'err'}.first + err_pin = smap.locals { |p| p.name == 'err' }.first expect(err_pin).not_to be_nil - var_pin = smap.pins.select{|p| p.name == '@y'}.first + var_pin = smap.pins.select { |p| p.name == '@y' }.first expect(var_pin).not_to be_nil end - it "maps begin/rescue nodes" do + it 'maps begin/rescue nodes' do smap = Solargraph::SourceMap.load_string(%( def foo begin @@ -892,70 +894,70 @@ def foo end end )) - err_pin = smap.locals{|p| p.name == 'err'}.first + err_pin = smap.locals { |p| p.name == 'err' }.first expect(err_pin).not_to be_nil - var_pin = smap.pins.select{|p| p.name == '@y'}.first + var_pin = smap.pins.select { |p| p.name == '@y' }.first expect(var_pin).not_to be_nil end - it "maps classes with long namespaces" do + it 'maps classes with long namespaces' do smap = Solargraph::SourceMap.load_string(%( class Foo::Bar end ), 'test.rb') - pin = smap.pins.select{|p| p.path == 'Foo::Bar'}.first + pin = smap.pins.select { |p| p.path == 'Foo::Bar' }.first expect(pin).not_to be_nil expect(pin.namespace).to eq('Foo') expect(pin.name).to eq('Bar') expect(pin.path).to eq('Foo::Bar') end - it "ignores aliases that do not map to methods or attributes" do - expect { - smap = Solargraph::SourceMap.load_string(%( + it 'ignores aliases that do not map to methods or attributes' do + expect do + Solargraph::SourceMap.load_string(%( class Foo xyz = String alias foo xyz alias_method :foo, :xyz end ), 'test.rb') - }.not_to raise_error + end.not_to raise_error end - it "ignores private_class_methods that do not map to methods or attributes" do - expect { - smap = Solargraph::SourceMap.load_string(%( + it 'ignores private_class_methods that do not map to methods or attributes' do + expect do + Solargraph::SourceMap.load_string(%( class Foo var = some_method private_class_method :var end ), 'test.rb') - }.not_to raise_error + end.not_to raise_error end - it "ignores private_constants that do not map to namespaces or constants" do - expect { - smap = Solargraph::SourceMap.load_string(%( + it 'ignores private_constants that do not map to namespaces or constants' do + expect do + Solargraph::SourceMap.load_string(%( class Foo var = some_method private_constant :var end ), 'test.rb') - }.not_to raise_error + end.not_to raise_error end - it "ignores module_functions that do not map to methods or attributes" do - expect { - smap = Solargraph::SourceMap.load_string(%( + it 'ignores module_functions that do not map to methods or attributes' do + expect do + Solargraph::SourceMap.load_string(%( class Foo var = some_method module_function :var end ), 'test.rb') - }.not_to raise_error + end.not_to raise_error end - it "handles parse directives" do + it 'handles parse directives' do smap = Solargraph::SourceMap.load_string(%( class Foo # @!parse @@ -965,18 +967,18 @@ class Foo expect(smap.pins.map(&:path)).to include('Foo::Bar') end - it "ignores syntax errors in parse directives" do - expect { + it 'ignores syntax errors in parse directives' do + expect do Solargraph::SourceMap.load_string(%( class Foo # @!parse # def end )) - }.not_to raise_error + end.not_to raise_error end - it "sets visibility for symbol parameters" do + it 'sets visibility for symbol parameters' do smap = Solargraph::SourceMap.load_string(%( class Foo def pub; end @@ -987,33 +989,33 @@ def pro; end protected 'pro' end )) - pub = smap.pins.select{|pin| pin.path == 'Foo#pub'}.first + pub = smap.pins.select { |pin| pin.path == 'Foo#pub' }.first expect(pub.visibility).to eq(:public) - bar = smap.pins.select{|pin| pin.path == 'Foo#bar'}.first + bar = smap.pins.select { |pin| pin.path == 'Foo#bar' }.first expect(bar.visibility).to eq(:private) - baz = smap.pins.select{|pin| pin.path == 'Foo#baz'}.first + baz = smap.pins.select { |pin| pin.path == 'Foo#baz' }.first expect(baz.visibility).to eq(:public) - pro = smap.pins.select{|pin| pin.path == 'Foo#pro'}.first + pro = smap.pins.select { |pin| pin.path == 'Foo#pro' }.first expect(pro.visibility).to eq(:protected) end - it "ignores errors in method directives" do - expect { + it 'ignores errors in method directives' do + expect do Solargraph::SourceMap.load_string(%[ class Foo # @!method bar( end ]) - }.not_to raise_error + end.not_to raise_error end - it "handles invalid byte sequences" do - expect { + it 'handles invalid utf8 sequences' do + expect do Solargraph::SourceMap.load(File.join('spec', 'fixtures', 'invalid_utf8.rb')) - }.not_to raise_error + end.not_to raise_error end - it "applies private_class_method to attributes" do + it 'applies private_class_method to attributes' do smap = Solargraph::SourceMap.load_string(%( module Foo class << self @@ -1022,7 +1024,7 @@ class << self private_class_method :bar end )) - pin = smap.pins.select{|pin| pin.path == 'Foo.bar'}.first + pin = smap.pins.select { |pin| pin.path == 'Foo.bar' }.first expect(pin.visibility).to eq(:private) end @@ -1230,7 +1232,7 @@ def bar; end # @!override Foo#bar # return [String] ), 'test.rb') - pins, _locals = Solargraph::SourceMap::Mapper.map(source) + pins, _locals = described_class.map(source) over = pins.select { |pin| pin.is_a?(Solargraph::Pin::Reference::Override) }.first expect(over.name).to eq('Foo#bar') end @@ -1241,7 +1243,7 @@ class Foo def bar(**baz); end end )) - _pins, locals = Solargraph::SourceMap::Mapper.map(source) + _pins, locals = described_class.map(source) param = locals.select { |pin| pin.is_a?(Solargraph::Pin::Parameter) }.first expect(param).to be_kwrestarg end @@ -1252,7 +1254,7 @@ class Foo def bar(baz = {}); end end )) - _pins, locals = Solargraph::SourceMap::Mapper.map(source) + _pins, locals = described_class.map(source) param = locals.select { |pin| pin.is_a?(Solargraph::Pin::Parameter) }.first expect(param).to be_kwrestarg end @@ -1263,7 +1265,7 @@ def bar(baz = {}); end var = 'var' end )) - _pins, locals = Solargraph::SourceMap::Mapper.map(source) + _pins, locals = described_class.map(source) expect(locals).to be_one end @@ -1283,7 +1285,7 @@ class Foo alias_method end )) - pins, locals = Solargraph::SourceMap::Mapper.map(source) + pins, locals = described_class.map(source) expect(pins).to be_one expect(locals).to be_empty end @@ -1352,9 +1354,9 @@ class Foo private_class_method end ) - expect { + expect do Solargraph::SourceMap.load_string(code, 'test.rb') - }.not_to raise_error + end.not_to raise_error end it 'positions method directive pins' do @@ -1431,7 +1433,7 @@ def foo bar:, **splat end it 'gracefully handles misunderstood macros' do - expect { + expect do Solargraph::SourceMap.load_string(%( module Foo # @!macro macro1 @@ -1442,7 +1444,7 @@ module Foo class Bar; end end )) - }.not_to raise_error + end.not_to raise_error end it 'maps autoload paths' do @@ -1570,15 +1572,15 @@ def barbaz; end end it 'handles invalid byte sequences' do - expect { + expect do Solargraph::SourceMap.load('spec/fixtures/invalid_byte.rb') - }.not_to raise_error + end.not_to raise_error end it 'handles invalid byte sequences in stringified node comments' do - expect { + expect do Solargraph::SourceMap.load('spec/fixtures/invalid_node_comment.rb') - }.not_to raise_error + end.not_to raise_error end it 'parses method directives that start with multiple hashes' do diff --git a/spec/source_map/node_processor_spec.rb b/spec/source_map/node_processor_spec.rb index a0ce0bc91..ae34cd49c 100644 --- a/spec/source_map/node_processor_spec.rb +++ b/spec/source_map/node_processor_spec.rb @@ -1,4 +1,6 @@ -describe 'Node processor (generic)' do +# frozen_string_literal: true + +describe Solargraph::Parser::NodeProcessor do it 'maps arg parameters' do map = Solargraph::SourceMap.load_string(%( class Foo diff --git a/spec/source_map_spec.rb b/spec/source_map_spec.rb index 60d4b523e..a5fcea154 100644 --- a/spec/source_map_spec.rb +++ b/spec/source_map_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + describe Solargraph::SourceMap do - it "locates named path pins" do - map = Solargraph::SourceMap.load_string(%( + it 'locates named path pins' do + map = described_class.load_string(%( class Foo def bar; end end @@ -9,19 +11,21 @@ def bar; end expect(pin.path).to eq('Foo#bar') end - it "queries symbols using fuzzy matching" do - map = Solargraph::SourceMap.load_string(%( + it 'queries symbols using fuzzy matching' do + map = described_class.load_string(%( class FooBar def baz_qux; end end )) - expect(map.query_symbols("foo")).to eq(map.document_symbols) - expect(map.query_symbols("foobar")).to eq(map.document_symbols) - expect(map.query_symbols("bazqux")).to eq(map.document_symbols.select{ |pin_namespace| pin_namespace.name == "baz_qux" }) + expect(map.query_symbols('foo')).to eq(map.document_symbols) + expect(map.query_symbols('foobar')).to eq(map.document_symbols) + expect(map.query_symbols('bazqux')).to eq(map.document_symbols.select { |pin_namespace| + pin_namespace.name == 'baz_qux' + }) end it 'returns all pins, except for references as document symbols' do - map = Solargraph::SourceMap.load_string(%( + map = described_class.load_string(%( class FooBar require 'foo' include SomeModule @@ -37,7 +41,7 @@ def baz_qux; end it 'includes convention pins in document symbols' do dummy_convention = Class.new(Solargraph::Convention::Base) do - def local(source_map) + def local source_map source_map.document_symbols # call memoized method Solargraph::Environ.new( @@ -54,7 +58,7 @@ def local(source_map) Solargraph::Convention.register dummy_convention - map = Solargraph::SourceMap.load_string(%( + map = described_class.load_string(%( class FooBar def baz_qux; end end @@ -65,8 +69,8 @@ def baz_qux; end Solargraph::Convention.unregister dummy_convention end - it "locates block pins" do - map = Solargraph::SourceMap.load_string(%( + it 'locates block pins' do + map = described_class.load_string(%( class Foo 100.times do end @@ -76,8 +80,8 @@ class Foo expect(pin).to be_a(Solargraph::Pin::Block) end - it 'scopes local variables correctly from root def blocks' do - map = Solargraph::SourceMap.load_string(%( + it 'scopes local variables correctly from root def methods' do + map = described_class.load_string(%( x = 'string' def foo x @@ -88,13 +92,27 @@ def foo expect(locals).to be_empty end + it 'scopes local variables correctly from class methods' do + map = described_class.load_string(%( + class Foo + x = 'string' + def foo + x + end + end + ), 'test.rb') + loc = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(4, 11, 3, 11)) + locals = map.locals_at(loc) + expect(locals).to be_empty + end + it 'handles op_asgn case with assertions on' do # set SOLARGRAPH_ASSERTS=onto test this old_asserts = ENV.fetch('SOLARGRAPH_ASSERTS', nil) ENV['SOLARGRAPH_ASSERTS'] = 'on' expect do - map = Solargraph::SourceMap.load_string(%( + map = described_class.load_string(%( Foo.bar += baz ), 'test.rb') loc = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(3, 9, 3, 9)) @@ -111,7 +129,7 @@ def foo ENV['SOLARGRAPH_ASSERTS'] = 'on' expect do - map = Solargraph::SourceMap.load_string(%( + map = described_class.load_string(%( Foo.bar ||= baz ), 'test.rb') loc = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(3, 9, 3, 9)) @@ -128,7 +146,7 @@ def foo ENV['SOLARGRAPH_ASSERTS'] = 'on' expect do - map = Solargraph::SourceMap.load_string(%( + map = described_class.load_string(%( Foo.bar = baz ), 'test.rb') loc = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(3, 9, 3, 9)) @@ -140,7 +158,7 @@ def foo end it 'scopes local variables correctly in class_eval blocks' do - map = Solargraph::SourceMap.load_string(%( + map = described_class.load_string(%( class Foo; end x = 'y' Foo.class_eval do @@ -149,16 +167,16 @@ class Foo; end end ), 'test.rb') locals = map.locals_at(Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 0, 5, 0))).map(&:name) - expect(locals).to eq(['x', 'foo']) + expect(locals).to eq(%w[x foo]) end it 'updates cached inference when the ApiMap changes' do - file1 = Solargraph::SourceMap.load_string(%( + file1 = described_class.load_string(%( def foo '' end ), 'file1.rb') - file2 = Solargraph::SourceMap.load_string(%( + file2 = described_class.load_string(%( foo ), 'file2.rb') @@ -170,7 +188,7 @@ def foo original_api_map_hash = api_map.hash original_source_map_hash = file1.hash - file1 = Solargraph::SourceMap.load_string(%( + file1 = described_class.load_string(%( def foo [] end diff --git a/spec/source_spec.rb b/spec/source_spec.rb index 93624313c..6357924f2 100644 --- a/spec/source_spec.rb +++ b/spec/source_spec.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + describe Solargraph::Source do - it "parses code" do + it 'parses code' do code = 'class Foo;def bar;end;end' source = described_class.new(code) expect(source.code).to eq(code) - expect(Solargraph::Parser.is_ast_node?(source.node)).to be_truthy + expect(Solargraph::Parser).to be_is_ast_node(source.node) expect(source).to be_parsed end - it "fixes invalid code" do + it 'fixes invalid code' do code = 'class Foo; def bar; x.' source = described_class.new(code) expect(source.code).to eq(code) @@ -17,7 +19,7 @@ expect(source).not_to be_parsed end - it "finds ranges" do + it 'finds ranges' do code = %( class Foo def bar @@ -29,7 +31,7 @@ def bar expect(source.at(range)).to eq('def bar') end - it "finds nodes" do + it 'finds nodes' do code = 'class Foo;def bar;end;end' source = described_class.new(code) node = source.node_at(0, 0) @@ -38,7 +40,7 @@ def bar expect(node.type).to eq(:def) end - it "synchronizes from incremental updates" do + it 'synchronizes from incremental updates' do code = 'class Foo;def bar;end;end' source = described_class.new(code) updater = Solargraph::Source::Updater.new( @@ -55,19 +57,19 @@ def bar expect(changed.node.children[0].children[1]).to eq(:Food) end - it "synchronizes from full updates" do + it 'synchronizes from full updates' do code1 = 'class Foo;end' code2 = 'class Bar;end' source = described_class.new(code1) updater = Solargraph::Source::Updater.new(nil, 0, [ - Solargraph::Source::Change.new(nil, code2) - ]) + Solargraph::Source::Change.new(nil, code2) + ]) changed = source.synchronize(updater) expect(changed.code).to eq(code2) expect(changed.node.children[0].children[1]).to eq(:Bar) end - it "repairs broken incremental updates" do + it 'repairs broken incremental updates' do code = %( class Foo def bar @@ -89,19 +91,19 @@ def bar expect(changed).to be_repaired end - it "flags irreparable updates" do + it 'flags irreparable updates' do code = 'class Foo;def bar;end;end' source = described_class.new(code) updater = Solargraph::Source::Updater.new(nil, 0, [ - Solargraph::Source::Change.new(nil, 'end;end') - ]) + Solargraph::Source::Change.new(nil, 'end;end') + ]) changed = source.synchronize(updater) expect(changed).to be_parsed expect(changed).to be_repaired end - it "finds references" do - source = Solargraph::Source.load_string(%( + it 'finds references' do + source = described_class.load_string(%( class Foo def bar end @@ -113,46 +115,46 @@ def bar= 𐐀.bar = 1 )) foos = source.references('Foo') - foobacks = foos.map{|f| source.at(f.range)} - expect(foobacks).to eq(['Foo', 'Foo']) + foobacks = foos.map { |f| source.at(f.range) } + expect(foobacks).to eq(%w[Foo Foo]) bars = source.references('bar') - barbacks = bars.map{|b| source.at(b.range)} - expect(barbacks).to eq(['bar', 'bar']) + barbacks = bars.map { |b| source.at(b.range) } + expect(barbacks).to eq(%w[bar bar]) assign_bars = source.references('bar=') - assign_barbacks = assign_bars.map{|b| source.at(b.range)} + assign_barbacks = assign_bars.map { |b| source.at(b.range) } expect(assign_barbacks).to eq(['bar=', 'bar =']) end - it "allows escape sequences incompatible with UTF-8" do - source = Solargraph::Source.new(' + it 'allows escape sequences incompatible with UTF-8' do + source = described_class.new(' x = " Un bUen café \x92" puts x ') expect(source.parsed?).to be(true) end - it "fixes invalid byte sequences in UTF-8 encoding" do - expect { - Solargraph::Source.load('spec/fixtures/invalid_byte.rb') - }.not_to raise_error + it 'fixes invalid byte sequences in UTF-8 encoding' do + expect do + described_class.load('spec/fixtures/invalid_byte.rb') + end.not_to raise_error end - it "loads files with Unicode characters" do - expect { - Solargraph::Source.load('spec/fixtures/unicode.rb') - }.not_to raise_error + it 'loads files with Unicode characters' do + expect do + described_class.load('spec/fixtures/unicode.rb') + end.not_to raise_error end - it "updates itself when code does not change" do - original = Solargraph::Source.load_string('x = y', 'test.rb') + it 'updates itself when code does not change' do + original = described_class.load_string('x = y', 'test.rb') updater = Solargraph::Source::Updater.new('test.rb', 1, []) updated = original.synchronize(updater) expect(original).to be(updated) expect(updated.version).to eq(1) end - it "handles unparseable code" do - source = Solargraph::Source.load_string(%( + it 'handles unparseable code' do + source = described_class.load_string(%( 100.times do |num| )) # @todo Unparseable code results in a nil node for now, but that could @@ -161,9 +163,9 @@ def bar= expect(source.parsed?).to be(false) end - it "finds foldable ranges" do + it 'finds foldable ranges' do # Of the 7 possible ranges, 2 are too short to be foldable - source = Solargraph::Source.load_string(%( + source = described_class.load_string(%( =begin Range 1 =end @@ -192,7 +194,7 @@ def range_2 end it 'folds multiline strings' do - source = Solargraph::Source.load_string(%( + source = described_class.load_string(%( a = 1 b = 2 c = 3 @@ -207,7 +209,7 @@ def range_2 end it 'folds multiline arrays' do - source = Solargraph::Source.load_string(%( + source = described_class.load_string(%( a = 1 b = 2 c = 3 @@ -222,7 +224,7 @@ def range_2 end it 'folds multiline hashes' do - source = Solargraph::Source.load_string(%( + source = described_class.load_string(%( a = 1 b = 2 c = 3 @@ -236,8 +238,8 @@ def range_2 expect(source.folding_ranges.first.start.line).to eq(4) end - it "finishes synchronizations for unbalanced lines" do - source1 = Solargraph::Source.load_string('x = 1', 'test.rb') + it 'finishes synchronizations for unbalanced lines' do + source1 = described_class.load_string('x = 1', 'test.rb') source2 = source1.synchronize Solargraph::Source::Updater.new( 'test.rb', 2, @@ -252,21 +254,21 @@ def range_2 expect(source2).to be_synchronized end - it "handles comment arrays that overlap lines" do + it 'handles comment arrays that overlap lines' do # Fixes negative argument error (castwide/solargraph#141) - source = Solargraph::Source.load_string(%( + source = described_class.load_string(%( =begin =end y = 1 #foo )) node = source.node_at(3, 0) - expect { + expect do source.comments_for(node) - }.not_to raise_error + end.not_to raise_error end - it "formats comments with multiple hash prefixes" do - source = Solargraph::Source.load_string(%( + it 'formats comments with multiple hash prefixes' do + source = described_class.load_string(%( ## # one # two @@ -274,11 +276,11 @@ class Foo; end )) node = source.node_at(4, 7) comments = source.comments_for(node) - expect(comments.lines.map(&:chomp)).to eq(['one', 'two']) + expect(comments.lines.map(&:chomp)).to eq(%w[one two]) end it 'does not include inner comments' do - source = Solargraph::Source.load_string(%( + source = described_class.load_string(%( # included class Foo # ignored @@ -291,12 +293,12 @@ class Foo end it 'handles long squiggly heredocs' do - source = Solargraph::Source.load('spec/fixtures/long_squiggly_heredoc.rb') + source = described_class.load('spec/fixtures/long_squiggly_heredoc.rb') expect(source.string_ranges).not_to be_empty end it 'handles string array substitutions' do - source = Solargraph::Source.load_string( + source = described_class.load_string( '%W[array of words #{\'with a substitution\'}]' ) expect(source.string_ranges.length).to eq(4) @@ -305,6 +307,6 @@ class Foo it 'handles errors in docstrings' do # YARD has a known problem with empty @overload tags comments = "@overload\n@return [String]" - expect { Solargraph::Source.parse_docstring(comments) }.not_to raise_error + expect { described_class.parse_docstring(comments) }.not_to raise_error end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 59d107aa3..65d3bb7d4 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'bundler/setup' require 'webmock/rspec' WebMock.disable_net_connect!(allow_localhost: true) @@ -26,21 +28,23 @@ c.example_status_persistence_file_path = 'rspec-examples.txt' end require 'solargraph' -# Suppress logger output in specs (if possible) +# execute any logging blocks to make sure they don't blow up +Solargraph::Logging.logger.sev_threshold = Logger::DEBUG +# ...but still suppress logger output in specs (if possible) if Solargraph::Logging.logger.respond_to?(:reopen) && !ENV.key?('SOLARGRAPH_LOG') Solargraph::Logging.logger.reopen(File::NULL) end # @param name [String] # @param value [String] -def with_env_var(name, value) - old_value = ENV[name] # Store the old value - ENV[name] = value # Set to new value +def with_env_var name, value + old_value = ENV.fetch(name, nil) # Store the old value + ENV[name] = value # Set to new value begin - yield # Execute the block + yield # Execute the block ensure - ENV[name] = old_value # Restore the old value + ENV[name] = old_value # Restore the old value end end diff --git a/spec/type_checker/checks_spec.rb b/spec/type_checker/checks_spec.rb deleted file mode 100644 index 41119cefd..000000000 --- a/spec/type_checker/checks_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -describe Solargraph::TypeChecker::Checks do - it 'validates simple core types' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String') - inf = Solargraph::ComplexType.parse('String') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'invalidates simple core types' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String') - inf = Solargraph::ComplexType.parse('Integer') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(false) - end - - it 'validates expected superclasses' do - source = Solargraph::Source.load_string(%( - class Sup; end - class Sub < Sup; end - )) - api_map = Solargraph::ApiMap.new - api_map.map source - sup = Solargraph::ComplexType.parse('Sup') - sub = Solargraph::ComplexType.parse('Sub') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, sup, sub) - expect(match).to be(true) - end - - it 'invalidates inferred superclasses (expected must be super)' do - # @todo This test might be invalid. There are use cases where inheritance - # between inferred and expected classes should be acceptable in either - # direction. - # source = Solargraph::Source.load_string(%( - # class Sup; end - # class Sub < Sup; end - # )) - # api_map = Solargraph::ApiMap.new - # api_map.map source - # sup = Solargraph::ComplexType.parse('Sup') - # sub = Solargraph::ComplexType.parse('Sub') - # match = Solargraph::TypeChecker::Checks.types_match?(api_map, sub, sup) - # expect(match).to be(false) - end - - it 'fuzzy matches arrays with parameters' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Array') - inf = Solargraph::ComplexType.parse('Array') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'fuzzy matches sets with parameters' do - source = Solargraph::Source.load_string("require 'set'") - source_map = Solargraph::SourceMap.map(source) - api_map = Solargraph::ApiMap.new - api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['set']) - exp = Solargraph::ComplexType.parse('Set') - inf = Solargraph::ComplexType.parse('Set') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'fuzzy matches hashes with parameters' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Hash{ Symbol => String}') - inf = Solargraph::ComplexType.parse('Hash') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'matches multiple types' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String, Integer') - inf = Solargraph::ComplexType.parse('String, Integer') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'matches multiple types out of order' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String, Integer') - inf = Solargraph::ComplexType.parse('Integer, String') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'invalidates inferred types missing from expected' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String') - inf = Solargraph::ComplexType.parse('String, Integer') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(false) - end - - it 'matches nil' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('nil') - inf = Solargraph::ComplexType.parse('nil') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'validates classes with expected superclasses' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Class') - inf = Solargraph::ComplexType.parse('Class') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'validates generic classes with expected Class' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Class') - inf = Solargraph::ComplexType.parse('Class') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'validates inheritance in both directions' do - source = Solargraph::Source.load_string(%( - class Sup; end - class Sub < Sup; end - )) - api_map = Solargraph::ApiMap.new - api_map.map source - sup = Solargraph::ComplexType.parse('Sup') - sub = Solargraph::ComplexType.parse('Sub') - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sup, sub) - expect(match).to be(true) - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sub, sup) - expect(match).to be(true) - end - - it 'invalidates inheritance in both directions' do - api_map = Solargraph::ApiMap.new - sup = Solargraph::ComplexType.parse('String') - sub = Solargraph::ComplexType.parse('Array') - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sup, sub) - expect(match).to be(false) - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sub, sup) - expect(match).to be(false) - end -end diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb new file mode 100644 index 000000000..aca95b9c3 --- /dev/null +++ b/spec/type_checker/levels/alpha_spec.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +describe Solargraph::TypeChecker do + context 'when at alpha level' do + def type_checker code + Solargraph::TypeChecker.load_string(code, 'test.rb', :alpha) + end + + it 'reports use of superclass when subclass is required' do + checker = type_checker(%( + class Sup; end + class Sub < Sup + # @return [Sub] + def foo + Sup.new + end + end + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('does not match inferred type') + end + + it 'allows a compatible function call from two distinct types in a union' do + checker = type_checker(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz.nil? + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'does not falsely enforce nil in return types' do + checker = type_checker(%( + # @return [Integer] + def foo + # @sg-ignore + # @type [Integer, nil] + a = bar + a || 123 + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'reports nilable type issues' do + checker = type_checker(%( + # @param a [String] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)) + .to eq(['Wrong argument type for #foo: a expected String, received String, nil']) + end + + it 'tracks type of ivar' do + checker = type_checker(%( + class Foo + # @return [void] + def initialize + @sync_count = 0 + end + + # @return [void] + def synchronized? + @sync_count < 2 + end + + # @return [void] + def catalog + @sync_count += 1 + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'accepts ivar assignments and references with no intermediate calls as safe' do + pending 'flow sensitive typing improvements' + + checker = type_checker(%( + class Foo + def initialize + # @type [Integer, nil] + @foo = nil + end + + # @return [void] + def twiddle + @foo = nil if rand if rand > 0.5 + end + + # @return [Integer] + def bar + @foo = 123 + out = @foo.round + twiddle + out + end + end + )) + + expect(checker.problems.map(&:message)).to be_empty + end + + it 'knows that ivar references with intermediate calls are not safe' do + checker = type_checker(%( + class Foo + def initialize + # @type [Integer, nil] + @foo = nil + end + + # @return [void] + def twiddle + @foo = nil if rand if rand > 0.5 + end + + # @return [Integer] + def bar + @foo = 123 + twiddle + @foo.round + end + end + )) + + expect(checker.problems.map(&:message)).to eq(['Foo#bar return type could not be inferred', + 'Unresolved call to round on Integer, nil']) + end + + it 'understands &. in return position' do + checker = type_checker(%( + class Baz + # @param bar [String, nil] + # @return [String] + def foo bar + bar&.upcase || 'undefined' + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'can infer types based on || and &&' do + checker = type_checker(%( + class Baz + # @param bar [String, nil] + # @return [Boolean, String] + def foo bar + !bar || bar.upcase + end + + # @param bar [String, nil] + # @return [String, nil] + def bing bar + bar && bar.upcase + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'resolves self correctly in arguments' do + checker = type_checker(%( + class Foo + # @param other [self] + # + # @return [String] + def bar other + other.bing + end + + # @return [String] + def bing + 'bing' + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'resolves self correctly in arguments (second case)' do + checker = type_checker(%( + class Blah + # @return [String] + attr_reader :filename + + # @param filename [String] + def initialize filename + @filename = filename + end + + # @param location [self] + def contain? location + filename == location.filename + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + end +end diff --git a/spec/type_checker/levels/normal_spec.rb b/spec/type_checker/levels/normal_spec.rb index 9af91efe1..21243d161 100644 --- a/spec/type_checker/levels/normal_spec.rb +++ b/spec/type_checker/levels/normal_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + describe Solargraph::TypeChecker do - context 'normal level' do - def type_checker(code) + context 'when checking at normal level' do + def type_checker code Solargraph::TypeChecker.load_string(code, 'test.rb', :normal) end @@ -201,7 +203,7 @@ def bar; end expect(checker.problems).to be_empty end - it 'reports unresolved return tags' do + it 'reports unresolved type tags' do checker = type_checker(%( # @type [UnknownClass] x = unknown_method @@ -221,9 +223,9 @@ def bar; end # @todo This test uses kramdown-parser-gfm because it's a gem dependency known to # lack typed methods. A better test wouldn't depend on the state of # vendored code. + workspace = Solargraph::Workspace.new(Dir.pwd) gemspec = Gem::Specification.find_by_name('kramdown-parser-gfm') - yard_pins = Solargraph::GemPins.build_yard_pins([], gemspec) - Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) + workspace.cache_gem(gemspec) checker = type_checker(%( require 'kramdown-parser-gfm' @@ -609,7 +611,7 @@ def bar end it 'verifies block passes in arguments' do - checker = Solargraph::TypeChecker.load_string(%( + checker = described_class.load_string(%( class Foo def map(&block) block.call(100) @@ -624,7 +626,7 @@ def to_s end it 'verifies args and block passes' do - checker = Solargraph::TypeChecker.load_string(%( + checker = described_class.load_string(%( class Foo def map(x, &block) block.call(x) @@ -639,14 +641,14 @@ def to_s end it 'verifies extra block passes in chained calls' do - checker = Solargraph::TypeChecker.load_string(%( + checker = described_class.load_string(%( ''.to_s(&:nil?) ), 'test.rb') expect(checker.problems).to be_empty end it 'verifies extra block variables in calls with args' do - checker = Solargraph::TypeChecker.load_string(%( + checker = described_class.load_string(%( def foo(bar); end foo(1, &block) ), 'test.rb') @@ -654,7 +656,7 @@ def foo(bar); end end it 'verifies splats passed to arguments' do - checker = Solargraph::TypeChecker.load_string(%( + checker = described_class.load_string(%( def foo(bar, baz); end foo(*splat) ), 'test.rb') diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 47bf45a2c..ea6515b80 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -1,10 +1,54 @@ +# frozen_string_literal: true + describe Solargraph::TypeChecker do - context 'strict level' do + context 'when at strict level' do # @return [Solargraph::TypeChecker] - def type_checker(code) + def type_checker code Solargraph::TypeChecker.load_string(code, 'test.rb', :strict) end + it 'can derive return types' do + checker = type_checker(%( + # @param a [String, nil] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'ignores nilable type issues' do + checker = type_checker(%( + # @param a [String] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'understands Class is not the same as String' do + checker = type_checker(%( + # @param str [String] + # @return [void] + def foo str; end + + foo File + )) + expect(checker.problems.map(&:message)) + .to eq(['Wrong argument type for #foo: str expected String, received Class']) + end + it 'handles compatible interfaces with self types on call' do checker = type_checker(%( # @param a [Enumerable] @@ -61,9 +105,9 @@ def bar(a); end require 'kramdown-parser-gfm' Kramdown::Parser::GFM.undefined_call ), 'test.rb') - api_map = Solargraph::ApiMap.load_with_cache('.', $stdout) + api_map = Solargraph::ApiMap.load '.' api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['kramdown-parser-gfm']) - checker = Solargraph::TypeChecker.new('test.rb', api_map: api_map, level: :strict) + checker = described_class.new('test.rb', api_map: api_map, level: :strict) expect(checker.problems).to be_empty end @@ -105,18 +149,6 @@ def bar *baz expect(checker.problems).to be_empty end - it 'reports mismatched argument types' do - checker = type_checker(%( - class Foo - # @param baz [Integer] - def bar(baz); end - end - Foo.new.bar('string') - )) - expect(checker.problems).to be_one - expect(checker.problems.first.message).to include('Wrong argument type') - end - it 'reports mismatched argument types in chained calls' do checker = type_checker(%( # @param baz [Integer] @@ -154,7 +186,9 @@ def bar(baz); "foo"; end xit 'complains about calling a non-existent method' - xit 'complains about inserting the wrong type into a tuple slot' do + it 'complains about inserting the wrong type into a tuple slot' do + pending 'Better error message from tuple support' + checker = type_checker(%( # @param a [::Solargraph::Fills::Tuple(String, Integer)] def foo(a) @@ -449,20 +483,6 @@ def bar(baz) expect(checker.problems.first.message).to include('Not enough arguments') end - it 'does not attempt to account for splats' do - checker = type_checker(%( - class Foo - def bar(baz, bing) - end - - def blah(args) - bar *args - end - end - )) - expect(checker.problems).to be_empty - end - it 'does not attempt to account for splats in arg counts' do checker = type_checker(%( class Foo @@ -539,7 +559,7 @@ def bar(baz:, bing:) expect(checker.problems).to be_empty end - xit 'requires strict return tags' do + it 'does not require nil correctness in return tags when nil is involved and used second in a ternary' do checker = type_checker(%( class Foo # The tag is [String] but the inference is [String, nil] @@ -550,11 +570,10 @@ def bar end end )) - expect(checker.problems).to be_one - expect(checker.problems.first.message).to include('does not match inferred type') + expect(checker.problems.map(&:message)).not_to include('does not match inferred type') end - xit 'requires strict return tags' do + it 'does not require nil correctness in return tags when nil is involved and used first in a ternary' do checker = type_checker(%( class Foo # The tag is [String] but the inference is [String, nil] @@ -565,8 +584,7 @@ def bar end end )) - expect(checker.problems).to be_one - expect(checker.problems.first.message).to include('does not match inferred type') + expect(checker.problems.map(&:message)).not_to include('does not match inferred type') end it 'validates strict return tags' do @@ -581,6 +599,46 @@ def bar expect(checker.problems).to be_empty end + it 'Can infer through simple ||= on ivar' do + checker = type_checker(%( + class Foo + def recipient + @recipient ||= true + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'Can infer through simple ||= on lvar' do + checker = type_checker(%( + def recipient + recip ||= true + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'Can infer through simple ||= on cvar' do + checker = type_checker(%( + class Foo + def recipient + @@recipient ||= true + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'Can infer through simple ||= on civar' do + checker = type_checker(%( + class Foo + @recipient ||= true + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'Can infer through ||= with a begin+end' do checker = type_checker(%( def recipient @@ -701,6 +759,18 @@ def test(foo: nil) expect(checker.problems).to be_empty end + it 'validates parameters in function calls' do + checker = type_checker(%( + # @param bar [String] + def foo(bar); end + + def baz + foo(123) + end + )) + expect(checker.problems.map(&:message)).to eq(['Wrong argument type for #foo: bar expected String, received 123']) + end + it 'validates inferred return types with complex tags' do checker = type_checker(%( # @param foo [Numeric, nil] a foo @@ -755,7 +825,9 @@ def meth(param1) expect(checker.problems).to be_one end - xit 'uses nil? to refine type' do + it 'uses nil? to refine type' do + pending 'nil? support in flow sensitive typing' + checker = type_checker(%( # @sg-ignore # @type [String, nil] @@ -769,19 +841,6 @@ def meth(param1) expect(checker.problems.map(&:message)).to eq(['Unresolved call to upcase']) end - it 'does not falsely enforce nil in return types' do - checker = type_checker(%( - # @return [Integer] - def foo - # @sg-ignore - # @type [Integer, nil] - a = bar - a || 123 - end - )) - expect(checker.problems.map(&:message)).to be_empty - end - it 'refines types on is_a? and && to downcast and avoid false positives' do checker = type_checker(%( def foo @@ -849,7 +908,7 @@ def foo *path, baz; end expect(checker.problems.map(&:message)).to eq([]) end - it "understands enough of define_method not to think the block is in class scope" do + it 'understands enough of define_method not to think the block is in class scope' do checker = type_checker(%( class Foo def initialize @@ -876,7 +935,9 @@ def bar expect(checker.problems.map(&:message)).to be_empty end - xit "Uses flow scope to specialize understanding of cvar types" do + it 'Uses flow scope to specialize understanding of cvar types' do + pending 'better cvar support' + checker = type_checker(%( class Bar # @return [String] @@ -902,10 +963,10 @@ def foo end end )) - expect(checker.problems.map(&:message)).to eq(["Unresolved call to upcase!"]) + expect(checker.problems.map(&:message)).to eq(['Unresolved call to upcase!']) end - it "does not lose track of place and false alarm when using kwargs after a splat" do + it 'does not lose track of place and false alarm when using kwargs after a splat' do checker = type_checker(%( def foo(a, b, c); end def bar(*args, **kwargs, &blk) @@ -915,7 +976,7 @@ def bar(*args, **kwargs, &blk) expect(checker.problems.map(&:message)).to eq([]) end - it "understands Array#+ overloads" do + it 'understands Array#+ overloads' do checker = type_checker(%( c = ['a'] + ['a'] c @@ -923,7 +984,7 @@ def bar(*args, **kwargs, &blk) expect(checker.problems.map(&:message)).to eq([]) end - it "understands String#+ overloads" do + it 'understands String#+ overloads' do checker = type_checker(%( detail = '' detail += "foo" @@ -932,7 +993,7 @@ def bar(*args, **kwargs, &blk) expect(checker.problems.map(&:message)).to eq([]) end - it "understands Enumerable#each via _Each self type" do + it 'understands Enumerable#each via _Each self type' do checker = type_checker(%( class Blah # @param e [Enumerable] @@ -1004,11 +1065,47 @@ def bar 123 elsif rand 456 + else + nil end end end )) expect(checker.problems.map(&:message)).to eq([]) end + + it 'does not complain on defaulted reader with un-elsed if' do + checker = type_checker(%( + class Foo + # @return [Integer, nil] + def bar + @bar ||= + if rand + 123 + elsif rand + 456 + end + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'does not complain on defaulted reader with with un-elsed unless' do + checker = type_checker(%( + class Foo + # @return [Integer, nil] + def bar + @bar ||= + unless rand + 123 + end + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end end end diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 7eeef1e96..5435058d5 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -1,12 +1,179 @@ +# frozen_string_literal: true + describe Solargraph::TypeChecker do - context 'strong level' do - def type_checker(code) + context 'with level set to strong' do + 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') + it 'requires strict return tags when nil is involved and used second in a ternary' do + checker = type_checker(%( + class Foo + # The tag is [String] but the inference is [String, nil] + # + # @return [String] + def bar + false ? 'bar' : nil + end + end + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('does not match inferred type') + end + + it 'requires strict return tags when nil is involved and used first in a ternary' do + checker = type_checker(%( + class Foo + # The tag is [String] but the inference is [String, nil] + # + # @return [String] + def bar + true ? nil : 'bar' + end + end + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('does not match inferred type') + end + + it 'understands self type when passed as parameter' do + checker = type_checker(%( + class Location + # @return [String] + attr_reader :filename + + # @param other [self] + # @return [-1, 0, 1, nil] + def <=>(other) + return nil unless other.is_a?(Location) + + filename <=> other.filename + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not misunderstand types during flow sensitive typing' do + checker = type_checker(%( + class A + # @param b [Hash{String => String}] + # @return [void] + def a b + c = b["123"] + return if c.nil? + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects pin visibility in if/nil? pattern' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param bar [Symbol, nil] + # @return [Symbol, Integer] + def foo bar + return 123 if bar.nil? + bar + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects || overriding nilable types' do + checker = type_checker(%( + # @return [String] + def global_config_path + ENV['SOLARGRAPH_GLOBAL_CONFIG'] || + File.join(Dir.home, '.config', 'solargraph', 'config.yml') + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'is able to probe type over an assignment' do + checker = type_checker(%( + # @return [String] + def global_config_path + out = 'foo' + out + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects pin visibility in if/foo pattern' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param bar [Symbol, nil] + # @return [Symbol, Integer] + def foo bar + baz = bar + return baz if baz + 123 + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'handles a flow sensitive typing if correctly' do + checker = type_checker(%( + # @param a [String, nil] + # @return [void] + def foo a = nil + b = a + if b + b.upcase + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'handles another flow sensitive typing if correctly' do + checker = type_checker(%( + class A + # @param e [String] + # @param f [String] + # @return [void] + def d(e, f:); end + # @return [void] + def a + c = rand ? nil : "foo" + if c + d(c, f: c) + end + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects pin visibility' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param baz [Integer, nil] + # @return [Integer, nil] + def foo baz = 123 + return nil if baz.nil? + baz + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'provides nil checking on calls from parameters without assignments' do checker = type_checker(%( # @param baz [String, nil] # @@ -21,7 +188,7 @@ def quux(baz) it 'does not complain on array dereference' do checker = type_checker(%( - # @param idx [Integer, nil] an index + # @param idx [Integer] an index # @param arr [Array] an array of integers # # @return [void] @@ -32,6 +199,23 @@ def foo(idx, arr) expect(checker.problems.map(&:message)).to be_empty end + it 'understands local evaluation with ||= removes nil from lhs type' do + checker = type_checker(%( + class Foo + def initialize + @bar = nil + end + + # @return [Integer] + def bar + @bar ||= 123 + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + it 'complains on bad @type assignment' do checker = type_checker(%( # @type [Integer] @@ -82,21 +266,6 @@ def bar; end expect(checker.problems.first.message).to include('Missing @return tag') end - it 'ignores nilable type issues' do - checker = type_checker(%( - # @param a [String] - # @return [void] - def foo(a); end - - # @param b [String, nil] - # @return [void] - def bar(b) - foo(b) - end - )) - expect(checker.problems.map(&:message)).to eq([]) - end - it 'calls out keyword issues even when required arg count matches' do checker = type_checker(%( # @param a [String] @@ -112,7 +281,7 @@ 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 + it 'understands complex use of self' do checker = type_checker(%( class A # @param other [self] @@ -294,6 +463,149 @@ def bar &block expect(checker.problems).to be_empty end + it 'does not need fully specified container types' do + checker = type_checker(%( + class Foo + # @param foo [Array] + # @return [void] + def bar foo: []; end + + # @param bing [Array] + # @return [void] + def baz(bing) + bar(foo: bing) + generic_values = [1,2,3].map(&:to_s) + bar(foo: generic_values) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'treats a parameter type of undefined as not provided' do + checker = type_checker(%( + class Foo + # @param foo [Array] + # @return [void] + def bar foo: []; end + + # @param bing [Array] + # @return [void] + def baz(bing) + bar(foo: bing) + generic_values = [1,2,3].map(&:to_s) + bar(foo: generic_values) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failure with no generic tag' do + checker = type_checker(%( + class Foo + # @param foo [Class] + # @return [void] + def bar foo:; end + + # @param bing [Class>] + # @return [void] + def baz(bing) + bar(foo: bing) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores undefined resolution failures' do + checker = type_checker(%( + class Foo + # @generic T + # @param klass [Class>] + # @return [Set>] + def pins_by_class klass; [].to_set; end + end + class Bar + # @return [Enumerable] + def block_pins + foo = Foo.new + foo.pins_by_class(Integer) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failures from current Solargraph limitation' do + checker = type_checker(%( + class Foo + # @generic T + # @param klass [Class>] + # @return [Set>] + def pins_by_class klass; [].to_set; end + end + class Bar + # @return [Enumerable] + def block_pins + foo = Foo.new + foo.pins_by_class(Integer) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failures with only one arg' do + checker = type_checker(%( + # @generic T + # @param path [String] + # @param klass [Class>] + # @return [void] + def code_object_at path, klass = Integer + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on select { is_a? } pattern' do + checker = type_checker(%( + # @param arr [Enumerable} + # @return [Enumerable] + def downcast_arr(arr) + arr.select { |pin| pin.is_a?(Integer) } + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on adding nil to types via return value' do + checker = type_checker(%( + # @param bar [Integer] + # @return [Integer, nil] + def foo(bar) + bar + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on adding nil to types via select' do + checker = type_checker(%( + # @return [Float, nil]} + def bar; rand; end + + # @param arr [Enumerable} + # @return [Integer, nil] + def downcast_arr(arr) + # @type [Object, nil] + foo = arr.select { |pin| pin.is_a?(Integer) && bar }.last + foo + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'inherits param tags from superclass methods' do checker = type_checker(%( class Foo @@ -311,6 +623,20 @@ def meth arg expect(checker.problems).to be_empty end + it 'understands Open3 methods' do + checker = type_checker(%( + require 'open3' + + # @return [void] + def run_command + # @type [Hash{String => String}] + foo = {'foo' => 'bar'} + Open3.capture2e(foo, 'ls', chdir: '/tmp') + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + context 'with class name available in more than one gate' do let(:checker) do type_checker(%( @@ -368,18 +694,203 @@ def baz expect(checker.problems.map(&:message)).to be_empty end - it 'understands Open3 methods' do + it 'handles "while foo" flow sensitive typing correctly' do checker = type_checker(%( - require 'open3' - + # @param a [String, nil] # @return [void] - def run_command - # @type [Hash{String => String}] - foo = {'foo' => 'bar'} - Open3.capture2e(foo, 'ls', chdir: '/tmp') + def foo a = nil + b = a + while b + b.upcase + b = nil if rand > 0.5 + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does flow sensitive typing even inside a block' do + checker = type_checker(%( + class Quux + # @param foo [String, nil] + # + # @return [void] + def baz(foo) + bar = foo + [].each do + bar.upcase unless bar.nil? + end + end + end)) + + expect(checker.problems.map(&:location).map(&:range).map(&:start)).to be_empty + end + + it 'accepts ivar assignments and references with no intermediate calls as safe' do + checker = type_checker(%( + class Foo + def initialize + # @type [Integer, nil] + @foo = nil + end + + # @return [void] + def twiddle + @foo = nil if rand if rand > 0.5 + end + + # @return [Integer] + def bar + @foo = 123 + out = @foo.round + twiddle + out + end + )) + + expect(checker.problems.map(&:message)).to be_empty + end + + it 'resolves self correctly in chained method calls' do + checker = type_checker(%( + class Foo + # @param other [self] + # + # @return [Symbol, nil] + def bar(other) + # @type [Symbol, nil] + baz(other) + end + + # @param other [self] + # + # @sg-ignore Missing @return tag + # @return [undefined] + def baz(other); end end )) + expect(checker.problems.map(&:message)).to be_empty end + + it 'knows that ivar references with intermediate calls are not safe' do + checker = type_checker(%( + class Foo + def initialize + # @type [Integer, nil] + @foo = nil + end + + # @return [void] + def twiddle + @foo = nil if rand if rand > 0.5 + end + + # @return [Integer] + def bar + @foo = 123 + twiddle + @foo.round + end + end + )) + + expect(checker.problems.map(&:message)).to eq(['Foo#bar return type could not be inferred', + 'Unresolved call to round on Integer, nil']) + end + + it 'performs simple flow-sensitive typing on lvars' do + checker = type_checker(%( + class Foo + # @param bar [Integer, nil] + # @return [::Boolean, ::Integer] + def foo bar + !bar || bar.abs + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'performs simple flow-sensitive typing on ivars' do + checker = type_checker(%( + class Foo + # @param bar [::Integer, nil] + def initialize bar: nil + @bar = bar + end + + # @return [::Boolean, ::Integer] + def foo + !@bar || @bar.abs + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'performs complex flow-sensitive typing on ivars' do + checker = type_checker(%( + class Foo + # @param bar [::Array, nil] + def initialize bar: nil + @bar = bar + end + + def maybe_bar? + return !@bar.empty? if defined?(@bar) && @bar + false + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'supports !@x.nil && @x.y' do + checker = type_checker(%( + class Bar + # @param foo [String, nil] + def initialize(foo) + @foo = foo + end + + def foo? + !@foo.nil? && @foo.upcase == 'FOO' + end + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'uses cast type instead of defined type' do + checker = type_checker(%( + # frozen_string_literal: true + + class Base; end + + class Subclass < Base + # @return [String] + attr_reader :bar + end + + class Foo + # @param bases [::Array] + # @return [void] + def baz(bases) + # @param sub [Subclass] + bases.each do |sub| + puts sub.bar + end + end + end + )) + + # expect 'sub' to be treated as 'Subclass' inside the block, and + # an error when trying to declare sub as Subclass + expect(checker.problems.map(&:message)).not_to include('Unresolved call to bar on Base') + end end end diff --git a/spec/type_checker/levels/typed_spec.rb b/spec/type_checker/levels/typed_spec.rb index b2071465e..4b9a3226a 100644 --- a/spec/type_checker/levels/typed_spec.rb +++ b/spec/type_checker/levels/typed_spec.rb @@ -1,9 +1,28 @@ +# frozen_string_literal: true + describe Solargraph::TypeChecker do - context 'typed level' do - def type_checker(code) + context 'when level set to typed' do + def type_checker code Solargraph::TypeChecker.load_string(code, 'test.rb', :typed) end + it 'respects pin visibility' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param bar [Array] + # @return [Symbol, Integer] + def foo bar + baz = bar.first + return 123 if baz.nil? + baz + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'reports mismatched types for empty methods' do checker = type_checker(%( class Foo @@ -38,6 +57,19 @@ def bar expect(checker.problems.first.message).to include('does not match') end + it 'reports mismatched key and subtypes' do + checker = type_checker(%( + # @return [Hash{String => String}] + def foo + # @type h [Hash{Integer => String}] + h = {} + h + end + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('does not match') + end + it 'reports mismatched inherited return tags' do checker = type_checker(%( class Sup @@ -159,21 +191,17 @@ def foo expect(checker.problems).to be_empty end - it 'reports superclasses of return types' do - # @todo This test might be invalid. There are use cases where inheritance - # between inferred and expected classes should be acceptable in either - # direction. - # checker = type_checker(%( - # class Sup; end - # class Sub < Sup - # # @return [Sub] - # def foo - # Sup.new - # end - # end - # )) - # expect(checker.problems).to be_one - # expect(checker.problems.first.message).to include('does not match inferred type') + it 'allows superclass of return types' do + checker = type_checker(%( + class Sup; end + class Sub < Sup + # @return [Sub] + def foo + Sup.new + end + end + )) + expect(checker.problems.map(&:message)).not_to include('does not match inferred type') end it 'validates generic subclasses of return types' do @@ -189,6 +217,31 @@ def foo expect(checker.problems).to be_empty end + it 'validates default values of parameters' do + checker = type_checker(%( + # @param bar [String] + def foo(bar = 123); end + )) + expect(checker.problems.map(&:message)) + .to eq(['Declared type String does not match inferred type 123 for variable bar']) + end + + it 'validates string default values of parameters' do + checker = type_checker(%( + # @param bar [String] + def foo(bar = 'foo'); end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'validates symbol default values of parameters' do + checker = type_checker(%( + # @param bar [Symbol] + def foo(bar = :baz); end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + it 'validates subclass arguments of param types' do checker = type_checker(%( class Sup diff --git a/spec/type_checker/rules_spec.rb b/spec/type_checker/rules_spec.rb index ded5302fa..1ecc30714 100644 --- a/spec/type_checker/rules_spec.rb +++ b/spec/type_checker/rules_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + describe Solargraph::TypeChecker::Rules do it 'sets normal rules' do - rules = Solargraph::TypeChecker::Rules.new(:normal, {}) + rules = described_class.new(:normal, {}) expect(rules.ignore_all_undefined?).to be(true) expect(rules.must_tag_or_infer?).to be(false) expect(rules.require_type_tags?).to be(false) @@ -9,7 +11,7 @@ end it 'sets typed rules' do - rules = Solargraph::TypeChecker::Rules.new(:typed, {}) + rules = described_class.new(:typed, {}) expect(rules.ignore_all_undefined?).to be(true) expect(rules.must_tag_or_infer?).to be(false) expect(rules.require_type_tags?).to be(false) @@ -18,7 +20,7 @@ end it 'sets strict rules' do - rules = Solargraph::TypeChecker::Rules.new(:strict, {}) + rules = described_class.new(:strict, {}) expect(rules.ignore_all_undefined?).to be(false) expect(rules.must_tag_or_infer?).to be(true) expect(rules.require_type_tags?).to be(false) @@ -27,7 +29,7 @@ end it 'sets strong rules' do - rules = Solargraph::TypeChecker::Rules.new(:strong, {}) + rules = described_class.new(:strong, {}) expect(rules.ignore_all_undefined?).to be(false) expect(rules.must_tag_or_infer?).to be(true) expect(rules.require_type_tags?).to be(true) diff --git a/spec/type_checker_spec.rb b/spec/type_checker_spec.rb index 33c2125e7..3113896d2 100644 --- a/spec/type_checker_spec.rb +++ b/spec/type_checker_spec.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + require 'timeout' describe Solargraph::TypeChecker do it 'does not raise errors checking unparsed sources' do - expect { - checker = Solargraph::TypeChecker.load_string(%( + expect do + checker = described_class.load_string(%( foo{ )) checker.problems - }.not_to raise_error + end.not_to raise_error end it 'ignores tagged problems' do - checker = Solargraph::TypeChecker.load_string(%( + checker = described_class.load_string(%( NotAClass # @sg-ignore @@ -21,7 +23,7 @@ end it 'uses caching in Solargraph::Chain to handle a degenerate case' do - checker = Solargraph::TypeChecker.load_string(%( + checker = described_class.load_string(%( def documentation @documentation = "a" @documentation += "b" @@ -38,7 +40,7 @@ def documentation end ), nil, :strict) timed_out = true - Timeout::timeout(5) do # seconds + Timeout.timeout(5) do # seconds checker.problems timed_out = false end diff --git a/spec/workspace/config_spec.rb b/spec/workspace/config_spec.rb index 9b14d4337..7bb04ba7a 100644 --- a/spec/workspace/config_spec.rb +++ b/spec/workspace/config_spec.rb @@ -1,51 +1,54 @@ +# frozen_string_literal: true + require 'fileutils' require 'tmpdir' describe Solargraph::Workspace::Config do - let(:dir_path) { File.realpath(Dir.mktmpdir) } - after(:each) { FileUtils.remove_entry(dir_path) } + let(:dir_path) { File.realpath(Dir.mktmpdir) } + + after { FileUtils.remove_entry(dir_path) } - it "includes .rb files by default" do + it 'includes .rb files by default' do file = File.join(dir_path, 'file.rb') File.write(file, 'exit') - config = Solargraph::Workspace::Config.new(dir_path) + config = described_class.new(dir_path) expect(config.calculated).to include(file) end - it "includes .rb files in subdirectories by default" do + it 'includes .rb files in subdirectories by default' do Dir.mkdir(File.join(dir_path, 'lib')) file = File.join(dir_path, 'lib', 'file.rb') File.write(file, 'exit') - config = Solargraph::Workspace::Config.new(dir_path) + config = described_class.new(dir_path) expect(config.calculated).to include(file) end - it "excludes test directories by default" do + it 'excludes test directories by default' do Dir.mkdir(File.join(dir_path, 'test')) file = File.join(dir_path, 'test', 'file.rb') File.write(file, 'exit') - config = Solargraph::Workspace::Config.new(dir_path) + config = described_class.new(dir_path) expect(config.calculated).not_to include(file) end - it "excludes spec directories by default" do + it 'excludes spec directories by default' do Dir.mkdir(File.join(dir_path, 'spec')) file = File.join(dir_path, 'spec', 'file.rb') File.write(file, 'exit') - config = Solargraph::Workspace::Config.new(dir_path) + config = described_class.new(dir_path) expect(config.calculated).not_to include(file) end - it "excludes vendor directories by default" do + it 'excludes vendor directories by default' do Dir.mkdir(File.join(dir_path, 'vendor')) file = File.join(dir_path, 'vendor', 'file.rb') File.write(file, 'exit') - config = Solargraph::Workspace::Config.new(dir_path) + config = described_class.new(dir_path) expect(config.calculated).not_to include(file) end - it "includes base reporters by default" do - config = Solargraph::Workspace::Config.new(dir_path) + it 'includes base reporters by default' do + config = described_class.new(dir_path) expect(config.reporters).to include('rubocop') expect(config.reporters).to include('require_not_found') end diff --git a/spec/workspace/gemspecs_fetch_dependencies_spec.rb b/spec/workspace/gemspecs_fetch_dependencies_spec.rb new file mode 100644 index 000000000..56504e7dd --- /dev/null +++ b/spec/workspace/gemspecs_fetch_dependencies_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +describe Solargraph::Workspace::Gemspecs, '#fetch_dependencies' do + subject(:deps) { gemspecs.fetch_dependencies(gemspec) } + + let(:gemspecs) { described_class.new(dir_path) } + let(:dir_path) { Dir.pwd } + + context 'when in our bundle' do + context 'with a Bundler::LazySpecification' do + let(:gemspec) do + Bundler::LazySpecification.new('solargraph', nil, nil) + end + + it 'finds a known dependency' do + expect(deps.map(&:name)).to include('backport') + end + end + + context 'with a Gem::Specification' do + let(:gemspec) do + Gem::Specification.find_by_name('solargraph') + end + + it 'finds a known dependency' do + expect(deps.map(&:name)).to include('backport') + end + end + + context 'with gem whose dependency does not exist in our bundle' do + let(:gemspec) do + instance_double(Gem::Specification, + dependencies: [Gem::Dependency.new('activerecord')], + development_dependencies: [], + name: 'my_fake_gem', + version: '123') + end + let(:gem_name) { 'my_fake_gem' } + + it 'gives a useful message' do + output = capture_both { deps.map(&:name) } + expect(output).to include('Please install the gem activerecord') + end + end + end + + context 'with external bundle' do + let(:dir_path) { File.realpath(Dir.mktmpdir).to_s } + + let(:gemspec) do + Bundler::LazySpecification.new(gem_name, nil, nil) + end + + before do + # write out Gemfile + File.write(File.join(dir_path, 'Gemfile'), <<~GEMFILE) + source 'https://rubygems.org' + gem '#{gem_name}' + GEMFILE + + # run bundle install + output, status = Solargraph.with_clean_env do + Open3.capture2e('bundle install --verbose', chdir: dir_path) + end + raise "Failure installing bundle: #{output}" unless status.success? + + # ensure Gemfile.lock exists + unless File.exist?(File.join(dir_path, 'Gemfile.lock')) + raise "Gemfile.lock not found after bundle install in #{dir_path}" + end + end + + context 'with gem that exists in our bundle' do + let(:gem_name) { 'undercover' } + + it 'finds dependencies' do + expect(deps.map(&:name)).to include('ast') + end + end + + context 'with gem does not exist in our bundle' do + let(:gem_name) { 'activerecord' } + + it 'gives a useful message' do + dep_names = nil + output = capture_both { dep_names = deps.map(&:name) } + expect(output).to include('Please install the gem activerecord') + end + end + end +end diff --git a/spec/workspace/gemspecs_find_gem_spec.rb b/spec/workspace/gemspecs_find_gem_spec.rb new file mode 100644 index 000000000..35f5e7a15 --- /dev/null +++ b/spec/workspace/gemspecs_find_gem_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +describe Solargraph::Workspace::Gemspecs, '#find_gem' do + subject(:gemspec) { gemspecs.find_gem(name, version, out: out) } + + let(:gemspecs) { described_class.new(dir_path) } + let(:out) { StringIO.new } + + context 'with local bundle' do + let(:dir_path) { File.realpath(Dir.pwd) } + + context 'with solargraph from bundle' do + let(:name) { 'solargraph' } + let(:version) { nil } + + it 'returns the gem' do + expect(gemspec.name).to eq(name) + end + end + + context 'with random from core' do + let(:name) { 'random' } + let(:version) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'does not complain' do + expect(out.string).to be_empty + end + end + + context 'with ripper from core' do + let(:name) { 'ripper' } + let(:version) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + end + + context 'with base64 from stdlib' do + let(:name) { 'base64' } + let(:version) { nil } + + it 'returns a gemspec' do + expect(gemspec).not_to be_nil + end + end + + context 'with gem not in bundle' do + let(:name) { 'checkoff' } + let(:version) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'complains' do + gemspec + + expect(out.string).to include('install the gem checkoff ') + end + end + + context 'with gem not in bundle but no logger' do + let(:name) { 'checkoff' } + let(:version) { nil } + let(:out) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'does not fail' do + expect { gemspec }.not_to raise_error + end + end + + context 'with gem not in bundle with version' do + let(:name) { 'checkoff' } + let(:version) { '1.0.0' } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'complains' do + gemspec + + expect(out.string).to include('install the gem checkoff:1.0.0') + end + end + end +end diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb new file mode 100644 index 000000000..8deba9ff8 --- /dev/null +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -0,0 +1,308 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +describe Solargraph::Workspace::Gemspecs, '#resolve_require' do + subject(:specs) { gemspecs.resolve_require(require) } + + let(:gemspecs) { described_class.new(dir_path) } + + def find_or_install gem_name, version + Gem::Specification.find_by_name(gem_name, version) + rescue Gem::LoadError + install_gem(gem_name, version) + end + + def add_bundle + # write out Gemfile + File.write(File.join(dir_path, 'Gemfile'), <<~GEMFILE) + source 'https://rubygems.org' + gem 'backport' + GEMFILE + # run bundle install + output, status = Solargraph.with_clean_env do + Open3.capture2e('bundle install --verbose', chdir: dir_path) + end + raise "Failure installing bundle: #{output}" unless status.success? + # ensure Gemfile.lock exists + return if File.exist?(File.join(dir_path, 'Gemfile.lock')) + raise "Gemfile.lock not found after bundle install in #{dir_path}" + end + + def install_gem gem_name, version + Bundler.with_unbundled_env do + cmd = Gem::Commands::InstallCommand.new + cmd.handle_options [gem_name, '-v', version] + cmd.execute + rescue Gem::SystemExitException => e + raise unless e.exit_code == 0 + end + end + + context 'with local bundle' do + let(:dir_path) { File.realpath(Dir.pwd) } + + context 'with a known gem' do + let(:require) { 'solargraph' } + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with an unknown type from Bundler / RubyGems' do + let(:require) { 'solargraph' } + let(:specish_objects) { [double] } + + before do + lockfile = instance_double(Pathname) + locked_gems = instance_double(Bundler::LockfileParser, specs: specish_objects) + + definition = instance_double(Bundler::Definition, + locked_gems: locked_gems, + lockfile: lockfile) + allow(Bundler).to receive(:definition).and_return(definition) + allow(lockfile).to receive(:to_s).and_return(dir_path) + end + + it 'raises a StandardException' do + expect { specs.size }.to raise_error(StandardError) + end + end + + def configure_bundler_spec stub_value + platform = Gem::Platform::RUBY + bundler_stub_spec = Bundler::StubSpecification.new('solargraph', '123', platform, spec_fetcher) + specish_objects = [bundler_stub_spec] + lockfile = instance_double(Pathname) + locked_gems = instance_double(Bundler::LockfileParser, specs: specish_objects) + definition = instance_double(Bundler::Definition, + locked_gems: locked_gems, + lockfile: lockfile) + # specish_objects = Bundler.definition.locked_gems.specs + allow(Bundler).to receive(:definition).and_return(definition) + allow(lockfile).to receive(:to_s).and_return(dir_path) + allow(bundler_stub_spec).to receive(:respond_to?).with(:name).and_return(true) + allow(bundler_stub_spec).to receive(:respond_to?).with(:version).and_return(true) + allow(bundler_stub_spec).to receive(:respond_to?).with(:gem_dir).and_return(false) + allow(bundler_stub_spec).to receive(:respond_to?).with(:materialize_for_installation).and_return(false) + allow(bundler_stub_spec).to receive(:respond_to?).with(:stub).and_return(false) + allow(bundler_stub_spec).to receive_messages(name: 'solargraph', stub: stub_value) + end + + context 'with a Bundler::StubSpecification from Bundler / RubyGems' do + # this can happen from local gems, which is hard to test + # organically + + let(:require) { 'solargraph' } + let(:spec_fetcher) { instance_double(Gem::SpecFetcher) } + + before do + platform = Gem::Platform::RUBY + real_spec = instance_double(Gem::Specification) + allow(real_spec).to receive(:name).and_return('solargraph') + gem_stub_spec = Gem::StubSpecification.new('solargraph', '123', platform, spec_fetcher) + configure_bundler_spec(gem_stub_spec) + allow(gem_stub_spec).to receive_messages(name: 'solargraph', version: '123', spec: real_spec) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with a Bundler::StubSpecification that resolves straight to Gem::Specification' do + # have seen different behavior with different versions of rubygems/bundler + + let(:require) { 'solargraph' } + let(:spec_fetcher) { instance_double(Gem::SpecFetcher) } + let(:real_spec) { Gem::Specification.new('solargraph', '123') } + + before do + configure_bundler_spec(real_spec) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with a less usual require mapping' do + let(:require) { 'diff/lcs' } + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['diff-lcs']) + end + end + + context 'with Bundler.require' do + let(:require) { 'bundler/require' } + + it 'returns the gemspec gem' do + expect(specs.map(&:name)).to include('solargraph') + end + end + end + + context 'with nil as directory' do + let(:dir_path) { nil } + + context 'with simple require' do + let(:require) { 'solargraph' } + + it 'finds solargraph' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with Bundler.require' do + let(:require) { 'bundler/require' } + + it 'finds nothing' do + expect(specs).to be_empty + end + end + end + + context 'with external bundle' do + let(:dir_path) { File.realpath(Dir.mktmpdir).to_s } + + context 'with no actual bundle' do + let(:require) { 'bundler/require' } + + it 'raises' do + expect { specs }.to raise_error(Solargraph::BundleNotFoundError) + end + end + + context 'with Gemfile and Bundler.require' do + before { add_bundle } + + let(:require) { 'bundler/require' } + + it 'does not raise' do + expect { specs }.not_to raise_error + end + + it 'returns gems' do + expect(specs.map(&:name)).to include('backport') + end + end + + context 'with Gemfile and deep require into a possibly-core gem' do + before { add_bundle } + + let(:require) { 'bundler/gem_tasks' } + + it 'returns gems' do + expect(specs&.map(&:name)).to include('bundler') + end + end + + context 'with Gemfile and deep require into a gem' do + before { add_bundle } + + let(:require) { 'rspec/mocks' } + + it 'returns gems' do + expect(specs&.map(&:name)).to include('rspec-mocks') + end + end + + context 'with Gemfile but an unknown gem' do + before { add_bundle } + + let(:require) { 'unknown_gemlaksdflkdf' } + + it 'returns nil' do + expect(specs).to be_nil + end + end + + context 'with a Gemfile and a gem preference' do + # find_or_install helper doesn't seem to work on older versions + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.1.0') + before do + add_bundle + find_or_install('backport', '1.0.0') + Gem::Specification.find_by_name('backport', '= 1.0.0') + end + + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = '1.0.0' + end + ] + end + + it 'returns the preferred gemspec' do + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq('1.0.0') + end + + context 'with a gem preference that does not exist' do + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = '99.0.0' + end + ] + end + + it 'returns the gemspec we do have' do + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq('1.2.0') + end + end + + context 'with a gem preference already set to the version we use' do + let(:version) { Gem::Specification.find_by_name('backport').version.to_s } + + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = version + end + ] + end + + it 'returns the gemspec we do have' do + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq(version) + end + end + end + end + end +end diff --git a/spec/workspace_spec.rb b/spec/workspace_spec.rb index 37275bb86..0924cd707 100644 --- a/spec/workspace_spec.rb +++ b/spec/workspace_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fileutils' require 'tmpdir' @@ -6,15 +8,15 @@ let(:dir_path) { File.realpath(Dir.mktmpdir) } let(:file_path) { File.join(dir_path, 'file.rb') } - before(:each) { File.write(file_path, 'exit') } - after(:each) { FileUtils.remove_entry(dir_path) } + before { File.write(file_path, 'exit') } + after { FileUtils.remove_entry(dir_path) } - it "loads sources from a directory" do + it 'loads sources from a directory' do expect(workspace.filenames).to include(file_path) expect(workspace.has_file?(file_path)).to be(true) end - it "ignores non-Ruby files by default" do + it 'ignores non-Ruby files by default' do not_ruby = File.join(dir_path, 'not_ruby.txt') File.write not_ruby, 'text' @@ -22,14 +24,14 @@ expect(workspace.filenames).not_to include(not_ruby) end - it "does not merge non-workspace sources" do + it 'does not merge non-workspace sources' do source = Solargraph::Source.load_string('exit', 'not_ruby.txt') workspace.merge source expect(workspace.filenames).not_to include(source.filename) end - it "updates sources" do + it 'updates sources' do original = workspace.source(file_path) updated = Solargraph::Source.load_string('puts "updated"', file_path) workspace.merge updated @@ -39,7 +41,7 @@ expect(workspace.source(file_path)).to eq(updated) end - it "removes deleted sources" do + it 'removes deleted sources' do expect(workspace.filenames).to include(file_path) original = workspace.source(file_path) @@ -49,30 +51,39 @@ expect(workspace.filenames).not_to include(file_path) end - it "raises an exception for workspace size limits" do - config = double(:config, calculated: Array.new(Solargraph::Workspace::Config::MAX_FILES + 1), max_files: Solargraph::Workspace::Config::MAX_FILES) + it 'raises an exception for workspace size limits' do + config = instance_double(Solargraph::Workspace::Config, + calculated: Array.new(Solargraph::Workspace::Config::MAX_FILES + 1), max_files: Solargraph::Workspace::Config::MAX_FILES) - expect { - Solargraph::Workspace.new('.', config) - }.to raise_error(Solargraph::WorkspaceTooLargeError) + expect do + described_class.new('.', config) + end.to raise_error(Solargraph::WorkspaceTooLargeError) end - it "allows for unlimited files in config" do + it 'allows for unlimited files in config' do gemspec_file = File.join(dir_path, 'test.gemspec') File.write(gemspec_file, '') calculated = Array.new(Solargraph::Workspace::Config::MAX_FILES + 1) { gemspec_file } # @todo Mock reveals tight coupling - config = double(:config, calculated: calculated, max_files: 0, allow?: true, require_paths: [], plugins: []) - expect { - Solargraph::Workspace.new('.', config) - }.not_to raise_error + config = instance_double(Solargraph::Workspace::Config, calculated: calculated, max_files: 0, allow?: true, + require_paths: [], plugins: []) + expect do + described_class.new('.', config) + end.not_to raise_error + end + + it 'detects gemspecs in workspaces' do + gemspec_file = File.join(dir_path, 'test.gemspec') + File.write(gemspec_file, '') + expect(workspace.gemspec?).to be(true) + expect(workspace.gemspec_files).to eq([gemspec_file]) end - it "generates default require path" do + it 'generates default require path' do expect(workspace.require_paths).to eq([File.join(dir_path, 'lib')]) end - it "generates require paths from gemspecs" do + it 'generates require paths from gemspecs' do gemspec_file = File.join(dir_path, 'test.gemspec') File.write(gemspec_file, %( Gem::Specification.new do |s| @@ -86,7 +97,7 @@ expect(workspace.require_paths).to eq([File.join(dir_path, 'other_lib')]) end - it "rescues errors in gemspecs" do + it 'rescues errors in gemspecs' do gemspec_file = File.join(dir_path, 'test.gemspec') File.write(gemspec_file, %( raise 'Error' @@ -94,7 +105,7 @@ expect(workspace.require_paths).to eq([File.join(dir_path, 'lib')]) end - it "rescues syntax errors in gemspecs" do + it 'rescues syntax errors in gemspecs' do gemspec_file = File.join(dir_path, 'test.gemspec') File.write(gemspec_file, %( 123. @@ -102,7 +113,7 @@ expect(workspace.require_paths).to eq([File.join(dir_path, 'lib')]) end - it "detects locally required paths" do + it 'detects locally required paths' do required_file = File.join(dir_path, 'lib', 'test.rb') Dir.mkdir(File.join(dir_path, 'lib')) File.write(required_file, 'exit') @@ -115,22 +126,45 @@ expect(workspace.would_require?('emptydir')).to be(false) end - it "uses configured require paths" do - workspace = Solargraph::Workspace.new('spec/fixtures/workspace') + it 'uses configured require paths' do + workspace = described_class.new('spec/fixtures/workspace') expect(workspace.require_paths).to eq([File.absolute_path('spec/fixtures/workspace/lib'), File.absolute_path('spec/fixtures/workspace/ext')]) end it 'ignores gemspecs in excluded directories' do # vendor/**/* is excluded by default - workspace = Solargraph::Workspace.new('spec/fixtures/vendored') + workspace = described_class.new('spec/fixtures/vendored') expect(workspace.require_paths).to eq([File.absolute_path('spec/fixtures/vendored/lib')]) end it 'rescues errors loading files into sources' do - config = double(:Config, directory: './path', calculated: ['./path/does_not_exist.rb'], max_files: 5000, require_paths: [], plugins: []) - expect { - Solargraph::Workspace.new('./path', config) - }.not_to raise_error + config = instance_double(Solargraph::Workspace::Config, directory: './path', + calculated: ['./path/does_not_exist.rb'], max_files: 5000, require_paths: [], plugins: []) + expect do + described_class.new('./path', config) + end.not_to raise_error + end + + describe '#cache_all_for_workspace!' do + let(:pin_cache) { instance_double(Solargraph::PinCache) } + + before do + allow(Solargraph::PinCache).to receive(:cache_core) + allow(Solargraph::PinCache).to receive(:possible_stdlibs) + allow(Solargraph::PinCache).to receive(:new).and_return(pin_cache) + allow(pin_cache).to receive_messages(cache_gem: nil, possible_stdlibs: []) + allow(Solargraph::PinCache).to receive(:cache_all_stdlibs) + end + + it 'caches core pins' do + allow(Solargraph::PinCache).to receive_messages(core?: false) + allow(pin_cache).to receive_messages(cached?: true, + cache_all_stdlibs: nil) + + workspace.cache_all_for_workspace!(nil, rebuild: false) + + expect(Solargraph::PinCache).to have_received(:cache_core).with(out: nil) + end end end diff --git a/spec/yard_map/mapper/to_method_spec.rb b/spec/yard_map/mapper/to_method_spec.rb index 9c5caa705..c3a46b914 100644 --- a/spec/yard_map/mapper/to_method_spec.rb +++ b/spec/yard_map/mapper/to_method_spec.rb @@ -1,39 +1,41 @@ +# frozen_string_literal: true + describe Solargraph::YardMap::Mapper::ToMethod do - let(:code_object) { + let(:code_object) do namespace = YARD::CodeObjects::ModuleObject.new(nil, 'Example') YARD::CodeObjects::MethodObject.new(namespace, 'foo') - } + end it 'parses args' do - code_object.parameters = [["bar", nil]] - pin = Solargraph::YardMap::Mapper::ToMethod.make(code_object) + code_object.parameters = [['bar', nil]] + pin = described_class.make(code_object) param = pin.parameters.first expect(param.decl).to be(:arg) - expect(param.name).to eq("bar") - expect(param.full).to eq("bar") + expect(param.name).to eq('bar') + expect(param.full).to eq('bar') end it 'parses optargs' do - code_object.parameters = [["bar", "'baz'"]] - pin = Solargraph::YardMap::Mapper::ToMethod.make(code_object) + code_object.parameters = [['bar', "'baz'"]] + pin = described_class.make(code_object) param = pin.parameters.first expect(param.decl).to be(:optarg) - expect(param.name).to eq("bar") + expect(param.name).to eq('bar') expect(param.full).to eq("bar = 'baz'") end it 'parses kwargs' do - code_object.parameters = [["bar:", nil]] - pin = Solargraph::YardMap::Mapper::ToMethod.make(code_object) + code_object.parameters = [['bar:', nil]] + pin = described_class.make(code_object) param = pin.parameters.first expect(param.name).to eq('bar') expect(param.decl).to be(:kwarg) - expect(param.full).to eq("bar:") + expect(param.full).to eq('bar:') end it 'parses kwoptargs' do - code_object.parameters = [["bar:", "'baz'"]] - pin = Solargraph::YardMap::Mapper::ToMethod.make(code_object) + code_object.parameters = [['bar:', "'baz'"]] + pin = described_class.make(code_object) param = pin.parameters.first expect(param.decl).to be(:kwoptarg) expect(param.name).to eq('bar') @@ -41,41 +43,43 @@ end it 'parses restargs' do - code_object.parameters = [["*bar", nil]] - pin = Solargraph::YardMap::Mapper::ToMethod.make(code_object) + code_object.parameters = [['*bar', nil]] + pin = described_class.make(code_object) param = pin.parameters.first expect(param.decl).to be(:restarg) expect(param.name).to eq('bar') - expect(param.full).to eq("*bar") + expect(param.full).to eq('*bar') end it 'parses kwrestargs' do - code_object.parameters = [["**bar", nil]] - pin = Solargraph::YardMap::Mapper::ToMethod.make(code_object) + code_object.parameters = [['**bar', nil]] + pin = described_class.make(code_object) param = pin.parameters.first expect(param.decl).to be(:kwrestarg) expect(param.name).to eq('bar') - expect(param.full).to eq("**bar") + expect(param.full).to eq('**bar') end it 'parses blockargs' do - code_object.parameters = [["&bar", nil]] - pin = Solargraph::YardMap::Mapper::ToMethod.make(code_object) + code_object.parameters = [['&bar', nil]] + pin = described_class.make(code_object) param = pin.parameters.first expect(param.decl).to be(:blockarg) expect(param.name).to eq('bar') - expect(param.full).to eq("&bar") + expect(param.full).to eq('&bar') end - xit 'parses undefined but typed blockargs' do + it 'parses undeclared but typed blockargs' do + pending('block args coming from YARD alone') + code_object.parameters = [] - code_object.docstring = <