diff --git a/.envrc b/.envrc index 92f925b172..2d6fa5f4f2 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,10 @@ +#!/bin/bash + +if [[ "$(uname)" == "Darwin" ]]; then + WORKERS=$(sysctl -n hw.physicalcpu) + export WORKERS +fi + # 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 e39cb99830..a956893906 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -46,17 +46,22 @@ jobs: echo "gem 'vernier', '>1.0', '<2'" >> .Gemfile bundle install - - name: Restore cache of gem annotations - id: dot-cache-restore - uses: actions/cache/restore@v4 + - name: Cache gem yardocs + uses: actions/cache@v4 with: - key: | - 2025-06-26-09-${{ runner.os }}-dot-cache-${{ hashFiles('Gemfile.lock') }} + # Only the yardoc subtree: its path embeds each gem name and version, so a + # restored entry is either correct or never looked up, and a prefix hit is + # safe. The sibling .ser pins are keyed on Solargraph::VERSION, which is + # identical across commits, so carrying those would serve pins built by + # different lib/ code. + # + # github.job and the matrix Ruby version are in the key so jobs sharing one gem + # set do not race to reserve the same entry and fail to save. + path: ~/.cache/solargraph/yard-* + key: sg-yard-${{ runner.os }}-${{ github.job }}-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }} restore-keys: | - 2025-06-26-09-${{ runner.os }}-dot-cache - 2025-06-26-09-${{ runner.os }}-dot-cache- - path: | - /home/runner/.cache/solargraph + sg-yard-${{ runner.os }}-${{ github.job }}- + sg-yard-${{ runner.os }}- - name: Install gem types run: bundle exec rbs collection install @@ -65,8 +70,6 @@ jobs: run: | bundle exec overcommit --sign SOLARGRAPH_ASSERTS=on bundle exec overcommit --run --diff origin/master - # @todo Temporary, expect to revert in 0.60 - continue-on-error: true rubocop: name: rubocop runs-on: ubuntu-latest @@ -108,7 +111,7 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 - bundler-cache: false + bundler-cache: true - name: Install gems run: bundle install @@ -118,6 +121,10 @@ jobs: rubocop_todo: name: .rubocop_todo.yml runs-on: ubuntu-latest + # Needed only so the auto-update step (push-to-master only, below) can + # push a commit back to the branch it checked out. + permissions: + contents: write steps: - uses: actions/checkout@v3 @@ -131,26 +138,30 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 - bundler-cache: false + bundler-cache: true - name: Install gems run: bundle install - name: Run RuboCop run: bundle exec rubocop -c .rubocop.yml - # @todo Temporary, expect to revert in 0.60 - continue-on-error: true - - name: Run RuboCop against todo file + # Diffs here can only be a shrink: "Run RuboCop" above already + # confirmed no new offenses exist against .rubocop_todo.yml. + - name: Auto-update .rubocop_todo.yml + if: github.event_name == 'push' && github.ref == 'refs/heads/master' continue-on-error: true run: | cmd="bundle exec rubocop --auto-gen-config --exclude-limit=5 --no-offense-counts --no-auto-gen-timestamp" - ${cmd:?} - set +e - if [ -n "$(git status --porcelain)" ] + ${cmd:?} || true + if [ -n "$(git status --porcelain -- .rubocop_todo.yml)" ] then - git status --porcelain - git diff -u . - >&2 echo "Please address any new issues, then run '${cmd:?}' and push up any improvements" - exit 1 + git diff -u -- .rubocop_todo.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .rubocop_todo.yml + git commit -m "Auto-update .rubocop_todo.yml [skip ci]" + git push origin HEAD:master + else + echo "No .rubocop_todo.yml cleanup needed." fi diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index e449dd0e24..a585df3485 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -15,7 +15,7 @@ permissions: contents: read jobs: - regression: + rails_and_rspec_typechecking: runs-on: ubuntu-latest steps: @@ -47,56 +47,27 @@ jobs: bundle exec solargraph config yq -yi '.plugins += ["solargraph-rails"]' .solargraph.yml yq -yi '.plugins += ["solargraph-rspec"]' .solargraph.yml - - name: Install gem types - run: bundle exec rbs collection update - - name: Ensure typechecking still works - run: bundle exec solargraph typecheck --level strong - # @todo Temporary, expect to revert in 0.60 - continue-on-error: true - - name: Ensure specs still run - run: bundle exec rake spec - rails: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Install libjitterentropy.a for rugged - # Ubuntu 26.04's libcrypto.pc links it statically - run: | - if pkg-config --libs --static libcrypto | grep -q jitterentropy; then - sudo apt-get update && sudo apt-get install -y libjitterentropy3-dev - fi - - name: Set up Ruby - uses: ruby/setup-ruby@v1 + - name: Cache gem yardocs + uses: actions/cache@v4 with: - 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: - packages: yq - version: 1.0 - - name: Install gems - run: | - echo 'gem "solargraph-rails"' > .Gemfile - # Add optional gem to be able to typecheck related code - echo "gem 'vernier', '>1.0', '<2'" >> .Gemfile - bundle install - bundle update --pre rbs - - name: Configure to use plugins - run: | - bundle exec solargraph config - yq -yi '.plugins += ["solargraph-rails"]' .solargraph.yml + # Only the yardoc subtree: its path embeds each gem name and version, so a + # restored entry is either correct or never looked up, and a prefix hit is + # safe. The sibling .ser pins are keyed on Solargraph::VERSION, which is + # identical across commits, so carrying those would serve pins built by + # different lib/ code. + # + # github.job and the matrix Ruby version are in the key so jobs sharing one gem + # set do not race to reserve the same entry and fail to save. + path: ~/.cache/solargraph/yard-* + key: sg-yard-${{ runner.os }}-${{ github.job }}-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }} + restore-keys: | + sg-yard-${{ runner.os }}-${{ github.job }}- + sg-yard-${{ runner.os }}- - name: Install gem types run: bundle exec rbs collection update - name: Ensure typechecking still works run: bundle exec solargraph typecheck --level strong - # @todo Temporary, expect to revert in 0.60 - continue-on-error: true - - name: Ensure specs still run - run: bundle exec rake spec - rspec: + rails_and_rspec_specs: runs-on: ubuntu-latest steps: @@ -110,32 +81,59 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4 # keep same as typecheck.yml - bundler-cache: false + ruby-version: 3.4 + bundler-cache: true - uses: awalsh128/cache-apt-pkgs-action@latest with: packages: yq version: 1.0 - name: Install gems run: | + echo 'gem "solargraph-rails"' > .Gemfile echo 'gem "solargraph-rspec"' >> .Gemfile - # Add optional gem to be able to typecheck related code - echo "gem 'vernier', '>1.0', '<2'" >> .Gemfile bundle install bundle update --pre rbs - name: Configure to use plugins run: | bundle exec solargraph config + yq -yi '.plugins += ["solargraph-rails"]' .solargraph.yml yq -yi '.plugins += ["solargraph-rspec"]' .solargraph.yml + - name: Cache gem yardocs + uses: actions/cache@v4 + with: + # Only the yardoc subtree: its path embeds each gem name and version, so a + # restored entry is either correct or never looked up, and a prefix hit is + # safe. The sibling .ser pins are keyed on Solargraph::VERSION, which is + # identical across commits, so carrying those would serve pins built by + # different lib/ code. + # + # github.job and the matrix Ruby version are in the key so jobs sharing one gem + # set do not race to reserve the same entry and fail to save. + path: ~/.cache/solargraph/yard-* + key: sg-yard-${{ runner.os }}-${{ github.job }}-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }} + restore-keys: | + sg-yard-${{ runner.os }}-${{ github.job }}- + sg-yard-${{ runner.os }}- - name: Install gem types - run: bundle exec rbs collection update - - name: Ensure typechecking still works - run: bundle exec solargraph typecheck --level strong - # @todo Temporary, expect to revert in 0.60 - continue-on-error: true + run: | + bundle exec rbs collection update + # avoid trying to do this in parallel during the specs + time bundle exec solargraph gems core stdlib default - name: Ensure specs still run - run: bundle exec rake spec + run: | + # Speed up some of the bundle installs we run inside the tests + # as well when we're testing different solargraph usage + # scenarios. This is already set in the local bundle config by + # the setup-ruby action. + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle + + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + bundle exec rake full_spec run_solargraph_rspec_specs: # check out solargraph-rspec as well as this project, and point the former to use the latter as a local gem runs-on: ubuntu-latest @@ -154,14 +152,14 @@ jobs: with: ruby-version: 3.4 rubygems: latest - bundler-cache: false + bundler-cache: true - name: Install gems run: | set -x cd ../solargraph-rspec echo "gem 'solargraph', path: '../solargraph'" >> Gemfile - bundle config path ${{ env.BUNDLE_PATH }} + bundle config set path ${{ env.BUNDLE_PATH }} bundle install --jobs 4 --retry 3 bundle exec appraisal install # @todo some kind of appraisal/bundle conflict? @@ -180,6 +178,22 @@ jobs: run: | cd ../solargraph-rspec cp .solargraph.yml.example .solargraph.yml + - name: Cache gem yardocs + uses: actions/cache@v4 + with: + # Only the yardoc subtree: its path embeds each gem name and version, so a + # restored entry is either correct or never looked up, and a prefix hit is + # safe. The sibling .ser pins are keyed on Solargraph::VERSION, which is + # identical across commits, so carrying those would serve pins built by + # different lib/ code. + # + # github.job and the matrix Ruby version are in the key so jobs sharing one gem + # set do not race to reserve the same entry and fail to save. + path: ${{ env.SOLARGRAPH_CACHE }}/yard-* + key: sg-yard-${{ runner.os }}-${{ github.job }}-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }} + restore-keys: | + sg-yard-${{ runner.os }}-${{ github.job }}- + sg-yard-${{ runner.os }}- - name: Solargraph generate RSpec gems YARD pins run: | cd ../solargraph-rspec @@ -191,7 +205,21 @@ jobs: bundle exec appraisal solargraph gems $rspec_gems - name: Run specs run: | + # Speed up some of the bundle installs we run inside the tests + # as well when we're testing different solargraph usage + # scenarios. This is already set in the local bundle config by + # the setup-ruby action. + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle cd ../solargraph-rspec + + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + + # avoid trying to do this in parallel during the specs + bundle exec solargraph gems core stdlib bundle exec appraisal rspec --format progress run_solargraph_rails_specs: @@ -210,7 +238,7 @@ jobs: # solargraph-rails supports Ruby 3.0+ # This job uses 3.2 due to a problem compiling sqlite3 in earlier versions ruby-version: '3.2' - bundler-cache: false + bundler-cache: true # https://github.com/apiology/solargraph/actions/runs/19400815835/job/55508092473?pr=17 rubygems: latest bundler: latest @@ -218,36 +246,66 @@ jobs: MATRIX_RAILS_VERSION: "7.0" - name: Install gems run: | - set -x - BUNDLE_PATH="${GITHUB_WORKSPACE:?}/vendor/bundle" - export BUNDLE_PATH - cd ../solargraph-rails - echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile - bundle install - bundle update --pre rbs - RAILS_DIR="$(pwd)/spec/rails7" - export RAILS_DIR - cd ${RAILS_DIR} - bundle install - bundle exec --gemfile ../../Gemfile rbs --version - bundle exec --gemfile ../../Gemfile rbs collection install - cd ../../ - # bundle exec rbs collection init - # bundle exec rbs collection install + set -x + # Share caches to speed up bundle install + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + cd ../solargraph-rails + echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile + bundle install + bundle update --pre rbs + env: + MATRIX_RAILS_VERSION: "7.0" + MATRIX_RAILS_MAJOR_VERSION: '7' + - name: Cache gem yardocs + uses: actions/cache@v4 + with: + # Only the yardoc subtree: its path embeds each gem name and version, so a + # restored entry is either correct or never looked up, and a prefix hit is + # safe. The sibling .ser pins are keyed on Solargraph::VERSION, which is + # identical across commits, so carrying those would serve pins built by + # different lib/ code. + # + # github.job and the matrix Ruby version are in the key so jobs sharing one gem + # set do not race to reserve the same entry and fail to save. + path: ~/.cache/solargraph/yard-* + key: sg-yard-${{ runner.os }}-${{ github.job }}-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }} + restore-keys: | + sg-yard-${{ runner.os }}-${{ github.job }}- + sg-yard-${{ runner.os }}- + - name: Install gem types + run: | + cd ../solargraph-rails + + RAILS_DIR="$(pwd)/spec/rails7" + export RAILS_DIR + cd ${RAILS_DIR} + bundle install + bundle exec --gemfile ../../Gemfile rbs --version + bundle exec --gemfile ../../Gemfile rbs collection install + cd ../../ + # bundle exec rbs collection init + # bundle exec rbs collection install env: MATRIX_RAILS_VERSION: "7.0" MATRIX_RAILS_MAJOR_VERSION: '7' - name: Run specs run: | - BUNDLE_PATH="${GITHUB_WORKSPACE:?}/vendor/bundle" - export BUNDLE_PATH + # Share caches to speed up bundle install + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle cd ../solargraph-rails bundle exec solargraph --version bundle info solargraph bundle info rbs bundle info yard + + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + ALLOW_IMPROVEMENTS=true bundle exec rake spec env: MATRIX_RAILS_VERSION: "7.0" - # @todo Temporary, expect to revert in 0.60 - continue-on-error: true diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 742e5f954c..17790360a5 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -22,18 +22,21 @@ jobs: runs-on: ${{ matrix.ruby-version == '3.1' && 'ubuntu-24.04' || 'ubuntu-latest' }} strategy: matrix: - # @todo Restore 'head' once ruby/setup-ruby publishes it for ubuntu-24.04. - # It currently 404s ("Unavailable version head for ruby"), failing CI: - # https://github.com/castwide/solargraph/actions/runs/25863741955/job/76000137015?pr=1187 - ruby-version: ['3.1', '3.2', '3.3', '3.4', '4.0'] - rbs-version: ['3.10.4', '4.0.0', '4.0.1', '4.0.2'] - exclude: - - ruby-version: '3.1' - rbs-version: '4.0.0' - - ruby-version: '3.1' - rbs-version: '4.0.1' + # Each Ruby once, and each supported rbs line once; older Rubies + # get the older rbs their users are likely to run. + include: - ruby-version: '3.1' + rbs-version: '3.10.4' + - ruby-version: '3.2' rbs-version: '4.0.2' + - ruby-version: '3.3' + rbs-version: '4.1.3' + - ruby-version: '3.4' + rbs-version: '4.2.0' + - ruby-version: '4.0' + rbs-version: '4.2.0' + - ruby-version: '4.1.0-preview1' + rbs-version: '4.2.0' steps: - uses: actions/checkout@v3 - name: Install libjitterentropy.a for rugged @@ -46,26 +49,53 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} + # See https://github.com/apiology/solargraph/actions/runs/21799883466/job/62893545490?pr=27 + rubygems: latest bundler-cache: true + # Each Ruby's bundled Bundler breaks the YAML spec in spec/api_map_method_spec.rb + bundler: latest - 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: - # warning: tsort was loaded from the standard library, - # but will no longer be part of the default gems - # starting from Ruby 3.6.0 - - name: Work around legacy rbs deprecation on ruby > 3.4 - run: echo "gem 'tsort'" >> .Gemfile - name: Install bundle run: bundle install - - name: Update gems - run: | - bundle update rbs # use latest available for this Ruby version + - name: Cache gem yardocs + uses: actions/cache@v4 + with: + # Only the yardoc subtree: its path embeds each gem name and version, so a + # restored entry is either correct or never looked up, and a prefix hit is + # safe. The sibling .ser pins are keyed on Solargraph::VERSION, which is + # identical across commits, so carrying those would serve pins built by + # different lib/ code. + # + # github.job and the matrix Ruby version are in the key so jobs sharing one gem + # set do not race to reserve the same entry and fail to save. + path: ~/.cache/solargraph/yard-* + key: sg-yard-${{ runner.os }}-${{ github.job }}-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }} + restore-keys: | + sg-yard-${{ runner.os }}-${{ github.job }}- + sg-yard-${{ runner.os }}- - name: Update types - run: bundle exec rbs collection update + run: | + bundle exec rbs collection update + # avoid trying to do this in parallel during the specs + bundle exec solargraph gems core stdlib - name: Run tests - run: bundle exec rake spec + run: | + # Speed up some of the bundle installs we run inside the tests + # as well when we're testing different solargraph usage + # scenarios. This is already set in the local bundle config by + # the setup-ruby action. + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + + bundle exec rake full_spec undercover: runs-on: ubuntu-latest + continue-on-error: true steps: - uses: actions/checkout@v3 with: @@ -82,11 +112,51 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' + # See the matching setting in the rspec matrix job above for why + # this is needed - keep both in sync. + bundler: latest bundler-cache: true + - name: Install gems + run: | + bundle install + bundle update rbs # use latest available for this Ruby version + - name: Cache gem yardocs + uses: actions/cache@v4 + with: + # Only the yardoc subtree: its path embeds each gem name and version, so a + # restored entry is either correct or never looked up, and a prefix hit is + # safe. The sibling .ser pins are keyed on Solargraph::VERSION, which is + # identical across commits, so carrying those would serve pins built by + # different lib/ code. + # + # github.job and the matrix Ruby version are in the key so jobs sharing one gem + # set do not race to reserve the same entry and fail to save. + path: ~/.cache/solargraph/yard-* + key: sg-yard-${{ runner.os }}-${{ github.job }}-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }} + restore-keys: | + sg-yard-${{ runner.os }}-${{ github.job }}- + sg-yard-${{ runner.os }}- - name: Update types - run: bundle exec rbs collection update + run: | + set -x + + bundle exec rbs collection update + # avoid trying to do this in parallel during the specs + bundle exec solargraph gems core stdlib ast parser - name: Run tests - run: bundle exec rake spec + run: | + set -x + + # Speed up some of the bundle installs we run inside the tests + # as well when we're testing different solargraph usage + # scenarios. This is already set in the local bundle config by + # the setup-ruby action. + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle + + bundle exec rake full_spec - name: Check PR coverage - run: bundle exec rake undercover - continue-on-error: true + run: | + bundle exec rake undercover diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 265878f98b..6c2af87dc6 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -40,9 +40,23 @@ jobs: # Add optional gem to be able to typecheck related code echo "gem 'vernier', '>1.0', '<2'" >> .Gemfile bundle install + - name: Cache gem yardocs + uses: actions/cache@v4 + with: + # Only the yardoc subtree: its path embeds each gem name and version, so a + # restored entry is either correct or never looked up, and a prefix hit is + # safe. The sibling .ser pins are keyed on Solargraph::VERSION, which is + # identical across commits, so carrying those would serve pins built by + # different lib/ code. + # + # github.job and the matrix Ruby version are in the key so jobs sharing one gem + # set do not race to reserve the same entry and fail to save. + path: ~/.cache/solargraph/yard-* + key: sg-yard-${{ runner.os }}-${{ github.job }}-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }} + restore-keys: | + sg-yard-${{ runner.os }}-${{ github.job }}- + sg-yard-${{ runner.os }}- - name: Install gem types run: bundle exec rbs collection install - name: Typecheck self run: SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strong - # @todo Temporary, expect to revert in 0.60 - continue-on-error: true diff --git a/.gitignore b/.gitignore index 75510d96bd..2b2b14fe1a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ coverage /.pryrc /.rspec-local vendor/cache +/vendor/bundle/ +/.bundle/ diff --git a/.rspec_parallel b/.rspec_parallel new file mode 100644 index 0000000000..622ba117ae --- /dev/null +++ b/.rspec_parallel @@ -0,0 +1,17 @@ +--seed 123 +--color +--require spec_helper +--profile +--order defined +--format progress +# Each ParallelTests formatter below also dumps a full RSpec summary to +# stdout, so every group reports its totals twice and parallel_tests +# doubles the numbers it sums from them. Give any you enable an --out. +# +# to update test relative timings for better balancing in CI, +# uncomment the next line, run the specs, and PR in the changes: +# --format ParallelTests::RSpec::RuntimeLogger --out spec/parallel_runtime_rspec.log +# collects every group's failures into one file: +# --format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log +# per-example start/finish lines, for debugging concurrency issues: +# --format ParallelTests::RSpec::VerboseLogger --out tmp/verbose.log diff --git a/.rubocop.yml b/.rubocop.yml index f4463bd113..bbfd0aa395 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,14 +12,14 @@ inherit_from: .rubocop_todo.yml # # See https://docs.rubocop.org/rubocop/configuration AllCops: - NewCops: enable + NewCops: disable Exclude: - - "spec/fixtures/invalid_byte.rb" - - "spec/fixtures/invalid_node_comment.rb" - - "spec/fixtures/invalid_utf8.rb" + - "spec/fixtures/invalid_*.rb" - "spec/fixtures/gem-with-yard-macros/**/*" - "vendor/**/*" - "vendor/**/.*" + - ".claude/**/*" + - ".cursor/**/*" TargetRubyVersion: 3.1 Gemspec/RequiredRubyVersion: @@ -29,6 +29,7 @@ Gemspec/RequiredRubyVersion: - 'spec/fixtures/vendored/vendor/do_not_use.gemspec' Gemspec/DevelopmentDependencies: + Enabled: true EnforcedStyle: gemspec Exclude: - 'spec/fixtures/**/*' @@ -56,6 +57,7 @@ Naming/AsciiIdentifiers: - 'spec/fixtures/unicode.rb' Lint/EmptyClass: + Enabled: true Exclude: - spec/fixtures/**/*.rb @@ -92,6 +94,341 @@ Metrics/PerceivedComplexity: Max: 40 RSpec/ExampleLength: Max: 310 +# Autocorrect mangles short-style Hash tags with nested generics/parens +# (e.g. Hash{Array(String, Array) => String}) into invalid syntax. +# Confirmed broken through rubocop-yard 1.3.0 (latest as of this writing), +# against yard 0.9.45 (also latest as of this writing). +YARD/CollectionStyle: + Enabled: false + +# +# Cops pending as of RuboCop 1.92.0 (and the plugins below), enabled +# explicitly so a RuboCop upgrade does not change which cops run. +# When bumping RuboCop, review its new pending cops and add them here. +# Naming/PredicateMethod and Style/SelectByKind are also pending but stay +# off: they have too many offenses for .rubocop_todo.yml to exclude, and +# an Enabled: false there would not override an Enabled: true here. +# +Gemspec/AddRuntimeDependency: + Enabled: true +Gemspec/AttributeAssignment: + Enabled: true +Gemspec/DeprecatedAttributeAssignment: + Enabled: true +Gemspec/RequireMFA: + Enabled: true +Layout/EmptyLinesAfterModuleInclusion: + Enabled: true +Layout/LineContinuationLeadingSpace: + Enabled: true +Layout/LineContinuationSpacing: + Enabled: true +Layout/LineEndStringConcatenationIndentation: + Enabled: true +Layout/SpaceBeforeBrackets: + Enabled: true +Lint/AmbiguousAssignment: + Enabled: true +Lint/AmbiguousOperatorPrecedence: + Enabled: true +Lint/AmbiguousRange: + Enabled: true +Lint/ArgumentMismatch: + Enabled: true +Lint/ArrayLiteralInRegexp: + Enabled: true +Lint/ConstantOverwrittenInRescue: + Enabled: true +Lint/ConstantReassignment: + Enabled: true +Lint/DataDefineOverride: + Enabled: true +Lint/DeprecatedConstants: + Enabled: true +Lint/DeprecatedReference: + Enabled: true +Lint/DuplicateBranch: + Enabled: true +Lint/DuplicateMagicComment: + Enabled: true +Lint/DuplicateMatchPattern: + Enabled: true +Lint/DuplicateRegexpCharacterClassElement: + Enabled: true +Lint/DuplicateSetElement: + Enabled: true +Lint/EmptyBlock: + Enabled: true +Lint/EmptyInPattern: + Enabled: true +Lint/HashNewWithKeywordArgumentsAsDefault: + Enabled: true +Lint/IncompatibleIoSelectWithFiberScheduler: + Enabled: true +Lint/ItWithoutArgumentsInBlock: + Enabled: true +Lint/LambdaWithoutLiteralBlock: + Enabled: true +Lint/LiteralAssignmentInCondition: + Enabled: true +Lint/MisplacedMagicComment: + Enabled: true +Lint/MixedCaseRange: + Enabled: true +Lint/NameTypo: + Enabled: true +Lint/NonAtomicFileOperation: + Enabled: true +Lint/NoReturnInBeginEndBlocks: + Enabled: true +Lint/NumberedParameterAssignment: + Enabled: true +Lint/NumericOperationWithConstantResult: + Enabled: true +Lint/OrAssignmentToConstant: + Enabled: true +Lint/RedundantDirGlobSort: + Enabled: true +Lint/RedundantRegexpQuantifiers: + Enabled: true +Lint/RedundantTypeConversion: + Enabled: true +Lint/RefinementImportMethods: + Enabled: true +Lint/RequireRangeParentheses: + Enabled: true +Lint/RequireRelativeSelfPath: + Enabled: true +Lint/SharedMutableDefault: + Enabled: true +Lint/SuperArgumentMismatch: + Enabled: true +Lint/SuppressedExceptionInNumberConversion: + Enabled: true +Lint/SymbolConversion: + Enabled: true +Lint/ToEnumArguments: + Enabled: true +Lint/TripleQuotes: + Enabled: true +Lint/UnescapedBracketInRegexp: + Enabled: true +Lint/UnexpectedBlockArity: + Enabled: true +Lint/UnmodifiedReduceAccumulator: + Enabled: true +Lint/UnreachablePatternBranch: + Enabled: true +Lint/UselessConstantScoping: + Enabled: true +Lint/UselessDefaultValueArgument: + Enabled: true +Lint/UselessDefined: + Enabled: true +Lint/UselessNumericOperation: + Enabled: true +Lint/UselessOr: + Enabled: true +Lint/UselessRescue: + Enabled: true +Lint/UselessRuby2Keywords: + Enabled: true +Metrics/CollectionLiteralLength: + Enabled: true +Naming/BlockForwarding: + Enabled: true +RSpec/IncludeExamples: + Enabled: true +Security/CompoundHash: + Enabled: true +Security/IoMethods: + Enabled: true +Style/AmbiguousEndlessMethodDefinition: + Enabled: true +Style/ArgumentsForwarding: + Enabled: true +Style/ArrayIntersect: + Enabled: true +Style/ArrayIntersectWithSingleElement: + Enabled: true +Style/BitwisePredicate: + Enabled: true +Style/CollectionCompact: + Enabled: true +Style/CollectionQuerying: + Enabled: true +Style/CombinableDefined: + Enabled: true +Style/ComparableBetween: + Enabled: true +Style/ComparableClamp: + Enabled: true +Style/ConcatArrayLiterals: + Enabled: true +Style/DataInheritance: + Enabled: true +Style/DigChain: + Enabled: true +Style/DirectiveScope: + Enabled: true +Style/DirEmpty: + Enabled: true +Style/DocumentDynamicEvalDefinition: + Enabled: true +Style/EmptyClassDefinition: + Enabled: true +Style/EmptyHeredoc: + Enabled: true +Style/EmptyStringInsideInterpolation: + Enabled: true +Style/EndlessMethod: + Enabled: true +Style/EnvHome: + Enabled: true +Style/ExactRegexpMatch: + Enabled: true +Style/FetchEnvVar: + Enabled: true +Style/FileEmpty: + Enabled: true +Style/FileNull: + Enabled: true +Style/FileOpen: + Enabled: true +Style/FileRead: + Enabled: true +Style/FileTouch: + Enabled: true +Style/FileWrite: + Enabled: true +Style/HashConversion: + Enabled: true +Style/HashExcept: + Enabled: true +Style/HashFetchChain: + Enabled: true +Style/HashSlice: + Enabled: true +Style/IfWithBooleanLiteralBranches: + Enabled: true +Style/InPatternThen: + Enabled: true +Style/ItAssignment: + Enabled: true +Style/ItBlockParameter: + Enabled: true +Style/KeywordArgumentsMerging: + Enabled: true +Style/MagicCommentFormat: + Enabled: true +Style/MapCompactWithConditionalBlock: + Enabled: true +Style/MapIntoArray: + Enabled: true +Style/MapJoin: + Enabled: true +Style/MapToHash: + Enabled: true +Style/MapToSet: + Enabled: true +Style/MinMaxComparison: + Enabled: true +Style/ModuleMemberExistenceCheck: + Enabled: true +Style/MultilineInPatternThen: + Enabled: true +Style/NegatedIfElseCondition: + Enabled: true +Style/NegativeArrayIndex: + Enabled: true +Style/NestedFileDirname: + Enabled: true +Style/NilLambda: + Enabled: true +Style/NumberedParameters: + Enabled: true +Style/NumberedParametersLimit: + Enabled: true +Style/ObjectThen: + Enabled: true +Style/OneClassPerFile: + Enabled: true +Style/OpenStructUse: + Enabled: true +Style/OperatorMethodCall: + Enabled: true +Style/PartitionInsteadOfDoubleSelect: + Enabled: true +Style/PredicateWithKind: + Enabled: true +Style/QuotedSymbols: + Enabled: true +Style/ReduceToHash: + Enabled: true +Style/RedundantArgument: + Enabled: true +Style/RedundantArrayConstructor: + Enabled: true +Style/RedundantArrayFlatten: + Enabled: true +Style/RedundantConstantBase: + Enabled: true +Style/RedundantCurrentDirectoryInPath: + Enabled: true +Style/RedundantDoubleSplatHashBraces: + Enabled: true +Style/RedundantEach: + Enabled: true +Style/RedundantFilterChain: + Enabled: true +Style/RedundantFormat: + Enabled: true +Style/RedundantHeredocDelimiterQuotes: + Enabled: true +Style/RedundantInitialize: + Enabled: true +Style/RedundantInterpolationUnfreeze: + Enabled: true +Style/RedundantLineContinuation: + Enabled: true +Style/RedundantMinMaxBy: + Enabled: true +Style/RedundantRegexpArgument: + Enabled: true +Style/RedundantRegexpConstructor: + Enabled: true +Style/RedundantSelfAssignmentBranch: + Enabled: true +Style/RedundantStringEscape: + Enabled: true +Style/ReturnNilInPredicateMethodDefinition: + Enabled: true +Style/ReverseFind: + Enabled: true +Style/SafeNavigationChainLength: + Enabled: true +Style/SelectByRange: + Enabled: true +Style/SelectByRegexp: + Enabled: true +Style/SendWithLiteralMethodName: + Enabled: true +Style/SingleLineDoEndBlock: + Enabled: true +Style/StringChars: + Enabled: true +Style/SuperArguments: + Enabled: true +Style/SuperWithArgsParentheses: + Enabled: true +Style/SwapValues: + Enabled: true +Style/TallyMethod: + Enabled: true +Style/TimeNow: + Enabled: true +Style/YAMLFileRead: + Enabled: true plugins: - rubocop-rspec diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 408a6dfcd5..597359a702 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,37 +1,53 @@ # This configuration was generated by # `rubocop --auto-gen-config --exclude-limit 5 --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.80.0. +# using RuboCop version 1.92.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # 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). -# Configuration parameters: Severity. Gemspec/DeprecatedAttributeAssignment: Exclude: - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleAlignWith, Severity. +Layout/ElseAlignment: + Exclude: + - 'lib/solargraph/pin/method.rb' + - 'lib/solargraph/rbs_translator.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleAlignWith. # SupportedStylesAlignWith: keyword, variable, start_of_line Layout/EndAlignment: Exclude: + - 'lib/solargraph/pin/method.rb' + - 'lib/solargraph/rbs_translator.rb' - 'lib/solargraph/shell.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Width, AllowedPatterns. +# Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns. +# SupportedStylesAlignWith: start_of_line, relative_to_receiver Layout/IndentationWidth: Exclude: + - 'lib/solargraph/pin/method.rb' + - 'lib/solargraph/rbs_map/conversions.rb' + - 'lib/solargraph/rbs_translator.rb' - 'lib/solargraph/shell.rb' +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowInHeredoc. +Layout/TrailingWhitespace: + Exclude: + - 'lib/solargraph/pin/base.rb' + Lint/BinaryOperatorWithIdenticalOperands: Exclude: - 'lib/solargraph/api_map/source_to_yard.rb' @@ -53,19 +69,35 @@ Lint/DuplicateBranch: Exclude: - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/rbs_map/conversions.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Lint/InterpolationCheck: Exclude: + - 'spec/pin/symbol_spec.rb' - 'spec/source/chain_spec.rb' - 'spec/source/cursor_spec.rb' +# This cop supports safe autocorrection (--autocorrect). +Lint/SafeNavigationWithEmpty: + Exclude: + - 'lib/solargraph/language_server/message/text_document/formatting.rb' + - 'lib/solargraph/location.rb' + # Configuration parameters: AllowKeywordBlockArguments. Lint/UnderscorePrefixedVariableName: Exclude: - 'lib/solargraph/library.rb' +Lint/UnmodifiedReduceAccumulator: + Exclude: + - 'lib/solargraph/rbs_map/helpers.rb' + +Lint/UnreachableCode: + Exclude: + - 'lib/solargraph/complex_type.rb' + - 'lib/solargraph/complex_type/type_methods.rb' + - 'lib/solargraph/complex_type/unique_type.rb' + # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. # NotImplementedExceptions: NotImplementedError @@ -76,6 +108,7 @@ Lint/UnusedMethodArgument: Lint/UselessAssignment: Exclude: - 'lib/solargraph/pin/block.rb' + - 'lib/solargraph/rbs_map/conversions.rb' - 'spec/fixtures/long_squiggly_heredoc.rb' - 'spec/fixtures/rubocop-unused-variable-error/app.rb' - 'spec/fixtures/unicode.rb' @@ -85,14 +118,12 @@ Metrics/AbcSize: 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: 61 + Max: 57 # Configuration parameters: CountBlocks, CountModifierForms. Metrics/BlockNesting: @@ -114,11 +145,15 @@ Metrics/CyclomaticComplexity: # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: - Enabled: false + Exclude: + - 'lib/solargraph/complex_type.rb' + - 'lib/solargraph/convention/struct_definition.rb' + - 'lib/solargraph/parser/parser_gem/node_chainer.rb' + - 'lib/solargraph/shell.rb' # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 195 + Max: 139 # Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: @@ -141,6 +176,16 @@ Naming/AccessorMethodName: - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/language_server/message/base.rb' +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, BlockForwardingName. +# SupportedStyles: anonymous, explicit +Naming/BlockForwarding: + Exclude: + - 'lib/solargraph/complex_type.rb' + - 'lib/solargraph/complex_type/type_methods.rb' + - 'lib/solargraph/pin/base.rb' + - 'lib/solargraph/pin/common.rb' + # Configuration parameters: ForbiddenDelimiters. # ForbiddenDelimiters: (?i-mx:(^|\s)(EO[A-Z]{1}|END)(\s|$)) Naming/HeredocDelimiterNaming: @@ -156,12 +201,6 @@ Naming/MethodParameterName: - 'lib/solargraph/source.rb' - 'lib/solargraph/yard_map/mapper/to_method.rb' -# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates. -# AllowedMethods: call -# WaywardPredicates: nonzero? -Naming/PredicateMethod: - Enabled: false - # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs. # NamePrefix: is_, has_, have_, does_ # ForbiddenPrefixes: is_, has_, have_, does_ @@ -172,7 +211,6 @@ Naming/PredicatePrefix: - 'spec/**/*' - 'lib/solargraph/api_map.rb' - 'lib/solargraph/parser/parser_gem/class_methods.rb' - - 'lib/solargraph/pin_cache.rb' - 'lib/solargraph/workspace.rb' RSpec/BeforeAfterAll: @@ -183,6 +221,7 @@ RSpec/BeforeAfterAll: - 'spec/api_map_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/protocol_spec.rb' + - 'spec/library_spec.rb' # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: @@ -194,11 +233,17 @@ RSpec/DescribeClass: - '**/spec/views/**/*' - 'spec/complex_type_spec.rb' +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowConsecutiveOneLiners. +RSpec/EmptyLineAfterHook: + Exclude: + - 'spec/rbs_map/gem_spec.rb' + # This cop supports safe autocorrection (--autocorrect). RSpec/ExpectActual: Exclude: - '**/spec/routing/**/*' - - 'spec/rbs_map/stdlib_map_spec.rb' + - 'spec/rbs_map/stdlib_spec.rb' - 'spec/source_map/mapper_spec.rb' # Configuration parameters: AssignmentOnly. @@ -212,18 +257,14 @@ RSpec/LeakyConstantDeclaration: RSpec/MultipleExpectations: Max: 14 +# Configuration parameters: AllowSubject. +RSpec/MultipleMemoizedHelpers: + Max: 7 + # Configuration parameters: AllowedGroups. RSpec/NestedGroups: Max: 4 -RSpec/RemoveConst: - Exclude: - - 'spec/diagnostics/rubocop_helpers_spec.rb' - -Security/MarshalLoad: - Exclude: - - 'lib/solargraph/pin_cache.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle, AllowModifiersOnSymbols, AllowModifiersOnAttrs, AllowModifiersOnAliasMethod. # SupportedStyles: inline, group @@ -257,6 +298,11 @@ Style/ConditionalAssignment: Exclude: - 'lib/solargraph/api_map/source_to_yard.rb' +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/DirectiveScope: + Exclude: + - 'lib/solargraph/parser/parser_gem/node_methods.rb' + # Configuration parameters: AllowedConstants. Style/Documentation: Enabled: false @@ -270,6 +316,10 @@ Style/EmptyMethod: - 'spec/fixtures/rdoc-lib/lib/example.rb' - 'spec/fixtures/workspace-with-gemfile/lib/thing.rb' +Style/FileOpen: + Exclude: + - 'lib/solargraph/source.rb' + # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never @@ -280,23 +330,44 @@ Style/FrozenStringLiteralComment: # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: Exclude: - - 'lib/solargraph/source_map/clip.rb' + - 'lib/solargraph/rbs_map/conversions.rb' # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Enabled: false +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MapJoin: + Exclude: + - 'lib/solargraph/pin/closure.rb' + - 'lib/solargraph/pin/documenting.rb' + # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline Style/MethodDefParentheses: - Exclude: - - 'spec/fixtures/rdoc-lib/lib/example.rb' + Enabled: false Style/MultilineBlockChain: Exclude: - 'lib/solargraph/pin/search.rb' +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle, Recursive. +# SupportedStyles: literals, strict +Style/MutableConstant: + Exclude: + - 'lib/solargraph/rbs_map/conversions.rb' + - 'lib/solargraph/rbs_translator.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_methods.rb' + - 'lib/solargraph/rbs_map/conversions.rb' + # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. # SupportedStyles: predicate, comparison @@ -316,21 +387,31 @@ Style/OptionalBooleanParameter: Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. -# AllowedMethods: present?, blank?, presence, try, try! -Style/SafeNavigation: +Style/PredicateWithKind: Exclude: - 'lib/solargraph/pin/base.rb' -# Configuration parameters: Max. -Style/SafeNavigationChainLength: +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Methods. +Style/RedundantArgument: + Exclude: + - 'lib/solargraph/source.rb' + - 'lib/solargraph/source_map/mapper.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantAssignment: Exclude: - - 'lib/solargraph/workspace/gemspecs.rb' + - 'lib/solargraph/rbs_map/core.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/SlicingWithRange: +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantParentheses: Exclude: - - 'lib/solargraph/convention/struct_definition/struct_definition_node.rb' + - 'lib/solargraph/pin/base.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantRegexpCharacterClass: + Exclude: + - 'lib/solargraph/pin/base.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. @@ -346,6 +427,13 @@ Style/SuperArguments: - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/pin/method.rb' +# 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/api_map.rb' + # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStylePrototypeName. # SupportedStylesPrototypeName: before, after @@ -354,13 +442,12 @@ YARD/MismatchName: - 'lib/solargraph/pin/reference.rb' YARD/TagTypeSyntax: - Enabled: false + Exclude: + - 'lib/solargraph/parser/comment_ripper.rb' + - 'lib/solargraph/yard_map/mapper/to_method.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. +# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https Layout/LineLength: - Max: 224 - -Naming/BlockForwarding: - Enabled: false + Max: 223 diff --git a/Gemfile b/Gemfile index fafce06ec2..d7e59c2507 100755 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,15 @@ source 'https://rubygems.org' gemspec name: 'solargraph' +# rubocop-yard 1.3 fixes YARD CollectionStyle crashes under yard 0.9.44 +# but requires Ruby >= 3.3, and a gemspec cannot express that condition. +# Specs run on 3.1 and 3.2, which keep 1.0.x. +if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0') + gem 'rubocop-yard', '~> 1.3.0' +else + gem 'rubocop-yard', '~> 1.0.0' +end + # 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/Rakefile b/Rakefile index 398957b1a1..e8b9c7310c 100755 --- a/Rakefile +++ b/Rakefile @@ -10,6 +10,8 @@ task :console do sh 'pry -I lib -r solargraph.rb' end +# `vernier` is an optional dependency typecheck needs; CI adds it before every run, so do the same locally: +# echo "gem 'vernier', '>1.0', '<2'" >> .Gemfile && bundle install desc 'Run the type checker' task typecheck: [:typecheck_strong] @@ -34,19 +36,60 @@ task :typecheck_alpha do end desc 'Run RSpec tests, starting with the ones that failed last time' -task spec: %i[spec_failed undercover_no_fail full_spec] do +task spec: %i[spec_failed 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 + # Build the pins the suite leans on hardest before forking. Four + # workers racing to generate the same ones is what takes + # rbs_map/conversions_spec from 17s to over four minutes. + # + # The specs cache any gem they need themselves, so this only saves + # time. It only warns on a name it cannot find, so check for a + # Caching line per gem when editing this list. + # + # The specs map gems both with no workspace and with the repo-root + # one, so cache once after loading each. + require 'solargraph' + puts 'Caching core' + Solargraph::Collection::Core.load + ['', Dir.pwd].each do |directory| + repo = Solargraph::Repo.new(Solargraph::Workspace.new(directory).directory_or_nil) + metagems = %w[ast parser backport reverse_markdown yard kramdown-parser-gfm].filter_map do |name| + repo.find_by_name(name) || warn("Gem #{name} not found") + end + # @sg-ignore filter_map result type keeps the nil the block can return + metagems.each { |metagem| puts "Caching #{metagem.name} #{metagem.version}" } + # @sg-ignore filter_map result type keeps the nil the block can return + Solargraph::Collection::Gem.load_all metagems, out: $stdout + end + # Five files the balancer must not split, each over state that is one + # copy for the whole run. library, external and yardoc uncache the + # backport gem from the pin cache every worker shares; external and + # repo delete and reinstall the same fixture Gemfile.lock under + # spec/fixtures/external_bundled_gem, which is where the path gem + # gem-with-yard-macros comes from. Apart, one worker destroys what + # another is mid-way through reading. + # + # rubocop_helpers is here for an unrelated reason: it needs rubocop + # unloaded, and RSpec requires every file in a group before running + # any example, so protocol_spec's top-level `require 'rubocop'` has to + # land in a different worker. --isolate is what keeps it out. + sh 'TEST_COVERAGE_COMMAND_NAME=full-new bundle exec parallel_rspec --runtime-log spec/parallel_runtime_rspec.log ' \ + '--single "spec/(library|external|yardoc|repo)_spec\.rb|spec/diagnostics/rubocop_helpers_spec\.rb" --isolate ' \ + '--verbose-command spec/' # --profile' + # clear now-outdated coverage + # @sg-ignore https://github.com/castwide/solargraph/issues/1255 FileUtils.rm_rf('coverage/full') - FileUtils.mv('coverage/full-new', 'coverage/full') + # move coverage/full-new to coverage/full on success so that we + # always have the last successful run's coverage info + unless ENV['SIMPLECOV_DISABLED'] + # @sg-ignore https://github.com/castwide/solargraph/issues/1255 + FileUtils.mv('coverage/full-new', 'coverage/full') + end end # @sg-ignore #undercover return type could not be inferred @@ -61,14 +104,16 @@ def undercover Open3.capture2e(cmd) end puts output + # @sg-ignore https://github.com/apiology/solargraph/pull/229 $stdout.flush status rescue StandardError => e warn "hit error: #{e.message}" - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/apiology/solargraph/pull/176 warn "Backtrace:\n#{e.backtrace.join("\n")}" warn "output: #{output}" puts 'Flushing' + # @sg-ignore https://github.com/apiology/solargraph/pull/229 $stdout.flush raise end @@ -90,6 +135,8 @@ 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 + # + # Note: prspec doesn't support --only-failures, so we have to use rspec directly here. sh 'TEST_COVERAGE_COMMAND_NAME=next-failure bundle exec rspec --only-failures || true' end @@ -125,6 +172,7 @@ def simplecov_collate rescue StandardError => e puts "Simplecov collate failed: #{e.message}" ensure + # @sg-ignore https://github.com/apiology/solargraph/pull/229 $stdout.flush end diff --git a/lib/solargraph.rb b/lib/solargraph.rb index e61fc8cf00..911a29889d 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -54,6 +54,7 @@ class InvalidRubocopVersionError < RuntimeError; end autoload :CacheDir, 'solargraph/cache_dir' autoload :RbsTranslator, 'solargraph/rbs_translator' autoload :RbsCollection, 'solargraph/rbs_collection' + autoload :Plugin, 'solargraph/plugin' dir = File.dirname(__FILE__) VIEWS_PATH = File.join(dir, 'solargraph', 'views') @@ -61,7 +62,8 @@ class InvalidRubocopVersionError < RuntimeError; end CHDIR_MUTEX = Mutex.new def self.asserts_on? - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? false elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' @@ -84,11 +86,8 @@ def self.assert_or_log type, msg = nil, &block 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') - # @sg-ignore flow sensitive typing needs to handle 'raise if' return if type == :alias_target_missing && msg.include?('rubocop-ast.rbs') # @todo :combine_with_visibility is not ready for prime time - # lots of disagreements found in practice that heuristics need @@ -116,7 +115,7 @@ def self.logger # # @generic T # @yieldreturn [generic] - # @sg-ignore dynamic call, but both functions behave the same + # @sg-ignore dynamic send has no inferable return type # @return [generic] def self.with_clean_env(&) meth = if Bundler.respond_to?(:with_original_env) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 76763389a3..7298a83d7b 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -43,7 +43,7 @@ def initialize pins: [], loose_unions: true # @param other [Object] def eql? other self.class == other.class && - # @sg-ignore flow sensitive typing needs to handle self.class == other.class + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 equality_fields == other.equality_fields end @@ -87,6 +87,7 @@ def index pins # @return [self] def map source, live: false map = Solargraph::SourceMap.map(source) + # @sg-ignore https://github.com/apiology/solargraph/pull/225 catalog Bench.new(source_maps: [map], live_map: live ? map : nil) self end @@ -108,21 +109,42 @@ def catalog bench end # @todo Determine what needs to be sent to global conventions conventions_environ.merge Convention.for_global(nil) - external_changed = external.update(bench.external_requires.to_a + conventions_environ.requires) - store_changed = store.update(@@core_pins, external.pins.clone, conventions_environ.pins, iced_pins, live_pins) { process_macros } + requires = bench.external_requires.to_a + conventions_environ.requires + # Resolve the workspace's gems first so plugins see them on the first catalog + external_changed = external.update(requires, external.extra_gem_names.dup) + addon = Plugin::Addon.new + Plugin.registry.each do |plugin| + panel ||= Plugin::Panel.new(source_maps: bench.source_maps, live_map: bench.live_map, gem_names: external.configured_gem_names | bench.workspace.available_gem_names) + added = plugin.new(panel).addon + addon.gem_names.concat added.gem_names + addon.pins.concat added.pins + rescue StandardError => e + Solargraph.logger.warn "#{plugin} failed - [#{e.class}] #{e.message}" + end + + external_changed = external.update(requires, addon.gem_names) || external_changed + store_changed = store.update(@@core_pins, external.pins.clone, conventions_environ.pins, addon.pins, iced_pins, live_pins) do + @processing_macros = true + process_macros + ensure + @processing_macros = false + end @cache.clear if external_changed || store_changed Solargraph.logger.info "Cataloging ApiMap finished in #{Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time} seconds" self end + # @return [External] def external @external ||= External.new(workspace&.directory, []) end + # @return [Array] def unresolved_requires external.unresolved_requires end + # @return [Set] def unloaded_gems external.unloaded_gems end @@ -139,11 +161,17 @@ def process_macros method_candidates.each do |node| closure = source_map.locate_closure_pin(node.location.line, node.location.column) chain = Solargraph::Parser::ParserGem::NodeChainer.chain(node) - next unless node.children[0].nil? && store.macro_method_name_pins.key?(node.children[1].to_s) - match = store.macro_method_name_pins[node.children[1].to_s].find do |pin| + # s(:send, nil, method_name, *args) - only receiverless calls expand macros + next unless node in [_, nil, Symbol => method_name, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + next unless store.macro_method_name_pins.key?(method_name.to_s) + + # @sg-ignore Unresolved call to find on Array, nil + match = store.macro_method_name_pins[method_name.to_s].find do |pin| get_complex_type_methods(closure.return_type).include?(pin) end next unless match + # @sg-ignore flow sensitive typing needs to handle Hash#key? guards match.macros.each do |macro| macro_pins.concat macro.generate_pins_from(chain, match, source_map) end @@ -161,7 +189,6 @@ def core_pins # @param name [String, nil] # @return [Solargraph::YardMap::Macro, nil] def named_macro name - # @sg-ignore Need to add nil check here store.named_macros[name] end @@ -178,9 +205,11 @@ def conventions_environ # @param filename [String] # @param position [Position, Array(Integer, Integer)] # @return [Source::Cursor] + # @sg-ignore https://github.com/castwide/solargraph/pull/1417 def cursor_at filename, position position = Position.normalize(position) raise FileNotFoundError, "File not found: #{filename}" unless source_map_hash.key?(filename) + # @sg-ignore https://github.com/castwide/solargraph/pull/1417 source_map_hash[filename].cursor_at(position) end @@ -223,14 +252,17 @@ class << self # @param loose_unions [Boolean] See #initialize # # @return [ApiMap] + # @api Used by solargraph-rails at least def self.load_with_cache directory, out = $stderr, loose_unions: true api_map = load(directory, loose_unions: loose_unions) - return api_map if api_map.external.unloaded_gems.empty? - - api_map.external.unloaded_gems.each do |metagem| - out&.puts "Caching gem #{metagem.name} (#{metagem.cache_name})" - Collection::Gem.load metagem + metagems = api_map.external.unloaded_gems + if metagems.empty? + logger.info { "All gems cached for #{directory}" } + return api_map end + + metagems.each { |metagem| out&.puts "Caching gem #{metagem.name} (#{metagem.cache_name})" } + Collection::Gem.load_all metagems, out: out load(directory, loose_unions: loose_unions) end @@ -360,7 +392,7 @@ def get_instance_variable_pins namespace, scope = :instance 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" + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 sc_fqns = store.constants.dereference(sc) result.concat store.get_instance_variables(sc_fqns, scope) end @@ -573,7 +605,7 @@ def get_method_stack rooted_tag, name, scope: :instance, visibility: %i[private # # @deprecated Use #get_path_pins instead. # - # @param path [String] The path to find + # @param path [String, nil] The path to find # @return [Array] def get_path_suggestions path return [] if path.nil? @@ -585,7 +617,7 @@ def get_path_suggestions path # @example # api_map.get_pins_by_path('String#split') # - # @param path [String] + # @param path [String, nil] # @return [Array] def get_path_pins path get_path_suggestions(path) @@ -601,6 +633,7 @@ def get_path_pins path def search query pins.map(&:path) .compact + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 .select { |path| path.downcase.include?(query.downcase) } end @@ -628,6 +661,7 @@ def query_symbols query # @return [Array] def locate_pins location return [] if location.nil? || !source_map_hash.key?(location.filename) + # @sg-ignore https://github.com/castwide/solargraph/pull/1417 resolve_method_aliases source_map_hash[location.filename].locate_pins(location) end @@ -646,6 +680,7 @@ def clip cursor # @return [Array] def document_symbols filename return [] unless source_map_hash.key?(filename) # @todo Raise error? + # @sg-ignore https://github.com/castwide/solargraph/pull/1417 resolve_method_aliases source_map_hash[filename].document_symbols end @@ -656,8 +691,9 @@ def source_maps # Get a source map by filename. # - # @param filename [String] + # @param filename [String, nil] # @return [SourceMap] + # @sg-ignore https://github.com/castwide/solargraph/pull/1417 def source_map filename raise FileNotFoundError, "Source map for `#{filename}` not found" unless source_map_hash.key?(filename) source_map_hash[filename] @@ -682,16 +718,16 @@ def super_and_sub? sup, 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 return false if sup.literal? && sub.literal? && sup.to_s != sub.to_s - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 sup = sup.simplify_literals.to_s - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 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" + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 sc_new = store.constants.dereference(sc) # Cyclical inheritance is invalid return false if sc_new == sc_fqns @@ -719,8 +755,7 @@ 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) + next nil if resolved.respond_to?(:visibility) && !visibility.include?(resolved&.visibility) resolved end.compact logger.debug do @@ -735,7 +770,7 @@ def 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 + # @param namespace_pin [Pin::Base, nil] Namespace pin for the rooted_type # parameter - used to pull generics information # @param type [ComplexType] The type which is having its # methods supplemented from fq_reference_tag @@ -837,7 +872,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false if scope == :instance store.get_includes(fqns).reverse.each do |ref| in_tag = dereference(ref) - # @sg-ignore Need to add nil check here + next if in_tag.nil? + result.concat inner_get_methods_from_reference(in_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true) end @@ -865,10 +901,6 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false result.concat inner_get_methods('Module', :instance, visibility, deep, skip, no_core) end end - store.domains(fqns).each do |d| - dt = ComplexType.try_parse(d) - result.concat inner_get_methods(dt.namespace, dt.scope, visibility, deep, skip) - end end result end @@ -941,6 +973,10 @@ def resolve_method_alias alias_pin break if original end if original.nil? + # Macro-generated pins join the store only after process_macros + # returns, so an alias to one cannot resolve yet + return nil if @processing_macros + # :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}" @@ -949,7 +985,6 @@ def resolve_method_alias alias_pin # :nocov: end - # @sg-ignore ignore `received nil` for original create_resolved_alias_pin(alias_pin, original) end @@ -991,11 +1026,12 @@ def create_resolved_alias_pin alias_pin, original resolved_pin end - # @param namespace_pin [Pin::Namespace] + # @param namespace_pin [Pin::Base, nil] # @param rooted_type [ComplexType] # @param pins [Enumerable] # @return [Array] def erase_generics namespace_pin, rooted_type, pins + return pins unless namespace_pin.is_a?(Pin::Namespace) 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}") @@ -1024,8 +1060,7 @@ def can_resolve_generics? namespace_pin, rooted_type 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 + # that this overload of 'protected' will typecheck def equality_fields # External is compared by identity and generation instead of by its pins: # Chain#infer hashes the ApiMap on every inference, and Array#hash would diff --git a/lib/solargraph/api_map/cache.rb b/lib/solargraph/api_map/cache.rb index c69d223b47..d7e57434f6 100644 --- a/lib/solargraph/api_map/cache.rb +++ b/lib/solargraph/api_map/cache.rb @@ -29,7 +29,7 @@ def inspect # @param scope [Symbol] # @param visibility [Array] # @param deep [Boolean] - # @return [Array] + # @return [Array, nil] def get_methods fqns, scope, visibility, deep @methods["#{fqns}|#{scope}|#{visibility}|#{deep}"] end @@ -46,7 +46,7 @@ def set_methods fqns, scope, visibility, deep, value # @param namespace [String] # @param contexts [Array] - # @return [Array] + # @return [Array, nil] def get_constants namespace, contexts @constants["#{namespace}|#{contexts}"] end @@ -62,6 +62,7 @@ def set_constants namespace, contexts, value # @param name [String] # @param context [String] # @return [String, nil] + # @sg-ignore https://github.com/apiology/solargraph/pull/225 def get_qualified_namespace name, context @qualified_namespaces["#{name}|#{context}"] end @@ -71,11 +72,12 @@ def get_qualified_namespace name, context # @param value [String, nil] # @return [void] def set_qualified_namespace name, context, value + # @sg-ignore https://github.com/apiology/solargraph/pull/225 @qualified_namespaces["#{name}|#{context}"] = value end # @param path [String] - # @return [Pin::Method] + # @return [Pin::Method, nil] def get_receiver_definition path @receiver_definitions[path] end diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 880adacb6d..b743f1b595 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -27,21 +27,17 @@ 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1245 # @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?('::') + return store.get_path_pins(name.delete_prefix('::')).first&.path if name.start_with?('::') flat = gates.flatten flat.push '' if flat.empty? - if cached_resolve.include? [name, flat] - cached_result = cached_resolve[[name, flat]] - # don't recurse - return nil if cached_result == :in_process - return cached_result - end + return cached_resolve[[name, flat]] if cached_resolve.include? [name, flat] + # don't recurse + return nil if in_process.include? [name, flat] resolve_and_cache(name, flat) end @@ -88,7 +84,6 @@ 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) @@ -98,7 +93,7 @@ def qualify_type type, *gates # @return [void] def clear - [cached_collect, cached_resolve].each(&:clear) + [cached_collect, cached_resolve, in_process].each(&:clear) end private @@ -108,11 +103,14 @@ def clear # @param name [String] # @param gates [Array] - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1425 # @return [String, nil] def resolve_and_cache name, gates - cached_resolve[[name, gates]] = :in_process + in_process.add [name, gates] + # @sg-ignore https://github.com/apiology/solargraph/pull/225 cached_resolve[[name, gates]] = resolve_uncached(name, gates) + ensure + in_process.delete [name, gates] end # @param name [String] @@ -124,12 +122,12 @@ def resolve_uncached name, gates parts = name.split('::') first = nil parts.each.with_index do |nam, idx| + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 resolved, remainder = complex_resolve(nam, base, idx != parts.length - 1) first ||= remainder 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 @@ -148,6 +146,7 @@ def complex_resolve name, gates, internal resolved = nil gates.each.with_index do |gate, idx| resolved = simple_resolve(name, gate, internal) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 return [resolved, gates[(idx + 1)..]] if resolved store.get_ancestor_references(gate).each do |ref| return ref.name.sub(/^::/, '') if ref.name.end_with?("::#{name}") && ref.name.start_with?('::') @@ -156,6 +155,7 @@ def complex_resolve name, gates, internal next unless mixin resolved = resolve(name, mixin) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 return [resolved, gates[(idx + 1)..]] if resolved end end @@ -170,7 +170,6 @@ 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) @@ -188,11 +187,18 @@ def collect_and_cache gates end end - # @return [Hash{Array(String, Array) => String, :in_process, nil}] + # @return [Hash{Array(String, Array) => String, nil}] def cached_resolve @cached_resolve ||= {} end + # Keys whose resolution is underway, so recursion stops + # + # @return [Set)>] + def in_process + @in_process ||= Set.new + end + # @return [Hash{Array => Array}] def cached_collect @cached_collect ||= {} @@ -210,8 +216,7 @@ def cached_collect # @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) + inner_qualify(namespace.delete_prefix('::'), '', Set.new) else inner_qualify(namespace, context_namespace, Set.new) end @@ -266,13 +271,13 @@ def inner_get_constants fqns, visibility, skip result = [] store.get_prepends(fqns).each do |pre| - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 inc_fqns = resolve(pin.name, pin.closure.gates - skip.to_a) result.concat inner_get_constants(inc_fqns, [:public], skip) end diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index dc40994fef..43320c2dd8 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -5,10 +5,10 @@ class ApiMap class Index include Logging - # @return [Array] + # @return [Set] attr_reader :macro_method_names - # @return [Hash{String => Array}] + # @return [Hash{String => Set}] attr_reader :macro_method_name_pins # @param pins [Array] @@ -23,22 +23,22 @@ def pins # @return [Hash{String => Array}] def namespace_hash - # @param h [String] - # @param k [Array] + # @param h [Hash{String => Array}] + # @param k [String] @namespace_hash ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{Class => Array}] def pin_class_hash - # @param h [String] - # @param k [Array] + # @param h [Hash{Class => Array}] + # @param k [Class] @pin_class_hash ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def path_pin_hash - # @param h [String] - # @param k [Array] + # @param h [Hash{String => Array}] + # @param k [String] @path_pin_hash ||= Hash.new { |h, k| h[k] = [] } end @@ -53,7 +53,7 @@ def alias_hash def pins_by_class klass # @type [Set>] s = Set.new - # @sg-ignore need to support destructured args in blocks + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 @pin_select_cache[klass] ||= pin_class_hash.each_with_object(s) { |(key, o), n| n.merge(o) if key <= klass } end @@ -111,6 +111,7 @@ def merge pins # @return [self] def deep_clone + # @param copy [Index] Index.allocate.tap do |copy| copy.pin_select_cache = {} copy.pins = pins.clone @@ -133,17 +134,20 @@ def catalog new_pins @pin_select_cache = {} pins.concat new_pins set = new_pins.to_set - # @param k [String] + # @param k [Class] # @param v [Set] set.classify(&:class) + # @sg-ignore https://github.com/castwide/solargraph/pull/1422 .map { |k, v| pin_class_hash[k].concat v.to_a } # @param k [String] # @param v [Set] set.classify(&:namespace) + # @sg-ignore https://github.com/castwide/solargraph/pull/1422 .map { |k, v| namespace_hash[k].concat v.to_a } # @param k [String] # @param v [Set] set.classify(&:path) + # @sg-ignore https://github.com/castwide/solargraph/pull/1422 .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 @@ -179,13 +183,10 @@ def map_overrides 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}" } + # @sg-ignore https://github.com/castwide/solargraph/pull/1422 pins.each do |pin| 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 end @@ -220,6 +221,7 @@ def redefine_return_type pin, tag # @return [void] def map_factory_parameters pins_by_class(Pin::FactoryParameter).each do |fp| + # @sg-ignore https://github.com/castwide/solargraph/pull/1422 factory_parameter_hash[fp.method_path] << fp end end diff --git a/lib/solargraph/api_map/source_to_yard.rb b/lib/solargraph/api_map/source_to_yard.rb index a121a348ba..b16ce3c39e 100644 --- a/lib/solargraph/api_map/source_to_yard.rb +++ b/lib/solargraph/api_map/source_to_yard.rb @@ -5,9 +5,7 @@ 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1419 # @generic T # @param path [String] # @param klass [Class>] @@ -34,22 +32,19 @@ def rake_yard store next end if pin.type == :class - # @param obj [YARD::CodeObjects::RootObject] + # @param obj [YARD::CodeObjects::ClassObject] 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] + # @param obj [YARD::CodeObjects::ModuleObject] 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 + # @sg-ignore flow sensitive typing needs to track reassignment code_object_map[pin.path].docstring = pin.docstring store.get_includes(pin.path).each do |ref| include_object = code_object_at(pin.path, YARD::CodeObjects::ClassObject) @@ -72,22 +67,20 @@ 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 ) do |obj| - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 next if pin.location.nil? || pin.location.filename.nil? - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 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 + next unless method_object + 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 @@ -102,7 +95,9 @@ def code_object_map end # @return [YARD::CodeObjects::RootObject] + # @sg-ignore https://github.com/apiology/solargraph/pull/225 def root_code_object + # @sg-ignore https://github.com/apiology/solargraph/pull/225 @root_code_object ||= YARD::CodeObjects::RootObject.new(nil, 'root') end end diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 4d77ebaab0..871a1ee8b5 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -32,9 +32,10 @@ def update *pinsets, &block # @todo Fix this map @fqns_pins_map = nil + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 return catalog(pinsets) if changed.zero? - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 pinsets[changed..].each_with_index do |pins, idx| @pinsets[changed + idx] = pins @indexes[changed + idx] = if pins.empty? @@ -44,7 +45,7 @@ def update *pinsets, &block end end # @type [Index] - @index = @indexes.last.clone + @index = @indexes.fetch(-1).clone @index = @index.merge(block.call) if block constants.clear cached_qualify_superclass.clear @@ -64,8 +65,10 @@ def inspect # @return [Enumerable] def get_constants fqns, visibility = [:public] 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) + next false if pin.name.empty? + next visibility.include?(pin.visibility) if pin.is_a?(Pin::Namespace) + + pin.is_a?(Pin::Constant) && visibility.include?(pin.visibility) end end @@ -90,6 +93,7 @@ def get_superclass fqns return nil if fqns.nil? || fqns.empty? return BOOLEAN_SUPERCLASS_PIN if %w[TrueClass FalseClass].include?(fqns) + # @sg-ignore https://github.com/castwide/solargraph/pull/1422 superclass_references[fqns].first || try_special_superclasses(fqns) end @@ -127,7 +131,7 @@ def get_extends fqns # @param path [String] # @return [Array] def get_path_pins path - index.path_pin_hash[path] + index.path_pin_hash[path] || [] end # @param fqns [String, nil] @@ -162,21 +166,11 @@ def namespace_pins pins_by_class(Solargraph::Pin::Namespace) end - # @return [Enumerable] + # @return [Set] def method_pins pins_by_class(Solargraph::Pin::Method) end - # @param fqns [String] - # @return [Array] - def domains fqns - result = [] - fqns_pins(fqns).each do |nspin| - result.concat nspin.domains - end - result - end - # @return [Hash{String => YardMap::Macro}] def named_macros @named_macros ||= begin @@ -215,14 +209,17 @@ def fqns_pins fqns base = '' name = fqns end - fqns_pins_map[[base, name]] + fqns_pins_map[[base, name]] || [] end # 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 - return [] if fqns.nil? || fqns.empty? + return [] if fqns.nil? + # Top-level methods live in the root namespace and Ruby defines + # them on Object + return ['', *get_ancestors('Object')] if fqns.empty? ancestors = [fqns] visited = Set.new @@ -245,14 +242,13 @@ def get_ancestors fqns # Add includes, prepends, and extends [get_includes(current), get_prepends(current), get_extends(current)].each do |refs| + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 next if refs.nil? # @param ref [String] + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 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 @@ -287,12 +283,12 @@ def unalias name index.alias_hash[name] end - # @return [Array] + # @return [Set] def macro_method_names index.macro_method_names end - # @return [Hash{String => Array}] + # @return [Hash{String => Set}] def macro_method_name_pins index.macro_method_name_pins end @@ -319,7 +315,7 @@ def catalog pinsets, &block end end @index = @indexes.last.clone - @index = @index.merge(block.call) if block + @index = index.merge(block.call) if block constants.clear cached_qualify_superclass.clear true @@ -373,6 +369,7 @@ def factory_parameter_hash # @param name [String] # @return [Enumerable] + # @sg-ignore https://github.com/castwide/solargraph/pull/1422 def namespace_children name return [] unless index.namespace_hash.key?(name) index.namespace_hash[name] @@ -397,7 +394,9 @@ def try_special_superclasses fqns # @param fq_sub_tag [String] # @return [String, nil] + # @sg-ignore https://github.com/apiology/solargraph/pull/225 def qualify_and_cache_superclass fq_sub_tag + # @sg-ignore https://github.com/apiology/solargraph/pull/225 cached_qualify_superclass[fq_sub_tag] = uncached_qualify_superclass(fq_sub_tag) end diff --git a/lib/solargraph/bench.rb b/lib/solargraph/bench.rb index dda2bbc88f..ffa1bc86a1 100644 --- a/lib/solargraph/bench.rb +++ b/lib/solargraph/bench.rb @@ -10,7 +10,7 @@ class Bench # @return [Workspace] attr_reader :workspace - # @return [SourceMap] + # @return [SourceMap, nil] attr_reader :live_map # @return [Set] @@ -29,7 +29,6 @@ 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 diff --git a/lib/solargraph/cache_dir.rb b/lib/solargraph/cache_dir.rb index 228f2e7999..e4609f0d6f 100644 --- a/lib/solargraph/cache_dir.rb +++ b/lib/solargraph/cache_dir.rb @@ -10,6 +10,7 @@ module CacheDir # The base directory for cached YARD documentation and serialized pins. # # @return [String] + # @sg-ignore https://github.com/apiology/solargraph/pull/225 def base_dir ENV['SOLARGRAPH_CACHE'] || (ENV['XDG_CACHE_HOME'] ? File.join(ENV['XDG_CACHE_HOME'], 'solargraph') : nil) || @@ -23,11 +24,58 @@ def work_dir File.join(base_dir, "ruby-#{RUBY_VERSION}", "rbs-#{RBS::VERSION}", "solargraph-#{Solargraph::VERSION}") end - # The current gem directory. + # The current gem directory. Plugins can change the pins mapped from + # gem source, so each set of loaded plugin versions gets its own. # # @return [String] def gem_dir - File.join(work_dir, 'gems') + return File.join(work_dir, 'gems') if plugins.empty? + key = plugins.sort.map { |name, version| [name, version].compact.join('-') }.join('+') + File.join(work_dir, "gems-#{key.tr(File::SEPARATOR, '_')}") + end + + # The plugins loaded in this process, with their gem versions. + # + # @return [Hash{String => String, nil}] + def plugins + @plugins ||= {} + end + + # What to require to load {plugins} again in another process, such as + # the cache subprocess, so that it writes to the same {gem_dir}. + # + # @return [Array] + def plugin_requires + @plugin_requires ||= [] + end + + # @param name [String] the plugin's require path + # @return [void] + def add_plugin name + # @sg-ignore Unresolved call to name on Gem::BasicSpecification, Hash{String => Gem::BasicSpecification} + spec = Gem.loaded_specs.each_value.find { |s| s.name == name || s.contains_requirable_file?(name) } + plugins[spec&.name || name] = spec&.version&.to_s + plugin_requires.push name unless plugin_requires.include?(name) + end + + # Record the gem that defines a convention, so a plugin loaded by a + # plain require still gets its own gem cache directory. + # + # @param convention [Class] + # @return [void] + def add_convention convention + name = convention.name + return if name.nil? + file = Object.const_source_location(name)&.first + return if file.nil? + # @sg-ignore each_value block param is typed as the Hash pair, not the value + spec = Gem.loaded_specs.each_value.find { |s| file.start_with?(File.join(s.full_gem_path, '')) } + # @sg-ignore each_value block param is typed as the Hash pair, not the value + return if spec.nil? || spec.name == 'solargraph' + # @sg-ignore each_value block param is typed as the Hash pair, not the value + plugins[spec.name] = spec.version.to_s + # @sg-ignore each_value block param is typed as the Hash pair, not the value + plugin_requires.push spec.name unless plugin_requires.include?(spec.name) end # The current stdlib directory. @@ -51,7 +99,9 @@ def yard_dir File.join(base_dir, "yard-#{YARD::VERSION}", "yard-activesupport-concern-#{YARD::ActiveSupport::Concern::VERSION}") end + # @return [void] def clear + # @sg-ignore https://github.com/castwide/solargraph/pull/1281 FileUtils.rm_rf base_dir end end diff --git a/lib/solargraph/collection/base.rb b/lib/solargraph/collection/base.rb index e9f4450e07..45e1ae1173 100644 --- a/lib/solargraph/collection/base.rb +++ b/lib/solargraph/collection/base.rb @@ -22,27 +22,50 @@ def load end # @return [Array] + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 def self.load(...) + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 new(...).load end + # @return [void] + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 def self.uncache(...) + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 cache_file = new(...).cache_file + # @sg-ignore https://github.com/castwide/solargraph/pull/1281 FileUtils.rm_rf cache_file Collection.mem_cache.delete cache_file end private + # @return [Array, nil] def file_cache_pins - Marshal.load(File.read(cache_file, mode: 'rb')) if File.exist?(cache_file) # rubocop:disable Security/MarshalLoad + return nil unless File.exist?(cache_file) + + # @type [Object] + loaded = Marshal.load(File.read(cache_file, mode: 'rb')) # rubocop:disable Security/MarshalLoad + # @param pin [Object] + return loaded.map { |pin| pin.is_a?(Pin::Base) ? pin : raise(TypeError, "#{cache_file} contains a non-pin") } if loaded.is_a?(Array) + + raise TypeError, "#{cache_file} does not contain an array of pins" end + # @return [Array] def generate_pin_caches Collection.mem_cache[cache_file] = pins serial = Marshal.dump(pins) + # @sg-ignore https://github.com/castwide/solargraph/pull/1281 FileUtils.mkdir_p File.dirname(cache_file) - File.write cache_file, serial, mode: 'wb' + # Write to a temp file in the same directory and rename into place + # (rename is atomic on the same filesystem) so that concurrent + # readers/writers - e.g. multiple parallel_tests worker processes, + # or the threads in Collection::Gem.load_all, caching the same gem + # for the first time - never observe a partially-written file. + tmp = "#{cache_file}.#{Process.pid}.#{Thread.current.object_id}.tmp" + File.write tmp, serial, mode: 'wb' + File.rename tmp, cache_file pins end end diff --git a/lib/solargraph/collection/gem.rb b/lib/solargraph/collection/gem.rb index c258cceb53..01ad0249a8 100644 --- a/lib/solargraph/collection/gem.rb +++ b/lib/solargraph/collection/gem.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'benchmark' +require 'concurrent-ruby' + module Solargraph module Collection # Cacheable gem pins. @@ -7,6 +10,9 @@ module Collection class Gem < Base include Logging + # Only report timing for batches slow enough to be worth noticing. + SLOW_BATCH_MS = 500 + attr_reader :metagem @@path_source_cache = {} @@ -21,6 +27,7 @@ def cache_file File.join CacheDir.gem_dir, "#{metagem.cache_name}.ser" end + # @sg-ignore https://github.com/castwide/solargraph/pull/1364 def load return super if metagem.cacheable? @@path_source_cache[metagem.full_path] ||= pins @@ -30,27 +37,78 @@ def pins @pins ||= without_yard end + # @param metagem [Metagem] def self.cached? metagem metagem.cacheable? && File.exist?(new(metagem).cache_file) end + # Build the pin caches for several gems at once, one gem per thread. + # + # Each gem is independent - it parses its own yardoc and RBS and + # writes its own cache file - so the batch scales with the machine + # rather than running end to end. Building a cold bundle serially is + # the dominant cost of a first run. + # + # @param gems [Enumerable] + # @param out [IO, StringIO, nil] stream for the timing summary + # @param rebuild [Boolean] discard existing caches and generate them again + # @return [void] + def self.load_all gems, out: nil, rebuild: false + metagems = gems.to_a + return if metagems.empty? + + pool_size = Concurrent.processor_count + pool = Concurrent::FixedThreadPool.new(pool_size) + time = Benchmark.measure do + futures = metagems.map do |metagem| + Concurrent::Promises.future_on(pool, metagem) do |mg| + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 + uncache mg if rebuild + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 + load mg + end + end + # #value! re-raises in this thread if any gem failed, matching + # the serial loop this replaces. + Concurrent::Promises.zip(*futures).value! + pool.shutdown + pool.wait_for_termination + ensure + # A raise or a caller's timeout skips the shutdown above; stop the + # workers rather than leave them mapping gems in the background. + pool.kill + end + + milliseconds = (time.real * 1000).round + # @sg-ignore Gem source pin without @return shadows RBS return type (Benchmark::Tms#real) + return unless out && milliseconds > SLOW_BATCH_MS + + out.puts "Built #{metagems.length} gems in #{milliseconds} ms in #{pool_size} threads" + end + private + # @return [Array] def with_yard code_objects = Yardoc.load!(metagem) yard_pins = YardMap::Mapper.new(code_objects, metagem).map + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 rbs_pins = RbsMap::Gem.pins(metagem) RbsMap::Helpers.combine(yard_pins, rbs_pins) end + # @return [Array] def without_yard - files = metagem.require_paths.flat_map { |path| Dir.glob(File.join(metagem.full_path, path, '**', '*.rb')) } + dirs = metagem.require_paths + + Convention.extra_source_paths(root: metagem.full_path, require_paths: metagem.require_paths) + files = dirs.flat_map { |path| Dir.glob(File.join(metagem.full_path, path, '**', '*.rb')) }.uniq source_maps = files.map { |file| Solargraph::SourceMap.load(file) } # Generating an ApiMap is necessary for processing macros - bench = Bench.new(source_maps: source_maps) + bench = Bench.new(source_maps: source_maps, workspace: Workspace.new(metagem: metagem)) source_pins = ApiMap.new.catalog(bench).pins.select { |pin| files.include?(pin.filename) } + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 rbs_pins = RbsMap::Gem.pins(metagem) RbsMap::Helpers.combine(source_pins, rbs_pins) end diff --git a/lib/solargraph/collection/stdlib.rb b/lib/solargraph/collection/stdlib.rb index 52ecbb4b9e..af4e253805 100644 --- a/lib/solargraph/collection/stdlib.rb +++ b/lib/solargraph/collection/stdlib.rb @@ -14,6 +14,7 @@ def initialize library end def pins + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 @pins ||= RbsMap::Stdlib.pins(library) end diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 5b0e984b25..f95c779fa5 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -56,7 +56,7 @@ def resolve_generics_from_context generics_to_resolve, context_type, resolved_ge end) end - # @return [UniqueType] + # @return [UniqueType, nil] def first @items.first end @@ -81,9 +81,7 @@ def self_to_type dst # @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 + # @sg-ignore https://github.com/apiology/solargraph/pull/112 def map &block @items.map(&block) end @@ -132,7 +130,7 @@ def to_a end # @param index [Integer] - # @return [UniqueType] + # @return [UniqueType, nil] def [] index @items[index] end @@ -311,6 +309,8 @@ def transform new_name = nil, &transform_type ComplexType.new(map { |ut| ut.transform(new_name, &transform_type) }) end + # @param named_types [Hash{String => ComplexType::UniqueType}] + # @return [ComplexType] def expand named_types ComplexType.new(map { |ut| ut.expand(named_types) }) end @@ -322,7 +322,7 @@ def force_rooted end end - # @param definitions [Pin::Namespace, Pin::Method] + # @param definitions [Pin::Namespace, Pin::Method, nil] # @param context_type [ComplexType] # @return [ComplexType] def resolve_generics definitions, context_type @@ -343,7 +343,7 @@ def without_nil # @return [Array] def all_params - @items.first.all_params || [] + @items.first&.all_params || [] end # @return [ComplexType] @@ -367,7 +367,7 @@ def all_rooted? def erased_version_of? other return false if items.length != 1 || other.items.length != 1 - @items.first.erased_version_of?(other.items.first) + @items.fetch(0).erased_version_of?(other.items.fetch(0)) end # every top-level type has resolved to be fully qualified; see @@ -444,14 +444,12 @@ class << self # 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] - # @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. + # @overload parse(*strings, partial: false) + # @param partial [false] + # @return [ComplexType] + # @overload parse(*strings, partial: true) + # @param partial [true] True if the string is part of a another type + # @return [Array] def parse *strings, partial: false # @type [Hash{Array => ComplexType, Array}] @cache ||= {} @@ -480,7 +478,7 @@ def parse *strings, partial: false elsif base.end_with?('=') 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 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 diff --git a/lib/solargraph/complex_type/conformance.rb b/lib/solargraph/complex_type/conformance.rb index c2a48b255f..dfa78e9e10 100644 --- a/lib/solargraph/complex_type/conformance.rb +++ b/lib/solargraph/complex_type/conformance.rb @@ -23,13 +23,13 @@ def initialize api_map, inferred, expected, @variance = variance # :nocov: unless expected.is_a?(UniqueType) - # @sg-ignore This should never happen and the typechecker is angry about it + # @sg-ignore https://github.com/castwide/solargraph/pull/1277 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1277 raise "Inferred type must be a UniqueType, got #{inferred.class} in #{inferred.inspect}" # :nocov: end diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index ce7897e497..1241a6d2ca 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -144,6 +144,8 @@ def namespace @namespace ||= lambda do return 'Object' if duck_type? return 'NilClass' if nil_type? + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top %w[Class Module].include?(name) && !subtypes.empty? ? subtypes.first.name : name end.call end @@ -205,7 +207,7 @@ def scope # @param other [Object] def == other return false unless self.class == other.class - # @sg-ignore flow sensitive typing should support .class == .class + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 tag == other.tag end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 4bbdda5b2d..3a5b50ea63 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -39,7 +39,6 @@ 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 unless !subs.is_a?(ComplexType) && (subs.length == 2) && !subs[0].is_a?(UniqueType) && !subs[1].is_a?(UniqueType) @@ -54,13 +53,15 @@ def self.parse name, substring = '', make_rooted: nil raise ComplexTypeError, "Bad hash type: name=#{name}, substring=#{substring} - must have exactly two parameters" end + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 key_types.concat(subs[0].map { |u| ComplexType.new([u]) }) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 subtypes.concat(subs[1].map { |u| ComplexType.new([u]) }) else subtypes.concat subs end end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1266 new(name, key_types, subtypes, rooted: rooted, parameters_type: parameters_type) end @@ -183,17 +184,17 @@ def determine_non_literal_name def eql? other self.class == other.class && - # @sg-ignore flow sensitive typing should support .class == .class + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 @name == other.name && - # @sg-ignore flow sensitive typing should support .class == .class + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 @key_types == other.key_types && - # @sg-ignore flow sensitive typing should support .class == .class + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 @subtypes == other.subtypes && - # @sg-ignore flow sensitive typing should support .class == .class + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 @rooted == other.rooted? && - # @sg-ignore flow sensitive typing should support .class == .class + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 @all_params == other.all_params && - # @sg-ignore flow sensitive typing should support .class == .class + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 @parameters_type == other.parameters_type end @@ -302,7 +303,6 @@ def desc rooted_tags end - # @sg-ignore Need better if/elseanalysis # @return [String] def to_rbs if duck_type? @@ -312,7 +312,7 @@ def to_rbs elsif name.downcase == 'nil' 'nil' elsif name == GENERIC_TAG_NAME - all_params.first&.name + all_params.first&.name || 'untyped' elsif %w[Class Module].include?(name) rbs_name elsif %w[Tuple Array].include?(name) && fixed_parameters? @@ -342,7 +342,7 @@ def parameters? # @return [String] def rbs_union types if types.length == 1 - types.first.to_rbs + types.fetch(0).to_rbs else "(#{types.map(&:to_rbs).join(' | ')})" end @@ -386,20 +386,18 @@ def downcast_to_literal_if_possible def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {} if name == ComplexType::GENERIC_TAG_NAME type_param = subtypes.first&.name - return self unless generics_to_resolve.include? type_param - # @sg-ignore flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo) + return self unless type_param && generics_to_resolve.include?(type_param) 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| + # @sg-ignore https://github.com/apiology/solargraph/pull/225 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 @@ -419,6 +417,7 @@ def resolve_generics_from_context generics_to_resolve, context_type, resolved_ge 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| + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 ct.items.flat_map do |ut| context_params = yield context_type if context_type if context_params && context_params[i] @@ -439,7 +438,7 @@ def resolve_param_generics_from_context generics_to_resolve, context_type, resol # parameters used in this type, and return a new type if # possible. # - # @param definitions [Pin::Namespace, Pin::Method] The module/class/method which uses generic types + # @param definitions [Pin::Namespace, Pin::Method, nil] The module/class/method which uses generic types # @param context_type [ComplexType] The receiver type # @return [UniqueType, ComplexType] def resolve_generics definitions, context_type @@ -503,7 +502,7 @@ def recreate new_name: nil, make_rooted: nil, new_key_types: nil, new_subtypes: 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 UniqueType.new(new_name, new_key_types, new_subtypes, rooted: make_rooted, parameters_type: parameters_type) end @@ -547,6 +546,8 @@ def transform new_name = nil, &transform_type yield new_type end + # @param named_types [Hash{String => UniqueType}] + # @return [UniqueType] def expand named_types named_types[name] || self end diff --git a/lib/solargraph/convention.rb b/lib/solargraph/convention.rb index 76bfa832e1..d5731e51ac 100644 --- a/lib/solargraph/convention.rb +++ b/lib/solargraph/convention.rb @@ -14,19 +14,28 @@ module Convention autoload :ClassDefinition, 'solargraph/convention/class_definition' autoload :ActiveSupportConcern, 'solargraph/convention/active_support_concern' + # Writers replace the set instead of mutating it, so readers in other + # threads (e.g. Library cache threads) iterate an unchanging snapshot. + # # @type [Set] - @@conventions = Set.new + @@conventions = Set.new.freeze + @@conventions_lock = Mutex.new # @param convention [Class] # @return [void] def self.register convention - @@conventions.add convention.new + @@conventions_lock.synchronize do + @@conventions = (@@conventions | [convention.new]).freeze + end + CacheDir.add_convention convention end # @param convention [Class] # @return [void] def self.unregister convention - @@conventions.delete_if { |c| c.is_a?(convention) } + @@conventions_lock.synchronize do + @@conventions = Set.new(@@conventions.grep_v(convention)).freeze + end end # @param source_map [SourceMap] @@ -73,6 +82,15 @@ def self.for_object api_map, rooted_tag, scope, visibility, result end + # @param root [String] the gem's root directory + # @param require_paths [Array] the gem's require paths, relative to root + # @return [Array] paths inside the gem, relative to root, to map beyond require_paths + def self.extra_source_paths root:, require_paths: + @@conventions.flat_map do |conv| + conv.extra_source_paths(root: root, require_paths: require_paths) + end.uniq + end + register Gemfile register Gemspec register Rakefile diff --git a/lib/solargraph/convention/active_support_concern.rb b/lib/solargraph/convention/active_support_concern.rb index ed1fba175f..8c9887887c 100644 --- a/lib/solargraph/convention/active_support_concern.rb +++ b/lib/solargraph/convention/active_support_concern.rb @@ -26,7 +26,7 @@ def object api_map, rooted_tag, scope, visibility, deep, skip, _no_core # yard-activesupport-concern pulls methods inside # 'class_methods' blocks into main class visible from YARD # - # @param _doc_map [DocMap] + # @param _doc_map [Object] def global _doc_map Environ.new(yard_plugins: ['activesupport-concern']) end diff --git a/lib/solargraph/convention/base.rb b/lib/solargraph/convention/base.rb index 22b328e50f..316ac1a87e 100644 --- a/lib/solargraph/convention/base.rb +++ b/lib/solargraph/convention/base.rb @@ -23,7 +23,7 @@ def local source_map # The Environ for a DocMap. # Subclasses can override this method. # - # @param doc_map [DocMap] + # @param doc_map [Object] # @return [Environ] def global doc_map EMPTY_ENVIRON @@ -45,6 +45,13 @@ def object api_map, rooted_tag, scope, visibility, deep, skip, no_core EMPTY_ENVIRON end + + # @param root [String] the gem's root directory + # @param require_paths [Array] the gem's require paths, relative to root + # @return [Array] paths inside the gem, relative to root, to map beyond require_paths + def extra_source_paths(root:, require_paths:, **) + [] + end end end end diff --git a/lib/solargraph/convention/class_definition/class_assignment_node.rb b/lib/solargraph/convention/class_definition/class_assignment_node.rb index 3f78dcecad..4258473754 100644 --- a/lib/solargraph/convention/class_definition/class_assignment_node.rb +++ b/lib/solargraph/convention/class_definition/class_assignment_node.rb @@ -32,27 +32,21 @@ def match? node # @param node [Parser::AST::Node] # @return [Parser::AST::Node, nil] def new_node node - value = node.children[2] - return nil if value.nil? - return value unless value.type == :block + # s(:casgn, scope, name, value) - value is absent when the target is in an mlhs + node => [:casgn, _, _, *rest] + return nil unless rest in [::Parser::AST::Node => value, *] - value.children[0] + (value in [:block, ::Parser::AST::Node => call, *]) ? call : value end private # @param send_node [Parser::AST::Node, nil] # @return [Boolean] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 def class_new_node? send_node - return false if send_node.nil? - return false unless send_node.type == :send - return false unless send_node.children[1] == :new - - receiver = send_node.children[0] - return false if receiver.nil? - return false unless receiver.type == :const - - receiver.children[1] == :Class + # s(:send, s(:const, scope, :Class), :new, *args) + send_node in [:send, ::Parser::AST::Node[:const, _, :Class], :new, *] end end @@ -63,10 +57,12 @@ def initialize node # @return [String] def class_name - namespace = node.children[0] - return node.children[1].to_s if namespace.nil? + # s(:casgn, scope, name, value) - scope is nil for a bare name + node => [:casgn, ::Parser::AST::Node | nil => scope, Symbol => name, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + return name.to_s if scope.nil? - "#{Parser::NodeMethods.unpack_name(namespace)}::#{node.children[1]}" + "#{Parser::NodeMethods.unpack_name(scope)}::#{name}" end # The superclass, when it is written as a constant. `Class.new(expr)` @@ -77,11 +73,10 @@ def superclass_name send_node = self.class.new_node(node) return nil if send_node.nil? - arg = send_node.children[2] - return nil if arg.nil? - return nil unless arg.type == :const + # s(:send, receiver, :new, superclass, *) - superclass is absent for a bare Class.new + return nil unless send_node in [:send, _, _, ::Parser::AST::Node[:const, *] => superclass, *] - Parser::NodeMethods.unpack_name(arg) + Parser::NodeMethods.unpack_name(superclass) end private diff --git a/lib/solargraph/convention/data_definition.rb b/lib/solargraph/convention/data_definition.rb index 960852caac..79c6f0a7d7 100644 --- a/lib/solargraph/convention/data_definition.rb +++ b/lib/solargraph/convention/data_definition.rb @@ -17,7 +17,7 @@ def process type: :class, location: loc, closure: region.closure, - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 name: data_definition_node.class_name, comments: comments_for(node), visibility: :public, @@ -40,7 +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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 data_definition_node.attributes.map do |attribute_node, attribute_name| initialize_method_pin.parameters.push( Pin::Parameter.new( @@ -53,7 +53,7 @@ def process end # define attribute readers and instance variables - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 data_definition_node.attributes.each do |attribute_node, attribute_name| name = attribute_name.to_s method_pin = Pin::Method.new( diff --git a/lib/solargraph/convention/data_definition/data_assignment_node.rb b/lib/solargraph/convention/data_definition/data_assignment_node.rb index 97ef272cfd..755c3ca95a 100644 --- a/lib/solargraph/convention/data_definition/data_assignment_node.rb +++ b/lib/solargraph/convention/data_definition/data_assignment_node.rb @@ -24,36 +24,34 @@ class << self # s(:send, nil, :bar)))) # @param node [::Parser::AST::Node] def match? node - return false unless node&.type == :casgn - return false if node.children[2].nil? + # s(:casgn, scope, name, value) - value is the call, or a block on it + return false unless node in [:casgn, _, _, ::Parser::AST::Node => value, *] - data_node = if node.children[2].type == :block - node.children[2].children[0] - else - node.children[2] - end + data_node = (value in [:block, ::Parser::AST::Node => call, *]) ? call : value data_definition_node?(data_node) end end def class_name - if node.children[0] - Parser::NodeMethods.unpack_name(node.children[0]) + "::#{node.children[1]}" + # s(:casgn, scope, name, value) - scope is nil for a bare name + node => [_, ::Parser::AST::Node | nil => scope, Symbol => name, *] + + if scope + Parser::NodeMethods.unpack_name(scope) + "::#{name}" else - node.children[1].to_s + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name.to_s end end private # @return [Parser::AST::Node] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 def data_node - if node.children[2].type == :block - node.children[2].children[0] - else - node.children[2] - end + node => [_, _, _, ::Parser::AST::Node => value, *] + (value in [:block, ::Parser::AST::Node => call, *]) ? call : value end end end diff --git a/lib/solargraph/convention/data_definition/data_definition_node.rb b/lib/solargraph/convention/data_definition/data_definition_node.rb index 5c4b2276a1..d67d60821a 100644 --- a/lib/solargraph/convention/data_definition/data_definition_node.rb +++ b/lib/solargraph/convention/data_definition/data_definition_node.rb @@ -28,23 +28,20 @@ class << self # # @param node [Parser::AST::Node] def match? node - return false unless node&.type == :class + return false unless node in [:class, _, ::Parser::AST::Node | nil => superclass, *] - data_definition_node?(node.children[1]) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + data_definition_node?(superclass) end private - # @param data_node [Parser::AST::Node] + # @param data_node [Parser::AST::Node, nil] # @return [Boolean] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 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 - return false unless data_node.children[0].children[1] == :Data - return false unless data_node.children[1] == :define - - true + # Data.define(*args) - s(:send, s(:const, scope, :Data), :define, *args) + data_node in ::Parser::AST::Node[:send, [:const, _, :Data, *], :define, *] end end @@ -61,14 +58,17 @@ def class_name # @return [Array] def attributes data_attribute_nodes.map do |data_def_param| - next unless data_def_param.type == :sym - [data_def_param, data_def_param.children[0].to_s] + next unless data_def_param in [:sym, Symbol => name] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + [data_def_param, name.to_s] end.compact end # @return [Parser::AST::Node, nil] def body_node - node.children[2] + # s(:class, name, superclass, body) + (node in [_, _, _, ::Parser::AST::Node | nil => body, *]) ? body : nil end private @@ -78,14 +78,14 @@ def body_node # @return [Parser::AST::Node, nil] def data_node - node.children[1] + (node in [_, _, ::Parser::AST::Node | nil => superclass, *]) ? superclass : nil end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 # @return [Array] def data_attribute_nodes - # @sg-ignore Need to add nil check here - data_node.children[2..] + data_node => [_, _, _, *args] + args end end end diff --git a/lib/solargraph/convention/struct_definition.rb b/lib/solargraph/convention/struct_definition.rb index b2c182c774..27bb7be29a 100644 --- a/lib/solargraph/convention/struct_definition.rb +++ b/lib/solargraph/convention/struct_definition.rb @@ -17,7 +17,7 @@ def process type: :class, location: loc, closure: region.closure, - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 name: struct_definition_node.class_name, docstring: docstring, visibility: :public, @@ -40,7 +40,7 @@ def process pins.push initialize_method_pin - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 struct_definition_node.attributes.map do |attribute_node, attribute_name| initialize_method_pin.parameters.push( Pin::Parameter.new( @@ -54,7 +54,7 @@ def process end # define attribute accessors and instance variables - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 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 } @@ -124,8 +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| + struct_definition_node&.attributes&.each do |attr_node, attr_name| comment = comments_for(attr_node) next if comment.nil? diff --git a/lib/solargraph/convention/struct_definition/struct_assignment_node.rb b/lib/solargraph/convention/struct_definition/struct_assignment_node.rb index 6dcafd068a..05353eebfa 100644 --- a/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +++ b/lib/solargraph/convention/struct_definition/struct_assignment_node.rb @@ -25,36 +25,34 @@ class << self # # @param node [Parser::AST::Node] def match? node - return false unless node&.type == :casgn - return false if node.children[2].nil? + # s(:casgn, scope, name, value) - value is the call, or a block on it + return false unless node in [:casgn, _, _, ::Parser::AST::Node => value, *] - struct_node = if node.children[2].type == :block - node.children[2].children[0] - else - node.children[2] - end + struct_node = (value in [:block, ::Parser::AST::Node => call, *]) ? call : value struct_definition_node?(struct_node) end end def class_name - if node.children[0] - Parser::NodeMethods.unpack_name(node.children[0]) + "::#{node.children[1]}" + # s(:casgn, scope, name, value) - scope is nil for a bare name + node => [_, ::Parser::AST::Node | nil => scope, Symbol => name, *] + + if scope + Parser::NodeMethods.unpack_name(scope) + "::#{name}" else - node.children[1].to_s + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name.to_s end end private # @return [Parser::AST::Node] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 def struct_node - if node.children[2].type == :block - node.children[2].children[0] - else - node.children[2] - end + node => [_, _, _, ::Parser::AST::Node => value, *] + (value in [:block, ::Parser::AST::Node => call, *]) ? call : value end end end diff --git a/lib/solargraph/convention/struct_definition/struct_definition_node.rb b/lib/solargraph/convention/struct_definition/struct_definition_node.rb index 51518a6874..a8fb59f29b 100644 --- a/lib/solargraph/convention/struct_definition/struct_definition_node.rb +++ b/lib/solargraph/convention/struct_definition/struct_definition_node.rb @@ -28,23 +28,20 @@ class << self # # @param node [Parser::AST::Node] def match? node - return false unless node&.type == :class + return false unless node in [:class, _, ::Parser::AST::Node | nil => superclass, *] - struct_definition_node?(node.children[1]) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + struct_definition_node?(superclass) end private - # @param struct_node [Parser::AST::Node] + # @param struct_node [Parser::AST::Node, nil] # @return [Boolean] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 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 - return false unless struct_node.children[0].children[1] == :Struct - return false unless struct_node.children[1] == :new - - true + # Struct.new(*args) - s(:send, s(:const, scope, :Struct), :new, *args) + struct_node in ::Parser::AST::Node[:send, [:const, _, :Struct, *], :new, *] end end @@ -61,25 +58,27 @@ def class_name # @return [Array] def attributes struct_attribute_nodes.map do |struct_def_param| - next unless struct_def_param.type == :sym - [struct_def_param, struct_def_param.children[0].to_s] + next unless struct_def_param in [:sym, Symbol => name] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + [struct_def_param, name.to_s] end.compact end + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 def keyword_init? + # keyword_init: true - s(:hash, s(:pair, s(:sym, :keyword_init), s(:true)), *pairs) keyword_init_param = struct_attribute_nodes.find do |struct_def_param| - struct_def_param.type == :hash && struct_def_param.children[0].type == :pair && - struct_def_param.children[0].children[0].children[0] == :keyword_init + struct_def_param in [:hash, [:pair, [_, :keyword_init, *], _], *] end - return false if keyword_init_param.nil? - - keyword_init_param.children[0].children[1].type == :true + keyword_init_param in [:hash, [:pair, _, [:true, *]], *] end - # @return [Parser::AST::Node] + # @return [Parser::AST::Node, nil] def body_node - node.children[2] + # s(:class, name, superclass, body) + (node in [_, _, _, ::Parser::AST::Node | nil => body, *]) ? body : nil end private @@ -87,15 +86,16 @@ def body_node # @return [Parser::AST::Node] attr_reader :node - # @return [Parser::AST::Node] + # @return [Parser::AST::Node, nil] def struct_node - node.children[1] + (node in [_, _, ::Parser::AST::Node | nil => superclass, *]) ? superclass : nil end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 # @return [Array] def struct_attribute_nodes - struct_node.children[2..-1] + struct_node => [_, _, _, *args] + args end end end diff --git a/lib/solargraph/diagnostics/base.rb b/lib/solargraph/diagnostics/base.rb index ff91a9062c..cbc181e7c9 100644 --- a/lib/solargraph/diagnostics/base.rb +++ b/lib/solargraph/diagnostics/base.rb @@ -20,8 +20,12 @@ def initialize *args # # @param source [Solargraph::Source] # @param api_map [Solargraph::ApiMap] + # @param workspace [Solargraph::Workspace, nil] + # Explicit workspace to use, instead of the current working + # directory's workspace. Useful in specs for isolation. + # # @return [Array] - def diagnose source, api_map + def diagnose source, api_map, workspace: nil [] end end diff --git a/lib/solargraph/diagnostics/require_not_found.rb b/lib/solargraph/diagnostics/require_not_found.rb index a71d9f9e80..b903b9f1f9 100644 --- a/lib/solargraph/diagnostics/require_not_found.rb +++ b/lib/solargraph/diagnostics/require_not_found.rb @@ -6,11 +6,13 @@ module Diagnostics # either a file in the workspace or a gem. # class RequireNotFound < Base - def diagnose source, api_map + # @param source [Solargraph::Source] + # @param api_map [Solargraph::ApiMap] + # @param workspace [Solargraph::Workspace, nil] unused; accepted for interface compatibility with Diagnostics::Base + def diagnose source, api_map, workspace: nil 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.unresolved_requires.each do |r| diff --git a/lib/solargraph/diagnostics/rubocop.rb b/lib/solargraph/diagnostics/rubocop.rb index 39b79d9b70..95e69008e7 100644 --- a/lib/solargraph/diagnostics/rubocop.rb +++ b/lib/solargraph/diagnostics/rubocop.rb @@ -19,14 +19,18 @@ class Rubocop < Base 'fatal' => Severities::ERROR }.freeze + # RuboCop lints its current directory when given no path, so a source + # without a filename is linted from stdin under this placeholder name. + UNSAVED_FILENAME = 'untitled.rb' + # @param source [Solargraph::Source] # @param _api_map [Solargraph::ApiMap] + # @param workspace [Solargraph::Workspace, nil] unused; accepted for interface compatibility with Diagnostics::Base # @return [Array] - def diagnose source, _api_map + def diagnose source, _api_map, workspace: nil @source = source require_rubocop(rubocop_version) - # @sg-ignore Need to add nil check here - options, paths = generate_options(source.filename, source.code) + options, paths = generate_options(source.filename || UNSAVED_FILENAME, source.code) store = RuboCop::ConfigStore.new runner = RuboCop::Runner.new(options, store) # Ensure only one instance of RuboCop::Runner is running at @@ -49,7 +53,7 @@ def diagnose source, _api_map # Extracts the rubocop version from _args_ # - # @return [String] + # @return [String, nil] def rubocop_version args.find { |a| a =~ /version=/ }.to_s.split('=').last end @@ -58,6 +62,7 @@ def rubocop_version # @return [Array] def make_array resp diagnostics = [] + # @sg-ignore https://github.com/castwide/solargraph/pull/1398 resp['files'].each do |file| file['offenses'].each do |off| diagnostics.push offense_to_diagnostic(off) @@ -90,14 +95,18 @@ def offense_range off # @param off [Hash{String => Hash{String => Integer}}] # @return [Position] def offense_start_position off + # @sg-ignore https://github.com/castwide/solargraph/pull/1398 Position.new(off['location']['start_line'] - 1, off['location']['start_column'] - 1) end # @param off [Hash{String => Hash{String => Integer}}] # @return [Position] def offense_ending_position off + # @sg-ignore https://github.com/castwide/solargraph/pull/1398 if off['location']['start_line'] == off['location']['last_line'] + # @sg-ignore https://github.com/castwide/solargraph/pull/1398 start_line = off['location']['start_line'] - 1 + # @sg-ignore https://github.com/castwide/solargraph/pull/1398 # @type [Integer] last_column = off['location']['last_column'] line = @source.code.lines[start_line] @@ -111,6 +120,7 @@ def offense_ending_position off start_line, last_column - col_off ) else + # @sg-ignore https://github.com/castwide/solargraph/pull/1398 Position.new(off['location']['start_line'], 0) end end diff --git a/lib/solargraph/diagnostics/rubocop_helpers.rb b/lib/solargraph/diagnostics/rubocop_helpers.rb index e97ca628ea..3626d8f62c 100644 --- a/lib/solargraph/diagnostics/rubocop_helpers.rb +++ b/lib/solargraph/diagnostics/rubocop_helpers.rb @@ -18,14 +18,14 @@ def require_rubocop version = nil # @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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 $LOAD_PATH.unshift(gem_lib_path) unless $LOAD_PATH.include?(gem_lib_path) rescue Gem::MissingSpecVersionError => e # @type [Array] specs = e.specs raise InvalidRubocopVersionError, "could not find '#{e.name}' (#{e.requirement}) - " \ - "did find: [#{specs.map { |s| s.version.version }.join(', ')}]" + "did find: [#{specs.map { |s| s.version.to_s }.join(', ')}]" end require 'rubocop' end @@ -39,7 +39,6 @@ def generate_options filename, code args = ['-f', 'j', '--force-exclusion', filename] base_options = RuboCop::Options.new options, paths = base_options.parse(args) - # @sg-ignore options[:stdin] = code [options, paths] end @@ -51,8 +50,7 @@ def generate_options filename, code # @return [String] def fix_drive_letter path return path unless path.match(/^[a-z]:/) - # @sg-ignore Need to add nil check here - path[0].upcase + path[1..] + path.sub(/\A[a-z]/, &:upcase) 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 8d857b647b..8ab217f629 100644 --- a/lib/solargraph/diagnostics/type_check.rb +++ b/lib/solargraph/diagnostics/type_check.rb @@ -7,11 +7,11 @@ module Diagnostics # class TypeCheck < Base # @return [Array] - def diagnose source, api_map + def diagnose source, api_map, workspace: nil # return [] unless args.include?('always') || api_map.workspaced?(source.filename) severity = Diagnostics::Severities::ERROR - level = args.reverse.find { |a| %w[normal typed strict strong].include?(a) } || :normal - checker = Solargraph::TypeChecker.new(source.filename, api_map: api_map, level: level.to_sym) + level = args.reverse.find { |a| %w[normal typed strict strong].include?(a) } || 'normal' + checker = Solargraph::TypeChecker.new(source.filename, api_map: api_map, level: level.to_sym, workspace: workspace) checker.problems .sort { |a, b| a.location.range.start.line <=> b.location.range.start.line } .map do |problem| diff --git a/lib/solargraph/diagnostics/update_errors.rb b/lib/solargraph/diagnostics/update_errors.rb index c2ca024089..137fa2b6ef 100644 --- a/lib/solargraph/diagnostics/update_errors.rb +++ b/lib/solargraph/diagnostics/update_errors.rb @@ -3,7 +3,10 @@ module Solargraph module Diagnostics class UpdateErrors < Base - def diagnose source, api_map + # @param source [Solargraph::Source] + # @param api_map [Solargraph::ApiMap] + # @param workspace [Solargraph::Workspace, nil] unused; accepted for interface compatibility with Diagnostics::Base + def diagnose source, api_map, workspace: nil combine_ranges(source.code, source.error_ranges).map do |range| { range: range.to_hash, severity: Diagnostics::Severities::ERROR, @@ -26,8 +29,8 @@ def combine_ranges code, ranges next if rng.nil? || lines.include?(rng.start.line) lines.push rng.start.line next if rng.start.line >= code.lines.length - scol = code.lines[rng.start.line].index(/[^\s]/) || 0 - ecol = code.lines[rng.start.line].length + scol = code.lines.fetch(rng.start.line).index(/[^\s]/) || 0 + ecol = code.lines.fetch(rng.start.line).length result.push Range.from_to(rng.start.line, scol, rng.start.line, ecol) end result diff --git a/lib/solargraph/equality.rb b/lib/solargraph/equality.rb index 0a266df792..f6a24346f1 100644 --- a/lib/solargraph/equality.rb +++ b/lib/solargraph/equality.rb @@ -6,13 +6,13 @@ module Solargraph # equality_fields() module Equality # @!method equality_fields - # @return [Array] + # @return [::Array] # @param other [Object] # @return [Boolean] def eql? other self.class.eql?(other.class) && - # @sg-ignore flow sensitive typing should support .class == .class + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 equality_fields.eql?(other.equality_fields) end diff --git a/lib/solargraph/external.rb b/lib/solargraph/external.rb index 5e643470ba..919bde0818 100644 --- a/lib/solargraph/external.rb +++ b/lib/solargraph/external.rb @@ -7,41 +7,62 @@ module Solargraph # bundle or dependency updates # class External - # @return [String] + # @return [String, nil] attr_reader :directory # @return [Array] attr_reader :requires - # @param directory [String] + # @return [Array] + attr_reader :extra_gem_names + + # @param directory [String, nil] # @param requires [Array] - def initialize directory, requires + # @param extra_gem_names [Array] + def initialize directory, requires, extra_gem_names = [] @repo = Repo.new(directory) @directory = directory @requires = requires + @extra_gem_names = extra_gem_names update! end + # @return [Array] + def configured_gem_names + loaded_gems.map(&:name) + unloaded_gems.map(&:name) + extra_gem_names + end + + # @return [Array] def unresolved_requires @unresolved_requires ||= [] end + # @return [Array] def unresolved_dependencies @unresolved_dependencies ||= [] end + # @return [Set] def loaded_gems @loaded_gems ||= Set.new end + # @return [Set] def unloaded_gems @unloaded_gems ||= Set.new end + # @return [Array] + def unfound_gem_names + @unfound_gem_names ||= [] + end + + # @return [Set] def loaded_stdlibs @loaded_stdlibs ||= Set.new end + # @return [Array] def pins @pins ||= [] end @@ -52,35 +73,25 @@ def generation end # @param new_requires [Array] + # @param new_gem_names [Array] # @return [Boolean] - def update new_requires - return false if requires == new_requires && !cache_changed? + def update new_requires, new_gem_names = [] + return false if requires == new_requires && new_gem_names == extra_gem_names && !cache_changed? requires.replace new_requires + extra_gem_names.replace new_gem_names update! true end - # @return [Array] - def rbs_collection_paths - @rbs_collection_paths ||= read_rbs_collection_paths - end - - # @return [String, nil] - def rbs_collection_config_path - # @todo Get rid of the '*' case - @rbs_collection_config_path ||= unless directory.nil? || directory.empty? || directory == '*' - yaml_file = File.join(directory, 'rbs_collection.yaml') - yaml_file if File.file?(yaml_file) - end - end - private + # @return [void] def update! @generation = generation + 1 clear_all load_requires + load_gem_names # load_rbs_collection end @@ -88,6 +99,7 @@ def cache_changed? unloaded_gems.any? { |gem| Collection::Gem.cached?(gem) } end + # @return [void] def load_requires bundler_require = false @@ -96,6 +108,7 @@ def load_requires bundler_require = true end if RbsMap::Stdlib.has?(path) + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 pins.concat Collection::Stdlib.load(path) if loaded_stdlibs.add?(path) else metagem = @repo.find_by_path(path) @@ -109,16 +122,29 @@ def load_requires @repo.find_by_group(:default).each { |metagem| process_gem metagem } end + # @return [void] + def load_gem_names + extra_gem_names.uniq.each do |gem| + metagem = @repo.find_by_name(gem) + next unfound_gem_names.push(gem) unless metagem + process_gem metagem + end + end + + # @return [RbsCollection] def rbs_collection @rbs_collection ||= RbsCollection.new(rbs_collection_lockfile) end + # @return [String, nil] def rbs_collection_lockfile + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 return if directory.nil? || directory.empty? || directory == '*' lockfile = File.join(directory, 'rbs_collection.lock.yaml') lockfile if File.file?(lockfile) end + # @return [void] def clear_all pins.clear unresolved_requires.clear @@ -126,8 +152,11 @@ def clear_all loaded_gems.clear unloaded_gems.clear loaded_stdlibs.clear + unfound_gem_names.clear end + # @param metagem [Metagem] + # @return [void] def process_gem metagem return if loaded_gems.include?(metagem) || unloaded_gems.include?(metagem) @@ -135,6 +164,7 @@ def process_gem metagem unloaded_gems.add metagem else loaded_gems.add metagem + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 base_pins = Collection::Gem.load(metagem) rbsc_pins = rbs_collection.load(metagem) if rbsc_pins.empty? @@ -146,12 +176,15 @@ def process_gem metagem # (e.g., `activesupport` requires `date`) base_pins.select { |pin| pin.is_a?(Pin::Reference::Require) && RbsMap::Stdlib.has?(pin.name) } .each do |pin| + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 pins.concat Collection::Stdlib.load(pin.name) if loaded_stdlibs.add?(pin.name) end end load_dependencies metagem end + # @param parent [Metagem] + # @return [void] def load_dependencies parent parent.dependencies.each do |name| next if loaded_gems.map(&:name).include?(name) || unloaded_gems.map(&:name).include?(name) diff --git a/lib/solargraph/language_server/host.rb b/lib/solargraph/language_server/host.rb index f503ea177f..42fbd48017 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -105,13 +105,16 @@ 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 + # rescued exceptions need https://github.com/apiology/solargraph/tree/type-rescued-exception-backtrace on top logger.warn e.backtrace.join("\n") message.set_error Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, "[#{e.class}] #{e.message}" end message elsif request['id'] if requests[request['id']] + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top requests[request['id']].process(request['result']) requests.delete request['id'] else @@ -301,9 +304,9 @@ def prepare directory, name = nil end end - # @sg-ignore Need to validate config + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 # @return [String] - # @sg-ignore Need to validate config + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 def command_path # @type [String] options['commandPath'] || 'solargraph' @@ -316,6 +319,7 @@ def command_path def prepare_folders array return if array.nil? array.each do |folder| + # @sg-ignore https://github.com/castwide/solargraph/pull/1398 prepare uri_to_file(folder['uri']), folder['name'] end end @@ -468,10 +472,28 @@ def stop notify_observers end + # @return [void] + def fully_stop + stop + # try for two minutes, raise if not fully stopped by then + start_time = Time.now + until fully_stopped? + if Time.now - start_time > 240 + raise 'Host did not fully stop within 240 seconds.' + end + logger.info 'Waiting for host to fully stop...' + sleep 0.1 + end + end + def stopped? @stopped end + def fully_stopped? + @stopped && diagnoser.fully_stopped? && message_worker.fully_stopped? + end + # Locate multiple pins that match a completion item. The first match is # based on the corresponding location in a library source if available. # Subsequent matches are based on path. @@ -502,13 +524,12 @@ def locate_pins params # might not exist. if result.empty? && params['data']['path'] =~ /\.new$/ result.concat(library.path_pins(params['data']['path'].sub(/\.new$/, '#initialize')).map do |pin| - next pin unless pin.name == 'initialize' + next pin unless pin.is_a?(Pin::Method) && pin.name == 'initialize' Pin::Method.new( name: 'new', scope: :class, location: pin.location, - # @sg-ignore Unresolved call to parameters on Solargraph::Pin::Base parameters: pin.parameters, return_type: ComplexType.try_parse(params['data']['path']), comments: pin.comments, @@ -546,7 +567,7 @@ def completions_at uri, line, column library.completions_at uri_to_file(uri), line, column end - # @return [Bool] if has pending completion request + # @return [Boolean] if has pending completion request def pending_completions? message_worker.messages.reverse_each.any? { |req| req['method'] == 'textDocument/completion' } end @@ -597,6 +618,7 @@ def references_from uri, line, column, strip: true, only: false # @return [Array] def query_symbols query result = [] + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 (libraries + [generic_library]).each { |lib| result.concat lib.query_symbols(query) } result.uniq end @@ -702,7 +724,12 @@ def client_capabilities @client_capabilities ||= {} end + # @return [Boolean] + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top def client_supports_progress? + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top client_capabilities['window'] && client_capabilities['window']['workDoneProgress'] end @@ -732,12 +759,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) + sep = File::ALT_SEPARATOR + return path if sep.nil? + path.gsub(sep, File::SEPARATOR) end # @param params [Hash] @@ -746,6 +772,7 @@ def generate_updater params changes = [] params['contentChanges'].each do |recvd| chng = check_diff(params['textDocument']['uri'], recvd) + # @sg-ignore https://github.com/apiology/solargraph/pull/225 changes.push Solargraph::Source::Change.new( (if chng['range'].nil? nil @@ -772,7 +799,10 @@ 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']) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 return change if diffs.empty? || diffs.length > 1 || diffs.first.length > 1 + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top # @type [Diff::LCS::Change] diff = diffs.first.first return change unless diff.adding? && ['.', ':', '(', ',', ' '].include?(diff.element) @@ -853,7 +883,12 @@ def dynamic_capability_options } end + # @return [Boolean] + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top def prepare_rename? + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top client_capabilities['rename'] && client_capabilities['rename']['prepareSupport'] end diff --git a/lib/solargraph/language_server/host/diagnoser.rb b/lib/solargraph/language_server/host/diagnoser.rb index 8c259c1318..b3dd744d2d 100644 --- a/lib/solargraph/language_server/host/diagnoser.rb +++ b/lib/solargraph/language_server/host/diagnoser.rb @@ -12,6 +12,7 @@ def initialize host @mutex = Mutex.new @queue = [] @stopped = true + @fully_stopped = true end # Schedule a file to be diagnosed. @@ -36,17 +37,26 @@ def stopped? @stopped end + def fully_stopped? + @fully_stopped + end + # Start the diagnosis thread. # # @return [self, nil] def start return unless @stopped - @stopped = false + @fully_stopped = @stopped = false Thread.new do until stopped? tick sleep 0.1 end + ensure + # Guarantee fully_stopped? eventually becomes true even if tick + # raised something unexpected, so Host#fully_stop can't hang + # forever waiting on a thread that already died. + @fully_stopped = true end self end @@ -63,6 +73,7 @@ def tick current = mutex.synchronize { queue.shift } return if queue.include?(current) begin + # @sg-ignore https://github.com/castwide/solargraph/pull/1426 host.diagnose current rescue InvalidOffsetError # @todo This error can occur when the Source is out of sync with @@ -70,6 +81,12 @@ def tick # but it's quick and easy. Logging.logger.warn "Deferring diagnosis due to invalid offset: #{current}" mutex.synchronize { queue.push current } + rescue StandardError => e + # Diagnosing a single file shouldn't be able to permanently + # kill the background thread (e.g. a file/directory disappearing + # mid-diagnosis, or a reporter misconfiguration) - log it and + # keep processing the rest of the queue. + Logging.logger.warn "Error diagnosing #{current}: [#{e.class}] #{e.message}" end end @@ -78,10 +95,10 @@ def tick # @return [Host] attr_reader :host - # @return [Mutex] + # @return [Thread::Mutex] attr_reader :mutex - # @return [::Array] + # @return [::Array] attr_reader :queue end end diff --git a/lib/solargraph/language_server/host/dispatch.rb b/lib/solargraph/language_server/host/dispatch.rb index b3c78ffca8..6a69180f2a 100644 --- a/lib/solargraph/language_server/host/dispatch.rb +++ b/lib/solargraph/language_server/host/dispatch.rb @@ -33,7 +33,7 @@ def libraries # @return [void] def update_libraries uri src = sources.find(uri) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1245 using = libraries.select { |lib| lib.contain?(src.filename) } using.push library_for(uri) if using.empty? using.each { |lib| lib.merge src } diff --git a/lib/solargraph/language_server/host/message_worker.rb b/lib/solargraph/language_server/host/message_worker.rb index 440e2c7cc2..e915e52db7 100644 --- a/lib/solargraph/language_server/host/message_worker.rb +++ b/lib/solargraph/language_server/host/message_worker.rb @@ -25,6 +25,7 @@ def initialize host @mutex = Mutex.new @resource = ConditionVariable.new @stopped = true + @fully_stopped = true end # pending handle messages @@ -37,9 +38,17 @@ def stopped? @stopped end + def fully_stopped? + @fully_stopped + end + # @return [void] def stop @stopped = true + # Wake up a thread that may be blocked in tick's + # @resource.wait with an empty queue, so it can notice + # stopped? and exit instead of hanging forever. + @mutex.synchronize { @resource.signal } end # @param message [Hash] The message to handle. Will be forwarded to Host#receive @@ -54,19 +63,22 @@ def queue message # @return [void] def start return unless @stopped - @stopped = false + @fully_stopped = @stopped = false Thread.new do tick until stopped? + ensure + @fully_stopped = true end end # @return [void] def tick message = @mutex.synchronize do - @resource.wait(@mutex) if messages.empty? + @resource.wait(@mutex) while messages.empty? && !stopped? next_message end - # @sg-ignore Need to add nil check here + return if message.nil? + 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 72a155d73d..96b0982f22 100644 --- a/lib/solargraph/language_server/host/sources.rb +++ b/lib/solargraph/language_server/host/sources.rb @@ -55,7 +55,6 @@ 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 170bfdb4fe..cd1406ccc8 100644 --- a/lib/solargraph/language_server/message.rb +++ b/lib/solargraph/language_server/message.rb @@ -37,6 +37,7 @@ def register path, message_class # @param path [String] # @return [Class] + # @sg-ignore flow sensitive typing needs to handle Hash#key? guards def select path if method_map.key?(path) method_map[path] diff --git a/lib/solargraph/language_server/message/base.rb b/lib/solargraph/language_server/message/base.rb index 6b61101c47..9c5bfebba4 100644 --- a/lib/solargraph/language_server/message/base.rb +++ b/lib/solargraph/language_server/message/base.rb @@ -85,6 +85,7 @@ def accept_or_cancel # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#cancelRequest # cancel should send response RequestCancelled Solargraph::Logging.logger.info "Cancelled response to ##{id} #{method}" + # @sg-ignore https://github.com/apiology/solargraph/pull/225 set_result nil set_error ErrorCodes::REQUEST_CANCELLED, 'Cancelled by client' else diff --git a/lib/solargraph/language_server/message/completion_item/resolve.rb b/lib/solargraph/language_server/message/completion_item/resolve.rb index 83cc5c1fe7..e316dc6d4c 100644 --- a/lib/solargraph/language_server/message/completion_item/resolve.rb +++ b/lib/solargraph/language_server/message/completion_item/resolve.rb @@ -22,8 +22,12 @@ def merge pins .reject { |pin| pin.documentation.empty? && pin.return_type.undefined? } result = params .transform_keys(&:to_sym) + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top .merge(pins.first.resolve_completion_item) .merge(documentation: markup_content(join_docs(docs))) + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top result[:detail] = pins.first.detail result end 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 8909406a42..7cce82b2f7 100644 --- a/lib/solargraph/language_server/message/extended/check_gem_version.rb +++ b/lib/solargraph/language_server/message/extended/check_gem_version.rb @@ -56,9 +56,11 @@ def process host.show_message "The Solargraph gem is up to date (version #{Solargraph::VERSION})." 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'] + # fetch_available sets an error whenever it finds no version + message = error + Solargraph::Logging.logger.warn message + # @sg-ignore https://github.com/castwide/solargraph/pull/1245 + host.show_message(message, MessageTypes::ERROR) if message && params['verbose'] end set_result({ installed: current, @@ -72,34 +74,33 @@ def process attr_reader :current # @return [Gem::Version] - # @sg-ignore Need to add nil check here def available - if !@available && !@fetched + @available ||= begin @fetched = true - begin - @available ||= begin - # @type [Gem::Dependency, nil] - tuple = CheckGemVersion.fetcher.search_for_dependency(Gem::Dependency.new('solargraph')).flatten.first - if tuple.nil? - @error = 'An error occurred fetching the gem data' - GEM_ZERO - else - tuple.version - end - end - rescue Errno::EADDRNOTAVAIL => e - @error = "Unable to connect to gem source: #{e.message}" - GEM_ZERO - end + fetch_available + end + end + + # @return [Gem::Version] + def fetch_available + # @type [Gem::Dependency, nil] + tuple = CheckGemVersion.fetcher.search_for_dependency(Gem::Dependency.new('solargraph')).flatten.first + if tuple.nil? + @error = 'An error occurred fetching the gem data' + GEM_ZERO + else + tuple.version end - @available + rescue Errno::EADDRNOTAVAIL => e + @error = "Unable to connect to gem source: #{e.message}" + GEM_ZERO end def fetched? @fetched ||= false end - # @return [String, nil] + # @return [String, Hash, nil] attr_reader :error end end diff --git a/lib/solargraph/language_server/message/extended/document.rb b/lib/solargraph/language_server/message/extended/document.rb index f379d0a6be..22d281e2ba 100644 --- a/lib/solargraph/language_server/message/extended/document.rb +++ b/lib/solargraph/language_server/message/extended/document.rb @@ -14,7 +14,8 @@ def process ) rescue StandardError => e Solargraph.logger.warn "Error processing document: [#{e.class}] #{e.message}" - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 + # rescued exceptions need https://github.com/apiology/solargraph/tree/type-rescued-exception-backtrace on top Solargraph.logger.debug e.backtrace.join("\n") end end diff --git a/lib/solargraph/language_server/message/initialize.rb b/lib/solargraph/language_server/message/initialize.rb index f0f54f22e4..b12b233429 100644 --- a/lib/solargraph/language_server/message/initialize.rb +++ b/lib/solargraph/language_server/message/initialize.rb @@ -55,7 +55,7 @@ def process private # @todo '?' methods should type like RBS 'boolish' rather than a strict true or false - # @sg-ignore + # @sg-ignore Unexplained typecheck gap; needs investigation def support_workspace_folders? params['capabilities'] && params['capabilities']['workspace'] && @@ -182,7 +182,7 @@ def static_highlights # @param capability [String] # @todo Need support for RBS' boolish "type", which doesn't # enforce strict true/false-ness - # @sg-ignore + # @sg-ignore Unexplained typecheck gap; needs investigation def dynamic_registration_for? section, capability result = params['capabilities'] && params['capabilities'][section] && diff --git a/lib/solargraph/language_server/message/text_document/completion.rb b/lib/solargraph/language_server/message/text_document/completion.rb index d2f352a3db..2a57e2b600 100644 --- a/lib/solargraph/language_server/message/text_document/completion.rb +++ b/lib/solargraph/language_server/message/text_document/completion.rb @@ -12,10 +12,11 @@ def process col = params['position']['character'] begin completion = host.completions_at(params['textDocument']['uri'], line, col) + return set_result(empty_result) unless completion + 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({ @@ -38,7 +39,8 @@ def process end rescue FileNotFoundError => e Logging.logger.warn "[#{e.class}] #{e.message}" - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 + # rescued exceptions need https://github.com/apiology/solargraph/tree/type-rescued-exception-backtrace on top 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 eb16317b5c..c74d67f3bf 100644 --- a/lib/solargraph/language_server/message/text_document/definition.rb +++ b/lib/solargraph/language_server/message/text_document/definition.rb @@ -16,12 +16,12 @@ def process # @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| { + # @sg-ignore pin.location relies on location always resolved uri: file_to_uri(pin.best_location.filename), + # @sg-ignore pin.location relies on location always resolved range: pin.best_location.range.to_hash } 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 04b706358d..db91bdd8c2 100644 --- a/lib/solargraph/language_server/message/text_document/document_symbol.rb +++ b/lib/solargraph/language_server/message/text_document/document_symbol.rb @@ -17,9 +17,9 @@ def process containerName: pin.namespace, kind: pin.symbol_kind, location: { - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 uri: file_to_uri(pin.best_location.filename), - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 range: pin.best_location.range.to_hash }, deprecated: pin.deprecated? diff --git a/lib/solargraph/language_server/message/text_document/formatting.rb b/lib/solargraph/language_server/message/text_document/formatting.rb index c6cc3353a5..726c04ce96 100644 --- a/lib/solargraph/language_server/message/text_document/formatting.rb +++ b/lib/solargraph/language_server/message/text_document/formatting.rb @@ -18,7 +18,6 @@ def process require_rubocop(config['version']) options, paths = ::RuboCop::Options.new.parse(args) - # @sg-ignore Unresolved call to []= options[:stdin] = original # Ensure only one instance of RuboCop::Runner is running at @@ -29,7 +28,6 @@ def process ::RuboCop::Runner.new(options, ::RuboCop::ConfigStore.new).run(paths) end end - # @sg-ignore Unresolved call to []= result = options[:stdin] log_corrections(corrections) @@ -84,12 +82,10 @@ def cli_args file_uri, config end # @param config [Hash{String => String}] - # @sg-ignore # @return [Class] def formatter_class config if self.class.const_defined?('BlankRubocopFormatter') - # @sg-ignore - BlankRubocopFormatter + self.class.const_get('BlankRubocopFormatter') else require_rubocop(config['version']) klass = Class.new(::RuboCop::Formatter::BaseFormatter) @@ -97,13 +93,11 @@ def formatter_class config end end - # @param value [Array, String] + # @param value [Array, String, nil] # # @return [String, nil] def cop_list value - # @type [String] - # @sg-ignore Translate to something flow sensitive typing understands - value = value.join(',') if value.respond_to?(:join) + value = value.join(',') if value.is_a?(Array) return nil if value == '' || !value.is_a?(String) value end @@ -125,6 +119,8 @@ def format original, result else { line: original.lines.length - 1, + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top character: original.lines.last.length } end diff --git a/lib/solargraph/language_server/message/text_document/hover.rb b/lib/solargraph/language_server/message/text_document/hover.rb index 66bf2bb607..3ef6dd6da1 100644 --- a/lib/solargraph/language_server/message/text_document/hover.rb +++ b/lib/solargraph/language_server/message/text_document/hover.rb @@ -13,9 +13,12 @@ def process last_link = nil suggestions&.each do |pin| parts = [] + # @sg-ignore https://github.com/castwide/solargraph/pull/1421 this_link = host.options['enablePages'] ? pin.link_documentation : pin.text_documentation parts.push this_link if !this_link.nil? && this_link != last_link + # @sg-ignore https://github.com/castwide/solargraph/pull/1421 parts.push "`#{pin.detail}`" unless pin.is_a?(Pin::Namespace) || pin.detail.nil? + # @sg-ignore https://github.com/castwide/solargraph/pull/1421 parts.push pin.documentation unless pin.documentation.nil? || pin.documentation.empty? unless parts.empty? data = parts.join("\n\n") @@ -29,8 +32,10 @@ def process ) rescue FileNotFoundError => e Logging.logger.warn "[#{e.class}] #{e.message}" - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 + # rescued exceptions need https://github.com/apiology/solargraph/tree/type-rescued-exception-backtrace on top Logging.logger.warn e.backtrace.join("\n") + # @sg-ignore https://github.com/apiology/solargraph/pull/225 set_result nil 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 675daaebe5..bf0a6cb393 100644 --- a/lib/solargraph/language_server/message/text_document/signature_help.rb +++ b/lib/solargraph/language_server/message/text_document/signature_help.rb @@ -14,8 +14,10 @@ def process }) rescue FileNotFoundError => e Logging.logger.warn "[#{e.class}] #{e.message}" - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 + # rescued exceptions need https://github.com/apiology/solargraph/tree/type-rescued-exception-backtrace on top Logging.logger.warn e.backtrace.join("\n") + # @sg-ignore https://github.com/apiology/solargraph/pull/225 set_result nil end 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 1035869c96..3a82009928 100644 --- a/lib/solargraph/language_server/message/text_document/type_definition.rb +++ b/lib/solargraph/language_server/message/text_document/type_definition.rb @@ -16,12 +16,12 @@ def process # @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 + return nil if suggestions.nil? || suggestions.empty? suggestions.reject { |pin| pin.best_location.nil? || pin.best_location.filename.nil? }.map do |pin| { + # @sg-ignore pin.location relies on location always resolved uri: file_to_uri(pin.best_location.filename), + # @sg-ignore pin.location relies on location always resolved range: pin.best_location.range.to_hash } end diff --git a/lib/solargraph/language_server/message/workspace/workspace_symbol.rb b/lib/solargraph/language_server/message/workspace/workspace_symbol.rb index 8e8c884a7c..22ba6175f9 100644 --- a/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +++ b/lib/solargraph/language_server/message/workspace/workspace_symbol.rb @@ -10,7 +10,7 @@ class WorkspaceSymbol < Solargraph::LanguageServer::Message::Base def process pins = host.query_symbols(params['query']) info = pins.map do |pin| - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 uri = file_to_uri(pin.best_location.filename) { name: pin.path, @@ -18,7 +18,7 @@ def process kind: pin.symbol_kind, location: { uri: uri, - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 range: pin.best_location.range.to_hash }, deprecated: pin.deprecated? diff --git a/lib/solargraph/language_server/request.rb b/lib/solargraph/language_server/request.rb index 2a2a292212..d47598be19 100644 --- a/lib/solargraph/language_server/request.rb +++ b/lib/solargraph/language_server/request.rb @@ -10,12 +10,8 @@ def initialize id, &block @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] + # @return [void] def process result @block&.call(result) end diff --git a/lib/solargraph/language_server/uri_helpers.rb b/lib/solargraph/language_server/uri_helpers.rb index fcf2e0dcb3..1a178b2c17 100644 --- a/lib/solargraph/language_server/uri_helpers.rb +++ b/lib/solargraph/language_server/uri_helpers.rb @@ -17,12 +17,15 @@ def uri_to_file uri decode(uri).sub(%r{^file:(?://)?}, '').sub(%r{^/([a-z]:)}i, '\1') end - # Convert a file path to a URI. + # Convert a file path to a URI. Sources loaded without a filename (e.g. + # Source.load_string in specs) cannot be sent to an LSP client. # - # @param file [String] + # @param file [String, nil] # @return [String] def file_to_uri file - "file://#{encode(file.gsub(/^([a-z]:)/i, '/\1'))}" + return "file://#{encode(file.gsub(/^([a-z]:)/i, '/\1'))}" unless file.nil? + + raise ArgumentError, 'Cannot build a URI for a source with no filename' 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 e521e47702..22e65c488a 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -85,6 +85,7 @@ def attached? filename # @return [Boolean] True if the specified file was detached def detach filename return false if @current.nil? || @current.filename != filename + # @sg-ignore https://github.com/apiology/solargraph/pull/225 attach nil true end @@ -189,7 +190,7 @@ def definitions_at filename, line, column # @type [MatchData, nil] rgt = source.code[offset..]&.match(/^([a-z0-9_]*)(:[a-z0-9_:]*)?[\]>, ]/i) if lft && rgt - # @sg-ignore lft and rgt are checked for nil above + # @sg-ignore https://github.com/apiology/solargraph/pull/193 tag = (lft[1] + rgt[1]).sub(/:+$/, '') clip = mutex.synchronize { api_map.clip(cursor) } clip.translate tag @@ -259,30 +260,35 @@ def references_from filename, line, column, strip: false, only: false (workspace.sources + (@current ? [@current] : [])) end files.uniq(&:filename).each do |source| + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 found = source.references(pin.name) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 found.select! do |loc| referenced = definitions_at(loc.filename, loc.range.ending.line, loc.range.ending.character)&.first referenced&.path == pin.path end if pin.path == 'Class#new' caller = cursor.chain.base.infer(api_map, clip.send(:closure), clip.locals).first - if caller.defined? + if caller&.defined? + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 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 caller == other end else + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 found.clear 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)) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 found.map! do |loc| - # @sg-ignore Unresolved call to [] 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)) end end + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 result.concat(found.sort do |a, b| a.range.start.line <=> b.range.start.line end) @@ -416,16 +422,18 @@ def diagnose filename else args = line.split(':').map(&:strip) name = args.shift + raise DiagnosticsError, "Invalid diagnostics reporter entry #{line.inspect}" if name.nil? + reporter = Diagnostics.reporter(name) raise DiagnosticsError, "Diagnostics reporter #{name} does not exist" if reporter.nil? - # @sg-ignore Hash errors repargs[reporter] ||= [] - # @sg-ignore Hash errors + # @sg-ignore https://github.com/castwide/solargraph/pull/1417 repargs[reporter].concat args end end repargs.each_pair do |reporter, args| - result.concat reporter.new(*args.uniq).diagnose(source, api_map) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 + result.concat reporter.new(*args.uniq).diagnose(source, api_map, workspace: workspace) end result end @@ -439,11 +447,11 @@ def catalog # @return [Bench] def bench + # @sg-ignore https://github.com/apiology/solargraph/pull/225 Bench.new( source_maps: source_map_hash.values, workspace: workspace, external_requires: external_requires, - # @sg-ignore OK if @current.filename is nil live_map: @current ? source_map_hash[@current.filename] : nil ) end @@ -483,10 +491,7 @@ def next_map src = workspace.sources.find { |s| !source_map_hash.key?(s.filename) } if src Logging.logger.debug "Mapping #{src.filename}" - # @sg-ignore OK if src.filename is nil - source_map_hash[src.filename] = Solargraph::SourceMap.map(src) - # @sg-ignore OK if src.filename is nil - source_map_hash[src.filename] + source_map_hash[src.filename!] = Solargraph::SourceMap.map(src) else false end @@ -495,10 +500,7 @@ def next_map # @return [self] def map! workspace.sources.each do |src| - # @sg-ignore OK if src.filename is nil - source_map_hash[src.filename] = Solargraph::SourceMap.map(src) - # @sg-ignore OK if src.filename is nil - find_external_requires source_map_hash[src.filename] + find_external_requires(source_map_hash[src.filename!] = Solargraph::SourceMap.map(src)) end self end @@ -527,8 +529,7 @@ def find_external_requires source_map new_set = source_map.requires.to_set(&:name) _filenames = nil filenames = -> { _filenames ||= workspace.filenames.to_set } - # @sg-ignore OK if source_map.filename is nil - source_map_external_require_hash[source_map.filename] = new_set.reject do |path| + source_map_external_require_hash[source_map.source.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') @@ -555,6 +556,8 @@ def api_map # @raise [FileNotFoundError] if the file does not exist # @param filename [String] # @return [Solargraph::Source] + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top def read filename return @current if @current && @current.filename == filename raise FileNotFoundError, "File not found: #{filename}" unless workspace.has_file?(filename) @@ -574,13 +577,11 @@ def handle_file_not_found filename, error # @return [void] def maybe_map source return unless source - # @sg-ignore Wrong argument type for Solargraph::Workspace#has_file?: filename expected String, received String, nil return unless @current == source || workspace.has_file?(source.filename) - # @sg-ignore OK if source.filename is nil - source_map_hash[source.filename] = Solargraph::SourceMap.map(source) + source_map_hash[source.filename!] = Solargraph::SourceMap.map(source) end - # @return [Set] + # @return [Set] def cache_errors @cache_errors ||= Set.new end @@ -589,7 +590,7 @@ def cache_errors def cache_next_gemspec return if @cache_progress - spec = api_map.unloaded_gems.first + spec = (api_map.unloaded_gems - cache_errors).first return end_cache_progress unless spec pending = api_map.unloaded_gems.length - cache_errors.length - 1 @@ -611,9 +612,16 @@ def cache_next_gemspec report_cache_progress spec.name, pending cmd = [workspace.command_path, 'cache', spec.name] + # Load the plugins this process has, not the workspace's + # configured ones, so the cache lands in the gem_dir read here. + cmd.push("--plugins=#{CacheDir.plugin_requires.join(File::PATH_SEPARATOR)}") cmd.push('--directory', workspace.directory) if workspace.directory - _o, e, s = Open3.capture3(*cmd) - if s.success? + begin + _o, e, s = Open3.capture3(*cmd) + rescue SystemCallError => e + # The command could not start, e.g. Errno::ENOENT - handled below + end + if s&.success? logger.info "Cached #{spec.name} #{spec.version}" else cache_errors.add spec @@ -627,7 +635,7 @@ def cache_next_gemspec end end - # @return [Array] + # @return [Array] def queued_gemspec_cache @queued_gemspec_cache ||= [] end @@ -637,15 +645,11 @@ def queued_gemspec_cache # @return [void] def report_cache_progress gem_name, pending @total ||= pending - # @sg-ignore Wrong argument type for Integer#>: arg_0 expected Numeric, received Integer, nil @total = pending if pending > @total - # @sg-ignore Unresolved call to - on Integer, nil finished = @total - pending - # @sg-ignore @total should always be an Integer pct = if @total.zero? 0 else - # @sg-ignore Unresolved call to to_f ((finished.to_f / @total) * 100).to_i end message = "#{gem_name}#{" (+#{pending})" if pending.positive?}" @@ -654,12 +658,12 @@ def report_cache_progress gem_name, pending @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 Unresolved call to begin + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 @cache_progress.begin(message, pct) changed notify_observers @cache_progress end - # @sg-ignore Unresolved call to report + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 @cache_progress.report(message, pct) changed notify_observers @cache_progress diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index 2317c3cb42..454499f5f4 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -8,7 +8,7 @@ class Location include Equality include Comparable - # @return [String] + # @return [String, nil] attr_reader :filename # @return [Solargraph::Range] @@ -33,8 +33,12 @@ def <=> other end end + # @return [Boolean] def rbs? - filename.end_with?('.rbs') + name = filename + return false if name.nil? + + name.end_with?('.rbs') end # @param location [self] @@ -59,10 +63,10 @@ def to_hash 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 filename = nil if filename.empty? range = Range.from_node(node) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 new(filename, range) end diff --git a/lib/solargraph/logging.rb b/lib/solargraph/logging.rb index c4415176ab..9a2496ad5d 100644 --- a/lib/solargraph/logging.rb +++ b/lib/solargraph/logging.rb @@ -22,7 +22,7 @@ module Logging DEFAULT_LOG_LEVEL end @@logger = Logger.new($stderr, level: level) - # @sg-ignore Fix cvar issue + # @sg-ignore Unexplained typecheck gap; needs investigation @@logger.formatter = proc do |severity, _datetime, _progname, msg| "[#{severity}] #{msg}\n" end diff --git a/lib/solargraph/metagem.rb b/lib/solargraph/metagem.rb index fa5926fdab..0d17daedb4 100644 --- a/lib/solargraph/metagem.rb +++ b/lib/solargraph/metagem.rb @@ -28,6 +28,13 @@ class Metagem # @return [String] attr_reader :cache_name + # @param name [String] + # @param full_path [String] + # @param spec_file [String] + # @param source [String] + # @param version [String] + # @param require_paths [Array] + # @param dependencies [Array] def initialize name:, full_path:, spec_file:, source:, version:, require_paths:, dependencies: @name = name @full_path = full_path @@ -46,12 +53,14 @@ def cacheable? !!@cache_name end + # @param path [String] def require? path require_paths.any? do |req| File.file?(File.join(full_path, req, "#{path}.rb")) end end + # @return [Gem::Specification, nil] def to_specification Gem::Specification.load(spec_file) end @@ -72,7 +81,7 @@ def self.from_specification gem full_path: gem.full_gem_path, spec_file: gem.spec_file, source: gem.source.to_s, - version: gem.version, + version: gem.version.to_s, require_paths: gem.require_paths, dependencies: gem.runtime_dependencies.map(&:name)) end diff --git a/lib/solargraph/page.rb b/lib/solargraph/page.rb index 207b846655..543ec7edcd 100644 --- a/lib/solargraph/page.rb +++ b/lib/solargraph/page.rb @@ -71,7 +71,8 @@ def initialize directory = VIEWS_PATH # @param template [String] # @param layout [Boolean] # @param locals [Hash] - # @sg-ignore + # @sg-ignore Library method has no usable type + # Proc#call is untyped; RBS can type a proc (^(String) -> String), YARD can't # @return [String] def render template, layout: true, locals: {} # @type [String] diff --git a/lib/solargraph/parser/comment_ripper.rb b/lib/solargraph/parser/comment_ripper.rb index 89d4a2c91f..54ad9fa468 100644 --- a/lib/solargraph/parser/comment_ripper.rb +++ b/lib/solargraph/parser/comment_ripper.rb @@ -11,6 +11,8 @@ class CommentRipper < Ripper::SexpBuilderPP # @return [Array(Symbol, String, Array)] # @!override Ripper::SexpBuilder#on_embdoc_end # @return [Array(Symbol, String, Array)] + # @!override Ripper::SexpBuilder#on_comment + # @return [Array(Symbol, String, Array(Integer, Integer))] # @param src [String] # @param filename [String] @@ -22,17 +24,21 @@ def initialize src, filename = '(ripper)', lineno = 0 end def on_comment *args - # @sg-ignore - # @type [Array(Symbol, String, Array([Integer, nil], [Integer, nil]))] + # @sg-ignore https://github.com/castwide/solargraph/pull/1364 + # @type [Array(Symbol, String, Array(Integer, Integer))] result = super # @sg-ignore Need to add nil check here if @buffer_lines[result[2][0]][0..result[2][1]].strip =~ /^#/ + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 chomped = result[1].chomp + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 if result[2][0].zero? && chomped.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '').match(/^#\s*frozen_string_literal:/) chomped = '#' end + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 @comments[result[2][0]] = + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 Snippet.new(Range.from_to(result[2][0], result[2][1], result[2][0], result[2][1] + chomped.length), chomped) end result @@ -41,34 +47,38 @@ def on_comment *args # @param result [Array(Symbol, String, Array([Integer, nil], [Integer, nil]))] # @return [void] def create_snippet result + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 chomped = result[1].chomp + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 @comments[result[2][0]] = Snippet.new( + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 Range.from_to(result[2][0] || 0, result[2][1] || 0, result[2][0] || 0, + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 (result[2][1] || 0) + chomped.length), chomped ) end - # @sg-ignore @override is adding, not overriding + # @sg-ignore https://github.com/castwide/solargraph/pull/1104 def on_embdoc_beg *args result = super - # @sg-ignore @override is adding, not overriding + # @sg-ignore https://github.com/castwide/solargraph/pull/1364 create_snippet(result) result end - # @sg-ignore @override is adding, not overriding + # @sg-ignore https://github.com/castwide/solargraph/pull/1104 def on_embdoc *args result = super - # @sg-ignore @override is adding, not overriding + # @sg-ignore https://github.com/castwide/solargraph/pull/1364 create_snippet(result) result end - # @sg-ignore @override is adding, not overriding + # @sg-ignore https://github.com/castwide/solargraph/pull/1104 def on_embdoc_end *args result = super - # @sg-ignore @override is adding, not overriding + # @sg-ignore https://github.com/castwide/solargraph/pull/1364 create_snippet(result) result end diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 1606a32e06..181c4f5658 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -22,14 +22,11 @@ def initialize locals, ivars, enclosing_breakable_pin, enclosing_compound_statem # # @return [void] 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] - rhs = and_node.children[1] + return unless and_node in [:and, ::Parser::AST::Node => lhs, ::Parser::AST::Node => rhs] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 before_rhs_loc = rhs.location.expression.adjust(begin_pos: -1) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 before_rhs_pos = Position.new(before_rhs_loc.line, before_rhs_loc.column) rhs_presence = Range.new(before_rhs_pos, @@ -48,14 +45,11 @@ def process_and and_node, true_ranges = [], false_ranges = [] # # @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] + return unless or_node in [:or, ::Parser::AST::Node => lhs, ::Parser::AST::Node => rhs] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 before_rhs_loc = rhs.location.expression.adjust(begin_pos: -1) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 before_rhs_pos = Position.new(before_rhs_loc.line, before_rhs_loc.column) rhs_presence = Range.new(before_rhs_pos, @@ -90,7 +84,10 @@ def process_calls node, true_presences, false_presences # # @return [void] def process_if if_node, true_ranges = [], false_ranges = [] - return if if_node.type != :if + if_node => [:if, + ::Parser::AST::Node => conditional_node, + ::Parser::AST::Node | nil => then_clause, + ::Parser::AST::Node | nil => else_clause] # # See if we can refine a type based on the result of 'if foo.nil?' @@ -103,11 +100,6 @@ def process_if if_node, true_ranges = [], false_ranges = [] # s(:send, nil, :foo), # s(:send, nil, :bar)) # [4] pry(main)> - conditional_node = if_node.children[0] - # @type [Parser::AST::Node, nil] - then_clause = if_node.children[1] - # @type [Parser::AST::Node, nil] - else_clause = if_node.children[2] unless enclosing_breakable_pin.nil? rest_of_breakable_body = Range.new(get_node_end_position(if_node), @@ -132,21 +124,27 @@ def process_if if_node, true_ranges = [], false_ranges = [] true_ranges << rest_of_returnable_body if always_leaves_compound_statement?(else_clause) end + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 unless then_clause.nil? # # If the condition is true we can assume things about the then clause # + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 before_then_clause_loc = then_clause.location.expression.adjust(begin_pos: -1) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 before_then_clause_pos = Position.new(before_then_clause_loc.line, before_then_clause_loc.column) true_ranges << Range.new(before_then_clause_pos, get_node_end_position(then_clause)) end + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 unless else_clause.nil? # # If the condition is true we can assume things about the else clause # + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 before_else_clause_loc = else_clause.location.expression.adjust(begin_pos: -1) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 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)) @@ -161,7 +159,7 @@ def process_if if_node, true_ranges = [], false_ranges = [] # # @return [void] def process_while while_node, true_ranges = [], false_ranges = [] - return if while_node.type != :while + while_node => [:while, ::Parser::AST::Node => conditional_node, ::Parser::AST::Node | nil => do_clause] # # See if we can refine a type based on the result of 'if foo.nil?' @@ -173,15 +171,15 @@ def process_while while_node, true_ranges = [], false_ranges = [] # 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] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 unless do_clause.nil? # # If the condition is true we can assume things about the do clause # + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 before_do_clause_loc = do_clause.location.expression.adjust(begin_pos: -1) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 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)) @@ -190,6 +188,99 @@ def process_while while_node, true_ranges = [], false_ranges = [] process_expression(conditional_node, true_ranges, false_ranges) end + # @param case_node [Parser::AST::Node] + # + # @return [void] + def process_case case_node + return if case_node.type != :case + + # + # See if we can narrow the subject's type inside each 'when' + # branch based on the classes tested for in that branch + # + # [3] pry(main)> Parser::CurrentRuby.parse("case a; when B; c; end") + # => s(:case, + # s(:send, nil, :a), + # s(:when, + # s(:const, nil, :B), + # s(:send, nil, :c)), nil) + # [4] pry(main)> + subject_node = case_node.children[0] + return if subject_node.nil? + # @sg-ignore Found after #1259 merged + return unless %i[lvar ivar].include?(subject_node.type) + + # @sg-ignore Found after #1259 merged + variable_name = parse_variable(subject_node) + return if variable_name.nil? + + # @sg-ignore Need to add nil check here + subject_position = Range.from_node(subject_node).start + pin = find_var(variable_name, subject_position) + return unless pin + + # @sg-ignore Need to add nil check here + when_nodes = case_node.children[1..].compact.select { |child| child.type == :when } + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check + when_nodes.each do |when_node| + # The last child is the body; the rest are the 'when' values + value_nodes = when_node.children.dup + body_node = value_nodes.pop + next if body_node.nil? + + type_names = value_nodes.map { |value_node| type_name(value_node) } + # Only narrow if every value in this 'when' clause is a + # simple constant reference - a splat, range, regexp, etc. + # isn't a type we can express as a downcast. + next if type_names.any?(&:nil?) + + before_body_loc = body_node.location.expression.adjust(begin_pos: -1) + before_body_pos = Position.new(before_body_loc.line, before_body_loc.column) + presence = Range.new(before_body_pos, get_node_end_position(body_node)) + + facts = { pin => [{ type: ComplexType.parse(*type_names) }] } + process_facts(facts, [presence]) + end + end + + # @param or_asgn_node [Parser::AST::Node] + # @param presence [Range] + # + # @return [void] + def process_or_asgn or_asgn_node, presence + return if or_asgn_node.type != :or_asgn + + # + # 'x ||= value' only actually assigns when x is falsy (nil or + # false), so if x was already truthy, it keeps whatever + # non-nil type it already had. Narrow the existing pin by + # excluding nil, scoped to the rest of the enclosing closure. + # + # [3] pry(main)> Parser::CurrentRuby.parse("x ||= 1") + # => s(:or_asgn, + # s(:lvasgn, :x), + # s(:int, 1)) + # [4] pry(main)> + lhs = or_asgn_node.children[0] + # @sg-ignore Parser::AST::Node#children is declared to return a bare Array, losing its element type here + return unless %i[lvasgn ivasgn].include?(lhs.type) + + # @sg-ignore Parser::AST::Node#children is declared to return a bare Array, losing its element type here + variable_name = lhs.children[0].to_s + # @sg-ignore Parser::AST::Node#children is declared to return a bare Array, losing its element type here + return if variable_name.empty? + + range = Range.from_node(or_asgn_node) + return if range.nil? + + position = range.start + pin = find_var(variable_name, position) + return unless pin + + facts = { pin => [{ not_type: ComplexType::NIL }] } + process_facts(facts, [presence]) + end + class << self include Logging end @@ -227,6 +318,7 @@ def process_facts facts_by_pin, presences # Add specialized vars for the rest of the block # facts_by_pin.each_pair do |pin, facts| + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 facts.each do |fact| downcast_type = fact.fetch(:type, nil) downcast_not_type = fact.fetch(:not_type, nil) @@ -258,24 +350,28 @@ def process_expression expression_node, true_ranges, false_ranges # 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)), # - call_receiver = call_node.children[0] - call_arg = type_name(call_node.children[2]) + return unless call_node in [:send, ::Parser::AST::Node | nil => call_receiver, ^method_name, *args] + + call_arg = (args in [::Parser::AST::Node => arg, *]) ? type_name(arg) : nil # check if call_receiver looks like this: # s(:send, nil, :foo) # and set variable_name to :foo - 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 + if call_receiver in [:send, nil, Symbol => receiver_method, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + variable_name = receiver_method.to_s end # or like this: # (lvar :repr) - variable_name = call_receiver.children[0].to_s if %i[lvar ivar].include?(call_receiver&.type) + if call_receiver in [:lvar | :ivar, Symbol => receiver_variable, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + variable_name = receiver_variable.to_s + end return unless variable_name [call_arg, variable_name] @@ -294,15 +390,14 @@ def parse_isa isa_node # @param variable_name [String] # @param position [Position] # - # @sg-ignore Solargraph::Parser::FlowSensitiveTyping#find_var - # return type could not be inferred + # @sg-ignore https://github.com/castwide/solargraph/pull/1397 # @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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 ivars.find { |ivar| ivar.name == variable_name && (!ivar.presence || ivar.presence.include?(position)) } else - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 locals.find { |pin| pin.name == variable_name && (!pin.presence || pin.presence.include?(position)) } end end @@ -315,7 +410,7 @@ def find_var variable_name, position 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 isa_position = Range.from_node(isa_node).start pin = find_var(variable_name, isa_position) @@ -352,7 +447,7 @@ def process_nilp nilp_node, true_presences, false_presences # we're looking for and typechecking will cover any invalid # ones return unless nilp_arg.nil? - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 nilp_position = Range.from_node(nilp_node).start pin = find_var(variable_name, nilp_position) @@ -387,9 +482,7 @@ def process_bang bang_node, true_presences, false_presences # => s(:send, # s(:int, 2), :!) # end - return unless bang_node.type == :send && bang_node.children[1] == :! - - receiver = bang_node.children[0] + return unless bang_node in [:send, ::Parser::AST::Node => receiver, :!, *] # swap the two presences process_expression(receiver, false_presences, true_presences) @@ -398,10 +491,12 @@ def process_bang bang_node, true_presences, false_presences # @param var_node [Parser::AST::Node] # # @return [String, nil] Variable name referenced + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 def parse_variable var_node - return if var_node.children.length != 1 + # only reached for lvar, ivar, cvar and gvar nodes + var_node => [_, Symbol => variable_name] - var_node.children[0]&.to_s + variable_name&.to_s end # @return [void] @@ -414,7 +509,7 @@ def process_variable node, true_presences, false_presences variable_name = parse_variable(node) return if variable_name.nil? - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 var_position = Range.from_node(node).start pin = find_var(variable_name, var_position) @@ -433,19 +528,22 @@ def process_variable node, true_presences, false_presences process_facts(if_false, false_presences) end - # @param node [Parser::AST::Node] + # @param node [Parser::AST::Node, nil] # # @return [String, nil] 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 unless node in [:const, ::Parser::AST::Node | nil => module_node, Symbol => class_node] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 return class_node.to_s if module_node.nil? + # e.g., ::Baz parses as s(:const, s(:cbase), :Baz) - the + # leading :cbase marks root-namespace resolution and isn't + # itself a :const node, so it needs to be recognized here + # rather than falling into the generic recursive case below. + # @sg-ignore Found after #1259 merged + return "::#{class_node}" if module_node.type == :cbase module_type_name = type_name(module_node) return unless module_type_name @@ -454,17 +552,10 @@ def type_name node end # @param clause_node [Parser::AST::Node, nil] - # @sg-ignore need boolish support for ? methods def always_breaks? clause_node clause_node&.type == :break end - # @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 - %i[return raise next redo retry].include?(clause_node&.type) - end - attr_reader :locals, :ivars, :enclosing_breakable_pin, :enclosing_compound_statement_pin end end diff --git a/lib/solargraph/parser/node_processor.rb b/lib/solargraph/parser/node_processor.rb index d3579df80d..91d188c222 100644 --- a/lib/solargraph/parser/node_processor.rb +++ b/lib/solargraph/parser/node_processor.rb @@ -18,8 +18,10 @@ class << self # @param type [Symbol] # @param cls [Class] # @return [Array>] + # @sg-ignore https://github.com/castwide/solargraph/pull/1417 def register type, cls @@processors[type] ||= [] + # @sg-ignore https://github.com/castwide/solargraph/pull/1417 @@processors[type] << cls end @@ -28,11 +30,11 @@ def register type, cls # # @return [void] def deregister type, cls - @@processors[type].delete(cls) + @@processors[type]&.delete(cls) end end - # @param node [Parser::AST::Node] + # @param node [Parser::AST::Node, nil] # @param region [Region] # @param pins [Array] # @param locals [Array] @@ -43,13 +45,16 @@ def self.process node, region = Region.new, pins = [], locals = [], ivars = [] pins.push Pin::Namespace.new( location: region.source.location, name: '', + node: node, source: :parser ) end return [pins, locals, ivars] unless Parser.is_ast_node?(node) + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 node_processor_classes = @@processors[node.type] || [NodeProcessor::Base] node_processor_classes.each do |klass| + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 processor = klass.new(node, region, pins, locals, ivars) process_next = processor.process diff --git a/lib/solargraph/parser/node_processor/base.rb b/lib/solargraph/parser/node_processor/base.rb index 48fd070f61..3b73519912 100644 --- a/lib/solargraph/parser/node_processor/base.rb +++ b/lib/solargraph/parser/node_processor/base.rb @@ -55,10 +55,10 @@ 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 + pins.reverse_each { |pin| return pin if pin.is_a?(Pin::Breakable) && pin.location&.range&.contain?(position) } + nil end # @todo downcast output of Enumerable#select @@ -73,8 +73,8 @@ def process_children subregion = region return if @processed_children @processed_children = true node.children.each do |child| - next unless Parser.is_ast_node?(child) - NodeProcessor.process(child, subregion, pins, locals, ivars) + next unless child in ::Parser::AST::Node => child_node + NodeProcessor.process(child_node, subregion, pins, locals, ivars) end end @@ -95,7 +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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 pin.is_a?(Pin::Closure) && pin.path && !pin.path.empty? && pin.location.range.contain?(position) end.last end @@ -105,7 +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 + # @sg-ignore Unresolved call to range on Location, nil pins.select { |pin| pin.is_a?(Pin::Closure) && pin.location.range.contain?(position) }.last end @@ -113,7 +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 + # @sg-ignore Unresolved call to range on Location, nil pins.select { |pin| pin.is_a?(Pin::Closure) && pin.location.range.contain?(position) }.last end end diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index 62aa33e4b7..0fcdcebebf 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -11,7 +11,8 @@ module ClassMethods # @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})] + # @return [Array(Parser::AST::Node, Hash{Integer => Solargraph::Parser::Snippet}), + # Array(nil, Hash{Integer => Solargraph::Parser::Snippet})] def parse_with_comments code, filename = nil, starting_line = 0 node = parse(code, filename, starting_line) comments = CommentRipper.new(code, filename, 0).parse @@ -21,8 +22,7 @@ def parse_with_comments code, filename = nil, starting_line = 0 # @param code [String] # @param filename [String, nil] # @param starting_line [Integer] - # @sg-ignore need to understand that raise does not return - # @return [Parser::AST::Node] + # @return [Parser::AST::Node, nil] nil when the code has no expressions def parse code, filename = nil, starting_line = 0 buffer = ::Parser::Source::Buffer.new(filename, starting_line) buffer.source = code @@ -31,9 +31,13 @@ def parse code, filename = nil, starting_line = 0 raise Parser::SyntaxError, e.message end + # A new parser per call: Prism::Translation::Parser keeps the buffer + # it is parsing in an instance variable, so one shared instance + # fails when several threads parse at once. + # # @return [::Parser::Base] def parser - @parser ||= Prism::Translation::Parser.new(FlawedBuilder.new).tap do |parser| + Prism::Translation::Parser.new(FlawedBuilder.new).tap do |parser| parser.diagnostics.all_errors_are_fatal = true parser.diagnostics.ignore_warnings = true end @@ -42,7 +46,6 @@ def parser # @param source [Source] # @return [Array(Array, Array)] def map 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] @@ -53,24 +56,32 @@ def map source # @return [Array] def references source, name if name.end_with?('=') + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 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) } + extract_offset = lambda do |code, offset| + match = reg.match(code, offset) + match ? match.offset(0) : [nil, nil] + end 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] } + extract_offset = lambda do |code, offset| + soff = code.index(name, offset) + soff ? [soff, soff + name.length] : [nil, nil] + end end - inner_node_references(name, source.node).map do |n| + inner_node_references(name, source.node).filter_map do |n| rng = Range.from_node(n) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 offset = Position.to_offset(source.code, rng.start) soff, eoff = extract_offset[source.code, offset] + # Operator calls like foo[0] do not spell the method name + next unless soff && eoff + Location.new( source.filename, Range.new( @@ -82,7 +93,7 @@ def references source, name end # @param name [String] - # @param top [AST::Node] + # @param top [AST::Node, Object, nil] # @return [Array] def inner_node_references name, top # @type [Array] @@ -139,20 +150,20 @@ def node_range node def string_ranges node return [] unless is_ast_node?(node) result = [] - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 result.push Range.from_node(node) if node.type == :str - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 node.children.each do |child| result.concat string_ranges(child) end - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 if node.type == :dstr && node.children.last.nil? - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 last = node.children[-2] - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 unless last.nil? rng = Range.from_node(last) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 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 341042f223..3f49594573 100644 --- a/lib/solargraph/parser/parser_gem/flawed_builder.rb +++ b/lib/solargraph/parser/parser_gem/flawed_builder.rb @@ -9,7 +9,7 @@ module ParserGem class FlawedBuilder < ::Parser::Builders::Default # @param token [::Parser::AST::Node] # @return [String] - # @sg-ignore + # @sg-ignore https://github.com/ruby/gem_rbs_collection/pull/1104 def string_value token value(token) end diff --git a/lib/solargraph/parser/parser_gem/node_chainer.rb b/lib/solargraph/parser/parser_gem/node_chainer.rb index ef452445fc..fe8c63ac3f 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -44,6 +44,7 @@ def chain node, filename = nil, parent = nil, tree = nil # @return [Source::Chain] def load_string code, filename, starting_line node = Parser.parse(code.sub(/\.$/, ''), filename, starting_line) + # @sg-ignore Need to add nil check here chain = NodeChainer.new(node).chain chain.links.push(Chain::Link.new) if code.end_with?('.') chain @@ -56,106 +57,126 @@ def load_string code, filename, starting_line # @return [Array] def generate_links n return [] unless n.is_a?(::Parser::AST::Node) - return generate_links(n.children[0]) if n.type == :splat + # s(:splat, value) - an anonymous splat in an mlhs has no value + if n in [:splat, *rest] + return (rest in [::Parser::AST::Node => splatted, *]) ? generate_links(splatted) : [] + end # @type [Array] result = [] - if n.type == :block - result.concat NodeChainer.chain(n.children[0], @filename, n).links - elsif n.type == :send - if n.children[0].is_a?(::Parser::AST::Node) - 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? - # @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 - raise "No idea what to do with #{n}" - end - elsif n.type == :csend - if n.children[0].is_a?(::Parser::AST::Node) - result.concat generate_links(n.children[0]) - result.push Chain::QCall.new(n.children[1].to_s, Location.from_node(n), node_args(n)) - elsif n.children[0].nil? - result.push Chain::QCall.new(n.children[1].to_s, Location.from_node(n), node_args(n)) - else - raise "No idea what to do with #{n}" + case n + in [:block, ::Parser::AST::Node => call, *] + result.concat NodeChainer.chain(call, @filename, n).links + in [:send, ::Parser::AST::Node => receiver, Symbol => method_name, *] + result.concat generate_links(receiver) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + result.push Chain::Call.new(method_name.to_s, Location.from_node(n), node_args(n), passed_block(n)) + in [:send, nil, Symbol => method_name, *args] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + args.map do |c| + NodeChainer.chain(c, @filename, n) end - elsif n.type == :self + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + result.push Chain::Call.new(method_name.to_s, Location.from_node(n), node_args(n), passed_block(n)) + in [:send, *] + raise "No idea what to do with #{n}" + in [:csend, ::Parser::AST::Node => receiver, Symbol => method_name, *] + result.concat generate_links(receiver) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + result.push Chain::QCall.new(method_name.to_s, Location.from_node(n), node_args(n)) + in [:csend, nil, Symbol => method_name, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + result.push Chain::QCall.new(method_name.to_s, Location.from_node(n), node_args(n)) + in [:csend, *] + raise "No idea what to do with #{n}" + in [:self, *] result.push Chain::Head.new('self') - elsif n.type == :zsuper + in [:zsuper, *] result.push Chain::ZSuper.new('super') - elsif n.type == :super - args = n.children.map { |c| NodeChainer.chain(c, @filename, n) } + in [:super, *] + # s(:super, *args) - every argument is a node + args = n.children.map do |child| + # raises NoMatchingPatternError on a non-node child + child => ::Parser::AST::Node => c + NodeChainer.chain(c, @filename, n) + end result.push Chain::Call.new('super', Location.from_node(n), args) - elsif n.type == :yield - args = n.children.map { |c| NodeChainer.chain(c, @filename, n) } + in [:yield, *] + # s(:yield, *args) - every argument is a node + args = n.children.map do |child| + # raises NoMatchingPatternError on a non-node child + child => ::Parser::AST::Node => c + NodeChainer.chain(c, @filename, n) + end result.push Chain::Call.new('yield', Location.from_node(n), args) - elsif n.type == :const + in [:const, *] const = unpack_name(n) result.push Chain::Constant.new(const) - elsif %i[lvar lvasgn].include?(n.type) - result.push Chain::Call.new(n.children[0].to_s, Location.from_node(n)) - 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 %i[gvar gvasgn].include?(n.type) - result.push Chain::GlobalVariable.new(n.children[0].to_s) - elsif n.type == :or_asgn + in [:lvar | :lvasgn, Symbol => name, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + result.push Chain::Call.new(name.to_s, Location.from_node(n)) + in [:ivar | :ivasgn, Symbol => name, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + result.push Chain::InstanceVariable.new(name.to_s, n, Location.from_node(n)) + in [:cvar | :cvasgn, Symbol => name, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + result.push Chain::ClassVariable.new(name.to_s) + in [:gvar | :gvasgn, Symbol => name, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + result.push Chain::GlobalVariable.new(name.to_s) + in [:or_asgn, ::Parser::AST::Node => lhs, ::Parser::AST::Node => rhs, *] # @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]) + lhs_chain = NodeChainer.chain lhs # s(:ivasgn, :@bar) + rhs_chain = NodeChainer.chain rhs # s(:int, 123) + or_asgn_rhs_never_returns = always_leaves_compound_statement?(rhs) + or_link = Chain::Or.new([lhs_chain, rhs_chain], rhs_never_returns: or_asgn_rhs_never_returns) # 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) + in [:class | :module | :def | :defs, *] # @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)]) - elsif n.type == :if - then_clause = if n.children[1] - NodeChainer.chain(n.children[1], @filename, n) + in [:and, *, ::Parser::AST::Node => last] + result.concat generate_links(last) + in [:or, ::Parser::AST::Node => lhs, ::Parser::AST::Node => rhs, *] + or_lhs_chain = NodeChainer.chain(lhs, @filename) + or_rhs_chain = NodeChainer.chain(rhs, @filename, n) + or_rhs_never_returns = always_leaves_compound_statement?(rhs) + result.push Chain::Or.new([or_lhs_chain, or_rhs_chain], rhs_never_returns: or_rhs_never_returns) + in [:if, _, ::Parser::AST::Node | nil => then_node, ::Parser::AST::Node | nil => else_node, *] + then_clause = if then_node + NodeChainer.chain(then_node, @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_clause = if else_node + NodeChainer.chain(else_node, @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] - if block_variable_name_node.nil? - # 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 - result.push Chain::BlockVariable.new("&#{block_variable_name_node.children[0]}") - end - elsif n.type == :hash + in [:begin | :kwbegin, *, ::Parser::AST::Node => last] + result.concat generate_links(last) + in [:begin | :kwbegin] + # an empty begin...end has no links + in [:block_pass, *] + result.push block_pass_link(n) + in [:hash, *] result.push Chain::Hash.new('::Hash', n, hash_is_splatted?(n)) - elsif n.type == :array - chained_children = n.children.map { |c| NodeChainer.chain(c) } + in [:array, *] + # s(:array, *elements) - every element is a node + chained_children = n.children.map do |child| + # raises NoMatchingPatternError on a non-node child + child => ::Parser::AST::Node => c + NodeChainer.chain(c) + end result.push Source::Chain::Array.new(chained_children, n) - elsif inside_method_call? # If the node is inside a method call, it may be a parameter. + in _ if inside_method_call? # If the node is inside a method call, it may be a parameter. lit = infer_literal_node_type(n) if lit + # @sg-ignore nil false alarm method_call_chain = NodeChainer.chain(send_node, @filename, nil, []) literal = Chain::Literal.new(lit, n) result.push Chain::Parameter.new(literal, method_call_chain) @@ -168,40 +189,62 @@ def generate_links n result end + # @param block_pass [Parser::AST::Node] + # @return [Chain::Link] + # @sg-ignore return type could not be inferred + def block_pass_link block_pass + case block_pass + in [_, [:sym, Symbol => symbol, *]] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + Chain::BlockSymbol.new(symbol.to_s) + in [_, ::Parser::AST::Node => block_variable_name_node, *] + # &blk passes s(:lvar, :blk); &method(:x) passes a send whose first child is nil + variable_name = (block_variable_name_node in [_, value, *]) ? value : nil + Chain::BlockVariable.new("&#{variable_name}") + else + # anonymous block forwarding (e.g., "&") + # added in Ruby 3.1 - https://bugs.ruby-lang.org/issues/11256 + Chain::BlockVariable.new(nil) + end + end + def inside_method_call? !send_node.nil? && send_node != @node end # @return [Parser::AST::Node, nil] + # @sg-ignore https://github.com/castwide/solargraph/pull/1421 def send_node + # @sg-ignore https://github.com/castwide/solargraph/pull/1421 @send_node ||= @tree&.find { |n| n.type == :send } end # @param node [Parser::AST::Node] 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 - if Parser.is_ast_node?(node.children.last.children[0]) && node.children.last.children[0].type == :hash - return false - end - true + # {**splatted} - s(:hash, *pairs, s(:kwsplat, splatted)), unless splatted is a hash literal + return false unless node in ::Parser::AST::Node[:hash, *, ::Parser::AST::Node[:kwsplat, ::Parser::AST::Node => splatted, *]] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + !(splatted in ::Parser::AST::Node[:hash, *]) end # @param node [Parser::AST::Node] # @return [Source::Chain, nil] def passed_block node - return unless node == @node && @parent&.type == :block + # s(:block, call, args, body) + return unless node == @node && (@parent in [:block, _, _, ::Parser::AST::Node | nil => body]) - # @sg-ignore Need to add nil check here - NodeChainer.chain(@parent.children[2], @filename) + # an empty block still passes a block, so it gets an empty chain, not nil + body ? NodeChainer.chain(body, @filename) : Chain.new([]) end # @param node [Parser::AST::Node] - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1391 # @return [Array] def node_args node - # @sg-ignore Need to add nil check here - node.children[2..].map do |child| + node => [_, _, _, *args] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + args.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 c44fdba851..7bfe0c8b31 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -11,13 +11,13 @@ module ParserGem module NodeMethods module_function - # @param node [Parser::AST::Node] + # @param node [Parser::AST::Node, nil] # @return [String] def unpack_name node pack_name(node).join('::') end - # @param node [Parser::AST::Node] + # @param node [Parser::AST::Node, nil] # @return [Array] def pack_name node # @type [Array] @@ -87,26 +87,31 @@ def get_node_end_position node # @return [String] def drill_signature node, signature return signature unless node.is_a?(AST::Node) - if %i[const cbase].include?(node.type) - signature += drill_signature(node.children[0], signature) unless node.children[0].nil? + if node in [:const, ::Parser::AST::Node | nil => scope, Symbol => name, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + signature += drill_signature(scope, signature) unless scope.nil? signature += '::' unless signature.empty? - signature += node.children[1].to_s - elsif %i[lvar ivar cvar].include?(node.type) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + signature += name.to_s + elsif node in [:cbase, *] + signature += '::' unless signature.empty? + elsif node in [:lvar | :ivar | :cvar, Symbol => name, *] signature += '.' unless signature.empty? - signature += node.children[0].to_s - elsif node.type == :send - signature += drill_signature(node.children[0], signature) unless node.children[0].nil? + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + signature += name.to_s + elsif node in [:send, ::Parser::AST::Node | nil => receiver, Symbol => name, *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + signature += drill_signature(receiver, signature) unless receiver.nil? signature += '.' unless signature.empty? - signature += node.children[1].to_s + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + signature += name.to_s end signature end # Convert a DSL method call argument with directly inferrable simple params. # @param node [Parser::AST::Node] - # @return [String, Integer, Float, Symbol, Array, Hash, Source::Chain, nil] - # @sg-ignore "does not match inferred type ::String, ::Parser::AST::Node" - this probably comes from the - # `.children[0]` call, which is not recognized as returning a literal value. + # @return [String, Integer, Float, Symbol, Parser::AST::Node, Array, Hash, Source::Chain, nil] def simple_convert node return nil unless Parser.is_ast_node?(node) @@ -114,7 +119,9 @@ def simple_convert node when :const unpack_name(node) when :str, :dstr, :int, :float, :sym, true, false - node.children[0] + # a dstr converts to its first part, e.g. s(:str, "foo") for "foo\#{bar}" + # str: String, int: Integer, float: Float, sym: Symbol, dstr: its first part node + (node in [_, String | Integer | Float | Symbol | ::Parser::AST::Node => value, *]) ? value : nil when :array simple_convert_array(node) when :hash @@ -128,7 +135,12 @@ def simple_convert node # @return [Array] def simple_convert_array node return [] unless Parser.is_ast_node?(node) && node.type == :array - node.children.compact.map { |c| simple_convert(c) } + # s(:array, *elements) - every element is a node + node.children.compact.map do |child| + # raises NoMatchingPatternError on a non-node child + child => ::Parser::AST::Node => c + simple_convert(c) + end end # @param node [Parser::AST::Node] @@ -138,29 +150,34 @@ def simple_convert_hash node result = {} # @param pair [Parser::AST::Node] node.children.each do |pair| - next unless Parser.is_ast_node?(pair) && pair.children[0] - result[pair.children[0].children[0]] = simple_convert(pair.children[1]) + # s(:pair, s(:sym, key), value); a kwsplat contributes its operand's name with a nil value + next unless pair in ::Parser::AST::Node[_, ::Parser::AST::Node => key_node, *rest] + + key = (key_node in [_, key_value, *]) ? key_value : nil + result[key] = simple_convert((rest in [::Parser::AST::Node => value, *]) ? value : nil) end result end # @param node [Parser::AST::Node, nil] - # @return [Hash{Symbol => Chain}] + # @return [Hash{Symbol => Source::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 - # @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]) + if node in [:kwsplat, ::Parser::AST::Node => splatted, *] + return convert_hash(splatted) end - # @sg-ignore Translate to something flow sensitive typing understands + if node in [_, ::Parser::AST::Node[:kwsplat, *] => first_child, *] + return convert_hash(first_child) + end + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 return {} unless node.type == :hash result = {} - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 node.children.each do |pair| - result[pair.children[0].children[0]] = Solargraph::Parser.chain(pair.children[1]) + # s(:pair, s(:sym, key), value) + pair => [_, ::Parser::AST::Node => key_node, *rest] + key = (key_node in [_, key_value, *]) ? key_value : nil + result[key] = Solargraph::Parser.chain((rest in [::Parser::AST::Node => value, *]) ? value : nil) end result end @@ -176,20 +193,49 @@ def const_nodes_from node if node.type == :const result.push node else - node.children.each { |child| result.concat const_nodes_from(child) } + node.children.each do |child| + # children that are not nodes (names, literals, nil) hold no constants + next unless child in ::Parser::AST::Node => child_node + + result.concat const_nodes_from(child_node) + end end result end + # Whether clause_node necessarily leaves its enclosing compound statement. + # + # @param clause_node [Parser::AST::Node, nil] + # @return [Boolean] + def always_leaves_compound_statement? clause_node + # https://docs.ruby-lang.org/en/2.2.0/keywords_rdoc.html + return true if %i[return next redo retry].include?(clause_node&.type) + return false if clause_node.nil? + + # A multi-statement clause parses as :begin; only its last child leaves. + # @sg-ignore Found after #1259 merged + return always_leaves_compound_statement?(clause_node.children.last) if clause_node.type == :begin + + return false unless clause_node.type == :send + + # raise/fail are plain :send calls, not a dedicated node type. + clause_node.children[0].nil? && %i[raise fail].include?(clause_node.children[1]) + end + # @param node [Parser::AST::Node] + # @return [Boolean] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 def splatted_hash? node - Parser.is_ast_node?(node.children[0]) && node.children[0].type == :kwsplat + node in [_, ::Parser::AST::Node[:kwsplat, *], *] end # @param node [Parser::AST::Node] def splatted_call? node - return false unless Parser.is_ast_node?(node) - Parser.is_ast_node?(node.children[0]) && node.children[0].type == :kwsplat && node.children[0].children[0].type != :hash + # **splatted as the first child, unless splatted is a hash literal + return false unless node in ::Parser::AST::Node[_, ::Parser::AST::Node[:kwsplat, ::Parser::AST::Node => splatted, *], *] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + splatted.type != :hash end # @param nodes [Enumerable] @@ -198,29 +244,39 @@ def any_splatted_call? nodes end # @todo Temporarily here for testing. Move to Solargraph::Parser. - # @param node [Parser::AST::Node] + # @param node [Parser::AST::Node, nil] # @return [Array] def call_nodes_from node return [] unless node.is_a?(::Parser::AST::Node) result = [] - if node.type == :block + if node in [:block, ::Parser::AST::Node => call, *block_rest] result.push node - if Parser.is_ast_node?(node.children[0]) && node.children[0].children.length > 2 - # @sg-ignore Need to add nil check here - node.children[0].children[2..].each { |child| result.concat call_nodes_from(child) } + if call in ::Parser::AST::Node[_, _, _, *call_args] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + call_args.each { |child| result.concat call_nodes_from(child) } end - # @sg-ignore Need to add nil check here - node.children[1..].each { |child| result.concat call_nodes_from(child) } - elsif node.type == :send + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + block_rest.each { |child| result.concat call_nodes_from(child) } + elsif node in [:send, ::Parser::AST::Node | nil => receiver, _, *args] result.push node - result.concat call_nodes_from(node.children.first) - # @sg-ignore Need to add nil check here - node.children[2..].each { |child| result.concat call_nodes_from(child) } + result.concat call_nodes_from(receiver) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + args.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) } + node.children.each do |child| + # children that are not nodes (names, literals, nil) hold no calls + next unless child in ::Parser::AST::Node => child_node + + result.concat call_nodes_from(child_node) + end else - node.children.each { |child| result.concat call_nodes_from(child) } + node.children.each do |child| + # children that are not nodes (names, literals, nil) hold no calls + next unless child in ::Parser::AST::Node => child_node + + result.concat call_nodes_from(child_node) + end end result end @@ -261,10 +317,10 @@ def find_recipient_node cursor position = cursor.position offset = cursor.offset tree = if source.synchronized? - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 match = source.code[0..(offset - 1)].match(/,\s*\z/) if match - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 source.tree_at(position.line, position.column - match[0].length) else source.tree_at(position.line, position.column) @@ -275,11 +331,10 @@ def find_recipient_node cursor # @type [AST::Node, nil] prev = nil tree.each do |node| - if node.type == :send - args = node.children[2..] + if node in [:send, _, _, *args] # @sg-ignore Need to add nil check here if !args.empty? - # @sg-ignore Need to add nil check here + # @sg-ignore Unresolved call to args return node if prev && args.include?(prev) elsif source.synchronized? return node if source.code[0..(offset - 1)] =~ /\(\s*\z/ && source.code[offset..] =~ /^\s*\)/ @@ -341,7 +396,7 @@ def find_recipient_node_by_text source, offset name_start = idx + 1 return nil if name_start >= name_end method_name = code[name_start...name_end] - return nil if method_name.empty? + return nil if method_name.nil? || method_name.empty? # Check for receiver pattern: receiver.method( or receiver::method( idx = name_start - 1 @@ -354,7 +409,7 @@ def find_recipient_node_by_text source, offset recv_start = idx + 1 if recv_start < recv_end recv_name = code[recv_start...recv_end] - unless recv_name.empty? + unless recv_name.nil? || recv_name.empty? receiver_node = ::Parser::AST::Node.new(:send, [nil, recv_name.to_sym]) return ::Parser::AST::Node.new(:send, [receiver_node, method_name.to_sym]) end @@ -364,7 +419,7 @@ def find_recipient_node_by_text source, offset const_start = const_end const_start -= 1 while const_start.positive? && code[const_start - 1] =~ /[a-zA-Z0-9_]/ const_name = code[const_start...const_end] - unless const_name.empty? || method_name.empty? + unless const_name.nil? || const_name.empty? || method_name.empty? const_node = ::Parser::AST::Node.new(:const, [nil, const_name.to_sym]) return ::Parser::AST::Node.new(:send, [const_node, method_name.to_sym]) end @@ -481,36 +536,44 @@ 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) - # @sg-ignore Need to add nil check here - result.concat reduce_to_value_nodes(node.children[1..]) + # s(:if, condition, then, else) + node => [_, _, *branches] + result.concat reduce_to_value_nodes(branches) # result.push NIL_NODE unless node.children[2] elsif ONLY_ONE_CHILD.include?(node.type) - result.concat reduce_to_value_nodes([node.children[0]]) + # s(:return) or s(:return, value, *more_values) + returned = (node in [_, ::Parser::AST::Node => value, *]) ? value : nil + result.concat reduce_to_value_nodes([returned]) elsif FIRST_TWO_CHILDREN.include?(node.type) - result.concat reduce_to_value_nodes([node.children[0], node.children[1]]) + # s(:rescue, body, resbody, *more_resbodies, else) + node => [_, ::Parser::AST::Node | nil => body, ::Parser::AST::Node => first_resbody, *] + result.concat reduce_to_value_nodes([body, first_resbody]) elsif FUNCTION_VALUE.include?(node.type) # the block itself is a first class value that could be returned result.push node # @todo any explicit returns actually return from # scope in which the proc is run. This asssumes # that the function is executed here. - if include_explicit_returns - result.concat explicit_return_values_from_compound_statement(node.children[2]) + # s(:block, call, args, body) + if include_explicit_returns && (node in [_, _, _, ::Parser::AST::Node => block_body, *]) + result.concat explicit_return_values_from_compound_statement(block_body) end elsif CASE_STATEMENT.include?(node.type) - # @sg-ignore Need to add nil check here - node.children[1..].each do |cc| + # s(:case, subject, *whens, else) + node => [_, _, *clauses] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + clauses.each do |cc| if cc.nil? result.push NIL_NODE - elsif cc.type == :when - result.concat reduce_to_value_nodes([cc.children.last]) + elsif cc in [:when, *, ::Parser::AST::Node | nil => when_body] + result.concat reduce_to_value_nodes([when_body]) else # else clause in case result.concat reduce_to_value_nodes([cc]) end end - elsif node.type == :resbody - result.concat reduce_to_value_nodes([node.children[2]]) + elsif node in [:resbody, _, _, ::Parser::AST::Node | nil => body, *] + result.concat reduce_to_value_nodes([body]) else result.push node end @@ -533,20 +596,25 @@ def from_value_position_statement node, include_explicit_returns: true def from_value_position_compound_statement parent result = [] 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]) - elsif node.type == :rescue + nodes.each_with_index do |child, idx| + # nodes holds only the node children + child => ::Parser::AST::Node => node + if node in [:block, _, _, ::Parser::AST::Node | nil => body, *] + result.concat explicit_return_values_from_compound_statement(body) if body + elsif node in [:rescue, ::Parser::AST::Node | nil => body, ::Parser::AST::Node => first_resbody, *] # body statements - result.concat from_value_position_statement(node.children[0]) + result.concat from_value_position_statement(body) if body # rescue statements - result.concat from_value_position_statement(node.children[1]) + result.concat from_value_position_statement(first_resbody) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 elsif SKIPPABLE.include?(node.type) next - elsif node.type == :resbody - result.concat reduce_to_value_nodes([node.children[2]]) + elsif node in [:resbody, _, _, ::Parser::AST::Node | nil => body, *] + result.concat reduce_to_value_nodes([body]) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 elsif node.type == :return - result.concat reduce_to_value_nodes([node.children[0]]) + returned = (node in [_, ::Parser::AST::Node => value, *]) ? value : nil + result.concat reduce_to_value_nodes([returned]) # Return here because the rest of the code is # unreachable and shouldn't be looked at return result @@ -558,6 +626,7 @@ def from_value_position_compound_statement parent # from above; now we need to also gather the value # position nodes if idx == nodes.length - 1 + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 result.concat from_value_position_statement(nodes.last, include_explicit_returns: false) end @@ -576,11 +645,15 @@ 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.each do |node| + parent.children.each do |child| + # names, literals and nil hold no return statements + next unless child in ::Parser::AST::Node => node + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 next if SKIPPABLE.include?(node.type) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 if node.type == :return - result.concat reduce_to_value_nodes([node.children[0]]) + returned = (node in [_, ::Parser::AST::Node => value, *]) ? value : nil + result.concat reduce_to_value_nodes([returned]) # Return the result here because the rest of the code is # unreachable return result @@ -598,25 +671,20 @@ 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) - # @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 + node => [_, _, *branches] + result.concat reduce_to_value_nodes(branches) 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 + returned = (node in [_, ::Parser::AST::Node => value, *]) ? value : nil + result.concat reduce_to_value_nodes([returned]) 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 + body = (node in [_, _, _, ::Parser::AST::Node | nil => block_body, *]) ? block_body : nil + result.concat explicit_return_values_from_compound_statement(body) unless body.nil? 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]]) + body = (node in [_, _, _, ::Parser::AST::Node | nil => resbody_body, *]) ? resbody_body : nil + result.concat reduce_to_value_nodes([body]) else result.push node end diff --git a/lib/solargraph/parser/parser_gem/node_processors.rb b/lib/solargraph/parser/parser_gem/node_processors.rb index 26cce40127..410975c0af 100644 --- a/lib/solargraph/parser/parser_gem/node_processors.rb +++ b/lib/solargraph/parser/parser_gem/node_processors.rb @@ -7,6 +7,7 @@ module Parser module ParserGem module NodeProcessors autoload :BeginNode, 'solargraph/parser/parser_gem/node_processors/begin_node' + autoload :CaseNode, 'solargraph/parser/parser_gem/node_processors/case_node' autoload :DefNode, 'solargraph/parser/parser_gem/node_processors/def_node' autoload :DefsNode, 'solargraph/parser/parser_gem/node_processors/defs_node' autoload :SendNode, 'solargraph/parser/parser_gem/node_processors/send_node' @@ -40,6 +41,7 @@ module NodeProcessor register :kwbegin, ParserGem::NodeProcessors::BeginNode register :rescue, ParserGem::NodeProcessors::BeginNode register :resbody, ParserGem::NodeProcessors::ResbodyNode + register :case, ParserGem::NodeProcessors::CaseNode register :def, ParserGem::NodeProcessors::DefNode register :defs, ParserGem::NodeProcessors::DefsNode register :if, ParserGem::NodeProcessors::IfNode diff --git a/lib/solargraph/parser/parser_gem/node_processors/alias_node.rb b/lib/solargraph/parser/parser_gem/node_processors/alias_node.rb index eb41ab64f2..a0f13b8911 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/alias_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/alias_node.rb @@ -6,12 +6,18 @@ module ParserGem module NodeProcessors class AliasNode < Parser::NodeProcessor::Base def process + # s(:alias, s(:sym, new), s(:sym, old)) - gvar aliases use s(:gvar, name); `alias :"" a` has s(:dsym) + node => [:alias, ::Parser::AST::Node => new_node, ::Parser::AST::Node => original_node] + + new_name = (new_node in [_, Symbol | ::Parser::AST::Node => value, *]) ? value : nil + original = (original_node in [_, Symbol | ::Parser::AST::Node => value, *]) ? value : nil + loc = get_node_location(node) pins.push Solargraph::Pin::MethodAlias.new( location: loc, closure: region.closure, - name: node.children[0].children[0].to_s, - original: node.children[1].children[0].to_s, + name: new_name.to_s, + original: original.to_s, scope: region.scope || :instance, source: :parser ) 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 9a22b8edd0..8277c461b8 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/args_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/args_node.rb @@ -11,16 +11,24 @@ def process if node.type == :forward_args forward(callable) else - node.children.each do |u| + node.children.each do |child| + # s(:args, *params) - every parameter is a node + # raises NoMatchingPatternError on a non-node child + child => ::Parser::AST::Node => u + # s(:arg, name), s(:optarg, name, default), s(:restarg) for an anonymous splat, + # or s(:mlhs, *args) for |(a, b)| + name = (u in [_, Symbol | ::Parser::AST::Node | nil => first, *]) ? first : nil + default = (u in [_, _, ::Parser::AST::Node => second, *]) ? second : nil loc = get_node_location(u) + # @sg-ignore https://github.com/apiology/solargraph/pull/225 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, - # @sg-ignore Need to add nil check here + name: name.to_s, + assignment: default, + asgn_code: default ? region.code_for(default) : nil, + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 presence: callable.location.range, decl: get_decl(u), source: :parser @@ -41,7 +49,7 @@ def forward callable locals.push Solargraph::Pin::Parameter.new( location: loc, closure: callable, - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 presence: region.closure.location.range, decl: get_decl(node), source: :parser 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 750bb99294..4ec4d8dea8 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb @@ -8,10 +8,14 @@ class BlockNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process + # s(:block, call, args, body) + node => [:block, ::Parser::AST::Node => call, *] + location = get_node_location(node) scope = region.scope || region.closure.context.scope - if other_class_eval? - clazz_name = unpack_name(node.children[0].children[0]) + # Foo.class_eval { ... } - s(:send, s(:const, ...), :class_eval, *args) + if call in [:send, ::Parser::AST::Node[:cbase | :const, *] => clazz, :class_eval, *] + clazz_name = unpack_name(clazz) # instance variables should come from the Class type # - i.e., treated as class instance variables context = ComplexType.try_parse("Class<#{clazz_name}>") @@ -22,7 +26,7 @@ def process closure: region.closure, node: node, context: context, - receiver: node.children[0], + receiver: call, comments: comments_for(node), scope: scope, source: :parser @@ -30,14 +34,6 @@ def process pins.push block_pin process_children region.update(closure: block_pin) end - - private - - def other_class_eval? - node.children[0].type == :send && - node.children[0].children[1] == :class_eval && - %i[cbase const].include?(node.children[0].children[0]&.type) - end end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/case_node.rb b/lib/solargraph/parser/parser_gem/node_processors/case_node.rb new file mode 100644 index 0000000000..d833818f20 --- /dev/null +++ b/lib/solargraph/parser/parser_gem/node_processors/case_node.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Solargraph + module Parser + module ParserGem + module NodeProcessors + class CaseNode < Parser::NodeProcessor::Base + include ParserGem::NodeMethods + + def process + FlowSensitiveTyping.new(locals, + ivars, + enclosing_breakable_pin, + enclosing_compound_statement_pin).process_case(node) + process_children + true + end + end + end + end + end +end diff --git a/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb index acdfe3064b..d895fb90a9 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb @@ -8,12 +8,16 @@ class CasgnNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process + # s(:casgn, scope, name, value) - scope is nil for a bare name; value is absent in an mlhs + node => [:casgn, ::Parser::AST::Node | nil => scope, Symbol => name, *rest] + assignment = (rest in [::Parser::AST::Node => value, *]) ? value : nil + pins.push Solargraph::Pin::Constant.new( location: get_node_location(node), closure: region.closure, - name: const_name, + name: const_name(scope, name), comments: comments_for(node), - assignment: node.children[2], + assignment: assignment, source: :parser ) process_children @@ -21,12 +25,14 @@ def process private + # @param scope [Parser::AST::Node, nil] + # @param name [Symbol] # @return [String] - def const_name - if node.children[0] - Parser::NodeMethods.unpack_name(node.children[0]) + "::#{node.children[1]}" + def const_name scope, name + if scope + Parser::NodeMethods.unpack_name(scope) + "::#{name}" else - node.children[1].to_s + name.to_s end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb index d6426a4488..21883725d6 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb @@ -6,13 +6,18 @@ module ParserGem module NodeProcessors class CvasgnNode < Parser::NodeProcessor::Base def process + # s(:cvasgn, name, value) - value is absent when the target is in an mlhs + node => [:cvasgn, Symbol => name, *rest] + assignment = (rest in [::Parser::AST::Node => value, *]) ? value : nil + loc = get_node_location(node) pins.push Solargraph::Pin::ClassVariable.new( location: loc, closure: region.closure, - name: node.children[0].to_s, + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name: name.to_s, comments: comments_for(node), - assignment: node.children[1], + assignment: assignment, source: :parser ) process_children 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 f45f5544df..1de55662ca 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb @@ -6,7 +6,10 @@ module ParserGem module NodeProcessors class DefNode < Parser::NodeProcessor::Base def process - name = node.children[0].to_s + node => [:def, Symbol => method_name, *] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name = method_name.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 @@ -19,6 +22,7 @@ def process context: method_context, comments: comments_for(node), scope: scope, + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 visibility: scope == :instance && name == 'initialize' ? :private : region.visibility, node: node, source: :parser 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 09679c7f76..32d7af7666 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb @@ -8,21 +8,25 @@ class DefsNode < DefNode include ParserGem::NodeMethods def process + # s(:defs, receiver, name, args, body) + node => [:defs, ::Parser::AST::Node => receiver, Symbol => method_name, *] + s_visi = region.visibility s_visi = :public if s_visi == :module_function || region.scope != :class loc = get_node_location(node) - closure = if node.children[0].is_a?(AST::Node) && node.children[0].type == :self + closure = if receiver in ::AST::Node[:self, *] region.closure else Solargraph::Pin::Namespace.new( - name: unpack_name(node.children[0]), + name: unpack_name(receiver), source: :parser ) end pins.push Solargraph::Pin::Method.new( location: loc, closure: closure, - name: node.children[1].to_s, + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name: method_name.to_s, comments: comments_for(node), scope: :class, visibility: s_visi, diff --git a/lib/solargraph/parser/parser_gem/node_processors/gvasgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/gvasgn_node.rb index 3a074d8737..fff132c052 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/gvasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/gvasgn_node.rb @@ -6,13 +6,18 @@ module ParserGem module NodeProcessors class GvasgnNode < Parser::NodeProcessor::Base def process + # s(:gvasgn, name, value) - value is absent when the target is in an mlhs + node => [:gvasgn, Symbol => name, *rest] + assignment = (rest in [::Parser::AST::Node => value, *]) ? value : nil + loc = get_node_location(node) pins.push Solargraph::Pin::GlobalVariable.new( location: loc, closure: region.closure, - name: node.children[0].to_s, + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name: name.to_s, comments: comments_for(node), - assignment: node.children[1], + assignment: assignment, source: :parser ) process_children 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 0b9a75e774..f2b74d0a6b 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb @@ -12,7 +12,12 @@ def process ivars, enclosing_breakable_pin, enclosing_compound_statement_pin).process_if(node) - condition_node = node.children[0] + # s(:if, cond, then, else) - each child is a node, or nil when absent + node => [:if, + ::Parser::AST::Node | nil => condition_node, + ::Parser::AST::Node | nil => then_node, + ::Parser::AST::Node | nil => else_node] + if condition_node pins.push Solargraph::Pin::CompoundStatement.new( location: get_node_location(condition_node), @@ -22,7 +27,6 @@ def process ) 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), @@ -33,7 +37,6 @@ def process NodeProcessor.process(then_node, region, pins, locals, ivars) end - else_node = node.children[2] if else_node pins.push Solargraph::Pin::CompoundStatement.new( location: get_node_location(else_node), 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 0c4099d419..5eabb4b22f 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb @@ -8,13 +8,18 @@ class IvasgnNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process + # s(:ivasgn, name, value) - value is absent when the target is in an mlhs + node => [:ivasgn, Symbol => name, *rest] + assignment = (rest in [::Parser::AST::Node => value, *]) ? value : nil + loc = get_node_location(node) ivars.push Solargraph::Pin::InstanceVariable.new( location: loc, closure: region.closure, - name: node.children[0].to_s, + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name: name.to_s, comments: comments_for(node), - assignment: node.children[1], + assignment: assignment, source: :parser ) if region.visibility == :module_function @@ -25,10 +30,11 @@ def process ivars.push Solargraph::Pin::InstanceVariable.new( location: loc, closure: Pin::Namespace.new(type: :module, closure: region.closure.closure, - name: region.closure.name), - name: node.children[0].to_s, + name: region.closure.name, source: :parser), + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name: name.to_s, comments: comments_for(node), - assignment: node.children[1], + assignment: assignment, source: :parser ) end 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 63e2c55dcd..8a9e5719e2 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb @@ -8,15 +8,20 @@ class LvasgnNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process + # s(:lvasgn, name, value) - value is absent in an mlhs or a rescue + node => [:lvasgn, Symbol => name, *rest] + assignment = (rest in [::Parser::AST::Node => value, *]) ? value : nil + here = get_node_start_position(node) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 presence = Range.new(here, region.closure.location.range.ending) loc = get_node_location(node) locals.push Solargraph::Pin::LocalVariable.new( location: loc, closure: region.closure, - name: node.children[0].to_s, - assignment: node.children[1], + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name: name.to_s, + assignment: assignment, comments: comments_for(node), presence: presence, source: :parser 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 b5a7805d95..e45be596fd 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb @@ -22,12 +22,10 @@ def process # s(:int, 2), # s(:int, 3))) masgn = node - # @type [Parser::AST::Node] - mlhs = masgn.children.fetch(0) + masgn => [:masgn, ::Parser::AST::Node => mlhs, ::Parser::AST::Node => mass_rhs] # @type [Array] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 lhs_arr = mlhs.children - # @type [Parser::AST::Node] - mass_rhs = node.children.fetch(1) # Get pins created for the mlhs node process_children diff --git a/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb b/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb index 7cecd22c09..fe5523388f 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb @@ -8,12 +8,15 @@ class NamespaceNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process - name = unpack_name(node.children[0]) + # s(:class, name, superclass, body) or s(:module, name, body) + node => [:class | :module, ::Parser::AST::Node => name_node, *] + + name = unpack_name(name_node) comments = comments_for(node) - superclass_name = if node.type == :class && node.children[1]&.type == :const - "#{type_from_node}#{parameters_from_inline_rbs}" - end + superclass_name = if node in [:class, _, ::Parser::AST::Node[:const, *] => superclass, *] + "#{unpack_name(superclass)}#{parameters_from_inline_rbs(superclass)}" + end loc = get_node_location(node) nspin = Solargraph::Pin::Namespace.new( @@ -24,6 +27,7 @@ def process comments: comments, visibility: :public, gates: region.closure.gates.freeze, + node: node, source: :parser ) pins.push nspin @@ -45,11 +49,9 @@ def process # it: directly after the superclass, on the same line, with no space # between `#` and `[`. Anything else is an ordinary comment. # + # @param superclass [Parser::AST::Node] # @return [String, nil] - def parameters_from_inline_rbs - superclass = node.children[1] - return unless superclass - + def parameters_from_inline_rbs superclass source = region.source.code pos = get_node_end_position(superclass) offset = Position.line_char_to_offset(source, pos.line, pos.character) @@ -57,15 +59,13 @@ def parameters_from_inline_rbs match = source[offset...eol].to_s.match(/\A\s*#\[([^\]]*)\]/) return unless match + # @sg-ignore https://github.com/apiology/solargraph/pull/193 code = match[1].strip + # @sg-ignore MatchData#[] is nilable even for a group that always matches return if code.empty? "<#{code}>" end - - def type_from_node - unpack_name(node.children[1]) if node.children[1]&.type == :const - end end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb index aab8106aa3..33901d31ce 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb @@ -9,17 +9,21 @@ module NodeProcessors class OpasgnNode < Parser::NodeProcessor::Base # @return [void] def process - target = node.children[0] - operator = node.children[1] - argument = node.children[2] + # s(:op_asgn, target, operator, argument) + node => [:op_asgn, + ::Parser::AST::Node => target, + Symbol => operator, + ::Parser::AST::Node => argument] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 if target.type == :send - # @sg-ignore Need a downcast here process_send_target(target, operator, argument) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 elsif target.type.to_s.end_with?('vasgn') - # @sg-ignore Need a downcast here process_vasgn_target(target, operator, argument) else Solargraph.assert_or_log(:opasgn_unknown_target, + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 "Unexpected op_asgn target type: #{target.type}") end end @@ -39,8 +43,8 @@ def process_send_target call, operator, argument # :+, # operator # s(:send, nil, :baz)) # argument # [11] pry(main)> - callee = call.children[0] - call_method = call.children[1] + call => [:send, ::Parser::AST::Node | nil => callee, Symbol => call_method, *] + asgn_method = :"#{call_method}=" # [8] pry(main)> Parser::CurrentRuby.parse("Foo.bar = Foo.bar + baz") @@ -71,8 +75,9 @@ def process_vasgn_target asgn, operator, argument # :+, # operator # s(:int, 2)) # argument - # @type [Parser::AST::Node] - variable_name = asgn.children[0] + # only reached for lvasgn, ivasgn, cvasgn and gvasgn targets + asgn => [_, Symbol => variable_name, *] + # for lvasgn, gvasgn, cvasgn, convert to lvar, gvar, cvar # [6] pry(main)> Parser::CurrentRuby.parse("a = a + 1") # => s(:lvasgn, :a, 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 17480adfb5..0c061c5eb9 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb @@ -5,9 +5,25 @@ module Parser module ParserGem module NodeProcessors class OrasgnNode < Parser::NodeProcessor::Base + include ParserGem::NodeMethods + # @return [void] def process - new_node = node.updated(node.children[0].type, node.children[0].children + [node.children[1]]) + # s(:or_asgn, s(:lvasgn, name), value) is processed as s(:lvasgn, name, value) + node => [:or_asgn, + ::Parser::AST::Node[Symbol => target_type, *target_children], + ::Parser::AST::Node => value] + + closure_location = region.closure.location + if closure_location + here = get_node_start_position(node) + presence = Range.new(here, closure_location.range.ending) + FlowSensitiveTyping.new(locals, ivars, enclosing_breakable_pin, + enclosing_compound_statement_pin).process_or_asgn(node, presence) + end + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + new_node = node.updated(target_type, target_children + [value]) NodeProcessor.process(new_node, region, pins, locals, ivars) 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 24846748fb..8c9041fe26 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb @@ -9,28 +9,39 @@ class ResbodyNode < Parser::NodeProcessor::Base # @return [void] 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 + # s(:resbody, s(:array, *exception_classes) | nil, s(:lvasgn, name) | nil, body) + node => [:resbody, + ::Parser::AST::Node | nil => exceptions, + ::Parser::AST::Node | nil => variable, + ::Parser::AST::Node | nil => body] + + # variable is an lvasgn, ivasgn, cvasgn or gvasgn (Symbol name), a + # send for `=> a.b` (receiver node) or a casgn for `=> A` (nil scope) + if variable in [_, Symbol | ::Parser::AST::Node | nil => variable_name, *] + here = get_node_start_position(variable) + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 presence = Range.new(here, region.closure.location.range.ending) - loc = get_node_location(node.children[1]) - types = if node.children[0].nil? + loc = get_node_location(variable) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + types = if exceptions.nil? ['Exception'] else - node.children[0].children.map do |child| + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + exceptions.children.map do |child| unpack_name(child) end end locals.push Solargraph::Pin::LocalVariable.new( location: loc, closure: region.closure, - name: node.children[1].children[0].to_s, + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name: variable_name.to_s, comments: "@type [#{types.join(',')}]", presence: presence, source: :parser ) end - NodeProcessor.process(node.children[2], region, pins, locals, ivars) + NodeProcessor.process(body, 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 2d3d967cc5..b461f20bec 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb @@ -5,9 +5,10 @@ module Parser module ParserGem module NodeProcessors class SclassNode < Parser::NodeProcessor::Base - # @sg-ignore @override is adding, not overriding def process - sclass = node.children[0] + # s(:sclass, target, body) - target is s(:self), s(:casgn, ...) or s(:const, ...) + node => [:sclass, ::Parser::AST::Node => sclass, *] + # @todo Changing Parser::AST::Node to AST::Node below will # cause type errors at strong level because the combined # pin for AST::Node#children has return type @@ -17,18 +18,22 @@ def process # types to "A" if the "A" comes from YARD, with the # rationale that folks tend to be less formal with types in # YARD. - if sclass.is_a?(::Parser::AST::Node) && sclass.type == :self + case sclass + in ::Parser::AST::Node[:self, *] closure = region.closure - elsif sclass.is_a?(::Parser::AST::Node) && sclass.type == :casgn + in ::Parser::AST::Node[:casgn, ::Parser::AST::Node | nil => scope, Symbol => const_name, *] names = [region.closure.namespace, region.closure.name] - if sclass.children[0].nil? && names.last != sclass.children[1].to_s - names << sclass.children[1].to_s + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + if scope.nil? && names.last != const_name.to_s + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + names << const_name.to_s else - names.push NodeMethods.unpack_name(sclass.children[0]), sclass.children[1].to_s + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + names.push NodeMethods.unpack_name(scope), const_name.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 + in ::Parser::AST::Node[:const, *] names = [region.closure.namespace, region.closure.name] also = NodeMethods.unpack_name(sclass) names << also if also != region.closure.name 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 336af1bb7e..ee3920b964 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb @@ -7,50 +7,51 @@ 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}") - return process_children - end - # :nocov: - if node.children[0].nil? + # s(:send, receiver, method_name, *args) + node => [:send, ::Parser::AST::Node | nil => receiver, Symbol => method_name, *args] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + if receiver.nil? if %i[private public protected].include?(method_name) process_visibility + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 elsif method_name == :module_function process_module_function elsif %i[attr_reader attr_writer attr_accessor].include?(method_name) process_attribute - elsif method_name == :class_attribute - process_class_attribute + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 elsif method_name == :include process_include + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 elsif method_name == :extend process_extend + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 elsif method_name == :prepend process_prepend + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 elsif method_name == :require process_require + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 elsif method_name == :autoload process_autoload + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 elsif method_name == :private_constant process_private_constant - elsif method_name == :alias_method && node.children[2] && node.children[2] && node.children[2].type == :sym && node.children[3] && node.children[3].type == :sym + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + elsif method_name == :alias_method && (args in [[:sym, *], [:sym, *], *]) process_alias_method elsif %i[def_delegator def_delegators].include?(method_name) && extends_forwardable? process_def_delegators - elsif method_name == :private_class_method && node.children[2].is_a?(AST::Node) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + elsif method_name == :private_class_method && (args in [::AST::Node, *]) # Processing a private class can potentially handle children on its own return if process_private_class_method end - elsif %i[include extend prepend].include?(method_name) && Parser.is_ast_node?(node.children[0]) && node.children[0].type == :const + elsif %i[include extend prepend].include?(method_name) && (receiver in [:const, *]) process_qualified_mixin method_name - elsif method_name == :require && node.children[0].to_s == '(const nil :Bundler)' + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + elsif method_name == :require && (receiver in [: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 @@ -81,10 +82,8 @@ def extends_forwardable? def symbol_word subject return nil unless subject.is_a?(::Parser::AST::Node) - # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check return nil unless %i[sym str].include?(subject.type) - # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check subject.children.first.to_s end @@ -148,21 +147,17 @@ def push_delegated_method name, receiver_method_name, receiver_chain # @return [void] def process_visibility - 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}") - return process_children - end - # :nocov: - if child.is_a?(::Parser::AST::Node) && %i[sym str].include?(child.type) - name = child.children[0].to_s + node => [:send, _, Symbol => visibility, *args] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + if args.empty? + # @todo Smelly instance variable access + region.instance_variable_set(:@visibility, visibility) + else + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + args.each do |child| + if child in ::Parser::AST::Node[:sym | :str, Symbol | String => value, *] + name = value.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.each do |pin| # @todo Smelly instance variable access @@ -172,24 +167,25 @@ def process_visibility process_children region.update(visibility: visibility) end end - else - # @todo Smelly instance variable access - region.instance_variable_set(:@visibility, node.children[1]) end end # @return [void] def process_attribute + node => [:send, _, Symbol => attr_method, *args] + # @sg-ignore Need to add nil check here - node.children[2..].each do |a| + args.each do |a| + # s(:sym, name) or s(:str, name); attr_reader(*X) passes s(:splat, s(:const, nil, :X)) + attr_name = (a in [_, value, *]) ? value : nil loc = get_node_location(node) clos = region.closure cmnt = comments_for(node) - if %i[attr_reader attr_accessor].include?(node.children[1]) + if %i[attr_reader attr_accessor].include?(attr_method) pins.push Solargraph::Pin::Method.new( location: loc, closure: clos, - name: a.children[0].to_s, + name: attr_name.to_s, comments: cmnt, scope: region.scope || :instance, visibility: region.visibility, @@ -197,11 +193,11 @@ def process_attribute source: :parser ) end - next unless %i[attr_writer attr_accessor].include?(node.children[1]) + next unless %i[attr_writer attr_accessor].include?(attr_method) method_pin = Solargraph::Pin::Method.new( location: loc, closure: clos, - name: "#{a.children[0]}=", + name: "#{attr_name}=", comments: cmnt, scope: region.scope || :instance, visibility: region.visibility, @@ -212,57 +208,12 @@ def process_attribute 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') + method_pin.docstring.add_tag YARD::Tags::Tag.new(:param, '', + method_pin.return_type.items.map(&:rooted_tags), 'value') end end end - # Process an ActiveSupport +class_attribute+ declaration. - # - # +class_attribute :name+ generates a singleton reader, writer and - # predicate plus, unless disabled by options, an instance reader, - # writer and predicate. The accessors are created at run time, so - # without pins for them every class that calls +class_attribute+ - # loses its configuration API. - # - # @return [void] - def process_class_attribute - options = class_attribute_options - instance_accessor = boolean_option options, 'instance_accessor', true - instance_reader = boolean_option options, 'instance_reader', instance_accessor - instance_writer = boolean_option options, 'instance_writer', instance_accessor - instance_predicate = boolean_option options, 'instance_predicate', true - node.children[2..].each do |a| - next unless Parser.is_ast_node?(a) && %i[sym str].include?(a.type) - name = a.children[0].to_s - next if name.empty? - pins.push build_class_attribute_pin name, scope: :class - pins.push build_class_attribute_pin "#{name}=", scope: :class, writer: true - pins.push build_class_attribute_pin "#{name}?", scope: :class if instance_predicate - if instance_reader - pins.push build_class_attribute_pin name, scope: :instance - pins.push build_class_attribute_pin "#{name}?", scope: :instance if instance_predicate - end - pins.push build_class_attribute_pin "#{name}=", scope: :instance, writer: true if instance_writer - end - end - - # The literal keyword arguments passed to a +class_attribute+ call. - # Values which cannot be evaluated statically are ignored. - # - # @return [Hash{String => AST::Node}] - def class_attribute_options - hash = node.children[-1] - return {} unless Parser.is_ast_node?(hash) && hash.type == :hash - hash.children.each_with_object({}) do |pair, result| - next unless Parser.is_ast_node?(pair) && pair.type == :pair - key = pair.children[0] - next unless Parser.is_ast_node?(key) && %i[sym str].include?(key.type) - result[key.children[0].to_s] = pair.children[1] - end - end - # @param options [Hash{String => AST::Node}] # @param name [String] # @param default [Boolean] @@ -271,37 +222,22 @@ def boolean_option options, name, default value = options[name] return default unless Parser.is_ast_node?(value) # rubocop:disable Lint/BooleanSymbol -- these are AST node types + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 return true if value.type == :true + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 return false if value.type == :false # rubocop:enable Lint/BooleanSymbol default end - # @param name [String] - # @param scope [Symbol] :class or :instance - # @param writer [Boolean] - # @return [Pin::Method] - def build_class_attribute_pin name, scope:, writer: false - pin = Pin::Method.new( - location: get_node_location(node), - closure: region.closure, - name: name, - comments: comments_for(node), - scope: scope, - visibility: region.visibility, - attribute: true, - source: :parser - ) - pin.parameters.push Pin::Parameter.new(name: 'value', decl: :arg, closure: pin, source: :parser) if writer - pin - end - # @return [void] def process_include - return unless node.children[2].is_a?(AST::Node) && node.children[2].type == :const + node => [:send, _, _, *args] + return unless args in [::AST::Node[:const, *], *] + cp = region.closure - # @sg-ignore Need to add nil check here - node.children[2..].each do |i| + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + args.each do |i| type = region.scope == :class ? Pin::Reference::Extend : Pin::Reference::Include pins.push type.new( location: get_node_location(i), @@ -314,10 +250,12 @@ def process_include # @return [void] def process_prepend - return unless node.children[2].is_a?(AST::Node) && node.children[2].type == :const + node => [:send, _, _, *args] + return unless args in [::AST::Node[:const, *], *] + cp = region.closure - # @sg-ignore Need to add nil check here - node.children[2..].each do |i| + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + args.each do |i| pins.push Pin::Reference::Prepend.new( location: get_node_location(i), closure: cp, @@ -342,12 +280,14 @@ def process_qualified_mixin keyword prepend: Pin::Reference::Prepend }[keyword] return if reference_class.nil? - receiver = node.children[0] + # s(:send, receiver, keyword, *args) - the caller checked receiver is a constant + node => [:send, ::Parser::AST::Node => receiver, _, *args] target = unpack_name(receiver) return if target.nil? || target.empty? closure = Pin::Namespace.new(location: get_node_location(receiver), name: target, source: :parser) - node.children[2..].each do |arg| - next unless Parser.is_ast_node?(arg) + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + args.each do |child| + next unless child in ::Parser::AST::Node => arg name = if arg.type == :self region.closure&.full_context&.namespace elsif arg.type == :const @@ -365,8 +305,10 @@ def process_qualified_mixin keyword # @return [void] def process_extend - # @sg-ignore Need to add nil check here - node.children[2..].each do |i| + node => [:send, _, _, *args] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + args.each do |i| loc = get_node_location(node) if i.type == :self pins.push Pin::Reference::Extend.new( @@ -388,27 +330,36 @@ def process_extend # @return [void] def process_require - return unless node.children[2].is_a?(AST::Node) && node.children[2].type == :str - path = node.children[2].children[0].to_s + # require 'path' - s(:send, nil, :require, s(:str, path)) + return unless node in [:send, _, _, ::AST::Node[:str, String => value, *], *] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + path = value.to_s pins.push Pin::Reference::Require.new(get_node_location(node), path, source: :parser) end # @return [void] def process_autoload - return unless node.children[3].is_a?(AST::Node) && node.children[3].type == :str - path = node.children[3].children[0].to_s + # autoload :Name, 'path' - s(:send, nil, :autoload, s(:sym, name), s(:str, path)) + return unless node in [:send, _, _, _, ::AST::Node[:str, String => value, *], *] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + path = value.to_s pins.push Pin::Reference::Require.new(get_node_location(node), path, source: :parser) end # @return [void] def process_module_function - if node.children[2].nil? + node => [:send, _, _, *args] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + if args.empty? # @todo Smelly instance variable access region.instance_variable_set(:@visibility, :module_function) - 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 + elsif args in [[:sym | :str, *], *] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + args.each do |x| + cn = (x in [_, Symbol | String => value, *]) ? value.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 } next if ref.nil? @@ -456,15 +407,17 @@ def process_module_function ) end end - elsif node.children[2].type == :def - NodeProcessor.process node.children[2], region.update(visibility: :module_function), pins, locals, ivars + elsif args in [::Parser::AST::Node[:def, *] => def_node, *] + NodeProcessor.process def_node, region.update(visibility: :module_function), pins, locals, ivars end end # @return [void] def process_private_constant - return unless node.children[2] && %i[sym str].include?(node.children[2].type) - cn = node.children[2].children[0].to_s + return unless node in [:send, _, _, [:sym | :str, Symbol | String => value, *], *] + + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + cn = value.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 @@ -475,12 +428,18 @@ def process_private_constant # @return [void] def process_alias_method + # alias_method :new, :old - s(:send, nil, :alias_method, s(:sym, new), s(:sym, old)) + # only reached when both arguments are sym nodes (see #process) + node => [:send, _, _, [_, Symbol => new_name, *], [_, Symbol => original, *], *] + get_node_location(node) pins.push Solargraph::Pin::MethodAlias.new( location: get_node_location(node), closure: region.closure, - name: node.children[2].children[0].to_s, - original: node.children[3].children[0].to_s, + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + name: new_name.to_s, + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + original: original.to_s, scope: region.scope || :instance, source: :parser ) @@ -488,9 +447,10 @@ def process_alias_method # @return [Boolean] def process_private_class_method - if %i[sym str].include?(node.children[2].type) + if node in [:send, _, _, [:sym | :str, Symbol | String => value, *], *] 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + p.is_a?(Pin::Method) && p.namespace == region.closure.full_context.namespace && p.name == value.to_s end.first # HACK: Smelly instance variable access ref&.instance_variable_set(:@visibility, :private) diff --git a/lib/solargraph/parser/parser_gem/node_processors/sym_node.rb b/lib/solargraph/parser/parser_gem/node_processors/sym_node.rb index e8cfc3af74..bfe9d618d8 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/sym_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/sym_node.rb @@ -7,9 +7,11 @@ module NodeProcessors class SymNode < Parser::NodeProcessor::Base # @return [void] def process + node => [:sym, Symbol => value] + pins.push Solargraph::Pin::Symbol.new( get_node_location(node), - ":#{node.children[0]}", + ":#{value}", source: :parser ) end diff --git a/lib/solargraph/parser/region.rb b/lib/solargraph/parser/region.rb index 8c4caf6acb..813cf7f760 100644 --- a/lib/solargraph/parser/region.rb +++ b/lib/solargraph/parser/region.rb @@ -9,7 +9,7 @@ class Region # @return [Pin::Closure] attr_reader :closure - # @return [Symbol] + # @return [Symbol, nil] attr_reader :scope # @return [Symbol] @@ -43,7 +43,7 @@ def filename # @return [Pin::Namespace, nil] def namespace_pin ns = closure - # @sg-ignore flow sensitive typing needs to handle while + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 ns = ns.closure while ns && !ns.is_a?(Pin::Namespace) ns end diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index f7ae58d388..f10290ad39 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -22,10 +22,10 @@ class Base # @return [String] attr_reader :name - # @return [String] + # @return [String, nil] attr_reader :path - # @return [::Symbol] + # @return [::Symbol, nil] attr_accessor :source # @type [::Numeric, nil] A priority for determining if pins should be combined or not @@ -42,7 +42,7 @@ def presence_certain? # @param closure [Solargraph::Pin::Closure, nil] # @param name [String] # @param comments [String, nil] - # @param source [Symbol, 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 @@ -134,7 +134,7 @@ def choose_priority other # @param other [self] # @param attr [::Symbol] - # @sg-ignore + # @sg-ignore https://github.com/castwide/solargraph/pull/1420 # @return [undefined] def choose_longer other, attr # @type [undefined] @@ -158,11 +158,27 @@ def combine_directives other # @param other [self] # @return [Pin::Closure, nil] def combine_closure other + # Source maps close a def in `class << self` or a block over that + # scope; RBS closes it over the namespace the scope sits in. + namespace = namespace_enclosing(closure, other.closure) || namespace_enclosing(other.closure, closure) + return namespace if namespace + choose_pin_attr_with_same_name(other, :closure) end + # @param scope [Pin::Closure, nil] + # @param namespace [Pin::Closure, nil] + # @return [Pin::Namespace, nil] namespace, if scope is a singleton class + # or block directly inside it + def namespace_enclosing scope, namespace + return unless namespace.is_a?(Pin::Namespace) + return unless scope.is_a?(Pin::Singleton) || scope.is_a?(Pin::Block) + + namespace if scope&.closure&.path == namespace.path + end + # @param other [self] - # @sg-ignore @type should override probed type + # @sg-ignore dynamic send has no inferable return type # @return [String] def combine_name other if needs_consistent_name? || other.needs_consistent_name? @@ -222,7 +238,7 @@ def combine_return_type other end end - # @sg-ignore need boolish support for ? methods + # @return [Boolean, nil] def dodgy_return_type_source? # uses a lot of 'Object' instead of 'self' location&.filename&.include?('core_ext/object/') || @@ -235,13 +251,13 @@ def dodgy_return_type_source? # @param other [Pin::Base] # @param attr [::Symbol] # - # @sg-ignore + # @sg-ignore https://github.com/castwide/solargraph/pull/1420 # @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| [true, false].include?(r) } + return results.include?(true) if results.intersect?([true, false]) return results.first if results.any? { |r| r.is_a? AST::Node } results.min rescue StandardError @@ -251,7 +267,7 @@ def choose other, attr # @param other [self] # @param attr [::Symbol] - # @sg-ignore + # @sg-ignore https://github.com/castwide/solargraph/pull/1420 # @return [undefined] def choose_node other, attr if other.object_id < attr.object_id @@ -263,7 +279,7 @@ def choose_node other, attr # @param other [self] # @param attr [::Symbol] - # @sg-ignore + # @sg-ignore https://github.com/castwide/solargraph/pull/1420 # @return [undefined] def prefer_rbs_location other, attr if rbs_location? && !other.rbs_location? @@ -275,7 +291,7 @@ def prefer_rbs_location other, attr end end - # @sg-ignore need boolish support for ? methods + # @return [Boolean, nil] def rbs_location? type_location&.rbs? end @@ -305,7 +321,6 @@ def assert_same_array_content(other, attr, &) values1 = arr1.map(&) # @type [undefined] values2 = arr2.map(&) - # @sg-ignore return arr1 if values1 == values2 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}") @@ -332,7 +347,7 @@ def assert_same_count other, attr # @param other [self] # @param attr [::Symbol] # - # @sg-ignore + # @sg-ignore https://github.com/castwide/solargraph/pull/1420 # @return [undefined] def assert_same other, attr if other.nil? @@ -350,8 +365,7 @@ def assert_same other, attr # @param other [self] # @param attr [::Symbol] - # @sg-ignore - # @return [undefined] + # @return [Pin::Base, nil] def choose_pin_attr_with_same_name other, attr # @type [Pin::Base, nil] val1 = send(attr) @@ -371,8 +385,8 @@ def choose_pin_attr_with_same_name other, attr # @param other [self] # @param attr [::Symbol] # - # @sg-ignore Missing @return tag for Solargraph::Pin::Base#choose_pin_attr - # @return [undefined] + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 + # @return [Pin::Base, nil] def choose_pin_attr other, attr # @type [Pin::Base, nil] val1 = send(attr) @@ -391,10 +405,10 @@ 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 closure.gates.length, # use basename so that results don't vary system to system - # @sg-ignore Need better handling of #compact + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 File.basename(closure.best_location.to_s) ] end @@ -431,7 +445,7 @@ def transform_types &transform # parameters used in this method based on the parameters passed # into the its class and return a new method pin. # - # @param definitions [Pin::Namespace] The module/class which uses generic types + # @param definitions [Pin::Namespace, nil] The module/class which uses generic types # @param context_type [ComplexType] The receiver type # @return [self] def resolve_generics definitions, context_type @@ -452,7 +466,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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 location.filename end @@ -488,18 +502,18 @@ def best_location # @return [Boolean] def nearly? other instance_of?(other.class) && - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 name == other.name && - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 (closure.equal?(other.closure) || (closure&.nearly?(other.closure))) && - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 (comments == other.comments || - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 (((maybe_directives? == false && other.maybe_directives? == false) || compare_directives(directives, - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 other.directives)) && - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1367 compare_docstring_tags(docstring, other.docstring)) ) end @@ -510,8 +524,7 @@ def nearly? other # @param other [Object] def == other return false unless nearly? other - # @sg-ignore Should add more explicit type check on other - comments == other.comments && location == other.location + other.is_a?(Pin::Base) && comments == other.comments && location == other.location end # The pin's return type. @@ -527,11 +540,11 @@ 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 - @directives + parsed = @directives + parsed.nil? ? raise("parse_comments did not set directives for #{path}") : parsed end # @return [::Array] @@ -539,6 +552,7 @@ def macros @macros ||= collect_macros end + # @return [Array] def macro_names parse_comments unless @macro_names @macro_names ||= collect_macro_names @@ -713,7 +727,7 @@ def all_location_text protected - # @sg-ignore def should infer as symbol - "Not enough arguments to Module#protected" + # @return [Array] def equality_fields [name, location, type_location, closure, source] end @@ -757,7 +771,7 @@ def parse_comments 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]) + return false unless compare_tags(docstring1.tags.fetch(i), docstring2.tags.fetch(i)) end true end @@ -768,7 +782,7 @@ def compare_docstring_tags docstring1, docstring2 def compare_directives dir1, dir2 return false if dir1.length != dir2.length dir1.each_index do |i| - return false unless compare_tags(dir1[i].tag, dir2[i].tag) + return false unless compare_tags(dir1.fetch(i).tag, dir2.fetch(i).tag) end true end @@ -793,6 +807,7 @@ def collect_macros end end + # @return [Array] def collect_macro_names "#{comments}\n".scan(/\s*?@macro +(\S+).*?[\n]/).map { |match| match[0] } end diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 33ea074419..3c503c5cbf 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -63,7 +63,7 @@ def initialize assignment: nil, assignments: [], mass_assignment: nil, # @param presence [Range] # @param exclude_return_type [ComplexType, nil] # @param intersection_return_type [ComplexType, nil] - # @param source [::Symbol] + # @param source [::Symbol, nil] # # @return [self] def downcast presence:, exclude_return_type: nil, intersection_return_type: nil, @@ -159,12 +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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 result = chain.infer(api_map, closure, clip.locals).self_to_type(closure.context) types.push result unless result.undefined? end @@ -207,15 +207,14 @@ def probe api_map # @param other [Object] def == other return false unless super - # @sg-ignore Should add type check on other - assignment == other.assignment + other.is_a?(BaseVariable) && assignment == other.assignment end def type_desc "#{super} = #{assignment&.type.inspect}" end - # @return [ComplexType, nil] + # @return [ComplexType] def return_type generate_complex_type || @return_type || intersection_return_type || ComplexType::UNDEFINED end @@ -226,17 +225,17 @@ def typify api_map adjust_type(api_map, raw_return_type) end - # @sg-ignore need boolish support for ? methods + # @return [ComplexType, nil] def presence_certain? exclude_return_type || intersection_return_type end # @param other_loc [Location] - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 def starts_at? other_loc location&.filename == other_loc.filename && presence && - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 presence.start == other_loc.range.start end @@ -248,7 +247,7 @@ def starts_at? other_loc def combine_presence other return presence || other.presence if presence.nil? || other.presence.nil? - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 Range.new([presence.start, other.presence.start].max, [presence.ending, other.presence.ending].min) end @@ -266,14 +265,17 @@ def combine_closure other return closure || other.closure end - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top if closure.location.nil? || other.closure.location.nil? - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top return closure if closure.location <= other.closure.location other.closure @@ -282,9 +284,9 @@ def combine_closure other # @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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 location.filename == other_loc.filename && - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 (!presence || presence.include?(other_loc.range.start)) && visible_in_closure?(other_closure) end @@ -293,7 +295,7 @@ def visible_at? other_closure, other_loc attr_accessor :exclude_return_type, :intersection_return_type - # @return [Range] + # @return [Range, nil] attr_writer :presence private @@ -319,13 +321,13 @@ def visible_in_closure? viewing_closure # 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 return false if viewing_closure.is_a?(Pin::Method) && closure.context.tags == 'Class<>' - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 return true if viewing_closure.binder.namespace == closure.binder.namespace - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 return true if viewing_closure.return_type == closure.context # classes and modules can't see local variables declared diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 1ad503317d..92d6d7cf8d 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -5,10 +5,10 @@ module Pin class Block < Callable include Breakable - # @return [Parser::AST::Node] + # @return [Parser::AST::Node, nil] attr_reader :receiver - # @return [Parser::AST::Node] + # @return [Parser::AST::Node, nil] attr_reader :node # @param receiver [Parser::AST::Node, nil] @@ -45,10 +45,11 @@ def context # @param parameters [::Array] # # @return [::Array] + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 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 + yield_type = yield_types.fetch(0) return yield_type.all_params if yield_type.tuple? && yield_type.all_params.length == parameters.length end parameters.map.with_index { |_, idx| yield_types[idx] || ComplexType::UNDEFINED } @@ -58,30 +59,34 @@ def destructure_yield_types yield_types, parameters # @return [::Array] def typify_parameters api_map chain = Parser.chain(receiver, filename, node) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 clip = api_map.clip_at(location.filename, location.range.start) locals = clip.locals - [self] - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 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 argument_types = destructure_yield_types(yield_types, parameters) param_types = argument_types.each_with_index.map do |arg_type, idx| - param = parameters[idx] + param = parameters.fetch(idx) param_type = chain.base.infer(api_map, param, locals) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 unless arg_type.nil? + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 if arg_type.generic? && param_type.defined? - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 namespace_pin = api_map.get_namespace_pins(meth.namespace, closure.namespace).first + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 arg_type.resolve_generics(namespace_pin, param_type) else + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 arg_type.self_to_type(chain.base.infer(api_map, self, locals)).qualify(api_map, *meth.gates) end end @@ -98,11 +103,11 @@ def typify_parameters api_map def maybe_rebind api_map return ComplexType::UNDEFINED unless receiver - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 chain = Parser.chain(receiver, location.filename, node) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 locals = api_map.source_map(location.filename).locals_at(location) - # @sg-ignore Need to add nil check here + # @sg-ignore Chain#define name_pin expected Pin::Base, received Pin::Closure, nil receiver_pin = chain.define(api_map, closure, locals).first return ComplexType::UNDEFINED unless receiver_pin @@ -116,7 +121,7 @@ def maybe_rebind api_map 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 closure.full_context end diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index ed87b79e41..1963d92d20 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -3,14 +3,11 @@ module Solargraph module Pin class Callable < Closure - # @return [Signature] + # @return [Signature, nil] attr_reader :block attr_accessor :parameters - # @return [ComplexType, nil] - attr_reader :return_type - # @param block [Signature, nil] # @param return_type [ComplexType, nil] # @param parameters [::Array] @@ -27,10 +24,10 @@ def reset_generated! super end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 # @return [String] def method_namespace - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 closure.namespace end @@ -90,7 +87,7 @@ def choose_parameters other end end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 # @return [Array] def blockless_parameters if parameters.last&.block? @@ -123,7 +120,6 @@ def type_arity # # @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 @@ -144,18 +140,21 @@ def resolve_generics_from_context generics_to_resolve, 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? + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 param.dup else + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 param.resolve_generics_from_context(generics_to_resolve, arg_types[i], resolved_generic_values: resolved_generic_values) end end - 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) + if callable.block + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + callable.block = callable.block.resolve_generics_from_context(generics_to_resolve, + yield_arg_types, + yield_return_type_context, + resolved_generic_values: resolved_generic_values) end callable end @@ -172,11 +171,11 @@ def typify api_map end end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 # @return [String] def method_name raise "closure was nil in #{inspect}" if closure.nil? - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 @method_name ||= closure.name end @@ -227,7 +226,8 @@ def resolve_generics_from_context_until_complete generics_to_resolve, 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? + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + callable.block = callable.block.transform_types(&transform) if callable.block callable.parameters = parameters.map do |param| param.transform_types(&transform) end @@ -240,12 +240,14 @@ def transform_types &transform def arity_matches? arguments, with_block argcount = arguments.length parcount = mandatory_positional_param_count + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top 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?) + return false if argcount < parcount && !(argcount == parcount - 1 && parameters.fetch(-1).restarg?) true end @@ -256,6 +258,7 @@ def mandatory_positional_param_count # @return [String] def parameters_to_rbs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 "#{rbs_generics}(#{parameters.map(&:to_rbs).join(', ')}) #{"{ #{block.to_rbs} } " unless block.nil?}" end diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index 46865854a4..01a0ed796a 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -57,7 +57,7 @@ def namespace end # @return [ComplexType, ComplexType::UniqueType] - # @sg-ignore https://github.com/castwide/solargraph/pull/1100 + # @sg-ignore https://github.com/apiology/solargraph/pull/190 def binder @binder || context end @@ -99,7 +99,7 @@ def find_context elsif here.is_a?(Pin::Method) return here.context end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1322 here = here.closure end ComplexType::ROOT diff --git a/lib/solargraph/pin/delegated_method.rb b/lib/solargraph/pin/delegated_method.rb index 3ca5f2445c..6922531c11 100644 --- a/lib/solargraph/pin/delegated_method.rb +++ b/lib/solargraph/pin/delegated_method.rb @@ -23,8 +23,7 @@ def body? # @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) + super(name: name || raise(ArgumentError, ':name is required with :receiver'), **splat) @receiver_chain = receiver @resolved_method = method @@ -49,7 +48,7 @@ def type_location %i[comments parameters return_type signatures].each do |method| define_method(method) do - # @sg-ignore Need to set context correctly in define_method blocks + # @sg-ignore https://github.com/castwide/solargraph/pull/1423 @resolved_method ? @resolved_method.send(method) : super() end end @@ -58,7 +57,7 @@ def type_location # @param api_map [ApiMap] define_method(method) do |api_map| resolve_method(api_map) - # @sg-ignore Need to set context correctly in define_method blocks + # @sg-ignore https://github.com/castwide/solargraph/pull/1423 @resolved_method ? @resolved_method.send(method, api_map) : super(api_map) end end @@ -75,40 +74,32 @@ 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 + # @sg-ignore https://github.com/apiology/solargraph/pull/178 def resolve_method api_map - return if @resolved_method + chain = @receiver_chain + return if @resolved_method || chain.nil? - # @sg-ignore Need to add nil check here - resolver = @receiver_chain.define(api_map, self, []).first + resolver = chain.define(api_map, self, []).first unless resolver - # @sg-ignore Need to add nil check here - Solargraph.logger.warn "Delegated receiver for #{path} was resolved to nil from `#{print_chain(@receiver_chain)}'" + Solargraph.logger.warn "Delegated receiver for #{path} was resolved to nil from `#{print_chain(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? + if 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 + # @sg-ignore ApiMap#get_method_stack name expected String, received String, nil method_stack = api_map.get_method_stack(receiver_path, @receiver_method_name, scope: method_scope) @resolved_method = method_stack.first end diff --git a/lib/solargraph/pin/documenting.rb b/lib/solargraph/pin/documenting.rb index 94bd8a551c..a4bc48153f 100644 --- a/lib/solargraph/pin/documenting.rb +++ b/lib/solargraph/pin/documenting.rb @@ -105,7 +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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1385 match[0].length end.min return text if left.nil? || left.zero? diff --git a/lib/solargraph/pin/factory_parameter.rb b/lib/solargraph/pin/factory_parameter.rb index 5600a86725..e949894d4e 100644 --- a/lib/solargraph/pin/factory_parameter.rb +++ b/lib/solargraph/pin/factory_parameter.rb @@ -25,6 +25,7 @@ class FactoryParameter < Base attr_reader :method_scope # @return [String, nil] attr_reader :param_name + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 # @return [::String, ::Symbol] The literal value attr_reader :value # @return [::Symbol] :arg, :optarg, :kwarg, :kwoptarg, :restarg, :kwrestarg, :block, :blockarg @@ -58,6 +59,7 @@ def initialize method_name:, @return_type = return_type end + # @return [String, nil] def name param_name end @@ -68,8 +70,6 @@ def text_documentation # @return [String] def method_path - # @sg-ignore false failure on method_scope "Wrong argument type for Solargraph::Pin::Base#==: other expected - # Solargraph::Pin::Base, received :instance" @method_path ||= "#{method_namespace}#{method_scope == :instance ? '#' : '.'}#{method_name}" end diff --git a/lib/solargraph/pin/instance_variable.rb b/lib/solargraph/pin/instance_variable.rb index b3c69f09cf..62ca1c2e0a 100644 --- a/lib/solargraph/pin/instance_variable.rb +++ b/lib/solargraph/pin/instance_variable.rb @@ -3,17 +3,17 @@ module Solargraph module Pin class InstanceVariable < BaseVariable - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 # @return [ComplexType, ComplexType::UniqueType] def binder - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 closure.binder end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 # @return [::Symbol] def scope - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 closure.binder.scope end diff --git a/lib/solargraph/pin/keyword.rb b/lib/solargraph/pin/keyword.rb index 08ea1c6e03..7d2644a63a 100644 --- a/lib/solargraph/pin/keyword.rb +++ b/lib/solargraph/pin/keyword.rb @@ -4,7 +4,8 @@ module Solargraph module Pin class Keyword < Base def initialize(name, **kwargs) - # @sg-ignore "Unrecognized keyword argument kwargs to Solargraph::Pin::Base#initialize" + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 + # double-splat checks need https://github.com/apiology/solargraph/tree/kwsplat-record-types on top super(name: name, **kwargs) end diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 077da21be3..58deb8f48f 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -9,7 +9,6 @@ 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 diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index e1d65326a3..bae3c9f55a 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -12,7 +12,7 @@ class Method < Callable attr_writer :signatures - # @return [Parser::AST::Node] + # @return [Parser::AST::Node, nil] attr_reader :node # @param visibility [::Symbol] :public, :protected, or :private @@ -29,7 +29,8 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil super(**splat) @visibility = visibility @explicit = explicit - @block = block + @block_known = block != :undefined + @block = block if block.is_a?(Signature) @node = node @attribute = attribute @signatures = signatures @@ -93,7 +94,7 @@ def transform_types &transform def reset_generated! super unless signatures.empty? - @block = :undefined + @block_known = false [] end block&.reset_generated! @@ -126,11 +127,10 @@ 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 + return @block if @block_known + @block_known = true @block = signatures.first&.block end @@ -142,6 +142,7 @@ def symbol_kind attribute? ? Solargraph::LanguageServer::SymbolKinds::PROPERTY : LanguageServer::SymbolKinds::METHOD end + # @return [ComplexType] def return_type @return_type ||= return_type_from_inline_rbs || ComplexType.new(signatures.map(&:return_type).flat_map(&:items)) end @@ -212,9 +213,9 @@ def detail detail += if signatures.length > 1 '(*) ' else + # @sg-ignore Unresolved call to parameters on Pin::Signature, nil "(#{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? '~' @@ -249,8 +250,11 @@ def inner_desc def to_rbs return nil if signatures.empty? + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top rbs = "def #{name}: #{signatures.first.to_rbs}" - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top signatures[1..].each do |sig| rbs += "\n" rbs += (' ' * (4 + name.length)) @@ -259,6 +263,7 @@ def to_rbs rbs end + # @return [String] def path @path ||= "#{namespace}#{scope == :instance ? '#' : '.'}#{name}" end @@ -270,14 +275,13 @@ def method_name def typify api_map 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 = if macro_names? types = macro_names.flat_map do |mac| directive = api_map.named_macro(mac) next unless directive - macro = Solargraph::YardMap::Macro.from_directive(directive, self) + macro = Solargraph::YardMap::Macro.from_directive(directive.directive, self) expanded = macro.macro_object.expand([name, *parameter_names]) docstring = Solargraph::Source.parse_docstring(expanded).to_docstring docstring.tags(:return).flat_map(&:types) @@ -295,7 +299,7 @@ def typify api_map 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 qualified = type.qualify(api_map, *closure.gates) logger.debug { "Method#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } return qualified @@ -381,14 +385,11 @@ def attribute? @attribute end - # @parm other [self] + # @param other [self] def nearly? other super && - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 parameters == other.parameters && - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 scope == other.scope && - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 visibility == other.visibility end @@ -410,7 +411,8 @@ def overloads generics: generics, # @param src [Array(String, String)] parameters: tag.parameters.map do |src| - name, decl = parse_overload_param(src.first) + name, decl = parse_overload_param(src.fetch(0)) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 Pin::Parameter.new( location: location, closure: self, @@ -418,7 +420,7 @@ def overloads name: name, decl: decl, presence: location&.range, - return_type: param_type_from_name(tag, src.first), + return_type: param_type_from_name(tag, src.fetch(0)), source: :overloads ) end, @@ -465,13 +467,20 @@ def rest_of_stack api_map protected - attr_writer :block, :signature_help, :documentation, :return_type + attr_writer :signature_help, :documentation, :return_type + + # @param value [Pin::Signature, nil] + # @return [void] + def block= value + @block_known = true + @block = value + end + # @return [Boolean] 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 - # @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::')) || @@ -507,6 +516,7 @@ def combine_signatures_by_type_arity(*signature_pins) by_type_arity = {} signature_pins.each do |signature_pin| by_type_arity[signature_pin.type_arity] ||= [] + # @sg-ignore flow sensitive typing needs to track reassignment by_type_arity[signature_pin.type_arity] << signature_pin end @@ -604,16 +614,15 @@ def see_reference api_map # This should actually be an intersection type # @param ref [YARD::Tags::Tag, YARD::Tags::RefTag] docstring.ref_tags.each do |ref| - # @sg-ignore ref should actually be an intersection type + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 next unless ref.tag_name == 'return' && ref.owner - # @sg-ignore should actually be an intersection type + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 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 + resolve_reference match[1].to_s, api_map end # @return [String] @@ -627,7 +636,6 @@ 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 @@ -637,14 +645,13 @@ def typify_from_super api_map # @param api_map [ApiMap] # @return [ComplexType, ComplexType::UniqueType, nil] def resolve_reference ref, api_map - parts = ref.split(/[.#]/) - if parts.first.empty? || parts.one? + prefix, separator, name = ref.rpartition(/[.#]/) + if prefix.empty? path = "#{namespace}#{ref}" else - fqns = api_map.qualify(parts.first, *gates) + fqns = api_map.qualify(prefix, *gates) return ComplexType::UNDEFINED if fqns.nil? - # @sg-ignore Need to add nil check here - path = fqns + ref[parts.first.length] + parts.last + path = fqns + separator + name end pins = api_map.get_path_pins(path) pins.each do |pin| @@ -657,11 +664,12 @@ def resolve_reference ref, api_map # @return [Parser::AST::Node, nil] 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 %i[def DEFS].include?(node.type) - return node.children[3] if node.type == :defs - nil + # s(:def, name, args, body) or s(:defs, receiver, name, args, body) + case node + in [:def, _, _, ::Parser::AST::Node | nil => body, *] then body + in [:defs, _, _, _, ::Parser::AST::Node | nil => defs_body, *] then defs_body + else nil + end end # @param api_map [ApiMap] @@ -679,11 +687,11 @@ def infer_from_return_nodes api_map rng = Range.from_node(n) next unless rng clip = api_map.clip_at( - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 location.filename, rng.ending ) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 chain = Solargraph::Parser.chain(n, location.filename) type = chain.infer(api_map, self, clip.locals) result.push type unless type.undefined? @@ -698,7 +706,12 @@ def infer_from_return_nodes api_map def infer_from_iv api_map types = [] varname = "@#{name.gsub(/=$/, '')}" - pins = api_map.get_instance_variable_pins(binder.namespace, binder.scope).select { |iv| iv.name == varname } + # Pins with a presence are flow-sensitive narrowings scoped to + # part of one method, not assignments, so they don't describe + # what the attribute returns + pins = api_map.get_instance_variable_pins(binder.namespace, binder.scope).select do |iv| + iv.name == varname && iv.presence.nil? + end pins.each do |pin| type = pin.typify(api_map) type = pin.probe(api_map) if type.undefined? @@ -740,6 +753,8 @@ def concat_example_tags def return_type_from_inline_rbs return nil if inline_rbs.empty? method_type = RBS::Parser.parse_method_type(inline_rbs) + return nil if method_type.nil? + RbsTranslator.to_complex_type(method_type.type.return_type) rescue RBS::ParsingError nil @@ -748,6 +763,8 @@ def return_type_from_inline_rbs # @return [Array] def signatures_from_inline_rbs method_type = RBS::Parser.parse_method_type(inline_rbs) + return signatures_from_yard if method_type.nil? + [RbsTranslator.to_signature(method_type, self, parameter_names)] rescue RBS::ParsingError signatures_from_yard @@ -769,7 +786,7 @@ def signatures_from_yard def inline_rbs comments.lines .select { |line| line.start_with?(': ') } - .map { |line| line[2..].strip } + .map { |line| line.delete_prefix(': ').strip } .join("\n") end end diff --git a/lib/solargraph/pin/method_alias.rb b/lib/solargraph/pin/method_alias.rb index 078c58e62e..f07770fa91 100644 --- a/lib/solargraph/pin/method_alias.rb +++ b/lib/solargraph/pin/method_alias.rb @@ -14,7 +14,7 @@ def body? # @return [::Symbol] attr_reader :scope - # @return [String] + # @return [String, nil] attr_reader :original # @param scope [::Symbol] diff --git a/lib/solargraph/pin/namespace.rb b/lib/solargraph/pin/namespace.rb index 55bb52b0e3..873036e8a9 100644 --- a/lib/solargraph/pin/namespace.rb +++ b/lib/solargraph/pin/namespace.rb @@ -14,8 +14,12 @@ class Namespace < Closure # does not assert like super, as a namespace without a closure # may be the root level namespace, or it may not yet be # qualified + # @return [Solargraph::Pin::Closure, nil] attr_reader :closure + # node: (see CompoundStatement) carries this namespace body, which + # flow-sensitive typing scans to bound where a narrowed type holds. + # # @param type [::Symbol] :class or :module # @param visibility [::Symbol] :public or :private # @param gates [::Array] @@ -39,7 +43,7 @@ def initialize type: :class, visibility: :public, gates: [''], name: '', **splat closure_name = if [Solargraph::Pin::ROOT_PIN, nil].include?(closure) '' else - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 "#{closure.full_context.namespace}::" end closure_name += parts.join('::') @@ -56,6 +60,7 @@ def reset_generated! end def to_rbs + # @sg-ignore Unresolved call to to_rbs on ComplexType, nil "#{@type} #{return_type.all_params.first.to_rbs}#{rbs_generics}".strip end @@ -101,11 +106,6 @@ def return_type @return_type ||= ComplexType.try_parse((type == :class ? '::Class' : '::Module') + "<::#{path}>") end - # @return [Array] - def domains - @domains ||= [] - end - def typify api_map return_type end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index ba20976ec6..ccb9464e50 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -6,7 +6,7 @@ class Parameter < LocalVariable # @return [::Symbol] attr_reader :decl - # @return [String] + # @return [String, nil] attr_reader :asgn_code # allow this to be set to the method after the method itself has @@ -63,7 +63,7 @@ def keyword? end def kwrestarg? - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 decl == :kwrestarg || (assignment && %i[HASH hash].include?(assignment.type)) end @@ -174,14 +174,13 @@ 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? || found.types.nil? - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 if @return_type.undefined? case decl when :restarg @@ -198,11 +197,11 @@ def return_type # The parameter's zero-based location in the block's signature. # - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 # @return [Integer] def index method_pin = closure - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 method_pin.parameter_names.index(name) end @@ -234,7 +233,7 @@ def compatible_arg? atype, api_map ptype.generic? end - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 def documentation tag = param_tag return '' if tag.nil? || tag.text.nil? @@ -249,13 +248,14 @@ def generate_complex_type # @return [YARD::Tags::Tag, nil] def param_tag - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 params = closure.docstring.tags(:param) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 params.each do |p| return p if p.name == name end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top params[index] if index && params[index] && (params[index].name.nil? || params[index].name.empty?) end @@ -263,14 +263,14 @@ def param_tag # @return [ComplexType] def typify_block_param api_map block_pin = closure - return block_pin.typify_parameters(api_map)[index] if block_pin.is_a?(Pin::Block) && block_pin.receiver && index + return block_pin.typify_parameters(api_map).fetch(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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 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| @@ -281,12 +281,13 @@ def typify_method_param api_map found = p break end + # @sg-ignore missing flow-sensitive typing on array elements 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 *meth.closure.gates) end end @@ -302,7 +303,7 @@ def see_reference heredoc, api_map, skip = [] # This should actually be an intersection type # @param ref [YARD::Tags::Tag, Solargraph::Yard::Tags::RefTag] heredoc.ref_tags.each do |ref| - # @sg-ignore ref should actually be an intersection type + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 next unless ref.tag_name == 'param' && ref.owner # @todo ref should actually be an intersection type result = resolve_reference(ref.owner.to_s, api_map, skip) @@ -318,14 +319,13 @@ def see_reference heredoc, api_map, skip = [] def resolve_reference ref, api_map, skip return nil if skip.include?(ref) skip.push ref - parts = ref.split(/[.#]/) - if parts.first.empty? + prefix, separator, name = ref.rpartition(/[.#]/) + if prefix.empty? path = "#{namespace}#{ref}" else - fqns = api_map.qualify(parts.first, namespace) + fqns = api_map.qualify(prefix, namespace) return nil if fqns.nil? - # @sg-ignore Need to add nil check here - path = fqns + ref[parts.first.length] + parts.last + path = fqns + separator + name end pins = api_map.get_path_pins(path) pins.each do |pin| diff --git a/lib/solargraph/pin/proxy_type.rb b/lib/solargraph/pin/proxy_type.rb index fd37bab858..415adaed79 100644 --- a/lib/solargraph/pin/proxy_type.rb +++ b/lib/solargraph/pin/proxy_type.rb @@ -27,7 +27,7 @@ def context def self.anonymous context, closure: nil, binder: nil, **kwargs unless closure parts = context.namespace.split('::') - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 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 f47be5a74c..7f3642766c 100644 --- a/lib/solargraph/pin/reference.rb +++ b/lib/solargraph/pin/reference.rb @@ -48,10 +48,10 @@ def type ) end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 # @return [Array] def reference_gates - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 closure.gates end end diff --git a/lib/solargraph/pin/reference/override.rb b/lib/solargraph/pin/reference/override.rb index 76711f5dd1..165b17cd0b 100644 --- a/lib/solargraph/pin/reference/override.rb +++ b/lib/solargraph/pin/reference/override.rb @@ -17,7 +17,7 @@ def closure # @param location [Location, nil] # @param name [String] # @param tags [::Array] - # @param delete [::Array] + # @param delete [::Array<::Symbol>] # @param splat [Hash] def initialize location, name, tags, delete = [], **splat super(location: location, name: name, **splat) @@ -27,10 +27,11 @@ def initialize location, name, tags, delete = [], **splat # @param name [String] # @param tags [::Array] - # @param delete [::Array] + # @param delete [::Array<::Symbol>] # @param splat [Hash] # @return [Solargraph::Pin::Reference::Override] def self.method_return name, *tags, delete: [], **splat + # @sg-ignore https://github.com/apiology/solargraph/pull/225 new(nil, name, [YARD::Tags::Tag.new('return', '', tags)], delete, **splat) end @@ -39,6 +40,7 @@ def self.method_return name, *tags, delete: [], **splat # @param splat [Hash] # @return [Solargraph::Pin::Reference::Override] def self.from_comment name, comment, **splat + # @sg-ignore https://github.com/apiology/solargraph/pull/225 new(nil, name, Solargraph::Source.parse_docstring(comment).to_docstring.tags, **splat) end end diff --git a/lib/solargraph/pin/reference/superclass.rb b/lib/solargraph/pin/reference/superclass.rb index c135226486..ae58cf5fbd 100644 --- a/lib/solargraph/pin/reference/superclass.rb +++ b/lib/solargraph/pin/reference/superclass.rb @@ -6,9 +6,9 @@ class Reference # A Superclass reference pin. # class Superclass < Reference - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 def reference_gates - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 @reference_gates ||= closure.gates - [closure.path] end end diff --git a/lib/solargraph/pin/search.rb b/lib/solargraph/pin/search.rb index d797d949fd..efe51274ba 100644 --- a/lib/solargraph/pin/search.rb +++ b/lib/solargraph/pin/search.rb @@ -37,14 +37,13 @@ def results # @return [::Array] def do_query return @pins if @query.nil? || @query.empty? - @pins.map do |pin| + @pins.filter_map do |pin| match = [fuzzy_string_match(pin.path, @query), fuzzy_string_match(pin.name, @query)].max Result.new(match, pin) if match > 0.7 end - .compact # @param a [self] # @param b [self] - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + # @sg-ignore https://github.com/castwide/solargraph/pull/1299 .sort { |a, b| b.match <=> a.match } .map(&:pin) end diff --git a/lib/solargraph/pin/signature.rb b/lib/solargraph/pin/signature.rb index 221682ccde..8aa5f5ce6a 100644 --- a/lib/solargraph/pin/signature.rb +++ b/lib/solargraph/pin/signature.rb @@ -16,7 +16,7 @@ def identity @identity ||= "signature#{object_id}" end - # @ sg-ignore need boolish support for ? methods + # @return [Boolean, nil] def dodgy_return_type_source? super || closure&.dodgy_return_type_source? end @@ -30,28 +30,25 @@ 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 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 return ComplexType::UNDEFINED if closure.nil? return ComplexType::UNDEFINED unless closure.is_a?(Pin::Method) - # @sg-ignore need is_a? support + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top # @type [Array] 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 == arity } next unless sig - # @sg-ignore Need to add nil check here next if sig.return_type.undefined? - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 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 diff --git a/lib/solargraph/pin/symbol.rb b/lib/solargraph/pin/symbol.rb index f28fb2a712..0e5ac05399 100644 --- a/lib/solargraph/pin/symbol.rb +++ b/lib/solargraph/pin/symbol.rb @@ -7,7 +7,8 @@ class Symbol < Base # @param name [String] # @param [Hash{Symbol => Object}] kwargs def initialize(location, name, **kwargs) - # @sg-ignore "Unrecognized keyword argument kwargs to Solargraph::Pin::Base#initialize" + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 + # double-splat checks need https://github.com/apiology/solargraph/tree/kwsplat-record-types on top super(location: location, name: name, **kwargs) # @name = name # @location = location diff --git a/lib/solargraph/plugin.rb b/lib/solargraph/plugin.rb new file mode 100644 index 0000000000..ceda61e131 --- /dev/null +++ b/lib/solargraph/plugin.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Solargraph + module Plugin + autoload :Addon, 'solargraph/plugin/addon' + autoload :Base, 'solargraph/plugin/base' + autoload :Panel, 'solargraph/plugin/panel' + autoload :ActiveSupport, 'solargraph/plugin/active_support' + + EMPTY_ADDON = Addon.new.freeze + + # @return [Set>] + def self.registry + @registry ||= Set.new + end + + # @param klass [Class] + # @return [void] + def self.register klass + registry.add klass + end + + # @param klass [Class] + # @return [void] + def self.unregister klass + registry.delete klass + end + + Plugin.register ActiveSupport + end +end diff --git a/lib/solargraph/plugin/active_support.rb b/lib/solargraph/plugin/active_support.rb new file mode 100644 index 0000000000..8de31b23e4 --- /dev/null +++ b/lib/solargraph/plugin/active_support.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +module Solargraph + module Plugin + class ActiveSupport < Base + ATTRIBUTE_NAME = /\A[_A-Za-z]\w*\z/ + + def addon + return EMPTY_ADDON unless gem_names.include?('activesupport') + + Addon.new(pins: classmethod_pins + attribute_pins) + end + + private + + # @return [Array] + def classmethod_pins + panel.pins_by_class(Pin::Reference::Include).map do |inc| + Pin::Reference::Extend.new(name: "#{inc.name}::ClassMethods", closure: inc.closure) + end + end + + # @return [Array] + def attribute_pins + panel.source_maps.flat_map { |map| map.calls(:class_attribute, :mattr_accessor, :cattr_accessor, :mattr_reader, :cattr_reader, :mattr_writer, :cattr_writer, :config_accessor) } + .select { |call| call.closure.is_a?(Pin::Namespace) } + .flat_map { |call| generate_call_pins(call) } + .compact + end + + # @param call [SourceMap::Call] + # @return [Array, nil] + def generate_call_pins call + link = call.chain.links.last + return [] unless link.is_a?(Source::Chain::Call) + + options = option_hash(link.arguments.last) + method_names = link.arguments.map { |arg| arg.links.last }.grep(Source::Chain::Literal).map(&:value).compact + .grep(ATTRIBUTE_NAME) + case link.word + when 'class_attribute' + class_suffixes = ['', '='] + class_suffixes.push '?' if options[:instance_predicate] + instance_suffixes = [] + instance_suffixes.push '' if options[:instance_accessor] && options[:instance_reader] + instance_suffixes.push '=' if options[:instance_accessor] && options[:instance_writer] + instance_suffixes.push '?' if options[:instance_accessor] && options[:instance_predicate] + method_names.flat_map { |name| method_pins(name, class_suffixes, instance_suffixes, call) } + when 'mattr_accessor', 'cattr_accessor', 'config_accessor' + class_suffixes = ['', '='] + instance_suffixes = [] + instance_suffixes.push '' if options[:instance_accessor] && options[:instance_reader] + instance_suffixes.push '=' if options[:instance_accessor] && options[:instance_writer] + method_names.flat_map { |name| method_pins(name, class_suffixes, instance_suffixes, call) } + when 'mattr_reader', 'cattr_reader' + class_suffixes = [''] + instance_suffixes = [] + instance_suffixes.push '' if options[:instance_accessor] && options[:instance_reader] + method_names.flat_map { |name| method_pins(name, class_suffixes, instance_suffixes, call) } + when 'mattr_writer', 'cattr_writer' + class_suffixes = ['='] + instance_suffixes = [] + instance_suffixes.push '=' if options[:instance_accessor] && options[:instance_writer] + method_names.flat_map { |name| method_pins(name, class_suffixes, instance_suffixes, call) } + end + end + + # @param args [Source::Chain, nil] + # @return [Hash{Symbol => Object}] + def option_hash args + defaults = { + instance_reader: true, + instance_writer: true, + instance_accessor: true, + instance_predicate: true + } + return defaults unless args + + kwargs = Parser::ParserGem::NodeMethods.convert_hash(args.node) + .select { |_, val| val.literal? } + # @sg-ignore Chain#literal? does not narrow links.last to Chain::Literal + .transform_values { |val| val.links.last.value } + return defaults unless kwargs.is_a?(Hash) + defaults.merge(kwargs) + end + + # @param call [SourceMap::Call] + # @param name [String] + # @param class_suffixes [Array] + # @param instance_suffixes [Array] + # @return [Array] + def method_pins name, class_suffixes, instance_suffixes, call + closure = call.closure + node = call.chain.node + range = node && Solargraph::Range.from_node(node) + return [] unless closure && node && range + + comments = call.source_map.source.comments_for(node) + location = Solargraph::Location.new(closure.filename, range) + class_suffixes.map do |sfx| + Pin::Method.new(name: "#{name}#{sfx}", closure: closure, scope: :class, attribute: true, location: location, comments: comments).tap do |pin| + next unless sfx == '=' + pin.parameters.push Pin::Parameter.new(name: 'value', closure: pin) + end + end + + instance_suffixes.map do |sfx| + Pin::Method.new(name: "#{name}#{sfx}", closure: closure, scope: :instance, attribute: true, location: location, comments: comments).tap do |pin| + next unless sfx == '=' + pin.parameters.push Pin::Parameter.new(name: 'value', closure: pin) + end + end + end + end + end +end diff --git a/lib/solargraph/plugin/addon.rb b/lib/solargraph/plugin/addon.rb new file mode 100644 index 0000000000..c54efebdc7 --- /dev/null +++ b/lib/solargraph/plugin/addon.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Solargraph + module Plugin + class Addon + # @return [Array] + attr_reader :gem_names + + # @return [Array] + attr_reader :pins + + # @param gem_names [Array] + # @param pins [Array] + def initialize gem_names: [], pins: [] + @gem_names = gem_names + @pins = pins + end + end + end +end diff --git a/lib/solargraph/plugin/base.rb b/lib/solargraph/plugin/base.rb new file mode 100644 index 0000000000..2c65c038aa --- /dev/null +++ b/lib/solargraph/plugin/base.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'forwardable' + +module Solargraph + module Plugin + class Base + extend Forwardable + + # @return [Panel] + attr_reader :panel + + def_delegators :@panel, :pins, :pins_by_class, :pins_by_path, :gem_names, :source_maps + + # @param panel [Panel] + def initialize panel + @panel = panel + end + + # @return [Addon] + def addon + EMPTY_ADDON + end + end + end +end diff --git a/lib/solargraph/plugin/panel.rb b/lib/solargraph/plugin/panel.rb new file mode 100644 index 0000000000..1ffbebf20a --- /dev/null +++ b/lib/solargraph/plugin/panel.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Solargraph + module Plugin + class Panel + # @return [Enumerable] + attr_reader :source_maps + + # @return [SourceMap, nil] + attr_reader :live_map + + # @return [Array] + attr_reader :gem_names + + # @param source_maps [Enumerable] + # @param live_map [SourceMap, nil] + # @param gem_names [Array] + def initialize source_maps: [], live_map: nil, gem_names: [] + @source_maps = source_maps + @live_map = live_map + @gem_names = gem_names + end + + # @return [Array] + def pins + @pins ||= source_maps.flat_map(&:pins) + end + + # @generic T + # @param klass [Class>] + # @return [Enumerable>] + def pins_by_class klass + pin_class_hash[klass] || [] + end + + # @param path [String] + # @return [Enumerable] + def pins_by_path path + pin_path_hash[path] || [] + end + + private + + # @return [Hash{Class => Set}] + def pin_class_hash + @pin_class_hash ||= pins.to_set.classify(&:class) + end + + # @return [Hash{String => Set}] + def pin_path_hash + @pin_path_hash ||= pins.to_set.classify(&:path) + end + end + end +end diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index 11d8eb8d5c..877eda964a 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -58,8 +58,7 @@ def self.to_offset text, position line = -1 last_line_index = 0 - # @sg-ignore Typechecker thinks `newline_index` inside of the assignment - # can be nil + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 while (newline_index = text.index("\n", newline_index + 1)) && line <= position.line line += 1 break if line == position.line @@ -68,8 +67,7 @@ def self.to_offset text, position end last_line_index += 1 if position.line.positive? - # @sg-ignore `last_line_index` is always an Integer because `newline_index` - # is never nil inside the while block + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 last_line_index + position.character end @@ -99,16 +97,15 @@ def self.from_offset text, offset character = offset newline_index = -1 - # @sg-ignore Typechecker thinks `newline_index` inside of the assignment - # can be nil + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 while (newline_index = text.index("\n", newline_index + 1)) && newline_index < offset line += 1 - # @sg-ignore `newline_index` is always an Integer inside the while block + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 character = offset - newline_index - 1 end 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' + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 Position.new(line, character) end @@ -121,6 +118,7 @@ def self.from_offset text, offset # @return [Position] def self.normalize object return object if object.is_a?(Position) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 return Position.new(object[0], object[1]) if object.is_a?(Array) raise ArgumentError, "Unable to convert #{object.class} to Position" end diff --git a/lib/solargraph/range.rb b/lib/solargraph/range.rb index e1ed895921..9615cad60b 100644 --- a/lib/solargraph/range.rb +++ b/lib/solargraph/range.rb @@ -32,7 +32,7 @@ def <=> other # Get a hash of the range. This representation is suitable for use in # the language server protocol. # - # @return [Hash{Symbol => Position}] + # @return [Hash{Symbol => Hash{Symbol => Integer}}] def to_hash { start: start.to_hash, @@ -46,11 +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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 return false if position.line < start.line || position.line > ending.line - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 return false if position.line == start.line && position.character < start.character - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 return false if position.line == ending.line && position.character > ending.character true end @@ -58,11 +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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 # @return [Boolean] def include? position position = Position.normalize(position) - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 contain?(position) && !(position.line == start.line && position.character == start.character) end @@ -79,10 +79,12 @@ def self.from_to l1, c1, l2, c2 # Get a range from a node. # - # @param node [::Parser::AST::Node] + # @param node [::Parser::AST::Node, nil] # @return [Range, nil] def self.from_node node return unless node&.loc&.expression + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top from_expr(node.loc.expression) end diff --git a/lib/solargraph/rbs_collection.rb b/lib/solargraph/rbs_collection.rb index 2168817424..338c47ed9a 100644 --- a/lib/solargraph/rbs_collection.rb +++ b/lib/solargraph/rbs_collection.rb @@ -6,6 +6,7 @@ module Solargraph class RbsCollection attr_reader :lockfile + # @param lockfile [String, nil] def initialize lockfile @lockfile = File.expand_path(lockfile) if lockfile end @@ -15,9 +16,12 @@ def initialize lockfile def load metagem key = best_key(metagem) return [] unless key - mem_cache[key] ||= RbsMap::Path.pins(gem_path_map[key]) + path = gem_path_map[key] + return [] unless path + mem_cache[key] ||= RbsMap::Path.pins(path) end + # @return [Array] def gem_keys gem_path_map.keys end @@ -29,25 +33,38 @@ def gem_keys # so they can never be located by a gem name/version lookup. # # @return [Array] + # @sg-ignore Library method has no usable type def stdlib_names @stdlib_names ||= raw_data[:gems].select { |gem| gem.dig(:source, :type).to_s == 'stdlib' } + # @sg-ignore Library method has no usable type .map { |gem| gem[:name].to_s }.uniq end private + # Gems whose lockfile entry resolves to no path are left out. + # + # @return [Hash{String => String}] def gem_path_map - @gem_path_map ||= raw_data[:gems].to_h { |gem| ["#{gem[:name]}-#{gem[:version]}", gem_source_path(gem)] } + @gem_path_map ||= begin + # @type [Array undefined}>] + gems = raw_data[:gems] + gems.to_h { |gem| ["#{gem[:name]}-#{gem[:version]}", gem_source_path(gem)] }.compact + end end # The versions available in the collection for each gem name. # # @return [Hash{String => Array}] + # @sg-ignore Library method has no usable type def versions_by_name @versions_by_name ||= raw_data[:gems].group_by { |gem| gem[:name].to_s } + # @sg-ignore Library method has no usable type .transform_values { |gems| gems.map { |gem| gem[:version].to_s } } end + # @param metagem [Metagem] + # @return [String, nil] def best_key metagem key = "#{metagem.name}-#{metagem.version}" return key if gem_path_map.key?(key) @@ -55,6 +72,7 @@ def best_key metagem full_key = "#{metagem.name}-#{metagem.version}." zero_key = "#{metagem.name}-0" found = gem_path_map.find { |key, _| full_key.start_with?("#{key}.") } || + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 gem_path_map.find { |key, _| key == zero_key } found&.first || closest_key(metagem) end @@ -92,6 +110,7 @@ def parse_version value end # @param gem_hash [Hash] + # @return [String, nil] def gem_source_path gem_hash root_path = gem_hash.dig(:source, :path) || raw_data[:path] return unless root_path @@ -99,11 +118,13 @@ def gem_source_path gem_hash File.expand_path(gem_path, File.dirname(lockfile)) end + # @return [String, nil] def gem_rbs_collection_path @gem_rbs_collection_path ||= File.expand_path(raw_data[:path]) if raw_data[:path] end # @return [Hash] + # @sg-ignore Library method has no usable type def raw_data @raw_data ||= if lockfile && File.file?(lockfile) YAML.load_file(lockfile, symbolize_names: true) @@ -112,6 +133,7 @@ def raw_data end end + # @return [Hash{String => Array}] def mem_cache @mem_cache ||= {} end diff --git a/lib/solargraph/rbs_map/base.rb b/lib/solargraph/rbs_map/base.rb index 9af36f7a41..b552f70822 100644 --- a/lib/solargraph/rbs_map/base.rb +++ b/lib/solargraph/rbs_map/base.rb @@ -3,6 +3,7 @@ module Solargraph module RbsMap class Base + # @return [Array] def pins @pins ||= RbsMap::Conversions.new(loader: loader).pins end @@ -14,6 +15,7 @@ def repository # @return [RBS::EnvironmentLoader] def loader + # @sg-ignore https://github.com/apiology/solargraph/pull/225 @loader ||= RBS::EnvironmentLoader.new(core_root: nil, repository: repository) end @@ -23,7 +25,10 @@ def path_pins path pins.select { |p| p.path == path } end + # @return [Array] + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 def self.pins(...) + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 new(...).pins end end diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 1417ffa669..df042dbfd6 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -59,47 +59,37 @@ 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 interface_decl_to_pin decl when RBS::AST::Declarations::TypeAlias - # @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 pins.push( - # @sg-ignore Wrong argument type for Solargraph::Pin::Reference::TypeAlias.new: return_type expected Solargraph::ComplexType, received Solargraph::ComplexType::UniqueType, Solargraph::ComplexType Solargraph::Pin::Reference::TypeAlias.new( - # @sg-ignore Unresolved calls to name, type, type_location; return_type type mismatch name: ComplexType.try_parse(decl.name.to_s).to_s, return_type: RbsTranslator.to_complex_type(decl.type).force_rooted, closure: closure, source: :rbs, type_location: location_decl_to_pin_location(decl.location) ) ) 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 @@ -142,6 +132,7 @@ def convert_self_types_to_pins decl, module_pin # @param type_name [RBS::TypeName] # # @return [String] + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 def rooted_name type_name name = type_name.to_s RBS_TO_CLASS.fetch(name, name) @@ -153,6 +144,7 @@ def rooted_name type_name # @param type_name [RBS::TypeName] # # @return [String] + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 def fqns type_name unless type_name.absolute? Solargraph.assert_or_log(:rbs_fqns, "Received unexpected unqualified type name: #{type_name}") @@ -192,44 +184,32 @@ 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}" @@ -256,6 +236,7 @@ def class_decl_to_pin decl generic_defaults = {} decl.type_params.each do |param| if param.default_type + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 complex_type = RbsTranslator.to_complex_type(param.default_type).force_rooted generic_defaults[param.name.to_s] = complex_type end @@ -350,7 +331,7 @@ def create_constant fqns, type, comments, decl, base = nil parts = fqns.split('::') if parts.length > 1 fqns = parts.last - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 closure = pins.select { |pin| pin && pin.path == parts[0..-2].join('::') }.first else fqns = parts.first @@ -461,8 +442,6 @@ def global_decl_to_pin decl # @param scope [Symbol] :instance or :class # @param name [String] The name of the method # @return [Symbol] - # @sg-ignore Declared return type ::Symbol does not match inferred type - # ::Symbol, :public, :private, nil for Solargraph::RbsMap::Conversions#calculate_method_visibility def calculate_method_visibility decl, context, closure, scope, name override_key = [closure.path, scope, name] visibility = VISIBILITY_OVERRIDE[override_key] @@ -544,15 +523,19 @@ def method_def_to_sigs decl, pin decl.overloads.map do |overload| # RBS attaches this annotation to a single overload, not to the method as a whole implicit_nil = overload.annotations.map(&:string).include?('implicitly-returns-nil') + # @sg-ignore https://github.com/castwide/solargraph/pull/1281 type_location = location_decl_to_pin_location(overload.method_type.location) generics = overload.method_type.type_params.map(&:name).map(&:to_s) signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin, implicit_nil) block = if overload.method_type.block # the annotation describes the method's return value, not the block's + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 block_parameters, block_return_type = parts_of_function(overload.method_type.block, pin, false) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, source: :rbs, type_location: type_location, closure: pin) end + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 Pin::Signature.new(generics: generics, parameters: signature_parameters, return_type: signature_return_type, block: block, source: :rbs, type_location: type_location, closure: pin) end @@ -560,8 +543,8 @@ def method_def_to_sigs decl, pin # @param location [RBS::Location, nil] # @return [Solargraph::Location, nil] - def location_decl_to_pin_location(location) - return nil if location&.name.nil? + def location_decl_to_pin_location location + return nil if location.nil? || location.name.nil? start_pos = Position.new(location.start_line - 1, location.start_column) end_pos = Position.new(location.end_line - 1, location.end_column) @@ -769,7 +752,8 @@ def alias_to_pin decl, closure # # This method will convert type aliases to concrete types. # - # @param type [RBS::MethodType] + # @param type [RBS::MethodType, RBS::Types::Block] + # @param implicit_nil [Boolean] # @return [ComplexType] def extract_method_type_return_type type, implicit_nil tag = RbsTranslator.to_complex_type(type.type.return_type) @@ -780,7 +764,7 @@ def extract_method_type_return_type type, implicit_nil # @param type_name [RBS::TypeName] # @param type_args [Enumerable] # @return [ComplexType::UniqueType] - def build_type(type_name, type_args = []) + 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 { |arg| RbsTranslator.to_complex_type(arg).force_rooted } if base == 'Hash' && params.length == 2 diff --git a/lib/solargraph/rbs_map/core.rb b/lib/solargraph/rbs_map/core.rb index 94a8b51c27..5070e363c9 100644 --- a/lib/solargraph/rbs_map/core.rb +++ b/lib/solargraph/rbs_map/core.rb @@ -5,6 +5,7 @@ module RbsMap class Core < Base FILLS_DIRECTORY = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'rbs', 'fills')) + # @return [Array] def pins @pins ||= generate_pins end @@ -25,6 +26,7 @@ def generate_pins new_pins = RbsMap::Conversions.new(loader: loader).pins # Avoid RBS::DuplicatedDeclarationError by loading in a different EnvironmentLoader + # @sg-ignore https://github.com/apiology/solargraph/pull/225 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) diff --git a/lib/solargraph/rbs_map/helpers.rb b/lib/solargraph/rbs_map/helpers.rb index a9fdb78304..be23f4cb1d 100644 --- a/lib/solargraph/rbs_map/helpers.rb +++ b/lib/solargraph/rbs_map/helpers.rb @@ -18,7 +18,7 @@ def combine orig_pins, rbs_pins next orig_pin unless rbs_pin && orig_pin.instance_of?(Pin::Method) unless rbs_pin - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore https://github.com/castwide/solargraph/pull/1297 Solargraph.logger.debug { "GemPins.combine: No rbs pin for #{orig_pin.path} - using YARD's '#{orig_pin.inspect} (return_type=#{orig_pin.return_type}; signatures=#{orig_pin.signatures})" } next orig_pin end diff --git a/lib/solargraph/rbs_map/stdlib.rb b/lib/solargraph/rbs_map/stdlib.rb index 17fc109d45..b017c1a278 100644 --- a/lib/solargraph/rbs_map/stdlib.rb +++ b/lib/solargraph/rbs_map/stdlib.rb @@ -7,16 +7,20 @@ module RbsMap # Ruby stdlib pins # class Stdlib < Base + # @param library [String] def initialize library super() - loader.add(library: library.gsub('/', '-')) if self.class.has?(library) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 + loader.add(library: library.gsub('/', '-'), version: nil) if self.class.has?(library) end def repository @repository ||= RBS::Repository.new(no_stdlib: false) end + # @param library [String] def self.has? library + # @sg-ignore https://github.com/apiology/solargraph/pull/225 !!RBS::Collection::Sources::Stdlib.instance.has?(library.gsub('/', '-'), nil) end end diff --git a/lib/solargraph/rbs_translator.rb b/lib/solargraph/rbs_translator.rb index a070de1e12..145126f49d 100644 --- a/lib/solargraph/rbs_translator.rb +++ b/lib/solargraph/rbs_translator.rb @@ -12,7 +12,9 @@ module RbsTranslator 'NilClass' => 'nil' } - # @param type [RBS::Types::Bases::Base] + # rubocop:disable Layout/LineLength + # @param type [RBS::Types::Bases::Bool, RBS::Types::Bases::Void, RBS::Types::Bases::Any, RBS::Types::Bases::Nil, RBS::Types::Bases::Top, RBS::Types::Bases::Bottom, RBS::Types::Bases::Self, RBS::Types::Bases::Instance, RBS::Types::Bases::Class, RBS::Types::Variable, RBS::Types::ClassSingleton, RBS::Types::Interface, RBS::Types::ClassInstance, RBS::Types::Alias, RBS::Types::Tuple, RBS::Types::Record, RBS::Types::Optional, RBS::Types::Union, RBS::Types::Intersection, RBS::Types::Proc, RBS::Types::Literal] + # rubocop:enable Layout/LineLength # @return [ComplexType] def self.to_complex_type(type) tag = type_to_tag(type) @@ -35,7 +37,7 @@ def self.to_parameter_pin(param_type, name, decl, closure) Solargraph::Pin::Parameter.new(decl: decl, name: name, closure: closure, return_type: return_type, source: :rbs, type_location: to_sg_location(param_type.location) || closure.type_location) end - # @param method_type [RBS::MethodType] + # @param method_type [RBS::MethodType, RBS::Types::Block] # @param closure [Pin::Closure] # @param parameter_names [Array] # @return [Array] @@ -49,10 +51,12 @@ def self.to_parameter_pins method_type, closure, parameter_names = [] arg_num = 0 params = [] method_type.type.required_positionals.each do |param| + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 params.push RbsTranslator.to_parameter_pin(param, param.name&.to_s || parameter_names[arg_num] || "arg_#{arg_num}", :arg, closure) arg_num += 1 end method_type.type.optional_positionals.each do |param| + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 params.push RbsTranslator.to_parameter_pin(param, param.name&.to_s || parameter_names[arg_num] || "arg_#{arg_num}", :optarg, closure) arg_num += 1 end @@ -61,10 +65,12 @@ def self.to_parameter_pins method_type, closure, parameter_names = [] arg_num += 1 end method_type.type.required_keywords.each do |param| + # @sg-ignore Need better generic inference here params.push RbsTranslator.to_parameter_pin(param.last, param.first.to_s, :kwarg, closure) arg_num += 1 end method_type.type.optional_keywords.each do |param| + # @sg-ignore Need better generic inference here params.push RbsTranslator.to_parameter_pin(param.last, param.first.to_s, :kwoptarg, closure) arg_num += 1 end @@ -87,6 +93,7 @@ def self.to_signature method_type, closure, parameter_names = [] parameters = to_parameter_pins(method_type, closure, parameter_names) return_type = to_complex_type(method_type.type.return_type) block = if method_type.block + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 block_parameters = to_parameter_pins(method_type.block, closure) block_return_type = to_complex_type(method_type.block.type.return_type) Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, source: :rbs, type_location: closure.location, closure: closure) @@ -112,7 +119,7 @@ def self.build_unique_type(type_name, type_args = []) # @param location [RBS::Location, nil] # @return [Solargraph::Location, nil] def self.to_sg_location(location) - return nil if location&.name.nil? + return nil if location.nil? || location.name.nil? start_pos = Position.new(location.start_line - 1, location.start_column) end_pos = Position.new(location.end_line - 1, location.end_column) @@ -123,7 +130,9 @@ def self.to_sg_location(location) class << self private - # @param type [RBS::Types::Bases::Base] + # rubocop:disable Layout/LineLength + # @param type [RBS::Types::Bases::Bool, RBS::Types::Bases::Void, RBS::Types::Bases::Any, RBS::Types::Bases::Nil, RBS::Types::Bases::Top, RBS::Types::Bases::Bottom, RBS::Types::Bases::Self, RBS::Types::Bases::Instance, RBS::Types::Bases::Class, RBS::Types::Variable, RBS::Types::ClassSingleton, RBS::Types::Interface, RBS::Types::ClassInstance, RBS::Types::Alias, RBS::Types::Tuple, RBS::Types::Record, RBS::Types::Optional, RBS::Types::Union, RBS::Types::Intersection, RBS::Types::Proc, RBS::Types::Literal] + # rubocop:enable Layout/LineLength # @return [String] def type_to_tag type case type diff --git a/lib/solargraph/repo.rb b/lib/solargraph/repo.rb index 179bc92636..c43e7b8b12 100644 --- a/lib/solargraph/repo.rb +++ b/lib/solargraph/repo.rb @@ -23,6 +23,7 @@ def initialize directory # # @param path [String] # @return [Metagem, nil] + # @sg-ignore https://github.com/apiology/solargraph/pull/225 def find_by_path path return system_find_by_path(path) unless bundled? @@ -90,16 +91,20 @@ def bundled_directory? # This method performs gem collection in a separate process to suppress # output from the `Bundler.definition.specs` call. # - # @return [Array, nil] + # @return [void] def build_from_directory return unless bundled_directory? && ENV['BUNDLE_GEMFILE'] != gemfile Solargraph.with_clean_env do cmd = ['ruby', '-e', bundle_script] o, e, s = Open3.capture3(*cmd, chdir: directory) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 if s.success? - json = o && !o.empty? ? JSON.parse(o.strip.split("\n").last, symbolize_names: true) : [] - @metagems = json[:metagems].map { |data| Metagem.new(**data) } + last_line = o.strip.split("\n").last + json = last_line ? JSON.parse(last_line, symbolize_names: true) : { metagems: [], groups: {} } + # @sg-ignore double-splat argument is checked as a keyword named after the variable + @metagems = bundle_array(json[:metagems]).map { |data| Metagem.new(**data) } + # @sg-ignore https://github.com/castwide/solargraph/pull/1245 @bundled_group_map = json[:groups].transform_values { |names| names.map { |name| bundled_metagem_name_map[name] } } else Solargraph.logger.warn "Failed to load gems from bundle at #{directory}: #{e}" @@ -108,6 +113,15 @@ def build_from_directory end end + # @param value [Object] + # @return [Array] + def bundle_array value + raise TypeError, "Unexpected bundle data from #{directory}" unless value.is_a?(Array) + + value + end + + # @return [String] def bundle_script " require 'bundler/setup' @@ -158,18 +172,21 @@ def system_find_by_group group .map { |spec| Metagem.from_specification(spec) } end + # @return [Hash{String => Metagem}] def bundled_metagem_name_map @bundled_metagem_name_map ||= bundled.to_set .classify(&:name) .transform_values(&:first) end + # @return [Hash{String => Metagem, nil}] def bundled_metagem_path_map @bundled_metagem_path_map ||= Hash.new do |hash, path| hash[path] = bundled.find { |mg| mg.require?(path) } end end + # @return [Hash{Symbol => Array}] def bundled_group_map @bundled_group_map ||= {} end diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index add79905c0..0cacecf5a4 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -41,8 +41,9 @@ def socket Signal.trap('TERM') do Backport.stop end - # @sg-ignore Wrong argument type for Backport.prepare_tcp_server: adapter expected Backport::Adapter, received Module + # @sg-ignore https://github.com/castwide/backport/pull/6 Backport.prepare_tcp_server host: options[:host], port: port, adapter: Solargraph::LanguageServer::Transport::Adapter + # @sg-ignore https://github.com/apiology/solargraph/pull/229 $stderr.puts "Solargraph is listening PORT=#{port} PID=#{Process.pid}" end end @@ -58,8 +59,9 @@ def stdio Signal.trap('TERM') do Backport.stop end - # @sg-ignore Wrong argument type for Backport.prepare_stdio_server: adapter expected Backport::Adapter, received Module + # @sg-ignore https://github.com/castwide/backport/pull/6 Backport.prepare_stdio_server adapter: Solargraph::LanguageServer::Transport::Adapter + # @sg-ignore https://github.com/apiology/solargraph/pull/229 $stderr.puts "Solargraph is listening on stdio PID=#{Process.pid}" end end @@ -88,6 +90,7 @@ def config directory = '.' File.open(File.join(directory, '.solargraph.yml'), 'w') do |file| file.puts conf.to_yaml end + # @sg-ignore https://github.com/apiology/solargraph/pull/229 $stdout.puts 'Configuration file initialized.' end @@ -110,19 +113,24 @@ def clear Documentation will be regenerated as needed. ) option :directory, type: :string, desc: 'Workspace directory', default: Dir.pwd + option :plugins, type: :string, + desc: "Plugins to require instead of the workspace's, separated by #{File::PATH_SEPARATOR}" # @param gems [Array] # @return [void] def uncache *gems raise ArgumentError, 'No gems specified.' if gems.empty? + require_plugins repo = Solargraph::Repo.new(options[:directory]) gems.each do |gem| if gem == 'core' Solargraph::Collection::Core.uncache elsif gem == 'stdlib' + # @sg-ignore https://github.com/castwide/solargraph/pull/1281 FileUtils.rm_rf CacheDir.stdlib_dir else metagem = repo.find_by_name(gem) + # @sg-ignore https://github.com/castwide/solargraph/pull/1400 Solargraph::Collection::Gem.uncache(metagem) if metagem end end @@ -153,11 +161,13 @@ def uncache *gems can be stored between CI runs. ) option :directory, type: :string, desc: 'Workspace directory', default: Dir.pwd + option :plugins, type: :string, + desc: "Plugins to require instead of the workspace's, separated by #{File::PATH_SEPARATOR}" option :rebuild, type: :boolean, desc: 'Rebuild existing documentation', default: false - # @param names [Array] + # @param gem_names [Array] # @return [void] - # @param [Array] gem_names def cache *gem_names + require_plugins repo = Solargraph::Repo.new(options[:directory]) metagems = if gem_names.empty? if repo.bundled? @@ -169,6 +179,7 @@ def cache *gem_names end else gem_names.each_with_object([]) do |name, result| + # @sg-ignore https://github.com/castwide/solargraph/pull/1326 if name == 'core' Collection::Core.uncache if options[:rebuild] puts 'Caching core' @@ -179,6 +190,7 @@ def cache *gem_names # TL;DR: `repo.find_by_path` should not be necessary found = repo.find_by_name(name) || repo.find_by_path(name) if found + # @sg-ignore https://github.com/castwide/solargraph/pull/1326 result.push found else warn "Gem #{name} not found" @@ -186,11 +198,8 @@ def cache *gem_names end end end - metagems.each do |metagem| - Collection::Gem.uncache(metagem) if options[:rebuild] - puts "Caching #{metagem.name} #{metagem.version} (#{metagem.cache_name})" - Collection::Gem.load(metagem) - end + metagems.each { |metagem| puts "Caching #{metagem.name} #{metagem.version} (#{metagem.cache_name})" } + Collection::Gem.load_all metagems, out: $stdout, rebuild: options[:rebuild] puts "Documentation cached for #{metagems.count} gems." end map 'gems' => :cache @@ -267,7 +276,7 @@ def scan api_map = nil time = Benchmark.measure do api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout) - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 api_map.pins.each do |pin| puts pin_description(pin) if options[:verbose] pin.typify api_map @@ -285,8 +294,7 @@ def scan exit 1 end end - # @sg-ignore Need to add nil check here - puts "Scanned #{directory} (#{api_map.pins.length} pins) in #{time.real} seconds." + puts "Scanned #{directory} (#{api_map&.pins&.length} pins) in #{time.real} seconds." end desc 'list', 'List the files in the workspace and the total count' @@ -320,9 +328,6 @@ def pin path [:class, *path.split('.', 2)] end - # @sg-ignore Wrong argument type for - # Solargraph::ApiMap#get_method_stack: rooted_tag - # expected String, received Array pins = api_map.get_method_stack(ns, meth, scope: scope) else pins = api_map.get_path_pins path @@ -336,7 +341,6 @@ def pin 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 @@ -355,6 +359,7 @@ def pin path print_pin(pin) end references.each do |key, refpin| + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 puts "\n# #{key.to_s.capitalize}:\n\n" print_pin(refpin) end @@ -372,8 +377,11 @@ def profile file = nil begin require 'vernier' rescue LoadError + # @sg-ignore https://github.com/apiology/solargraph/pull/229 $stderr.puts 'vernier gem not found. Please install this dependency:' + # @sg-ignore https://github.com/apiology/solargraph/pull/229 $stderr.puts + # @sg-ignore https://github.com/apiology/solargraph/pull/229 $stderr.puts " gem 'vernier', '>1.0', '<2'" return @@ -432,20 +440,10 @@ def host.send_notification method, params 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 - puts 'No Ruby files found in workspace' - return - end - end + test_file = profile_test_file(directory, file) + unless test_file + puts 'No Ruby files found in workspace' + return end file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(File.absolute_path(test_file)) @@ -464,9 +462,9 @@ def host.send_notification method, params } ) puts 'Processing go-to-definition request...' - result = message.process + message.process - puts "Result: #{result.inspect}" + puts "Result: #{message.result.inspect}" end definition_time = Time.now - definition_start rescue Interrupt @@ -497,6 +495,7 @@ def host.send_notification method, params desc 'rbs', 'Generate RBS definitions' option :filename, type: :string, alias: :f, desc: 'Generated file name', default: 'sig.rbs' option :inference, type: :boolean, desc: 'Enhance definitions with type inference', default: true + # @return [void] def rbs api_map = Solargraph::ApiMap.load('.') pins = api_map.source_maps.flat_map(&:pins) @@ -507,6 +506,7 @@ def rbs next unless pin.return_type.undefined? type = pin.typify(api_map) type = pin.probe(api_map) if type.undefined? + # @sg-ignore https://github.com/apiology/solargraph/pull/225 pin.docstring.add_tag YARD::Tags::Tag.new('return', nil, type.items.map(&:to_s)) pin.instance_variable_set(:@return_type, type) end @@ -522,6 +522,7 @@ def rbs rel_dir = File.join('sig', options[:filename]) puts "Writing #{rel_dir}..." target = File.join(work_dir, rel_dir) + # @sg-ignore https://github.com/castwide/solargraph/pull/1281 FileUtils.mkdir_p(File.join(work_dir, 'sig')) `sord #{target} --rbs --no-regenerate` end @@ -531,12 +532,45 @@ def rbs private + # The file to profile go-to-definition against: the given file, + # lib/other.rb, or any Ruby file in the workspace. + # + # @param directory [String] + # @param file [String, nil] + # @return [String, nil] + def profile_test_file directory, file + return File.join(directory, file) if file + + test_file = File.join(directory, 'lib', 'other.rb') + return test_file if File.exist?(test_file) + + workspace = Solargraph::Workspace.new(directory) + workspace.filenames.find { |f| f.end_with?('.rb') } + end + + # Plugins change the pins mapped from gem source and the gem cache + # directory, so the language server's cache subprocess needs them too. + # + # @return [void] + def require_plugins + return Workspace::Config.new(options[:directory] || Dir.pwd).require_plugins if options[:plugins].nil? + + options[:plugins].split(File::PATH_SEPARATOR).each do |plugin| + require plugin + CacheDir.add_plugin plugin + rescue LoadError + Solargraph.logger.warn "Failed to load plugin '#{plugin}'" + end + end + # @param pin [Solargraph::Pin::Base] # @return [String] + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 def pin_description pin + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 desc = if pin.path.nil? || pin.path.empty? if pin.closure - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 "#{pin.closure.path} | #{pin.name}" else "#{pin.context.namespace} | #{pin.name}" @@ -544,7 +578,7 @@ def pin_description pin else pin.path end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 desc += " (#{pin.location.filename} #{pin.location.range.start.line})" if pin.location desc end diff --git a/lib/solargraph/source.rb b/lib/solargraph/source.rb index 94bd3569b0..98c0df4b1b 100644 --- a/lib/solargraph/source.rb +++ b/lib/solargraph/source.rb @@ -12,12 +12,20 @@ class Source autoload :Cursor, 'solargraph/source/cursor' autoload :Chain, 'solargraph/source/chain' autoload :SourceChainer, 'solargraph/source/source_chainer' + autoload :NodeMap, 'solargraph/source/node_map' include EncodingFixes # @return [String, nil] attr_reader :filename + # @return [String] + def filename! + name = filename + return name if name + raise ArgumentError, 'Source has no filename' + end + # @return [String] def code finalize @@ -48,10 +56,11 @@ def initialize code, filename = nil, version = 0 @repaired = code @filename = filename @version = version + @finalize_mutex = Mutex.new end # @param range [Solargraph::Range] - # @return [String] + # @return [String, nil] def at range from_to range.start.line, range.start.character, range.ending.line, range.ending.character end @@ -61,8 +70,7 @@ def at range # @param l2 [Integer] # @param c2 [Integer] # - # @sg-ignore Need to add nil check here - # @return [String] + # @return [String, nil] 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) @@ -73,7 +81,7 @@ def from_to l1, c1, l2, c2 # # @param line [Integer] # @param column [Integer] - # @return [AST::Node] + # @return [AST::Node, nil] def node_at line, column tree_at(line, column).first end @@ -133,29 +141,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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 next if range.ending.line < position.line - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 break if range.ending.line > position.line - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 return true if node.type == :str && range.include?(position) && range.start != position - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 next unless range.include?(inner_range.ending) return true if inner.type == :str - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 inner_code = at(Solargraph::Range.new(inner_range.start, position)) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 return true if (inner.type == :dstr && inner_range.ending.character <= position.character && !inner_code.end_with?('}')) || - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 (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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 break if range.ending.line > position.line end false @@ -192,9 +200,9 @@ def error_ranges # @return [String] def code_for node rng = Range.from_node(node) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 b = Position.line_char_to_offset(code, rng.start.line, rng.start.column) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 e = Position.line_char_to_offset(code, rng.ending.line, rng.ending.column) frag = code[b..(e - 1)].to_s frag.strip.gsub(/,$/, '') @@ -205,9 +213,9 @@ def code_for node # @return [String, nil] def comments_for node rng = Range.from_node(node) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 stringified_comments[rng.start.line] ||= begin - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 buff = associated_comments[rng.start.line] stringify_comment_array(buff) end @@ -252,11 +260,13 @@ def synchronized? # @return [Hash{Integer => Array}] def associated_comments @associated_comments ||= begin - # @type [Hash{Integer => String}] + # @type [Hash{Integer => Array}] result = {} buffer = [] # @type [Integer, nil] last = nil + # @param num [Integer] + # @param snip [Solargraph::Parser::Snippet] comments.each_pair do |num, snip| if !last || num == last + 1 buffer.push "#{snip.text}\n" @@ -277,7 +287,7 @@ def associated_comments # @return [Integer] def first_not_empty_from line cursor = line - cursor += 1 while cursor < code_lines.length && code_lines[cursor].strip.empty? + cursor += 1 while cursor < code_lines.length && code_lines.fetch(cursor).strip.empty? cursor = line if cursor > code_lines.length - 1 cursor end @@ -288,16 +298,15 @@ def first_not_empty_from line # @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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 if FOLDING_NODE_TYPES.include?(top.type) - # @sg-ignore Translate to something flow sensitive typing understands range = Range.from_node(top) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 top.children.each do |child| inner_folding_ranges(child, result, top.type) end @@ -311,6 +320,7 @@ def stringify_comment_array comments ctxt = [] started = false skip = nil + # @param l [String] comments&.each do |l| if l =~ /^#-\R/ ctxt.clear @@ -323,8 +333,8 @@ def stringify_comment_array comments ctxt.push p else here = p.index(/[^ \t]/) - # @sg-ignore flow sensitive typing should be able to handle redefinition - skip = here if skip.nil? || here < skip + indent = here.nil? ? raise('Non-blank comment line has no non-blank character') : here + skip = indent if skip.nil? || indent < skip ctxt.push p[skip..] end started = true @@ -374,11 +384,11 @@ def foldable_comment_block_ranges def string_nodes_in n result = [] if Parser.is_ast_node?(n) - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 if %i[str dstr STR DSTR].include?(n.type) result.push n else - # @sg-ignore Translate to something flow sensitive typing understands + # @sg-ignore https://github.com/castwide/solargraph/pull/1396 n.children.each { |c| result.concat string_nodes_in(c) } end end @@ -392,9 +402,10 @@ def string_nodes_in n def inner_tree_at node, position, stack return if node.nil? here = Range.from_node(node) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 return unless here.contain?(position) stack.unshift node + # @param c [Parser::AST::Node] node.children.each do |c| next unless Parser.is_ast_node?(c) next if c.loc.expression.nil? @@ -409,7 +420,7 @@ def changes @changes ||= [] end - # @return [String] + # @return [String, nil] attr_writer :filename # @return [Integer] @@ -417,6 +428,13 @@ def changes # @return [void] def finalize + # @finalized is set before the parse finishes, so a thread arriving + # mid-parse must wait for it instead of reading a nil node. + @finalize_mutex.synchronize { finalize_unsynchronized } + end + + # @return [void] + def finalize_unsynchronized return if @finalized && changes.empty? changes.each do |change| @@ -476,10 +494,11 @@ def repaired @repaired end - # @return [Boolean] + # @return [Boolean, nil] attr_writer :parsed - # @return [Hash{Integer => String} + # @return [Hash{Integer => Solargraph::Parser::Snippet}] + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 attr_writer :comments # @return [Boolean] @@ -512,6 +531,7 @@ def load_string code, filename = nil, version = 0 # @param comments [String] # @return [YARD::DocstringParser] + # @sg-ignore Library method has no usable type def parse_docstring comments # HACK: Pass a dummy code object to the parser for plugins that # expect it not to be nil diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index f448a83885..2107200b2a 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -39,6 +39,7 @@ class Chain @@inference_stack = [] @@inference_depth = 0 @@inference_invalidation_key = nil + # @type [::Hash{::Array => ComplexType}] @@inference_cache = {} UNDEFINED_CALL = Chain::Call.new('', nil) @@ -67,7 +68,7 @@ def initialize links, node = nil, splat = false # @return [Chain] def base - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 @base ||= Chain.new(links[0..-2]) end @@ -110,7 +111,7 @@ 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 links[0..-2].each do |link| pins = link.resolve(api_map, working_pin, locals) type = infer_from_definitions(pins, working_pin, api_map, locals) @@ -130,15 +131,14 @@ def define api_map, name_pin, locals "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) + links.fetch(-1).last_context = working_pin + links.fetch(-1).resolve(api_map, working_pin, locals) end # @param api_map [ApiMap] # @param name_pin [Pin::Base] The pin for the closure in which this code runs # @param locals [::Array] # @return [ComplexType] - # @sg-ignore def infer api_map, name_pin, locals # includes binder as it is mutable in Pin::Block cache_key = [node, node&.location, links, name_pin&.return_type, name_pin&.binder, locals] @@ -147,8 +147,10 @@ def infer api_map, name_pin, locals return cached if cached else @@inference_invalidation_key = api_map.hash + # @type [::Hash{::Array => ComplexType}] @@inference_cache = {} end + # @todo Missed nil violation out = infer_uncached(api_map, name_pin, locals).downcast_to_literal_if_possible 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}" @@ -159,7 +161,7 @@ def infer api_map, name_pin, locals # @param api_map [ApiMap] # @param name_pin [Pin::Base] # @param locals [::Array] - # @return [ComplexType, ComplexType::UniqueType] + # @return [ComplexType] def infer_uncached api_map, name_pin, locals pins = define(api_map, name_pin, locals) if pins.empty? @@ -168,7 +170,7 @@ def infer_uncached api_map, name_pin, locals end return ComplexType::UNDEFINED end - type = infer_from_definitions(pins, links.last.last_context, api_map, locals) + type = infer_from_definitions(pins, links.fetch(-1).last_context, api_map, locals) out = maybe_nil(type, api_map) logger.debug do "Chain#infer_uncached(links=#{links.map(&:desc)}, locals=#{locals.map(&:desc)}, " \ @@ -184,9 +186,11 @@ def literal? end # @return [Boolean] - # @sg-ignore false "return type could not be inferred" + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top def require_parameter? - # @sg-ignore false "Unresolved call to require_parameter? on Solargraph::Source::Chain::Link" + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # pure calls need https://github.com/apiology/solargraph/tree/narrow-pure-rbs-calls on top links.last.is_a?(Chain::Parameter) && links.last.require_parameter? end @@ -235,7 +239,6 @@ def nullable? api_map next unless link.is_a?(Chain::Call) pin = api_map.get_method_stack('NilClass', link.word, scope: :instance).first - # @sg-ignore Need to add nil check here currently_nullable = pin.nil? || pin.return_type.tag == 'self' end currently_nullable @@ -260,7 +263,7 @@ def to_s # @param name_pin [Pin::Base] # @param api_map [ApiMap] # @param locals [::Enumerable] - # @return [ComplexType, ComplexType::UniqueType] + # @return [ComplexType] def infer_from_definitions pins, name_pin, api_map, locals # @type [::Array] types = [] @@ -280,7 +283,7 @@ def infer_from_definitions pins, name_pin, 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 - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 type = type.resolve_generics(pin.closure, name_pin.binder) end types << type @@ -323,9 +326,9 @@ def infer_from_definitions pins, name_pin, api_map, locals type.self_to_type(name_pin.context) end - # @param type [ComplexType, ComplexType::UniqueType] + # @param type [ComplexType] # @param api_map [ApiMap] - # @return [ComplexType, ComplexType::UniqueType] + # @return [ComplexType] def maybe_nil type, api_map return type if type.undefined? || type.void? || type.nullable? return type unless nullable?(api_map) @@ -334,7 +337,6 @@ def maybe_nil type, api_map protected - # @sg-ignore Fix "Not enough arguments to Module#protected" def equality_fields [links, node] end diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index f6bbb6087e..a31cfd109e 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -107,16 +107,15 @@ def match_overload_type overload, pin, api_map, name_pin, locals, type, new_sign atypes = [] arguments.each_with_index do |arg, idx| param = overload.parameters[idx] - if param.nil? - match = overload.parameters.any?(&:restarg?) - break - end + match = overload.parameters.any?(&:restarg?) if param.nil? + break if param.nil? + 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) - # @sg-ignore flow sensitive typing should handle is_a? and next + # @sg-ignore https://github.com/castwide/solargraph/pull/1245 unless param.compatible_arg?(atype, api_map) || param.restarg? match = false break @@ -125,10 +124,10 @@ def match_overload_type overload, pin, api_map, name_pin, locals, type, new_sign return [type, new_signature_pin] unless match if overload.block && with_block? + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 block_atypes = overload.block.parameters.map(&:return_type) # @todo Need to add nil check here - # @sg-ignore Need to add nil check here - blocktype = if block.links.map(&:class) == [BlockSymbol] + 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 @@ -138,12 +137,12 @@ def match_overload_type overload, pin, api_map, name_pin, locals, type, new_sign new_signature_pin = overload.resolve_generics_from_context_until_complete(overload.generics, atypes, nil, nil, blocktype) # @todo It shouldn't be necessary to choose either generics or macros - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 new_return_type = if new_signature_pin.return_type.defined? - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 new_signature_pin.return_type else - # @sg-ignore Need to add nil check here + # @sg-ignore Found after #1418 merged named_types = pin.parameter_names.zip(arguments.map { |arg| ComplexType.try_parse(simple_convert(arg.node).to_s) }).to_h pin.typify(api_map).expand(named_types) end @@ -194,12 +193,10 @@ 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 + # @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| type, new_signature_pin = match_overload_type(ol, p, api_map, name_pin, locals, type, new_signature_pin) @@ -209,11 +206,11 @@ 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 next result unless result.return_type.undefined? end p @@ -226,11 +223,8 @@ def inferred_pins pins, api_map, name_pin, locals 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 @@ -243,11 +237,12 @@ def inferr_from_factory_parameters api_map, method_pin factory_parameter = api_map.factory_parameters_for_method(method_pin).find do |factory_param| method_pin.parameters.each_with_index.find do |param, index| current_argument = arguments[index] - next unless current_argument&.literal? - # @type [Solargraph::Source::Chain::Literal] + next if current_argument.nil? || !current_argument.literal? last_link = current_argument.links.last + next unless last_link.is_a?(Chain::Literal) argument_value = last_link.value + # @sg-ignore https://github.com/castwide/solargraph/pull/1245 param.name == factory_param.param_name && argument_value == factory_param.value end end @@ -264,13 +259,6 @@ def inferr_from_factory_parameters api_map, method_pin # @return [Pin::Base] def process_macro pin, api_map, context, locals pin.macros.each do |macro| - # @todo 'Wrong argument type for - # Solargraph::Source::Chain::Call#inner_process_macro: - # macro expected YARD::Tags::MacroDirective, received - # generic' is because we lose 'rooted' information - # in the 'Chain::Array' class internally, leaving - # ::Array#each shadowed when it shouldn't be. - # @sg-ignore macro is Solargraph::YardMap::Macro, wraps a YARD::Tags::MacroDirective result = inner_process_macro(pin, macro, api_map, context, locals) return result unless result.return_type.undefined? end @@ -286,7 +274,6 @@ def process_directive pin, api_map, context, locals pin.directives.each do |dir| macro = api_map.named_macro(dir.tag.name) next if macro.nil? - # @sg-ignore macro is Solargraph::YardMap::Macro, wraps a YARD::Tags::MacroDirective result = inner_process_macro(pin, macro, api_map, context, locals) return result unless result.return_type.undefined? end @@ -294,26 +281,23 @@ def process_directive pin, api_map, context, locals end # @param pin [Pin::Base] - # @param macro [YARD::Tags::MacroDirective] + # @param macro [YardMap::Macro] # @param api_map [ApiMap] # @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) } - txt = macro.tag.text.clone - # @sg-ignore Need to add nil check here + txt = macro.text.clone if txt.empty? && macro.tag.name named = api_map.named_macro(macro.tag.name) - txt = named.tag.text.clone if named + txt = named.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? @@ -339,7 +323,7 @@ def extra_return_type docstring, context 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 method_pin = method_pin.closure return if method_pin.nil? end @@ -365,15 +349,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 type [ComplexType, ComplexType::UniqueType] # @param context [ComplexType, ComplexType::UniqueType] - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] def with_params type, context return type unless type.to_s.include?('$') ComplexType.try_parse(type.to_s.gsub('$', context.value_types.map(&:rooted_tag).join(', ')).gsub('<>', '')) @@ -393,27 +376,25 @@ def fix_block_pass 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_link = block&.links&.first + block_symbol_name = block_symbol_link&.word block_symbol_call_path = "#{block_parameter_types.first}##{block_symbol_name}" callee = api_map.get_path_pins(block_symbol_call_path).first return_type = callee&.return_type # @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 ||= api_map.get_path_pins("#{context.subtypes.first}##{block_symbol_name}").first&.return_type return_type || ComplexType::UNDEFINED end # @param api_map [ApiMap] # @return [Pin::Block, nil] def find_block_pin api_map - # @sg-ignore Need to add nil check here - node_location = Solargraph::Location.from_node(block.node) + node_location = Solargraph::Location.from_node(block&.node) return if node_location.nil? block_pins = api_map.get_block_pins - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 block_pins.find { |pin| pin.location.contain?(node_location) } end @@ -428,15 +409,12 @@ def block_call_type api_map, name_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) + 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 diff --git a/lib/solargraph/source/chain/constant.rb b/lib/solargraph/source/chain/constant.rb index e558a2e1fb..53f2a578ef 100644 --- a/lib/solargraph/source/chain/constant.rb +++ b/lib/solargraph/source/chain/constant.rb @@ -19,9 +19,8 @@ def resolve api_map, name_pin, locals base = word gates = name_pin.gates end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 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/hash.rb b/lib/solargraph/source/chain/hash.rb index a75963478a..bcdfaddebb 100644 --- a/lib/solargraph/source/chain/hash.rb +++ b/lib/solargraph/source/chain/hash.rb @@ -26,9 +26,7 @@ def splatted? 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 diff --git a/lib/solargraph/source/chain/if.rb b/lib/solargraph/source/chain/if.rb index 186f6e6f04..097a3e372c 100644 --- a/lib/solargraph/source/chain/if.rb +++ b/lib/solargraph/source/chain/if.rb @@ -19,9 +19,7 @@ def resolve api_map, name_pin, locals 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 diff --git a/lib/solargraph/source/chain/link.rb b/lib/solargraph/source/chain/link.rb index 4eb6c7d5c8..11fde6528c 100644 --- a/lib/solargraph/source/chain/link.rb +++ b/lib/solargraph/source/chain/link.rb @@ -74,13 +74,6 @@ def desc 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 diff --git a/lib/solargraph/source/chain/literal.rb b/lib/solargraph/source/chain/literal.rb index 40542cfaf8..143803a33f 100644 --- a/lib/solargraph/source/chain/literal.rb +++ b/lib/solargraph/source/chain/literal.rb @@ -8,7 +8,7 @@ class Chain class Literal < Link attr_reader :word - # @return [::String, ::Symbol] + # @return [::String, ::Symbol, ::Boolean] attr_reader :value # @return [Parser::AST::Node] @@ -25,8 +25,8 @@ def initialize type, node @value = true elsif node.type == :false @value = false - elsif %i[int sym].include?(node.type) - @value = node.children.first + elsif node in [:int | :sym, Integer | Symbol => value, *] + @value = value end end @type = type @@ -35,9 +35,7 @@ def initialize type, node @complex_type = ComplexType.try_parse(type) end - # @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 327d465b71..4db2c8ae2e 100644 --- a/lib/solargraph/source/chain/or.rb +++ b/lib/solargraph/source/chain/or.rb @@ -7,22 +7,41 @@ class Or < Link attr_reader :links # @param links [::Array] - def initialize links + # @param rhs_never_returns [Boolean] true if the right-hand side + # necessarily leaves the enclosing compound statement (e.g., + # `x || raise('missing')`) - it never contributes a value to + # this expression's result, so continuing past this + # expression implies the left-hand side was truthy + def initialize links, rhs_never_returns: false super('') @links = links + @rhs_never_returns = rhs_never_returns end def resolve api_map, name_pin, locals types = @links.map { |link| link.infer(api_map, name_pin, locals) } + + if @rhs_never_returns + lhs_type = types.first + return [Solargraph::Pin::ProxyType.anonymous(Solargraph::ComplexType::UNDEFINED, source: :chain)] if lhs_type.nil? + + return [Solargraph::Pin::ProxyType.anonymous(lhs_type.without_nil, source: :chain)] + end + 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 + + protected + + def equality_fields + super + [@links, @rhs_never_returns] + end end end end diff --git a/lib/solargraph/source/chain/parameter.rb b/lib/solargraph/source/chain/parameter.rb index 3f66b9f981..b7b8f595ed 100644 --- a/lib/solargraph/source/chain/parameter.rb +++ b/lib/solargraph/source/chain/parameter.rb @@ -25,9 +25,12 @@ def word # @param locals [::Array] # @return [::Array] def resolve api_map, name_pin, locals - # @type [Pin::Method] method_pin = method_call_chain.define(api_map, name_pin, locals)&.first - return [] unless method_pin + return [] unless method_pin.is_a?(Pin::Method) + # A literal nested inside an argument, e.g. foo([:bar]), is not + # itself an argument + arg_index = current_index + return [] unless arg_index # Use indexed lookup for better performance api_map.factory_parameters_for_method(method_pin).select do |fp| @@ -36,20 +39,20 @@ def resolve api_map, name_pin, locals next false unless fp.value == literal_value # a splat takes every argument from its position on - method_pin.parameters[param_index].restarg? ? current_index >= param_index : current_index == param_index + method_pin.parameters.fetch(param_index).restarg? ? arg_index >= param_index : arg_index == param_index end end # @return [Boolean] true if this is a parameter of Kernel#require def require_parameter? - method_call_chain.links.last.word == 'require' + method_call_chain.links.fetch(-1).word == 'require' end private # @return [Chain::Literal] attr_reader :literal - # @ return [Chain] + # @return [Chain] attr_reader :method_call_chain # @return [String] The name of the method that this parameter belongs to @@ -57,21 +60,28 @@ def method_name @method_name ||= method_call.word end - # @return [Chain::Call] + # @return [Chain::Link] + # @sg-ignore method_call inferred type includes nil def method_call @method_call_chain.links.last end # The index of the literal in the method call chain. - # @return [Integer] + # @return [Integer, nil] + # @sg-ignore https://github.com/castwide/solargraph/pull/1245 def current_index - @current_index ||= method_call_chain.node.children[2..].index(literal.node) + return @current_index if @current_index + + # s(:send, receiver, method_name, *args) + method_call_chain.node => [_, _, _, *args] + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + @current_index = args.index(literal.node) end - # @return [::String, ::Symbol] The literal value of the parameter - # @sg-ignore false "return type could not be inferred" + # @return [::String, ::Symbol, nil] The literal value of the parameter, + # or nil when it is not a String or Symbol def literal_value - literal.node.children.first + (literal.node in [_, ::String | ::Symbol => value, *]) ? value : nil end end end diff --git a/lib/solargraph/source/change.rb b/lib/solargraph/source/change.rb index acea51b67d..ad86e44b1a 100644 --- a/lib/solargraph/source/change.rb +++ b/lib/solargraph/source/change.rb @@ -31,11 +31,11 @@ def write text, nullable = false if nullable && !range.nil? && new_text.match(/[.\[{(@$:]$/) [':', '@'].each do |dupable| next unless new_text == dupable - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 r = Change.new(Range.new(p, range.start), ' ') text = r.write(text) end @@ -60,12 +60,12 @@ def repair text fixed else result = commit text, fixed - # @sg-ignore flow sensitive typing needs to handle attrs + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 off = Position.to_offset(text, range.start) - # @sg-ignore Need to add nil check here + # @sg-ignore slice [0, n] is nil only for a negative n match = result[0, off].match(/[.:]+\z/) if match - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore slice [0, n] is nil only for a negative n result = result[0, off].sub(/#{match[0]}\z/, ' ' * match[0].length) + result[off..] end result @@ -78,9 +78,9 @@ def repair text # @param insert [String] # @return [String] def commit text, insert - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 start_offset = Position.to_offset(text, range.start) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 end_offset = Position.to_offset(text, range.ending) (start_offset.zero? ? '' : text[0..(start_offset - 1)].to_s) + normalize(insert) + text[end_offset..].to_s end diff --git a/lib/solargraph/source/cursor.rb b/lib/solargraph/source/cursor.rb index 077364910a..d06ed9b737 100644 --- a/lib/solargraph/source/cursor.rb +++ b/lib/solargraph/source/cursor.rb @@ -35,14 +35,14 @@ 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1385 # @return [String] def start_of_word @start_of_word ||= begin 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 - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 if source.code[0..(offset - result.length - 1)].end_with?(':') && !source.code[0..(offset - result.length - 1)].end_with?('::') result = ":#{result}" end @@ -54,7 +54,7 @@ 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 def end_of_word @end_of_word ||= begin match = source.code[offset..].to_s.match(end_word_pattern) @@ -112,6 +112,7 @@ def string? # as an argument. # # @return [Cursor, nil] + # @sg-ignore https://github.com/apiology/solargraph/pull/225 def recipient @recipient ||= begin node = recipient_node @@ -130,7 +131,7 @@ def recipient end alias receiver recipient - # @return [AST::Node] + # @return [AST::Node, nil] def node @node ||= source.node_at(position.line, position.column) end @@ -138,10 +139,10 @@ def node # @return [Position] def node_position @node_position ||= if start_of_word.empty? - # @sg-ignore Need to add nil check here + # @sg-ignore slice [0, n] is nil only for a negative n match = source.code[0, offset].match(/\s*(\.|:+)\s*$/) if match - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/apiology/solargraph/pull/193 Position.from_offset(source.code, offset - match[0].length) else position diff --git a/lib/solargraph/source/node_map.rb b/lib/solargraph/source/node_map.rb new file mode 100644 index 0000000000..40e2f11a48 --- /dev/null +++ b/lib/solargraph/source/node_map.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Solargraph + class Source + class NodeMap + # @param node [Parser::AST::Node, nil] + def initialize node + @node = node + end + + # @param type [Symbol] + # @return [Enumerable] + def find_by_type type + node_map[type] || [] + end + + private + + # @return [Hash{Symbol => Set}] + def node_map + @node_map ||= flatten(@node).to_set.classify(&:type) + end + + # @param from [Object] + # @return [Array] + def flatten from + return [] unless from.is_a?(::Parser::AST::Node) + [from].concat(from.children.flat_map { |child| flatten child }) + end + end + end +end diff --git a/lib/solargraph/source/source_chainer.rb b/lib/solargraph/source/source_chainer.rb index c9c95677c9..8ab42919ad 100644 --- a/lib/solargraph/source/source_chainer.rb +++ b/lib/solargraph/source/source_chainer.rb @@ -13,7 +13,7 @@ class SourceChainer private_class_method :new class << self - # @param source [Source] + # @param source [Solargraph::Source] # @param position [Position, Array(Integer, Integer)] # @return [Source::Chain] def chain source, position @@ -21,7 +21,7 @@ def chain source, position end end - # @param source [Source] + # @param source [Solargraph::Source] # @param position [Position] def initialize source, position @source = source @@ -60,15 +60,20 @@ def chain # @return [Array(Parser::AST::Node, Parser::AST::Node, Array)] # parent node, child node, and the tree at the position def node_from_position + # @type [Parser::AST::Node, nil] node = nil + # @type [Parser::AST::Node, nil] parent = nil + # @type [Array, nil] tree = nil if source.parsed? && !source.repaired? && source.synchronized? tree = source.tree_at(position.line, position.column) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 node, parent = tree[0..2] elsif source.parsed? && source.repaired? && end_of_phrase == '.' tree = source.tree_at(fixed_position.line, fixed_position.column) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 node, parent = tree[0..2] # 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? @@ -79,6 +84,7 @@ def node_from_position else unless source.error_ranges.any? { |r| r.nil? || r.include?(fixed_position) } tree = source.tree_at(fixed_position.line, fixed_position.column) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 node, parent = tree[0..2] end # Exception for positions that chain literal nodes in unsynchronized sources @@ -96,13 +102,12 @@ def node_from_position # @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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 # @return [String] def fixed_phrase @fixed_phrase ||= phrase[0..-(end_of_phrase.length + 1)] @@ -114,7 +119,7 @@ def fixed_position end # @return [String] - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 def end_of_phrase @end_of_phrase ||= begin match = phrase.match(/\s*(\.{1}|::)\s*$/) @@ -168,13 +173,10 @@ def get_signature_data_at index if brackets.zero? && parens.zero? && squares.zero? && [' ', "\r", "\n", "\t"].include?(char) in_whitespace = true else - # @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) + if brackets.zero? && parens.zero? && squares.zero? && in_whitespace && !((char == '.') || (@source.code[(index + 1)..] || '').strip.start_with?('.')) + rest = @source.code[(index + 1)..] || '' + rest.lstrip + index += (rest.length - rest.lstrip.length) break end case char diff --git a/lib/solargraph/source/updater.rb b/lib/solargraph/source/updater.rb index 9debc02833..6651f0a97b 100644 --- a/lib/solargraph/source/updater.rb +++ b/lib/solargraph/source/updater.rb @@ -29,21 +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 + # @sg-ignore flow sensitive typing should support ivars # @return [String] def write text, nullable = false can_nullify = (nullable and changes.length == 1) - return @output if (@input == text) && (can_nullify == @did_nullify) + return @output if @output && (@input == text) && (can_nullify == @did_nullify) @input = text - @output = text @did_nullify = can_nullify + output = text 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) + output = ch.write(output, can_nullify) end - @output + @output = output end # @param text [String] diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index e2abff92c2..2ebdd68d93 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -12,6 +12,7 @@ class SourceMap autoload :Clip, 'solargraph/source_map/clip' autoload :Completion, 'solargraph/source_map/completion' autoload :Data, 'solargraph/source_map/data' + autoload :Call, 'solargraph/source_map/call' # @return [Source] attr_reader :source @@ -82,6 +83,19 @@ def requires pins_by_class(Pin::Reference::Require) end + # @param method_names [Array] + # @return [Array] + def calls *method_names + symbolized = method_names.map(&:to_sym) + node_map.find_by_type(:send).select { |node| node.children[0].nil? && symbolized.include?(node.children[1].to_sym) } + .map { |node| Call.new(self, Parser.chain(node), locate_namespace_pin(node.loc.line, node.loc.column)) } + end + + # @return [Source::NodeMap] + def node_map + @node_map ||= Source::NodeMap.new(source.node) + end + # @return [Environ] def conventions_environ @conventions_environ ||= Environ.new @@ -92,6 +106,7 @@ def conventions_environ # @return [Array] def document_symbols @document_symbols ||= (pins + convention_pins).select do |pin| + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 pin.path && !pin.path.empty? end end @@ -109,7 +124,7 @@ def cursor_at position end # @param path [String] - # @return [Pin::Base] + # @return [Pin::Base, nil] def first_pin path pins.select { |p| p.path == path }.first end @@ -124,13 +139,23 @@ def locate_pins location # @param line [Integer] # @param character [Integer] # @return [Pin::Method,Pin::Namespace] + # @sg-ignore Need better generic inference here def locate_named_path_pin line, character _locate_pin line, character, Pin::Namespace, Pin::Method end + # @param line [Integer] + # @param character [Integer] + # @return [Pin::Namespace, Pin::Singleton, nil] + # @sg-ignore generic from _locate_pin is not resolved from its class arguments + def locate_namespace_pin line, character + _locate_pin line, character, Pin::Namespace, Pin::Singleton + end + # @param line [Integer] # @param character [Integer] # @return [Pin::Closure] + # @sg-ignore Need better generic inference here def locate_closure_pin line, character _locate_pin line, character, Pin::Closure end @@ -154,16 +179,18 @@ def locals_at location # @return [Array] def method_call_nodes - # @sg-ignore node expected Parser::AST::Node, received Parser::AST::Node, nil @method_call_nodes ||= Solargraph::Parser::ParserGem::NodeMethods.call_nodes_from(source.node) end - # @param macro_method_names [Array] + # @param macro_method_names [Set] # @return [Array] def macro_method_candidates macro_method_names return @macro_method_candidates if @macro_method_names == macro_method_names @macro_method_names = macro_method_names - @macro_method_candidates = method_call_nodes.select { |node| macro_method_names.include?(node.children[1].to_s) } + @macro_method_candidates = method_call_nodes.select do |node| + # @sg-ignore https://github.com/castwide/solargraph/pull/1416 + (node in [_, _, Symbol | ::Parser::AST::Node | nil => method_name, *]) && macro_method_names.include?(method_name.to_s) + end end class << self @@ -209,7 +236,7 @@ def data end # @param pins [Array] - # @return [Array] + # @return [void] def convention_pins= pins # unmemoizing the document_symbols in case it was called from any of conventions @document_symbols = nil @@ -230,9 +257,9 @@ def _locate_pin line, character, *klasses next if pin.is_a?(Pin::Method) && !pin.body? found = pin if (klasses.empty? || klasses.any? do |kls| pin.is_a?(kls) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 end) && pin.location.range.contain?(position) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 break if pin.location.range.start.line > line end # Assuming the root pin is always valid diff --git a/lib/solargraph/source_map/call.rb b/lib/solargraph/source_map/call.rb new file mode 100644 index 0000000000..a825e1b632 --- /dev/null +++ b/lib/solargraph/source_map/call.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Solargraph + class SourceMap + class Call + # @return [SourceMap] + attr_reader :source_map + + # @return [Source::Chain] + attr_reader :chain + + # @return [Pin::Closure, nil] + attr_reader :closure + + # @param source_map [SourceMap] + # @param chain [Source::Chain] + # @param closure [Pin::Closure, nil] + def initialize source_map, chain, closure + @source_map = source_map + @chain = chain + @closure = closure + end + end + end +end diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index 434fa97513..40f58d28d9 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -12,8 +12,8 @@ def initialize api_map, cursor @api_map = api_map @cursor = cursor closure_pin = closure - # @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) + if closure_pin.is_a?(Pin::Block) && closure_pin.receiver && + !Solargraph::Range.from_node(closure_pin.receiver)&.contain?(cursor.range.start) closure_pin.rebind(api_map) end end @@ -25,7 +25,7 @@ def define result.concat file_global_methods if result.empty? result.concat((source_map.pins + source_map.locals).select do |p| - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 p.name == cursor.word && p.location.range.contain?(cursor.position) end) end @@ -41,7 +41,7 @@ def types def complete return package_completions([]) if !source_map.source.parsed? || cursor.string? # TODO: Improve magic word comparsion == '<::Symbol>', too fragile - if cursor.chain.literal? && cursor.chain.links.last.word == '<::Symbol>' + if cursor.chain.literal? && cursor.chain.links.fetch(-1).word == '<::Symbol>' return package_completions(api_map.get_symbols) end return Completion.new([], cursor.range) if cursor.chain.literal? @@ -108,7 +108,6 @@ 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 @@ -142,6 +141,8 @@ def complete_keyword_parameters next unless param.keyword? result.push Pin::KeywordParam.new(pin.location, "#{param.name}:") end + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top next unless !pin.parameters.empty? && pin.parameters.last.kwrestarg? pin.docstring.tags(:param).each do |tag| next if done.include?(tag.name) @@ -166,23 +167,23 @@ def package_completions result # @return [Completion] def tag_complete result = [] - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 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 + full = match[1].to_s + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 if full.include?('::') - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 if full.end_with?('::') - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 result.concat api_map.get_constants(full[0..-3], *gates) else - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 result.concat api_map.get_constants(full.split('::')[0..-2].join('::'), *gates) end else - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1395 result.concat api_map.get_constants('', full.end_with?('::') ? '' : context_pin.full_context.namespace, *gates) # .select { |pin| pin.name.start_with?(full) } end end @@ -194,11 +195,11 @@ def code_complete result = [] result.concat complete_keyword_parameters if cursor.chain.constant? || cursor.start_of_constant? - full = cursor.chain.links.first.word + full = cursor.chain.links.fetch(0).word type = if cursor.chain.undefined? 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 + # @sg-ignore Unresolved call to join on Array, nil ComplexType.try_parse(full.split('::')[0..-2].join('::')) elsif cursor.chain.links.length > 1 ComplexType.try_parse(full) diff --git a/lib/solargraph/source_map/data.rb b/lib/solargraph/source_map/data.rb index 906273fbce..28d3d3a69f 100644 --- a/lib/solargraph/source_map/data.rb +++ b/lib/solargraph/source_map/data.rb @@ -8,15 +8,12 @@ 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 - # @sg-ignore Translate to something flow sensitive typing understands # @return [Array] def locals generate diff --git a/lib/solargraph/source_map/mapper.rb b/lib/solargraph/source_map/mapper.rb index 98e1863997..4179c71e24 100644 --- a/lib/solargraph/source_map/mapper.rb +++ b/lib/solargraph/source_map/mapper.rb @@ -12,12 +12,12 @@ class Mapper private_class_method :new - DIRECTIVE_REGEXP = /(@!method|@method|@!attribute|@!visibility|@!scope|@!domain|@!macro|@!parse|@!override)/ + DIRECTIVE_REGEXP = /(@!method|@method|@!attribute|@!visibility|@!scope|@!macro|@!parse|@!override)/ # Generate the data. # # @param source [Source] - # @return [Array] + # @return [Array(Array, Array)] def map source @source = source @filename = source.filename @@ -35,9 +35,9 @@ def map source # [[], []] end - # @param filename [String] + # @param filename [String, nil] # @param code [String] - # @return [Array] + # @return [Array(Array, Array)] def unmap filename, code s = Position.new(0, 0) e = Position.from_offset(code, code.length) @@ -47,9 +47,8 @@ def unmap filename, code class << self # @param source [Source] - # @return [Array] + # @return [Array(Array, 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 @@ -86,13 +85,11 @@ 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 - # @sg-ignore Need to add nil check here - num = comment.lines[start..].find_index do |line| + num = comment.lines.drop(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 @@ -127,8 +124,7 @@ 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 + num = cur if cur && num && cur < num end ctxt += p[num..].to_s if started end @@ -139,6 +135,8 @@ def remove_inline_comment_hashes comment def process_comment_directives return unless @code.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP code_lines = @code.lines + # @param line [Integer] + # @param comments [Array] @source.associated_comments.each do |line, comments| src_pos = if line Position.new(line, @@ -148,7 +146,6 @@ def process_comment_directives code_lines.length, 0 ) end - # @sg-ignore Need to add nil check here com_pos = Position.new(line + 1 - comments.length, 0) process_comment(src_pos, com_pos, comments.join('')) end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 0f61490f46..87ce93dafd 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -11,7 +11,7 @@ class TypeChecker # include Solargraph::Parser::ParserGem::NodeMethods include Parser::NodeMethods - # @return [String] + # @return [String, nil] attr_reader :filename # @return [Rules] @@ -118,7 +118,6 @@ def load_string code, filename = nil, level = :normal, api_map: nil 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, rules: rules) end @@ -146,7 +145,6 @@ 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) @@ -157,13 +155,20 @@ def method_return_type_problems_for pin 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? result.push Problem.new(pin.location, "Untyped method #{pin.path} could not be inferred") end - elsif rules.validate_tags? + elsif rules.validate_tags? && !(pin.return_type.undefined? && declared.any?(&:void?)) + # An ancestor tag that includes void alongside other types is + # documenting an optional return, not a contract every override + # commits to. When this pin has no @return tag of its own, + # Pin::Method#typify_from_super borrows that tag only as a + # best-effort guess for hover/completion; there is nothing to + # cross-check an inferred body against in that case. A + # single-type ancestor tag (no void) still validates normally. + # # Attribute pins never have a node (attr_reader/attr_writer/attr_accessor are # synthesized, not parsed method bodies), but their inferred type is still # meaningful: #probe walks the backing ivar's assignments via infer_from_iv. @@ -208,7 +213,7 @@ def resolved_constant? pin # @param pin [Pin::Base] def virtual_pin? pin - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 pin.location && source.comment_at?(pin.location.range.ending) end @@ -235,11 +240,10 @@ def method_param_type_problems_for pin end end # @param name [String] - # @param data [Hash{Symbol => BasicObject}] + # @param data [Hash{Symbol => String, ComplexType}] params.each_pair do |name, data| - # @type [ComplexType] type = data[:qualified] - if type.undefined? + if type.is_a?(ComplexType) && type.undefined? result.push Problem.new(pin.location, "Unresolved type #{data[:tagged]} for #{name} param on #{pin.path}", pin: pin) end @@ -257,7 +261,6 @@ 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? @@ -304,10 +307,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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 location = Location.new(filename, rng) locals = source_map.locals_at(location) pins = chain.define(api_map, closure_pin, locals) @@ -324,10 +327,10 @@ 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 next if @marked_ranges.any? { |d| d.contain?(rng.start) } chain = Solargraph::Parser.chain(call, filename) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) if call.type == :block # blocks in the AST include the method call as well, so the @@ -337,12 +340,12 @@ def call_problems # @todo Should warn on nil deference here closure_pin = closure_pin.closure end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 closure_pin.rebind(api_map) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 location = Location.new(filename, rng) locals = source_map.locals_at(location) - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 type = chain.infer(api_map, closure_pin, locals) if type.undefined? && !rules.ignore_all_undefined? base = chain @@ -351,8 +354,8 @@ def call_problems found = nil # @type [Array] all_found = [] - until base.links.first.undefined? - # @sg-ignore Need to add nil check here + until base.links.fetch(0).undefined? + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 all_found = base.define(api_map, closure_pin, locals) found = all_found.first break if found @@ -364,14 +367,14 @@ def call_problems # @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))) && !(closest.generic? || ignored_pins.include?(found)) if closest.defined? - result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}") + result.push Problem.new(location, "Unresolved call to #{missing.links.fetch(-1).word} on #{closest}") else - result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}") + result.push Problem.new(location, "Unresolved call to #{missing.links.fetch(-1).word}") end @marked_ranges.push rng end end - # @sg-ignore Need to add nil check here + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 result.concat argument_problems_for(chain, api_map, closure_pin, locals, location) end result @@ -525,8 +528,10 @@ def signature_argument_problems_for location, locals, closure_pin, params, argum def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx result = [] kwargs = convert_hash(argchain.node) - par = sig.parameters[idx] - # @type [Solargraph::Source::Chain] + # idx always came from the caller's own sig.parameters.each_with_index + par = sig.parameters.fetch(idx) + + # @type [Solargraph::Source::Chain, nil] 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) @@ -559,19 +564,23 @@ 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 => Hash{Symbol => undefined}}] # @param kwargs [Hash{Symbol => Source::Chain}] # @return [Array] def kwrestarg_problems_for api_map, closure_pin, locals, location, pin, params, kwargs result = [] kwargs.each_pair do |pname, argchain| + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 next unless params.key?(pname.to_s) - # @sg-ignore + # @sg-ignore https://github.com/apiology/solargraph/pull/198 # @type [ComplexType] raw_ptype = params[pname.to_s][:qualified] ptype = raw_ptype.self_to_type(pin.context) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 argtype = argchain.infer(api_map, closure_pin, locals) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 argtype = argtype.self_to_type(closure_pin.context) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 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}") @@ -596,7 +605,7 @@ def add_restkwarg_param_tag_details param_details, pin, relevant_pin qualified: Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, pin.full_context.namespace) } # don't complain about a param that didn't come from the pin we're looking at anyway - if details[:qualified].defined? || + if details.fetch(:qualified).defined? || relevant_pin.parameter_names.include?(tag.name.to_s) param_details[tag.name.to_s] = details end @@ -624,7 +633,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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1393 qualified: Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, *pin.closure.gates) } end @@ -644,7 +653,11 @@ def add_to_param_details param_details, param_names, new_param_details next unless param_names.include?(param_name) param_details[param_name] ||= {} + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 + # also needs https://github.com/castwide/solargraph/pull/1417 param_details[param_name][:tagged] ||= details[:tagged] + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 + # also needs https://github.com/castwide/solargraph/pull/1417 param_details[param_name][:qualified] ||= details[:qualified] end end @@ -674,7 +687,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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 pin.location && api_map.bundled?(pin.location.filename) end @@ -695,11 +708,10 @@ def declared_externally? pin 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) - # @sg-ignore flow sensitive typing needs to handle "if foo.nil?" + # @sg-ignore https://github.com/castwide/solargraph/pull/1394 location = Location.new(filename, Range.from_node(pin.assignment)) locals = source_map.locals_at(location) type = chain.infer(api_map, closure_pin, locals) @@ -709,7 +721,7 @@ def declared_externally? pin found = nil # @type [Array] all_found = [] - until base.links.first.undefined? + until base.links.fetch(0).undefined? all_found = base.define(api_map, closure_pin, locals) found = all_found.first break if found @@ -732,7 +744,7 @@ def arity_problems_for pin, arguments, location return [] if r.empty? r end - results.first + results.first || [] end # @param pin [Pin::Method] @@ -754,6 +766,8 @@ def parameterized_arity_problems_for pin, parameters, arguments, location if any_splatted_call?(unchecked.map(&:node)) settled_kwargs = parameters.count(&:keyword?) else + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top kwargs = convert_hash(unchecked.last.node) if parameters.any? { |param| %i[kwarg kwoptarg].include?(param.decl) || param.kwrestarg? } if kwargs.empty? @@ -766,7 +780,7 @@ def parameterized_arity_problems_for pin, parameters, arguments, location kwargs.delete param.name.to_sym settled_kwargs += 1 elsif param.decl == :kwarg - last_arg_last_link = arguments.last.links.last + last_arg_last_link = arguments.fetch(-1).links.fetch(-1) return [] if last_arg_last_link.is_a?(Solargraph::Source::Chain::Hash) && last_arg_last_link.splatted? return [Problem.new(location, "Missing keyword argument #{param.name} to #{pin.path}")] end @@ -791,6 +805,8 @@ def parameterized_arity_problems_for pin, parameters, arguments, location end 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}")] + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 + # empty? guards need https://github.com/apiology/solargraph/tree/narrow-first-last-after-empty-guard on top 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. # See https://github.com/castwide/solargraph/issues/418 @@ -830,13 +846,13 @@ def fake_args_for pin with_block = false # @param pin [Pin::Parameter] pin.parameters.each do |pin| - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1424 if %i[kwarg kwoptarg kwrestarg].include?(pin.decl) with_opts = true - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1424 elsif pin.decl == :block with_block = true - # @sg-ignore flow sensitive typing should be able to handle redefinition + # @sg-ignore https://github.com/castwide/solargraph/pull/1424 elsif pin.decl == :restarg args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)], nil, true) else @@ -858,7 +874,6 @@ 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.any? { |t| t.include?('@sg-ignore') } end.keys.to_set end diff --git a/lib/solargraph/type_checker/problem.rb b/lib/solargraph/type_checker/problem.rb index 8375a58ff3..5d0a97766c 100644 --- a/lib/solargraph/type_checker/problem.rb +++ b/lib/solargraph/type_checker/problem.rb @@ -7,13 +7,14 @@ class TypeChecker class Problem # @todo Missed nil violation # @return [Solargraph::Location] + # @sg-ignore https://github.com/castwide/solargraph/pull/1399 attr_reader :location # @return [String] attr_reader :message # @todo Missed nil violation - # @return [Pin::Base] + # @return [Pin::Base, nil] attr_reader :pin # @return [String, nil] diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 294c7a2c1c..2950a4cf4b 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -67,52 +67,67 @@ def require_inferred_type_params? # # False negatives: # - # @todo 4: Missed nil violation + # @todo 3: Missed nil violation # - # pending code fixes (277): + # pending code fixes (589): # - # @todo 281: Need to add nil check here - # @todo 22: Translate to something flow sensitive typing understands - # @todo 3: Need a downcast here + # @todo 141: Need to add nil check here + # @todo 79: https://github.com/castwide/solargraph/pull/1223 + # @todo 66: node.children[] relies on grammar-guaranteed arity + # @todo 50: https://github.com/castwide/solargraph/pull/1245 + # @todo 48: Array#first/#last relies on non-empty invariant + # @todo 33: pin.closure relies on closure always resolved + # @todo 29: Range.from_node result assumed always present + # @todo 26: Translate to something flow sensitive typing understands + # @todo 23: Hash#[] relies on key always being present + # @todo 22: String/Array Range slice relies on valid bounds + # @todo 19: pin.location relies on location always resolved + # @todo 17: Need a downcast here + # @todo 12: MatchData relies on regex always matching + # @todo 10: Array#[] integer index relies on fixed arity + # @todo 7: Missing @return tag + # @todo 1: Unresolved call to == + # @todo 1: Unresolved call to []= + # @todo 1: Unresolved constant BlankRubocopFormatter + # @todo 1: Proc#call return type isn't inferred without generics + # @todo 1: super is typed void though it returns a value + # @todo 1: parser gem's Builders::Default#value has no return type + # @todo 1: @@inference_cache has no declared value type # - # flow sensitive typing could handle (99): + # flow sensitive typing could handle (162): # - # @todo 36: flow sensitive typing needs to handle attrs - # @todo 29: flow sensitive typing should be able to handle redefinition - # @todo 21: flow sensitive typing needs to narrow down type with an if is_a? check - # @todo 13: Need to validate config + # @todo 30: https://github.com/castwide/solargraph/issues/1249 + # @todo 28: https://github.com/castwide/solargraph/issues/1241 + # @todo 20: https://github.com/castwide/solargraph/issues/1250 + # @todo 12: https://github.com/castwide/solargraph/issues/1251 + # @todo 11: flow sensitive typing needs better handling of ||= on lvars + # @todo 11: 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 7: need boolish support for ? methods + # @todo 5: missing flow-sensitive typing on array elements + # @todo 4: flow sensitive typing ought to be able to handle 'when ClassName' + # @todo 4: literal arrays in this module turn into ::Solargraph::Source::Chain::Array + # @todo 4: flow sensitive typing needs to handle 'raise if' # @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: flow sensitive typing needs to create separate ranges for postfix if # @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 2: downcast output of Enumerable#select + # @todo 1: MatchData#[] is nilable even for a group that always matches # @todo 1: flow sensitive typing should support ivars + # @todo 1: Need to be able to resolve generics based on a + # @todo 1: Need to resolve @generic T from a block's yieldreturn # @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 needs to handle "if foo.nil?" # @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 + # @todo 1: flow sensitive typing not smart enough to handle this case + # @todo 1: flow sensitive typing needs to handle self.class == other.class + # @todo 1: flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo) + # @todo 1: Should better support meaning of '&' in RBS + # @todo 1: flow sensitive typing needs to handle constants + # @todo 1: Need to handle duck-typed method calls on union types + # @todo 1: flow sensitive typing needs to remove literal with # @todo 1: Array#flatten returns a bare Array def require_all_unique_types_match_expected? report?(:require_all_unique_types_match_expected, :strong) diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 01b02f1f3f..8f3d281d9a 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -19,10 +19,13 @@ class Workspace attr_reader :gemnames alias source_gems gemnames - # @param directory [String] TODO: Remove '' and '*' special cases + # @todo Remove '*' special case + # @param directory [String] If empty, no config will be loaded, + # and no RBS collection will be used. Useful for specs. # @param config [Config, nil] # @param server [Hash] - def initialize directory = '', config = nil, server = {} + # @param metagem [Metagem, nil] the gem whose source the workspace is + def initialize directory = '', config = nil, server = {}, metagem: nil raise ArgumentError, 'directory must be a String' unless directory.is_a?(String) @directory = if ['*', ''].include?(directory) @@ -34,9 +37,20 @@ def initialize directory = '', config = nil, server = {} @server = server load_sources @gemnames = [] + @metagem = metagem require_plugins end + # Names of the gems the workspace's code can use beyond those its bundle + # and requires provide: for a gem's source, the gem and its runtime + # dependencies + # + # @return [Array] + def available_gem_names + return [] unless @metagem + [@metagem.name] + @metagem.dependencies + end + # The require paths associated with the workspace. # # @return [Array] @@ -70,10 +84,11 @@ def merge *sources includes_any = false sources.each do |source| - next unless directory == '*' || config.calculated.include?(source.filename) + filename = source.filename + next if filename.nil? + next unless directory == '*' || config.calculated.include?(filename) - # @sg-ignore Wrong argument type for Hash#[]=: arg0 expected String, received String, nil - source_hash[source.filename] = source + source_hash[filename] = source includes_any = true end @@ -101,7 +116,7 @@ def sources source_hash.values end - # @param filename [String] + # @param filename [String, nil] # @return [Boolean] def has_file? filename source_hash.key?(filename) @@ -110,7 +125,7 @@ def has_file? filename # Get a source by its filename. # # @param filename [String] - # @return [Solargraph::Source] + # @return [Solargraph::Source, nil] def source filename source_hash[filename] end @@ -134,10 +149,11 @@ def rbs_collection_path # @return [String, nil] def rbs_collection_config_path - @rbs_collection_config_path ||= unless directory.empty? || directory == '*' - yaml_file = File.join(directory, 'rbs_collection.yaml') - yaml_file if File.file?(yaml_file) - end + @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] @@ -154,10 +170,13 @@ def find_gem name, version = nil, out: nil # @param updater [Source::Updater] # @return [void] def synchronize! updater - source_hash[updater.filename] = source_hash[updater.filename].synchronize(updater) + source = source_hash[updater.filename] + return unless source + + source_hash[updater.filename] = source.synchronize(updater) end - # @sg-ignore return type could not be inferred + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 # @return [String] def command_path server['commandPath'] || 'solargraph' @@ -222,11 +241,7 @@ def load_sources # @return [void] def require_plugins - config.plugins.each do |plugin| - require plugin - rescue LoadError - Solargraph.logger.warn "Failed to load plugin '#{plugin}'" - end + config.require_plugins end # @return [String, nil] diff --git a/lib/solargraph/workspace/config.rb b/lib/solargraph/workspace/config.rb index f8ba2d4b89..6ffbaee4f9 100644 --- a/lib/solargraph/workspace/config.rb +++ b/lib/solargraph/workspace/config.rb @@ -20,7 +20,11 @@ class Config # @param directory [String] def initialize directory = '' - @directory = File.absolute_path(directory) + @directory = if directory.empty? + '' + else + File.absolute_path(directory) + end @raw_data = config_data included excluded @@ -65,7 +69,7 @@ def calculated # namespace. It's typically used to identify available DSLs. # # @return [Array] - # @sg-ignore Need to validate config + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 def domains raw_data['domains'] end @@ -73,23 +77,23 @@ def domains # An array of required paths to add to the workspace. # # @return [Array] - # @sg-ignore Need to validate config + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 def required raw_data['require'] end # An array of load paths for required paths. # - # @sg-ignore Need to validate config + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 # @return [Array] - # @sg-ignore Need to validate config + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 def require_paths raw_data['require_paths'] || [] end # An array of reporters to use for diagnostics. # - # @sg-ignore Need to validate config + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 # @return [Array] def reporters raw_data['reporters'] @@ -97,7 +101,7 @@ def reporters # A hash of options supported by the formatter # - # @sg-ignore Need to validate config + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 # @return [Hash] def formatter raw_data['formatter'] @@ -105,15 +109,28 @@ def formatter # An array of plugins to require. # - # @sg-ignore Need to validate config + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 # @return [Array] def plugins raw_data['plugins'] end + # Require the configured plugins, recording each one that loads in + # the gem cache key. + # + # @return [void] + def require_plugins + plugins.each do |plugin| + require plugin + CacheDir.add_plugin plugin + rescue LoadError + Solargraph.logger.warn "Failed to load plugin '#{plugin}'" + end + end + # The maximum number of files to parse from the workspace. # - # @sg-ignore Need to validate config + # @sg-ignore https://github.com/castwide/solargraph/pull/1366 # @return [Integer] def max_files raw_data['max_files'] diff --git a/lib/solargraph/workspace/require_paths.rb b/lib/solargraph/workspace/require_paths.rb index d12364b07b..f8630d25c1 100644 --- a/lib/solargraph/workspace/require_paths.rb +++ b/lib/solargraph/workspace/require_paths.rb @@ -13,7 +13,7 @@ class RequirePaths attr_reader :directory, :config # @param directory [String, nil] - # @param config [Config, nil] + # @param config [Config] def initialize directory, config @directory = directory @config = config @@ -77,13 +77,15 @@ def require_path_from_gemspec_file gemspec_file_path 'return unless Gem::Specification === spec; ' \ 'puts({name: spec.name, paths: spec.require_paths}.to_json)'] o, e, s = Open3.capture3(*cmd) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 if s.success? begin - hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {} + last_line = o.split("\n").last + hash = last_line ? JSON.parse(last_line) : {} 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 + # @sg-ignore https://github.com/castwide/solargraph/pull/1338 Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}" [] end diff --git a/lib/solargraph/yard_map.rb b/lib/solargraph/yard_map.rb index 08dd8bd423..111499d7b2 100755 --- a/lib/solargraph/yard_map.rb +++ b/lib/solargraph/yard_map.rb @@ -10,7 +10,6 @@ module Solargraph module YardMap class NoYardocError < StandardError; end - autoload :Cache, 'solargraph/yard_map/cache' autoload :Mapper, 'solargraph/yard_map/mapper' autoload :Helpers, 'solargraph/yard_map/helpers' autoload :Macro, 'solargraph/yard_map/macro' diff --git a/lib/solargraph/yard_map/cache.rb b/lib/solargraph/yard_map/cache.rb deleted file mode 100644 index 32d7b0a839..0000000000 --- a/lib/solargraph/yard_map/cache.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - module YardMap - class Cache - def initialize - # @type [Hash{String => Array}] - @path_pins = {} - end - - # @param path [String] - # @param pins [Array] - # @return [Array] - def set_path_pins path, pins - @path_pins[path] = pins - end - - # @param path [String] - # @return [Array] - def get_path_pins path - @path_pins[path] - end - end - end -end diff --git a/lib/solargraph/yard_map/directives.rb b/lib/solargraph/yard_map/directives.rb index 7141c14b48..c4738a7c19 100644 --- a/lib/solargraph/yard_map/directives.rb +++ b/lib/solargraph/yard_map/directives.rb @@ -5,22 +5,19 @@ module YardMap module Directives autoload :AttributeDirective, 'solargraph/yard_map/directives/attribute_directive' autoload :MethodDirective, 'solargraph/yard_map/directives/method_directive' - autoload :DomainDirective, 'solargraph/yard_map/directives/domain_directive' autoload :OverrideDirective, 'solargraph/yard_map/directives/override_directive' autoload :ParseDirective, 'solargraph/yard_map/directives/parse_directive' autoload :ScopeDirective, 'solargraph/yard_map/directives/scope_directive' autoload :VisibilityDirective, 'solargraph/yard_map/directives/visibility_directive' # @param directive [YARD::Tags::Directive] - # @return [Class, Class, Class, Class, Class, Class, Class, nil] + # @return [Class, Class, Class, Class, Class, Class, nil] def self.for directive case directive.tag.tag_name when 'attribute' AttributeDirective when 'method' MethodDirective - when 'domain' - DomainDirective when 'override' OverrideDirective when 'parse' diff --git a/lib/solargraph/yard_map/directives/attribute_directive.rb b/lib/solargraph/yard_map/directives/attribute_directive.rb index d3d4130460..0d54a07f7e 100644 --- a/lib/solargraph/yard_map/directives/attribute_directive.rb +++ b/lib/solargraph/yard_map/directives/attribute_directive.rb @@ -55,9 +55,12 @@ def process_directive source, pins, source_position, comment_position, directive # @param [Array] pins # @param [Position] position - # @return [Pin::Closure] + # @return [Pin::Closure, nil] def closure_at pins, position - pins.select { |pin| pin.is_a?(Pin::Closure) and pin.location&.range&.contain?(position) }.last + # Skip the method the directive documents; attributes belong to its namespace + pins.select do |pin| + pin.is_a?(Pin::Closure) && !pin.is_a?(Pin::Method) && pin.location&.range&.contain?(position) + end.last end end end diff --git a/lib/solargraph/yard_map/directives/domain_directive.rb b/lib/solargraph/yard_map/directives/domain_directive.rb deleted file mode 100644 index 516710098e..0000000000 --- a/lib/solargraph/yard_map/directives/domain_directive.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - module YardMap - module Directives - module DomainDirective - module_function - - # @param source [Solargraph::Source] - # @param pins [Array] - # @param source_position [Position] - # @param _comment_position [Position] - # @param directive [YARD::Tags::Directive] - # @return [Array] - def process_directive source, pins, source_position, _comment_position, directive - namespace = closure_at(pins, source_position) || Pin::ROOT_PIN - namespace.domains.concat directive.tag.types unless directive.tag.types.nil? - [] - end - - # @param [Array] pins - # @param [Position] position - # @return [Pin::Namespace] - def closure_at pins, position - pins.select { |pin| pin.is_a?(Pin::Namespace) and pin.location&.range&.contain?(position) }.last - end - end - end - end -end diff --git a/lib/solargraph/yard_map/directives/method_directive.rb b/lib/solargraph/yard_map/directives/method_directive.rb index 4f93416c6a..8aeab0bf60 100644 --- a/lib/solargraph/yard_map/directives/method_directive.rb +++ b/lib/solargraph/yard_map/directives/method_directive.rb @@ -13,13 +13,15 @@ module MethodDirective # @param directive [YARD::Tags::Directive] # @return [Array] def process_directive source, pins, source_position, comment_position, directive - namespace = closure_at(pins, source_position) || pins.first + namespace = closure_at(pins, source_position) || pins.fetch(0) + # @sg-ignore Unresolved call to location namespace = closure_at(pins, comment_position) if namespace.location&.range&.start&.line&.< comment_position.line # rubocop:disable Style/SafeNavigationChainLength begin src = Solargraph::Source.load_string("def #{directive.tag.name};end", source.filename) region = Parser::Region.new(source: src, closure: namespace) - method_gen_pins = Parser.process_node(src.node, region).first.select { |pin| pin.is_a?(Pin::Method) } + pins_from_directive, = Parser.process_node(src.node, region) + method_gen_pins = pins_from_directive.grep(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 @@ -41,7 +43,7 @@ def process_directive source, pins, source_position, comment_position, directive # @param [Array] pins # @param [Position] position - # @return [Pin::Closure] + # @return [Pin::Closure, nil] def closure_at pins, position pins.select { |pin| pin.is_a?(Pin::Closure) and pin.location&.range&.contain?(position) }.last end diff --git a/lib/solargraph/yard_map/directives/override_directive.rb b/lib/solargraph/yard_map/directives/override_directive.rb index 0891ca0823..bed3ac0be8 100644 --- a/lib/solargraph/yard_map/directives/override_directive.rb +++ b/lib/solargraph/yard_map/directives/override_directive.rb @@ -20,7 +20,7 @@ def process_directive source, _pins, _source_position, comment_position, directi # @param [Array] pins # @param [Position] position - # @return [Pin::Closure] + # @return [Pin::Closure, nil] def closure_at pins, position pins.select { |pin| pin.is_a?(Pin::Closure) and pin.location&.range&.contain?(position) }.last end diff --git a/lib/solargraph/yard_map/directives/parse_directive.rb b/lib/solargraph/yard_map/directives/parse_directive.rb index 520bb90091..d3d92e741d 100644 --- a/lib/solargraph/yard_map/directives/parse_directive.rb +++ b/lib/solargraph/yard_map/directives/parse_directive.rb @@ -19,7 +19,7 @@ def process_directive source, pins, source_position, comment_position, directive region = Parser::Region.new(source: src, closure: ns) # @todo These pins may need to be marked not explicit old_pins_index = pins.length - loff = if source.code.lines[comment_position.line].strip.end_with?('@!parse') + loff = if source.code.lines.fetch(comment_position.line).strip.end_with?('@!parse') comment_position.line + 1 else comment_position.line @@ -29,9 +29,9 @@ def process_directive source, pins, source_position, comment_position, directive new_pins.each do |p| # @todo Smelly instance variable access next if p.location.nil? - # @sg-ignore Unresolved call to range on Solargraph::Location, nil - does not account for next clause above. + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 p.location.range.start.instance_variable_set(:@line, p.location.range.start.line + loff) - # @sg-ignore Unresolved call to range on Solargraph::Location, nil + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 p.location.range.ending.instance_variable_set(:@line, p.location.range.ending.line + loff) end @@ -43,7 +43,7 @@ def process_directive source, pins, source_position, comment_position, directive # @param [Array] pins # @param [Position] position - # @return [Pin::Closure] + # @return [Pin::Closure, nil] def closure_at pins, position pins.select { |pin| pin.is_a?(Pin::Closure) and pin.location&.range&.contain?(position) }.last end diff --git a/lib/solargraph/yard_map/directives/scope_directive.rb b/lib/solargraph/yard_map/directives/scope_directive.rb index 9f90b9c52e..da3184552b 100644 --- a/lib/solargraph/yard_map/directives/scope_directive.rb +++ b/lib/solargraph/yard_map/directives/scope_directive.rb @@ -139,7 +139,7 @@ def apply_to_namespace pins, kind, source_position pins.each do |pin| next unless pin.is_a?(Pin::Method) next unless pin.namespace == namespace.path - # @sg-ignore flow sensitive typing should narrow location after the nil check + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 next if pin.location.nil? || pin.location.range.start.line < source_position.line pin.scope = kind diff --git a/lib/solargraph/yard_map/directives/visibility_directive.rb b/lib/solargraph/yard_map/directives/visibility_directive.rb index 466d25acdc..a358a0ab7c 100644 --- a/lib/solargraph/yard_map/directives/visibility_directive.rb +++ b/lib/solargraph/yard_map/directives/visibility_directive.rb @@ -17,11 +17,11 @@ module VisibilityDirective def process_directive source, pins, source_position, comment_position, directive kind = directive.tag.text&.to_sym - # @sg-ignore include? only expects Symbol, but receives Symbol or nil - return [] unless VALID_VISIBILITIES.include?(kind.to_sym) + return [] unless VALID_VISIBILITIES.include?(kind) name = directive.tag.name - closure = closure_at(pins, source_position) || pins.first + closure = closure_at(pins, source_position) || pins.fetch(0) + # @sg-ignore Unresolved call to location closure = closure_at(pins, comment_position) if closure.location&.range&.start&.line&.< comment_position.line # rubocop:disable Style/SafeNavigationChainLength if closure.is_a?(Pin::Method) && no_empty_lines?(source.code, comment_position.line, source_position.line) # @todo Smelly instance variable access @@ -52,15 +52,15 @@ def process_directive source, pins, source_position, comment_position, directive # @param [Integer] line1 # @param [Integer] line2 # @return [Boolean] - # @sg-ignore return type could not be inferred + # @sg-ignore Need to add nil check here def no_empty_lines? code, line1, line2 - # @sg-ignore unresolved call none? on the array. + # @sg-ignore Need to add nil check here code.lines[line1..line2].none? { |line| line.strip.empty? } end # @param [Array] pins # @param [Position] position - # @return [Pin::Closure] + # @return [Pin::Closure, nil] def closure_at pins, position pins.select { |pin| pin.is_a?(Pin::Closure) and pin.location&.range&.contain?(position) }.last end diff --git a/lib/solargraph/yard_map/helpers.rb b/lib/solargraph/yard_map/helpers.rb index 23fdc50ce5..25d789b414 100644 --- a/lib/solargraph/yard_map/helpers.rb +++ b/lib/solargraph/yard_map/helpers.rb @@ -16,7 +16,6 @@ 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 diff --git a/lib/solargraph/yard_map/macro.rb b/lib/solargraph/yard_map/macro.rb index 0128cffad7..8c96f04a44 100644 --- a/lib/solargraph/yard_map/macro.rb +++ b/lib/solargraph/yard_map/macro.rb @@ -48,9 +48,6 @@ def method_object_from_pin method_pin namespace_pin.name.to_sym ) end - # @sg-ignore Wrong argument type for YARD::CodeObjects::MethodObject.new: namespace - # expected YARD::CodeObjects::NamespaceObject, got nil. - # False positive because namespace_object is set in the loop above. YARD::CodeObjects::MethodObject.new(namespace_object, method_pin.name) end end @@ -101,7 +98,7 @@ def generate_pins_from chain, pin, source_map # @param generated_pins [Array] generate_yardoc_from(chain, source_map).reduce([]) do |generated_pins, directive| - # @sg-ignore flow sensitive typing confuses this block variable with the #directive attr reader + # @sg-ignore https://github.com/castwide/solargraph/pull/1326 if directive.tag.tag_name == 'scope' inherited_scope = Directives::ScopeDirective.parse_scope(directive) || inherited_scope next generated_pins @@ -127,11 +124,14 @@ def generate_pins_from chain, pin, source_map # @param [SourceMap] source_map # @return [Array] def generate_yardoc_from chain, source_map - name = chain.links.last.word - # @sg-ignore chain.links.last is assumed to be a Chain::Call - values = chain.links.last.arguments.map(&:node).map { |arg| Solargraph::Parser::ParserGem::NodeMethods.simple_convert(arg).to_s } - # @sg-ignore chain.node is assumed to exist - code = source_map.source.code_for(chain.node) + call = chain.links.last + # Macros expand only from method calls, which carry arguments + return [] unless call.is_a?(Source::Chain::Call) + + name = call.word + values = call.arguments.map(&:node).map { |arg| Solargraph::Parser::ParserGem::NodeMethods.simple_convert(arg).to_s } + node = chain.node + code = node ? source_map.source.code_for(node) : raise("Macro call chain has no node: #{chain.inspect}") expanded_comment = macro_object.expand([name, *values], code) .gsub(/\n(?!@!|\s)/, "\n ") parsed_docstring = Solargraph::Source.parse_docstring(expanded_comment) @@ -148,8 +148,7 @@ def generate_yardoc_from chain, source_map directives.each do |directive| next if VALUE_DIRECTIVES.include? directive.tag.tag_name - # @sg-ignore chain.node is assumed to exist - comments = source_map.source.comments_for(chain.node) + comments = source_map.source.comments_for(node) if comments&.length&.positive? directive.tag.text += "\n#{comments}" end diff --git a/lib/solargraph/yard_map/mapper.rb b/lib/solargraph/yard_map/mapper.rb index 90a551a6b9..d868cf8e9c 100644 --- a/lib/solargraph/yard_map/mapper.rb +++ b/lib/solargraph/yard_map/mapper.rb @@ -25,13 +25,14 @@ def map end # Some yardocs contain documentation for dependencies that can be # ignored here. The YardMap will load dependencies separately. - # @sg-ignore does not consider `pin.location.nil? || ` condition + # @sg-ignore https://github.com/castwide/solargraph/pull/1258 @pins.keep_if { |pin| pin.location.nil? || File.file?(pin.location.filename) } if @spec @pins end private + # @return [ApiMap::Store] def core_store @core_store ||= ApiMap::Store.new(RbsMap::Core.new.pins) end @@ -45,13 +46,17 @@ def generate_pins code_object nspin = namespace_with_bug_fix(code_object) @namespace_pins[code_object.path] = nspin result.push nspin + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check 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. + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check superclass = if code_object.superclass.is_a?(YARD::CodeObjects::Proxy) + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check "::#{code_object.superclass}" else + # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check code_object.superclass.to_s end # YARD fills in `Object` for every class whose definition it never @@ -65,11 +70,9 @@ def generate_pins code_object source: :yard_map) end 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 @@ -80,7 +83,6 @@ def generate_pins code_object when YARD::CodeObjects::MethodObject closure = @namespace_pins[code_object.namespace.to_s] macros_for_method_object(code_object) - # @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) @@ -110,7 +112,7 @@ def attached_macros_by_method_object # @param method_object [YARD::CodeObjects::MethodObject] # @return [Array] def macros_for_method_object method_object - attached_macros_by_method_object[method_object] + attached_macros_by_method_object[method_object] || [] end # Handle a bug in YARD where code that opens a constant's singleton class diff --git a/lib/solargraph/yard_map/mapper/to_method.rb b/lib/solargraph/yard_map/mapper/to_method.rb index 8d32627878..ad5acf5ebf 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -26,12 +26,9 @@ 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]] - # @sg-ignore Need to add nil check here if closure.path == 'Kernel' && Kernel.private_method_defined?(name.to_sym, false) final_visibility ||= :private end @@ -55,7 +52,6 @@ def self.make code_object, name = nil, scope = nil, visibility = nil, closure = source: :yardoc ) else - # @sg-ignore Need to add nil check here pin = Pin::Method.new( location: location, closure: closure, @@ -106,21 +102,27 @@ def get_parameters code_object, location, comments, pin end end - # @param a [Array] + # @param a [Array(String, String | nil)] # @return [String] + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 def arg_name a + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 a[0].gsub(/[^a-z0-9_]/i, '') end - # @param a [Array] + # @param a [Array(String, String | nil)] # @return [::Symbol] def arg_type a + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 if a[0].start_with?('**') :kwrestarg + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 elsif a[0].start_with?('*') :restarg + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 elsif a[0].start_with?('&') :blockarg + # @sg-ignore https://github.com/castwide/solargraph/pull/1231 elsif a[0].end_with?(':') a[1] ? :kwoptarg : :kwarg elsif a[1] diff --git a/lib/solargraph/yard_map/mapper/to_namespace.rb b/lib/solargraph/yard_map/mapper/to_namespace.rb index b073e18926..c324ddd437 100644 --- a/lib/solargraph/yard_map/mapper/to_namespace.rb +++ b/lib/solargraph/yard_map/mapper/to_namespace.rb @@ -21,7 +21,6 @@ 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 ) diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 5f189fb736..ab35b9b003 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -9,6 +9,8 @@ module Solargraph module Yardoc module_function + # @param metagem [Metagem] + # @return [String] def path_for metagem File.join(CacheDir.yard_dir, "#{metagem.cache_name}.yardoc") end @@ -21,10 +23,17 @@ def cache metagem, force: false Solargraph.logger.info "Caching yardoc for #{metagem.cache_name}" path = path_for(metagem) + # @sg-ignore https://github.com/castwide/solargraph/pull/1281 FileUtils.mkdir_p File.dirname(path) - cmd = ['yardoc', '--db', path, '--no-output', '--plugin', 'solargraph', '--plugin', 'activesupport-concern'] + # Resolve the actual executable instead of relying on a bare `yardoc` + # being on PATH - it may only exist inside the current bundle's own bin + # directory, which a subprocess spawned outside `bundle exec` does not + # inherit. + cmd = [Gem.bin_path('yard', 'yardoc'), '--db', path, '--no-output', + '--plugin', 'solargraph', '--plugin', 'activesupport-concern'] Solargraph.logger.debug "Running: #{cmd.inspect}" output, status = Open3.capture2e(*cmd, chdir: metagem.full_path) + # @sg-ignore https://github.com/castwide/solargraph/pull/1223 return if status.success? Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{metagem.full_path}" } @@ -38,6 +47,7 @@ def cache metagem, force: false # @param metagem [Metagem] # @return [void] def uncache metagem + # @sg-ignore https://github.com/castwide/solargraph/pull/1281 FileUtils.rm_rf path_for(metagem) end diff --git a/rbs/fills/rubocop/0/options.rbs b/rbs/fills/rubocop/0/options.rbs new file mode 100644 index 0000000000..ce82b0c44c --- /dev/null +++ b/rbs/fills/rubocop/0/options.rbs @@ -0,0 +1,7 @@ +# The rubocop 1.57 RBS in gem_rbs_collection has no RuboCop::Options. +# https://github.com/ruby/gem_rbs_collection/pull/1099 +module RuboCop + class Options + def parse: (Array[String] command_line_args) -> [Hash[Symbol, untyped], Array[String]] + end +end diff --git a/rbs/fills/rubygems/0/specification.rbs b/rbs/fills/rubygems/0/specification.rbs index ff51ef0b1d..ef0f55a394 100644 --- a/rbs/fills/rubygems/0/specification.rbs +++ b/rbs/fills/rubygems/0/specification.rbs @@ -886,7 +886,7 @@ class Gem::Specification < Gem::BasicSpecification # --> # Loads Ruby format gemspec from `file`. # - def self.load: (untyped file) -> (nil | untyped) + def self.load: (untyped file) -> Gem::Specification? #