diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b0f22ec3e..cfef25a97 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -1,16 +1,21 @@ -name: Plugin Backwards Compatibility Tests +--- +name: Plugin + +# To debug locally: +# npm install -g act +# cd /Users/broz/src/solargraph/ && act pull_request -j run_solargraph_rails_specs # e.g. on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] permissions: contents: read jobs: - test: + regression: runs-on: ubuntu-latest steps: @@ -19,7 +24,7 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: '3.0' - bundler-cache: false + bundler-cache: true - uses: awalsh128/cache-apt-pkgs-action@latest with: packages: yq @@ -29,14 +34,154 @@ jobs: echo 'gem "solargraph-rails"' > .Gemfile echo 'gem "solargraph-rspec"' >> .Gemfile bundle install + bundle update 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: Install gem types + run: bundle exec rbs collection update + - name: Ensure typechecking still works + run: bundle exec solargraph typecheck --level typed + - name: Ensure specs still run + run: bundle exec rake spec + rails: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.0' + 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 + bundle install + bundle update rbs - name: Configure to use plugins run: | bundle exec solargraph config yq -yi '.plugins += ["solargraph-rails"]' .solargraph.yml + - name: Install gem types + run: bundle exec rbs collection update + - name: Ensure typechecking still works + run: bundle exec solargraph typecheck --level typed + - name: Ensure specs still run + run: bundle exec rake spec + rspec: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.0' + bundler-cache: false + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: yq + version: 1.0 + - name: Install gems + run: | + echo 'gem "solargraph-rspec"' >> .Gemfile + bundle install + bundle update rbs + - name: Configure to use plugins + run: | + bundle exec solargraph config yq -yi '.plugins += ["solargraph-rspec"]' .solargraph.yml - name: Install gem types - run: bundle exec rbs collection install + run: bundle exec rbs collection update - name: Ensure typechecking still works run: bundle exec solargraph typecheck --level typed - name: Ensure specs still run run: bundle exec rake spec + + # run_solargraph_rspec_specs: + # # check out solargraph-rspec as well as this project, and point the former to use the latter as a local gem + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - name: clone https://github.com/lekemula/solargraph-rspec/ + # run: | + # cd .. + # git clone https://github.com/lekemula/solargraph-rspec.git + # cd solargraph-rspec + # - name: Set up Ruby + # uses: ruby/setup-ruby@v1 + # with: + # ruby-version: '3.0' + # bundler-cache: false + # - name: Install gems + # run: | + # cd ../solargraph-rspec + # echo "gem 'solargraph', path: '../solargraph'" >> Gemfile + # bundle install + # - name: Run specs + # run: | + # cd ../solargraph-rspec + # bundle exec rake spec + + run_solargraph_rails_specs: + # check out solargraph-rails as well as this project, and point the former to use the latter as a local gem + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: clone solargraph-rails + run: | + cd .. + git clone https://github.com/iftheshoefritz/solargraph-rails.git + cd solargraph-rails + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + # solargraph-rails supports Ruby 3.0+ + ruby-version: '3.0' + bundler-cache: false + bundler: latest + env: + MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' + 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 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 + env: + MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' + MATRIX_RAILS_VERSION: "7.0" + MATRIX_RAILS_MAJOR_VERSION: '7' + - name: Run specs + run: | + BUNDLE_PATH="${GITHUB_WORKSPACE:?}/vendor/bundle" + export BUNDLE_PATH + cd ../solargraph-rails + bundle exec solargraph --version + bundle info solargraph + bundle info rbs + bundle info yard + bundle exec rake spec + env: + MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' + MATRIX_RAILS_VERSION: "7.0" diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 35f7a1d13..175d8684e 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -21,14 +21,29 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', 'head'] + ruby-version: + - '3.0' + - '3.1' + - '3.2' + - '3.3' + - '3.4' + # - 'head' - see https://github.com/castwide/solargraph/issues/1022 rbs-version: ['3.6.1', '3.9.4', '4.0.0.dev.4'] # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 exclude: + # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 - ruby-version: '3.0' rbs-version: '3.9.4' - ruby-version: '3.0' rbs-version: '4.0.0.dev.4' + # Missing require in 'rbs collection update' - hopefully + # fixed in next RBS release + - ruby-version: 'head' + rbs-version: '4.0.0.dev.4' + - ruby-version: 'head' + rbs-version: '3.9.4' + - ruby-version: 'head' + rbs-version: '3.6.1' steps: - uses: actions/checkout@v3 - name: Set up Ruby @@ -47,7 +62,8 @@ jobs: - name: Install gems run: | bundle install - bundle update rbs # use latest available for this Ruby version + - name: Install types + run: bundle exec rbs collection update - name: Run tests run: bundle exec rake spec undercover: @@ -64,7 +80,11 @@ jobs: ruby-version: '3.4' bundler-cache: false - name: Install gems - run: bundle install + run: | + bundle install + bundle update rbs # use latest available for this Ruby version + - name: Install types + run: bundle exec rbs collection update - name: Run tests run: bundle exec rake spec - name: Check PR coverage diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 0ae8a3d8a..2a7068116 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -36,4 +36,4 @@ jobs: - name: Install gem types run: bundle exec rbs collection install - name: Typecheck self - run: SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level typed + run: SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strong diff --git a/.rubocop.yml b/.rubocop.yml index a73324db2..6ec069a5b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -34,6 +34,10 @@ Metrics/ParameterLists: Max: 7 CountKeywordArgs: false +# we don't use the spec/solargraph directory, instead keeping things +# directly under spec. +RSpec/SpecFilePathFormat: + Enabled: false # we tend to use @@ and the risk doesn't seem high Style/ClassVars: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d8283c5c6..245cec1bb 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,51 +1,44 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.79.2. +# using RuboCop version 1.80.0. # 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: Include. -# Include: **/*.gemspec Gemspec/AddRuntimeDependency: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/DeprecatedAttributeAssignment: Exclude: - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' -# Configuration parameters: EnforcedStyle, AllowedGems, Include. +# Configuration parameters: EnforcedStyle, AllowedGems. # SupportedStyles: Gemfile, gems.rb, gemspec -# Include: **/*.gemspec, **/Gemfile, **/gems.rb Gemspec/DevelopmentDependencies: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. -# Include: **/*.gemspec +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation. Gemspec/OrderedDependencies: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/RequiredRubyVersion: Exclude: - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' @@ -71,7 +64,6 @@ Layout/BlockAlignment: Layout/ClosingHeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment. @@ -99,7 +91,6 @@ Layout/ElseAlignment: - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' - 'lib/solargraph/type_checker/rules.rb' - 'lib/solargraph/yard_map/mapper.rb' @@ -107,7 +98,6 @@ Layout/ElseAlignment: # Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. Layout/EmptyLineBetweenDefs: Exclude: - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/initialize.rb' - 'lib/solargraph/pin/delegated_method.rb' @@ -116,7 +106,6 @@ Layout/EmptyLines: Exclude: - 'lib/solargraph/bench.rb' - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - 'lib/solargraph/language_server/message/initialize.rb' - 'lib/solargraph/pin/delegated_method.rb' @@ -126,13 +115,6 @@ Layout/EmptyLines: - 'spec/pin/symbol_spec.rb' - 'spec/type_checker/levels/strict_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only -Layout/EmptyLinesAroundClassBody: - Exclude: - - 'lib/solargraph/rbs_map/core_map.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines @@ -159,7 +141,6 @@ Layout/EndAlignment: - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' - 'lib/solargraph/type_checker/rules.rb' - 'lib/solargraph/yard_map/mapper.rb' @@ -178,7 +159,6 @@ Layout/ExtraSpacing: Exclude: - 'lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb' - 'lib/solargraph/pin/closure.rb' - - 'lib/solargraph/pin/local_variable.rb' - 'lib/solargraph/rbs_map/conversions.rb' - 'lib/solargraph/type_checker.rb' - 'spec/spec_helper.rb' @@ -236,7 +216,6 @@ Layout/HashAlignment: Layout/HeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - 'spec/yard_map/mapper/to_method_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -252,7 +231,6 @@ Layout/IndentationWidth: - 'lib/solargraph/parser/parser_gem/node_processors/block_node.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/rbs_map.rb' - 'lib/solargraph/shell.rb' - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source_map/clip.rb' @@ -306,7 +284,6 @@ Layout/MultilineMethodCallIndentation: # SupportedStyles: aligned, indented Layout/MultilineOperationIndentation: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/language_server/host/dispatch.rb' - 'lib/solargraph/source.rb' @@ -340,7 +317,6 @@ Layout/SpaceAroundOperators: Exclude: - 'lib/solargraph/library.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin/local_variable.rb' - 'lib/solargraph/source.rb' - 'lib/solargraph/source/change.rb' - 'lib/solargraph/source/cursor.rb' @@ -348,7 +324,6 @@ Layout/SpaceAroundOperators: - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/workspace/config.rb' - 'spec/library_spec.rb' - - 'spec/yard_map/mapper_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. @@ -399,7 +374,6 @@ Layout/SpaceBeforeComma: # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideBlockBraces: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/diagnostics/update_errors.rb' - 'lib/solargraph/language_server/host.rb' @@ -491,10 +465,6 @@ Lint/AssignmentInCondition: Exclude: - 'lib/solargraph/library.rb' -Lint/BinaryOperatorWithIdenticalOperands: - Exclude: - - 'lib/solargraph/api_map/source_to_yard.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Lint/BooleanSymbol: Exclude: @@ -523,7 +493,6 @@ Lint/DuplicateMethods: - 'lib/solargraph/pin/base.rb' - 'lib/solargraph/pin/signature.rb' - 'lib/solargraph/rbs_map.rb' - - 'lib/solargraph/rbs_map/core_map.rb' - 'lib/solargraph/source/chain/link.rb' # Configuration parameters: AllowComments, AllowEmptyLambdas. @@ -571,7 +540,6 @@ Lint/NonAtomicFileOperation: # This cop supports safe autocorrection (--autocorrect). Lint/ParenthesesAsGroupedExpression: Exclude: - - 'lib/solargraph.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'spec/language_server/host_spec.rb' - 'spec/source_map/clip_spec.rb' @@ -627,7 +595,7 @@ Lint/UnmodifiedReduceAccumulator: - 'lib/solargraph/pin/method.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Exclude: - 'lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb' @@ -635,16 +603,14 @@ Lint/UnusedBlockArgument: - 'spec/language_server/transport/data_reader_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. +# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. # NotImplementedExceptions: NotImplementedError Lint/UnusedMethodArgument: Exclude: - - 'lib/solargraph.rb' - 'lib/solargraph/complex_type/type_methods.rb' - 'lib/solargraph/convention/base.rb' - 'lib/solargraph/diagnostics/base.rb' - 'lib/solargraph/diagnostics/update_errors.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/pin/namespace.rb' - 'lib/solargraph/rbs_map/conversions.rb' - 'lib/solargraph/source.rb' @@ -665,16 +631,8 @@ Lint/UnusedMethodArgument: - 'spec/doc_map_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, ContextCreatingMethods, MethodCreatingMethods. -Lint/UselessAccessModifier: - Exclude: - - 'lib/solargraph/api_map.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. Lint/UselessAssignment: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - 'lib/solargraph/language_server/message/extended/document_gems.rb' @@ -703,7 +661,6 @@ Lint/UselessConstantScoping: - 'lib/solargraph/rbs_map/conversions.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect. Lint/UselessMethodDefinition: Exclude: - 'lib/solargraph/pin/signature.rb' @@ -714,7 +671,6 @@ Metrics/AbcSize: - 'lib/solargraph/api_map.rb' - 'lib/solargraph/api_map/source_to_yard.rb' - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/language_server/message/initialize.rb' - 'lib/solargraph/library.rb' @@ -759,6 +715,7 @@ Metrics/ClassLength: # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/CyclomaticComplexity: Exclude: + - 'lib/solargraph/api_map.rb' - 'lib/solargraph/api_map/source_to_yard.rb' - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' @@ -778,14 +735,12 @@ Metrics/MethodLength: - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ModuleLength: Exclude: - 'lib/solargraph/complex_type/type_methods.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin_cache.rb' # Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: @@ -832,7 +787,6 @@ Naming/MemoizedInstanceVariableName: - 'lib/solargraph/convention/gemfile.rb' - 'lib/solargraph/convention/gemspec.rb' - 'lib/solargraph/convention/rakefile.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/rbs_map.rb' - 'lib/solargraph/workspace.rb' @@ -886,7 +840,6 @@ Naming/VariableName: RSpec/Be: Exclude: - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - 'spec/source/source_chainer_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). @@ -909,7 +862,6 @@ RSpec/BeforeAfterAll: - '**/spec/rails_helper.rb' - '**/spec/support/**/*.rb' - 'spec/api_map_spec.rb' - - 'spec/doc_map_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/protocol_spec.rb' @@ -922,10 +874,6 @@ RSpec/ContextWording: - 'spec/pin/method_spec.rb' - 'spec/pin/parameter_spec.rb' - 'spec/pin/symbol_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: @@ -946,7 +894,6 @@ RSpec/DescribedClass: - 'spec/api_map/cache_spec.rb' - 'spec/api_map/source_to_yard_spec.rb' - 'spec/api_map/store_spec.rb' - - 'spec/api_map_spec.rb' - 'spec/diagnostics/base_spec.rb' - 'spec/diagnostics/require_not_found_spec.rb' - 'spec/diagnostics/rubocop_helpers_spec.rb' @@ -955,7 +902,6 @@ RSpec/DescribedClass: - 'spec/diagnostics/update_errors_spec.rb' - 'spec/diagnostics_spec.rb' - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - 'spec/language_server/host/diagnoser_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/host/message_worker_spec.rb' @@ -1004,7 +950,6 @@ RSpec/DescribedClass: - 'spec/source_map/mapper_spec.rb' - 'spec/source_map_spec.rb' - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - 'spec/type_checker/levels/normal_spec.rb' - 'spec/type_checker/levels/strict_spec.rb' - 'spec/type_checker/rules_spec.rb' @@ -1015,7 +960,6 @@ RSpec/DescribedClass: - 'spec/yard_map/mapper_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect. RSpec/EmptyExampleGroup: Exclude: - 'spec/convention_spec.rb' @@ -1060,7 +1004,6 @@ RSpec/ExampleLength: # DisallowedExamples: works RSpec/ExampleWording: Exclude: - - 'spec/convention/struct_definition_spec.rb' - 'spec/pin/base_spec.rb' - 'spec/pin/method_spec.rb' @@ -1101,7 +1044,6 @@ RSpec/ImplicitExpect: RSpec/InstanceVariable: Exclude: - 'spec/api_map/config_spec.rb' - - 'spec/api_map_spec.rb' - 'spec/diagnostics/require_not_found_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/host_spec.rb' @@ -1117,16 +1059,10 @@ RSpec/LeakyConstantDeclaration: - 'spec/complex_type_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. RSpec/LetBeforeExamples: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: . -# SupportedStyles: have_received, receive -RSpec/MessageSpies: - EnforcedStyle: receive - RSpec/MissingExampleGroupArgument: Exclude: - 'spec/diagnostics/rubocop_helpers_spec.rb' @@ -1145,8 +1081,6 @@ RSpec/MultipleExpectations: - 'spec/diagnostics/type_check_spec.rb' - 'spec/diagnostics/update_errors_spec.rb' - 'spec/diagnostics_spec.rb' - - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - 'spec/language_server/host/message_worker_spec.rb' - 'spec/language_server/host_spec.rb' - 'spec/language_server/message/completion_item/resolve_spec.rb' @@ -1173,7 +1107,6 @@ RSpec/MultipleExpectations: - 'spec/rbs_map/core_map_spec.rb' - 'spec/rbs_map/stdlib_map_spec.rb' - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - 'spec/source/chain/call_spec.rb' - 'spec/source/chain/class_variable_spec.rb' - 'spec/source/chain/global_variable_spec.rb' @@ -1187,7 +1120,6 @@ RSpec/MultipleExpectations: - 'spec/source_map/node_processor_spec.rb' - 'spec/source_map_spec.rb' - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - 'spec/type_checker/levels/normal_spec.rb' - 'spec/type_checker/levels/strict_spec.rb' - 'spec/type_checker/levels/strong_spec.rb' @@ -1198,6 +1130,11 @@ RSpec/MultipleExpectations: - 'spec/yard_map/mapper/to_method_spec.rb' - 'spec/yard_map/mapper_spec.rb' +# Configuration parameters: AllowSubject, Max. +RSpec/MultipleMemoizedHelpers: + Exclude: + - 'spec/shell_spec.rb' + # Configuration parameters: Max, AllowedGroups. RSpec/NestedGroups: Exclude: @@ -1212,7 +1149,6 @@ RSpec/NoExpectationExample: - 'spec/pin/block_spec.rb' - 'spec/pin/method_spec.rb' - 'spec/source/chain/call_spec.rb' - - 'spec/type_checker/checks_spec.rb' - 'spec/type_checker/levels/typed_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -1220,7 +1156,6 @@ RSpec/NoExpectationExample: # SupportedStyles: not_to, to_not RSpec/NotToNot: Exclude: - - 'spec/api_map_spec.rb' - 'spec/rbs_map/core_map_spec.rb' RSpec/PendingWithoutReason: @@ -1256,7 +1191,6 @@ RSpec/RemoveConst: RSpec/RepeatedDescription: Exclude: - - 'spec/api_map_spec.rb' - 'spec/language_server/protocol_spec.rb' - 'spec/parser/node_methods_spec.rb' - 'spec/source/chain/call_spec.rb' @@ -1275,112 +1209,10 @@ RSpec/RepeatedExample: - 'spec/type_checker/levels/strict_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. RSpec/ScatteredLet: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. -# Include: **/*_spec.rb -RSpec/SpecFilePathFormat: - Exclude: - - '**/spec/routing/**/*' - - 'spec/api_map/cache_spec.rb' - - 'spec/api_map/config_spec.rb' - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/api_map/store_spec.rb' - - 'spec/api_map_spec.rb' - - 'spec/convention/activesupport_concern_spec.rb' - - 'spec/convention/struct_definition_spec.rb' - - 'spec/convention_spec.rb' - - 'spec/diagnostics/base_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/diagnostics/rubocop_helpers_spec.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/diagnostics/type_check_spec.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/diagnostics_spec.rb' - - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/message/text_document/formatting_spec.rb' - - 'spec/language_server/message/text_document/hover_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message/text_document/type_definition_spec.rb' - - 'spec/language_server/message/workspace/did_change_configuration_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/message_spec.rb' - - 'spec/language_server/transport/adapter_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/language_server/uri_helpers_spec.rb' - - 'spec/library_spec.rb' - - 'spec/logging_spec.rb' - - 'spec/parser/flow_sensitive_typing_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/parser_spec.rb' - - 'spec/pin/base_spec.rb' - - 'spec/pin/base_variable_spec.rb' - - 'spec/pin/block_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/pin/delegated_method_spec.rb' - - 'spec/pin/documenting_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/pin/keyword_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/pin/search_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/position_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - - 'spec/source/chain/array_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/chain/constant_spec.rb' - - 'spec/source/chain/global_variable_spec.rb' - - 'spec/source/chain/head_spec.rb' - - 'spec/source/chain/instance_variable_spec.rb' - - 'spec/source/chain/link_spec.rb' - - 'spec/source/chain/literal_spec.rb' - - 'spec/source/chain/z_super_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source/change_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source/updater_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' - - 'spec/type_checker/rules_spec.rb' - - 'spec/type_checker_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' - - 'spec/yard_map/mapper_spec.rb' - -RSpec/StubbedMock: - Exclude: - - 'spec/language_server/host/message_worker_spec.rb' - # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: @@ -1436,7 +1268,6 @@ Style/AccessorGrouping: # SupportedStyles: always, conditionals Style/AndOr: Exclude: - - 'lib/solargraph/api_map/source_to_yard.rb' - 'lib/solargraph/complex_type/unique_type.rb' - 'lib/solargraph/language_server/message/base.rb' - 'lib/solargraph/page.rb' @@ -1625,7 +1456,6 @@ Style/Documentation: - 'lib/solargraph/parser.rb' - 'lib/solargraph/parser/comment_ripper.rb' - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - 'lib/solargraph/parser/node_processor/base.rb' - 'lib/solargraph/parser/parser_gem.rb' - 'lib/solargraph/parser/parser_gem/class_methods.rb' @@ -1711,7 +1541,7 @@ Style/EmptyLambdaParameter: - 'spec/rbs_map/core_map_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, EnforcedStyle. +# Configuration parameters: EnforcedStyle. # SupportedStyles: compact, expanded Style/EmptyMethod: Exclude: @@ -1766,7 +1596,6 @@ Style/FrozenStringLiteralComment: - 'lib/solargraph/parser.rb' - 'lib/solargraph/parser/comment_ripper.rb' - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - 'lib/solargraph/parser/parser_gem.rb' - 'lib/solargraph/parser/snippet.rb' - 'lib/solargraph/pin/breakable.rb' @@ -1779,7 +1608,6 @@ Style/FrozenStringLiteralComment: - 'spec/api_map/cache_spec.rb' - 'spec/api_map/config_spec.rb' - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/api_map_spec.rb' - 'spec/complex_type_spec.rb' - 'spec/convention/struct_definition_spec.rb' - 'spec/convention_spec.rb' @@ -1851,7 +1679,6 @@ Style/FrozenStringLiteralComment: - 'spec/rbs_map/core_map_spec.rb' - 'spec/rbs_map/stdlib_map_spec.rb' - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - 'spec/source/chain/array_spec.rb' - 'spec/source/chain/call_spec.rb' - 'spec/source/chain/class_variable_spec.rb' @@ -1873,7 +1700,6 @@ Style/FrozenStringLiteralComment: - 'spec/source_map_spec.rb' - 'spec/source_spec.rb' - 'spec/spec_helper.rb' - - 'spec/type_checker/checks_spec.rb' - 'spec/type_checker/levels/normal_spec.rb' - 'spec/type_checker/levels/strict_spec.rb' - 'spec/type_checker/levels/strong_spec.rb' @@ -1900,7 +1726,6 @@ Style/GuardClause: - 'lib/solargraph/api_map.rb' - 'lib/solargraph/library.rb' - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin_cache.rb' - 'lib/solargraph/range.rb' - 'lib/solargraph/rbs_map/conversions.rb' - 'lib/solargraph/source.rb' @@ -1950,23 +1775,17 @@ Style/IfInsideElse: # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/index.rb' - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/completion_item/resolve.rb' - 'lib/solargraph/language_server/message/initialize.rb' - 'lib/solargraph/language_server/message/text_document/completion.rb' - 'lib/solargraph/language_server/message/text_document/hover.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/class_methods.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' - 'lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb' - 'lib/solargraph/pin/base.rb' - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/common.rb' - 'lib/solargraph/pin/constant.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/parameter.rb' @@ -1981,7 +1800,6 @@ Style/IfUnlessModifier: - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' - 'lib/solargraph/type_checker.rb' - - 'lib/solargraph/workspace.rb' - 'lib/solargraph/workspace/config.rb' - 'lib/solargraph/yard_map/helpers.rb' - 'lib/solargraph/yard_map/mapper/to_method.rb' @@ -1998,7 +1816,6 @@ Style/MapIntoArray: Exclude: - 'lib/solargraph/diagnostics/update_errors.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/type_checker/param_def.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Style/MapToHash: @@ -2031,7 +1848,6 @@ Style/MethodDefParentheses: - 'lib/solargraph/convention/struct_definition/struct_assignment_node.rb' - 'lib/solargraph/convention/struct_definition/struct_definition_node.rb' - 'lib/solargraph/diagnostics/rubocop_helpers.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/equality.rb' - 'lib/solargraph/gem_pins.rb' - 'lib/solargraph/language_server/host/message_worker.rb' @@ -2040,7 +1856,6 @@ Style/MethodDefParentheses: - 'lib/solargraph/location.rb' - 'lib/solargraph/parser/comment_ripper.rb' - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - 'lib/solargraph/parser/node_processor/base.rb' - 'lib/solargraph/parser/parser_gem/flawed_builder.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' @@ -2066,9 +1881,7 @@ Style/MethodDefParentheses: - 'lib/solargraph/source_map.rb' - 'lib/solargraph/source_map/mapper.rb' - 'lib/solargraph/type_checker.rb' - - 'lib/solargraph/type_checker/checks.rb' - 'lib/solargraph/yard_map/helpers.rb' - - 'lib/solargraph/yardoc.rb' - 'spec/doc_map_spec.rb' - 'spec/fixtures/rdoc-lib/lib/example.rb' - 'spec/source_map_spec.rb' @@ -2076,7 +1889,6 @@ Style/MethodDefParentheses: - 'spec/type_checker/levels/normal_spec.rb' - 'spec/type_checker/levels/strict_spec.rb' - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' Style/MultilineBlockChain: Exclude: @@ -2135,13 +1947,6 @@ Style/NegatedIfElseCondition: - 'lib/solargraph/shell.rb' - 'lib/solargraph/type_checker.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods. -# AllowedMethods: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with -Style/NestedParenthesizedCalls: - Exclude: - - 'lib/solargraph/type_checker.rb' - # This cop supports safe autocorrection (--autocorrect). Style/NestedTernaryOperator: Exclude: @@ -2156,7 +1961,6 @@ Style/Next: - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - 'lib/solargraph/pin/signature.rb' - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/type_checker/checks.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinDigits, Strict, AllowedNumbers, AllowedPatterns. @@ -2237,7 +2041,7 @@ Style/RedundantBegin: - 'lib/solargraph/shell.rb' - 'lib/solargraph/source/cursor.rb' - 'lib/solargraph/source/encoding_fixes.rb' - - 'lib/solargraph/type_checker.rb' + - 'lib/solargraph/source_map/mapper.rb' - 'lib/solargraph/workspace.rb' # This cop supports safe autocorrection (--autocorrect). @@ -2251,16 +2055,9 @@ Style/RedundantFreeze: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/source_map/mapper.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect, AllowComments. -Style/RedundantInitialize: - Exclude: - - 'lib/solargraph/rbs_map/core_map.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: - - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/source_map/mapper.rb' @@ -2284,13 +2081,24 @@ Style/RedundantRegexpArgument: - 'spec/diagnostics/rubocop_spec.rb' - 'spec/language_server/host_spec.rb' +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantRegexpCharacterClass: + Exclude: + - 'lib/solargraph/source/cursor.rb' + - 'lib/solargraph/source/source_chainer.rb' + # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpEscape: Exclude: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/diagnostics/rubocop.rb' - 'lib/solargraph/language_server/uri_helpers.rb' + - 'lib/solargraph/parser/parser_gem/node_methods.rb' + - 'lib/solargraph/pin/method.rb' + - 'lib/solargraph/pin/parameter.rb' - 'lib/solargraph/shell.rb' + - 'lib/solargraph/source/change.rb' + - 'lib/solargraph/source/cursor.rb' - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' @@ -2300,7 +2108,6 @@ Style/RedundantReturn: Exclude: - 'lib/solargraph/api_map.rb' - 'lib/solargraph/complex_type/type_methods.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' - 'lib/solargraph/source/chain/z_super.rb' @@ -2337,7 +2144,6 @@ Style/RescueStandardError: Style/SafeNavigation: Exclude: - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/completion_item/resolve.rb' - 'lib/solargraph/language_server/request.rb' - 'lib/solargraph/language_server/transport/data_reader.rb' @@ -2345,15 +2151,9 @@ Style/SafeNavigation: - 'lib/solargraph/pin/base.rb' - 'lib/solargraph/pin/conversions.rb' - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin_cache.rb' - 'lib/solargraph/range.rb' - 'lib/solargraph/type_checker.rb' -# Configuration parameters: Max. -Style/SafeNavigationChainLength: - Exclude: - - 'lib/solargraph/doc_map.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: Exclude: @@ -2374,7 +2174,6 @@ Style/SlicingWithRange: - 'lib/solargraph/source/cursor.rb' - 'lib/solargraph/source/source_chainer.rb' - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker/checks.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowModifier. @@ -2418,7 +2217,6 @@ Style/StringLiterals: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - 'lib/solargraph/convention/struct_definition.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/language_server/message/extended/document_gems.rb' - 'lib/solargraph/language_server/message/extended/download_core.rb' @@ -2619,7 +2417,6 @@ Style/WordArray: - 'spec/pin/method_spec.rb' - 'spec/source/cursor_spec.rb' - 'spec/source/source_chainer_spec.rb' - - 'spec/source_map/clip_spec.rb' - 'spec/source_map/mapper_spec.rb' - 'spec/source_map_spec.rb' - 'spec/source_spec.rb' @@ -2653,7 +2450,6 @@ YARD/MismatchName: Exclude: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/gem_pins.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/language_server/host/dispatch.rb' - 'lib/solargraph/language_server/request.rb' @@ -2674,7 +2470,6 @@ YARD/MismatchName: - 'lib/solargraph/pin/until.rb' - 'lib/solargraph/pin/while.rb' - 'lib/solargraph/pin_cache.rb' - - 'lib/solargraph/source/chain.rb' - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source/chain/z_super.rb' - 'lib/solargraph/type_checker.rb' @@ -2696,7 +2491,6 @@ Layout/LineLength: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - 'lib/solargraph/convention/data_definition.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/gem_pins.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' @@ -2721,7 +2515,6 @@ Layout/LineLength: - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - 'lib/solargraph/pin/base.rb' - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/common.rb' - 'lib/solargraph/pin/documenting.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/parameter.rb' @@ -2741,10 +2534,8 @@ Layout/LineLength: - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' - 'lib/solargraph/type_checker.rb' - - 'lib/solargraph/workspace.rb' - 'lib/solargraph/workspace/config.rb' - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'spec/api_map_spec.rb' - 'spec/complex_type_spec.rb' - 'spec/language_server/message/completion_item/resolve_spec.rb' - 'spec/language_server/message/extended/check_gem_version_spec.rb' @@ -2754,4 +2545,3 @@ Layout/LineLength: - 'spec/source/chain_spec.rb' - 'spec/source_map/clip_spec.rb' - 'spec/source_map_spec.rb' - - 'spec/workspace_spec.rb' diff --git a/Gemfile b/Gemfile index 3a5ae2b84..2da498052 100755 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,9 @@ source 'https://rubygems.org' gemspec name: 'solargraph' +# allow rubocop-yard to understand literal symbols in type annotations +gem 'yard', github: 'apiology/yard', branch: 'literal_symbols', require: false + # Local gemfile for development tools, etc. local_gemfile = File.expand_path(".Gemfile", __dir__) instance_eval File.read local_gemfile if File.exist? local_gemfile diff --git a/README.md b/README.md index 3e94a60b9..f1b704bed 100755 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ Plug-ins and extensions are available for the following editors: Solargraph's behavior can be controlled via optional [configuration](https://solargraph.org/guides/configuration) files. The highest priority file is a `.solargraph.yml` file at the root of the project. If not present, any global configuration at `~/.config/solargraph/config.yml` will apply. The path to the global configuration can be overridden with the `SOLARGRAPH_GLOBAL_CONFIG` environment variable. +Use `bundle exec solargraph init` to create a configuration file. + ### Plugins Solargraph supports [plugins](https://solargraph.org/guides/plugins) that implement their own Solargraph features, such as diagnostics reporters and conventions to provide LSP features and type-checking, e.g. for frameworks which use metaprogramming and/or DSLs. diff --git a/Rakefile b/Rakefile index d731fc786..2e3f2bed0 100755 --- a/Rakefile +++ b/Rakefile @@ -9,7 +9,8 @@ task :console do end desc "Run the type checker" -task typecheck: [:typecheck_typed] + +task typecheck: [:typecheck_strong] desc "Run the type checker at typed level - return code issues provable without annotations being correct" task :typecheck_typed do @@ -133,5 +134,5 @@ end desc "Show quality checks on this development branch so far, including any staged files" task :overcommit do # OVERCOMMIT_DEBUG=1 will show more detail - sh 'SOLARGRAPH_ASSERTS=on bundle exec overcommit --run --diff origin/master' + sh 'SOLARGRAPH_ASSERTS=on bundle exec overcommit --run --diff origin/2025-07-02' end diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 038e7bccf..5aa391422 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -55,8 +55,7 @@ class InvalidRubocopVersionError < RuntimeError; end CHDIR_MUTEX = Mutex.new - # @param type [Symbol] Type of assert. - def self.asserts_on?(type) + def self.asserts_on? if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? false elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' @@ -72,7 +71,26 @@ def self.asserts_on?(type) # @param block [Proc] A block that returns a message to log # @return [void] def self.assert_or_log(type, msg = nil, &block) - raise (msg || block.call) if asserts_on?(type) && ![:combine_with_visibility].include?(type) + if asserts_on? + # @type [String, nil] + msg ||= block.call + + raise "No message given for #{type.inspect}" if msg.nil? + + # @todo :combine_with_visibility is not ready for prime time - + # lots of disagreements found in practice that heuristics need + # to be created for and/or debugging needs to resolve in pin + # generation. + # @todo :api_map_namespace_pin_stack triggers in a badly handled + # self type case - 'keeps track of self type in method + # parameters in subclass' in call_spec.rb + return if %i[api_map_namespace_pin_stack combine_with_visibility].include?(type) + + # conditional aliases to handle compatibility corner cases + return if type == :alias_target_missing && msg.include?('highline/compatibility.rb') + return if type == :alias_target_missing && msg.include?('lib/json/add/date.rb') + raise msg + end logger.info msg, &block end diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index eed02b4ef..4cd1cd121 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -22,6 +22,9 @@ class ApiMap # @return [Array] attr_reader :missing_docs + # @return [Solargraph::Workspace::Gemspecs] + attr_reader :gemspecs + # @param pins [Array] def initialize pins: [] @source_map_hash = {} @@ -30,6 +33,12 @@ def initialize pins: [] index pins end + # @param out [IO, nil] output stream for logging + # @return [void] + def self.reset_core out: nil + @@core_map = RbsMap::CoreMap.new + end + # # This is a mutable object, which is cached in the Chain class - # if you add any fields which change the results of calls (not @@ -37,16 +46,17 @@ def initialize pins: [] # # @param other [Object] - def eql?(other) + def eql? other self.class == other.class && equality_fields == other.equality_fields end # @param other [Object] - def ==(other) + def == other self.eql?(other) end + # @return [Integer] def hash equality_fields.hash end @@ -96,11 +106,12 @@ def catalog bench end unresolved_requires = (bench.external_requires + implicit.requires + bench.workspace.config.required).to_a.compact.uniq recreate_docmap = @unresolved_requires != unresolved_requires || - @doc_map&.uncached_yard_gemspecs&.any? || - @doc_map&.uncached_rbs_collection_gemspecs&.any? || - @doc_map&.rbs_collection_path != bench.workspace.rbs_collection_path + workspace.rbs_collection_path != bench.workspace.rbs_collection_path || + @doc_map.any_uncached? + if recreate_docmap - @doc_map = DocMap.new(unresolved_requires, [], bench.workspace) # @todo Implement gem preferences + @doc_map = DocMap.new(unresolved_requires, bench.workspace, out: nil) # @todo Implement gem preferences + @gemspecs = @doc_map.workspace.gemspecs @unresolved_requires = @doc_map.unresolved_requires end @cache.clear if store.update(@@core_map.pins, @doc_map.pins, implicit.pins, iced_pins, live_pins) @@ -115,26 +126,14 @@ def catalog bench [self.class, @source_map_hash, implicit, @doc_map, @unresolved_requires] end - # @return [DocMap] - def doc_map - @doc_map ||= DocMap.new([], []) - end + # @return [DocMap, nil] + attr_reader :doc_map # @return [::Array] def uncached_gemspecs @doc_map&.uncached_gemspecs || [] end - # @return [::Array] - def uncached_rbs_collection_gemspecs - @doc_map.uncached_rbs_collection_gemspecs - end - - # @return [::Array] - def uncached_yard_gemspecs - @doc_map.uncached_yard_gemspecs - end - # @return [Array] def core_pins @@core_map.pins @@ -178,6 +177,7 @@ def clip_at filename, position # Create an ApiMap with a workspace in the specified directory. # # @param directory [String] + # # @return [ApiMap] def self.load directory api_map = new @@ -191,8 +191,8 @@ def self.load directory # @param out [IO, nil] # @return [void] - def cache_all!(out) - @doc_map.cache_all!(out) + def cache_all_for_doc_map! out + @doc_map.cache_doc_map_gems!(out) end # @param gemspec [Gem::Specification] @@ -211,17 +211,17 @@ class << self # any missing gems. # # - # @param directory [String] + # @param directory [String] workspace directory # @param out [IO] The output stream for messages # @return [ApiMap] - def self.load_with_cache directory, out + def self.load_with_cache directory, out = $stderr api_map = load(directory) if api_map.uncached_gemspecs.empty? logger.info { "All gems cached for #{directory}" } return api_map end - api_map.cache_all!(out) + api_map.cache_all_for_doc_map!(out) load(directory) end @@ -237,13 +237,6 @@ def keyword_pins store.pins_by_class(Pin::Keyword) end - # An array of namespace names defined in the ApiMap. - # - # @return [Set] - def namespaces - store.namespaces - end - # True if the namespace exists. # # @param name [String] The namespace to match @@ -308,12 +301,11 @@ def qualify tag, context_tag = '' return unless type return tag if type.literal? - context_type = ComplexType.try_parse(context_tag) - return unless context_type - fqns = qualify_namespace(type.rooted_namespace, context_type.rooted_namespace) return unless fqns + return fqns if %w[Class Module].include? type.name + fqns + type.substring end @@ -327,7 +319,7 @@ def qualify tag, context_tag = '' # @param context_namespace [String] The context namespace in which the # tag was referenced; start from here to resolve the name # @return [String, nil] fully qualified namespace - def qualify_namespace(namespace, context_namespace = '') + def qualify_namespace namespace, context_namespace = '' cached = cache.get_qualified_namespace(namespace, context_namespace) return cached.clone unless cached.nil? result = if namespace.start_with?('::') @@ -341,13 +333,13 @@ def qualify_namespace(namespace, context_namespace = '') # @param fqns [String] # @return [Array] - def get_extends(fqns) + def get_extends fqns store.get_extends(fqns) end # @param fqns [String] # @return [Array] - def get_includes(fqns) + def get_includes fqns store.get_includes(fqns) end @@ -357,7 +349,7 @@ def get_includes(fqns) # @param namespace [String] A fully qualified namespace # @param scope [Symbol] :instance or :class # @return [Array] - def get_instance_variable_pins(namespace, scope = :instance) + def get_instance_variable_pins namespace, scope = :instance result = [] used = [namespace] result.concat store.get_instance_variables(namespace, scope) @@ -371,6 +363,9 @@ def get_instance_variable_pins(namespace, scope = :instance) end # @see Solargraph::Parser::FlowSensitiveTyping#visible_pins + # @param (see Solargraph::Parser::FlowSensitiveTyping#visible_pins) + # @return (see Solargraph::Parser::FlowSensitiveTyping#visible_pins) + # @sg-ignore Missing @return tag for Solargraph::ApiMap#visible_pins def visible_pins(*args, **kwargs, &blk) Solargraph::Parser::FlowSensitiveTyping.visible_pins(*args, **kwargs, &blk) end @@ -379,7 +374,7 @@ def visible_pins(*args, **kwargs, &blk) # # @param namespace [String] A fully qualified namespace # @return [Enumerable] - def get_class_variable_pins(namespace) + def get_class_variable_pins namespace prefer_non_nil_variables(store.get_class_variables(namespace)) end @@ -406,16 +401,18 @@ def get_block_pins # @param deep [Boolean] True to include superclasses, mixins, etc. # @return [Array] def get_methods rooted_tag, scope: :instance, visibility: [:public], deep: true + rooted_tag = qualify(rooted_tag, '') + return [] unless rooted_tag if rooted_tag.start_with? 'Array(' # Array() are really tuples - use our fill, as the RBS repo # does not give us definitions for it rooted_tag = "Solargraph::Fills::Tuple(#{rooted_tag[6..-2]})" end - rooted_type = ComplexType.try_parse(rooted_tag) - fqns = rooted_type.namespace - namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first cached = cache.get_methods(rooted_tag, scope, visibility, deep) return cached.clone unless cached.nil? + rooted_type = ComplexType.try_parse(rooted_tag) + fqns = rooted_type.namespace + namespace_pin = get_namespace_pin(fqns) # @type [Array] result = [] skip = Set.new @@ -533,12 +530,23 @@ def get_complex_type_methods complex_type, context = '', internal = false # @param name [String] Method name to look up # @param scope [Symbol] :instance or :class # @param visibility [Array] :public, :protected, and/or :private - # @param preserve_generics [Boolean] + # @param preserve_generics [Boolean] True to preserve any + # unresolved generic parameters, false to erase them # @return [Array] - def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], preserve_generics: false - rooted_type = ComplexType.parse(rooted_tag) + def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], + preserve_generics: false + rooted_tag = qualify(rooted_tag, '') + return [] unless rooted_tag + rooted_type = ComplexType.try_parse(rooted_tag) + return [] if rooted_type.nil? fqns = rooted_type.namespace - namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first + namespace_pin = get_namespace_pin(fqns) + if namespace_pin.nil? + # :nocov: + Solargraph.assert_or_log(:api_map_namespace_pin_stack, "Could not find namespace pin for #{fqns} while looking for method #{name}") + return [] + # :nocov: + end methods = get_methods(rooted_tag, scope: scope, visibility: visibility).select { |p| p.name == name } methods = erase_generics(namespace_pin, rooted_type, methods) unless preserve_generics methods @@ -549,7 +557,7 @@ def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, # @deprecated Use #get_path_pins instead. # # @param path [String] The path to find - # @return [Enumerable] + # @return [Array] def get_path_suggestions path return [] if path.nil? resolve_method_aliases store.get_path_pins(path) @@ -558,7 +566,7 @@ def get_path_suggestions path # Get an array of pins that match the specified path. # # @param path [String] - # @return [Enumerable] + # @return [Array] def get_path_pins path get_path_suggestions(path) end @@ -648,7 +656,7 @@ def bundled? filename # @param sup [String] The superclass # @param sub [String] The subclass # @return [Boolean] - def super_and_sub?(sup, sub) + def super_and_sub? sup, sub fqsup = qualify(sup) cls = qualify(sub) tested = [] @@ -667,7 +675,7 @@ def super_and_sub?(sup, sub) # @param module_ns [String] The module namespace (no type parameters) # # @return [Boolean] - def type_include?(host_ns, module_ns) + def type_include? host_ns, module_ns store.get_includes(host_ns).map { |inc_tag| ComplexType.parse(inc_tag).name }.include?(module_ns) end @@ -677,10 +685,15 @@ def type_include?(host_ns, module_ns) def resolve_method_aliases pins, visibility = [:public, :private, :protected] with_resolved_aliases = pins.map do |pin| resolved = resolve_method_alias(pin) - next nil if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility) + if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility) + Solargraph.assert_or_log(:alias_visibility) { "Rejecting alias - visibility of target is #{resolved.visibility}, looking for visibility #{visibility}" } + next nil + end resolved end.compact - logger.debug { "ApiMap#resolve_method_aliases(pins=#{pins.map(&:name)}, visibility=#{visibility}) => #{with_resolved_aliases.map(&:name)}" } + logger.debug do + "ApiMap#resolve_method_aliases(pins=#{pins.map(&:name)}, visibility=#{visibility}) => #{with_resolved_aliases.map(&:name)}" + end GemPins.combine_method_pins_by_path(with_resolved_aliases) end @@ -695,7 +708,7 @@ def resolve_method_aliases pins, visibility = [:public, :private, :protected] # @param skip [Set] # @param no_core [Boolean] Skip core classes if true # @return [Array] - def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scope, visibility, deep, skip, no_core) + def inner_get_methods_from_reference fq_reference_tag, namespace_pin, type, scope, visibility, deep, skip, no_core logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) starting" } # Ensure the types returned by the methods in the referenced @@ -709,7 +722,7 @@ def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scop # @todo Can inner_get_methods be cached? Lots of lookups of base types going on. methods = inner_get_methods(resolved_reference_type.tag, scope, visibility, deep, skip, no_core) if namespace_pin && !resolved_reference_type.all_params.empty? - reference_pin = store.get_path_pins(resolved_reference_type.name).select { |p| p.is_a?(Pin::Namespace) }.first + reference_pin = get_namespace_pin(resolved_reference_type.namespace) # logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) - resolving generics with #{reference_pin.generics}, #{resolved_reference_type.rooted_tags}" } methods = methods.map do |method_pin| method_pin.resolve_generics(reference_pin, resolved_reference_type) @@ -719,6 +732,11 @@ def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scop methods end + # @return [Workspace, nil] + def workspace + @doc_map&.workspace + end + private # A hash of source maps with filename keys. @@ -734,6 +752,13 @@ def store # @return [Solargraph::ApiMap::Cache] attr_reader :cache + # @param fqns [String] + # @return [Pin::Namespace, nil] + def get_namespace_pin fqns + # fqns = ComplexType.parse(fqns).namespace + store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first + end + # @param rooted_tag [String] A fully qualified namespace, with # generic parameter values if applicable # @param scope [Symbol] :class or :instance @@ -743,17 +768,32 @@ def store # @param no_core [Boolean] Skip core classes if true # @return [Array] def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false + rooted_tag = qualify(rooted_tag, '') + return [] if rooted_tag.nil? + return [] unless rooted_tag rooted_type = ComplexType.parse(rooted_tag).force_rooted fqns = rooted_type.namespace - fqns_generic_params = rooted_type.all_params - namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first + namespace_pin = get_namespace_pin(fqns) + if namespace_pin.nil? + # :nocov: + Solargraph.assert_or_log(:api_map_namespace_pin_inner, "Could not find namespace pin for #{fqns}") + return [] + # :nocov: + end return [] if no_core && fqns =~ /^(Object|BasicObject|Class|Module)$/ + # @todo should this by by rooted_tag_? reqstr = "#{fqns}|#{scope}|#{visibility.sort}|#{deep}" return [] if skip.include?(reqstr) skip.add reqstr result = [] + environ = Convention.for_object(self, rooted_tag, scope, visibility, deep, skip, no_core) - result.concat environ.pins + # ensure we start out with any immediate methods in this + # namespace so we roughly match the same ordering of get_methods + # and obey the 'deep' instruction + direct_convention_methods, convention_methods_by_reference = environ.pins.partition { |p| p.namespace == rooted_tag } + result.concat direct_convention_methods + if deep && scope == :instance store.get_prepends(fqns).reverse.each do |im| fqim = qualify(im, fqns) @@ -767,14 +807,20 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false logger.info { "ApiMap#inner_get_methods(rooted_tag=#{rooted_tag.inspect}, scope=#{scope.inspect}, visibility=#{visibility.inspect}, deep=#{deep.inspect}, skip=#{skip.inspect}, fqns=#{fqns}) - added from store: #{methods}" } result.concat methods if deep + result.concat convention_methods_by_reference + if scope == :instance store.get_includes(fqns).reverse.each do |include_tag| rooted_include_tag = qualify(include_tag, rooted_tag) - result.concat inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true) + if rooted_include_tag + result.concat inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type, scope, + visibility, deep, skip, true) + end end rooted_sc_tag = qualify_superclass(rooted_tag) unless rooted_sc_tag.nil? - result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, no_core) + result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, + visibility, true, skip, no_core) end else logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}, #{skip}) - looking for get_extends() from #{fqns}" } @@ -784,7 +830,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false end rooted_sc_tag = qualify_superclass(rooted_tag) unless rooted_sc_tag.nil? - result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, true) + result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, + visibility, true, skip, true) end unless no_core || fqns.empty? type = get_namespace_type(fqns) @@ -817,9 +864,7 @@ def inner_get_constants fqns, visibility, skip result.concat inner_get_constants(qualify(is, fqns), [:public], skip) end fqsc = qualify_superclass(fqns) - unless %w[Object BasicObject].include?(fqsc) - result.concat inner_get_constants(fqsc, [:public], skip) - end + result.concat inner_get_constants(fqsc, [:public], skip) unless %w[Object BasicObject].include?(fqsc) result end @@ -864,16 +909,21 @@ def inner_qualify name, root, skip if root == '' return '' else + root = root[2..-1] if root&.start_with?('::') return inner_qualify(root, '', skip) end else - return name if root == '' && store.namespace_exists?(name) roots = root.to_s.split('::') while roots.length > 0 - fqns = roots.join('::') + '::' + name - return fqns if store.namespace_exists?(fqns) - incs = store.get_includes(roots.join('::')) + potential_root = roots.join('::') + potential_root = potential_root[2..-1] if potential_root.start_with?('::') + potential_fqns = potential_root + '::' + name + potential_fqns = potential_fqns[2..-1] if potential_fqns.start_with?('::') + fqns = resolve_fqns(potential_fqns) + return fqns if fqns + incs = store.get_includes(potential_root) incs.each do |inc| + next if potential_root == root && inc == name foundinc = inner_qualify(name, inc, skip) possibles.push foundinc unless foundinc.nil? end @@ -886,11 +936,51 @@ def inner_qualify name, root, skip possibles.push foundinc unless foundinc.nil? end end - return name if store.namespace_exists?(name) + resolved_fqns = resolve_fqns(name) + return resolved_fqns if resolved_fqns + return possibles.last end end + # @param fqns [String] + # @return [String, nil] + def resolve_fqns fqns + return fqns if store.namespace_exists?(fqns) + + constant_namespace = nil + constant = store.constant_pins.find do |c| + constant_fqns = if c.namespace.empty? + c.name + else + c.namespace + '::' + c.name + end + constant_namespace = c.namespace + constant_fqns == fqns + end + return nil unless constant + + return constant.return_type.namespace if constant.return_type.defined? + + target_ns = resolve_trivial_constant(constant.assignment) + return nil unless target_ns + qualify_namespace target_ns, constant_namespace + end + + # @param node [AST::Node] + # @return [String, nil] + def resolve_trivial_constant node + return nil unless node.is_a?(::Parser::AST::Node) + return nil unless node.type == :const + return nil if node.children.empty? + prefix_node = node.children[0] + prefix = '' + prefix = resolve_trivial_constant(prefix_node) + '::' unless prefix_node.nil? || prefix_node.children.empty? + const_name = node.children[1].to_s + return nil if const_name.empty? + return prefix + const_name + end + # Get the namespace's type (Class or Module). # # @param fqns [String] A fully qualified namespace @@ -898,7 +988,7 @@ def inner_qualify name, root, skip def get_namespace_type fqns return nil if fqns.nil? # @type [Pin::Namespace, nil] - pin = store.get_path_pins(fqns).select{|p| p.is_a?(Pin::Namespace)}.first + pin = get_namespace_pin(fqns) return nil if pin.nil? pin.type end @@ -928,7 +1018,10 @@ def resolve_method_alias pin @method_alias_stack.push pin.path origin = get_method_stack(pin.full_context.tag, pin.original, scope: pin.scope, preserve_generics: true).first @method_alias_stack.pop - return nil if origin.nil? + if origin.nil? + Solargraph.assert_or_log(:alias_target_missing) { "Rejecting alias - target is missing while looking for #{pin.full_context.tag} #{pin.original}() in #{pin.scope} scope = #{pin.inspect}" } + return nil + end args = { location: pin.location, type_location: origin.type_location, @@ -962,13 +1055,11 @@ def resolve_method_alias pin include Logging - private - # @param namespace_pin [Pin::Namespace] # @param rooted_type [ComplexType] # @param pins [Enumerable] # @return [Array] - def erase_generics(namespace_pin, rooted_type, pins) + def erase_generics namespace_pin, rooted_type, pins return pins unless should_erase_generics_when_done?(namespace_pin, rooted_type) logger.debug("Erasing generics on namespace_pin=#{namespace_pin} / rooted_type=#{rooted_type}") @@ -979,18 +1070,18 @@ def erase_generics(namespace_pin, rooted_type, pins) # @param namespace_pin [Pin::Namespace] # @param rooted_type [ComplexType] - def should_erase_generics_when_done?(namespace_pin, rooted_type) + def should_erase_generics_when_done? namespace_pin, rooted_type has_generics?(namespace_pin) && !can_resolve_generics?(namespace_pin, rooted_type) end # @param namespace_pin [Pin::Namespace] - def has_generics?(namespace_pin) + def has_generics? namespace_pin namespace_pin && !namespace_pin.generics.empty? end # @param namespace_pin [Pin::Namespace] # @param rooted_type [ComplexType] - def can_resolve_generics?(namespace_pin, rooted_type) + def can_resolve_generics? namespace_pin, rooted_type has_generics?(namespace_pin) && !rooted_type.all_params.empty? end end diff --git a/lib/solargraph/api_map/cache.rb b/lib/solargraph/api_map/cache.rb index 329a1e5e1..0052d91ea 100644 --- a/lib/solargraph/api_map/cache.rb +++ b/lib/solargraph/api_map/cache.rb @@ -4,9 +4,9 @@ module Solargraph class ApiMap class Cache def initialize - # @type [Hash{Array => Array}] + # @type [Hash{String => Array}] @methods = {} - # @type [Hash{(String, Array) => Array}] + # @type [Hash{String, Array => Array}] @constants = {} # @type [Hash{String => String}] @qualified_namespaces = {} @@ -101,6 +101,7 @@ def empty? private + # @return [Array] def all_caches [@methods, @constants, @qualified_namespaces, @receiver_definitions, @clips] end diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index ea358297e..7f0e37153 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -37,27 +37,28 @@ def path_pin_hash # @param klass [Class>] # @return [Set>] def pins_by_class klass - # @type [Set] + # @type [Set>] s = Set.new + # @sg-ignore Need to handle block parameter destructuring @pin_select_cache[klass] ||= pin_class_hash.each_with_object(s) { |(key, o), n| n.merge(o) if key <= klass } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def include_references @include_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def extend_references @extend_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def prepend_references @prepend_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def superclass_references @superclass_references ||= Hash.new { |h, k| h[k] = [] } end @@ -73,7 +74,7 @@ def merge pins attr_writer :pins, :pin_select_cache, :namespace_hash, :pin_class_hash, :path_pin_hash, :include_references, :extend_references, :prepend_references, :superclass_references - # @return [self] + # @return [Solargraph::ApiMap::Index] def deep_clone Index.allocate.tap do |copy| copy.pin_select_cache = {} @@ -89,8 +90,10 @@ def deep_clone end # @param new_pins [Array] + # # @return [self] def catalog new_pins + # @type [Hash{Class> => Set>}] @pin_select_cache = {} pins.concat new_pins set = new_pins.to_set @@ -110,7 +113,7 @@ def catalog new_pins end # @param klass [Class] - # @param hash [Hash{String => Array}] + # @param hash [Hash{String => Array}] # @return [void] def map_references klass, hash pins_by_class(klass).each do |pin| @@ -120,7 +123,7 @@ def map_references klass, hash # Add references to a map # - # @param hash [Hash{String => Array}] + # @param hash [Hash{String => Array}] # @param reference_pin [Pin::Reference] # # @return [void] @@ -144,9 +147,7 @@ def map_overrides pins = path_pin_hash[ovr.name] logger.debug { "ApiMap::Index#map_overrides: pins for path=#{ovr.name}: #{pins}" } pins.each do |pin| - new_pin = if pin.path.end_with?('#initialize') - path_pin_hash[pin.path.sub(/#initialize/, '.new')].first - end + new_pin = (path_pin_hash[pin.path.sub(/#initialize/, '.new')].first if pin.path.end_with?('#initialize')) (ovr.tags.map(&:tag_name) + ovr.delete).uniq.each do |tag| pin.docstring.delete_tags tag new_pin.docstring.delete_tags tag if new_pin @@ -154,10 +155,13 @@ def map_overrides ovr.tags.each do |tag| pin.docstring.add_tag(tag) redefine_return_type pin, tag - if new_pin - new_pin.docstring.add_tag(tag) - redefine_return_type new_pin, tag - end + pin.reset_generated! + + next unless new_pin + + new_pin.docstring.add_tag(tag) + redefine_return_type new_pin, tag + new_pin.reset_generated! end end end @@ -174,7 +178,6 @@ def redefine_return_type pin, tag pin.signatures.each do |sig| sig.instance_variable_set(:@return_type, ComplexType.try_parse(tag.type)) end - pin.reset_generated! end end end diff --git a/lib/solargraph/api_map/source_to_yard.rb b/lib/solargraph/api_map/source_to_yard.rb index b44ebbf1a..7774a0d0b 100644 --- a/lib/solargraph/api_map/source_to_yard.rb +++ b/lib/solargraph/api_map/source_to_yard.rb @@ -45,7 +45,8 @@ def rake_yard store 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) - include_object.instance_mixins.push code_object_map[ref] unless include_object.nil? or include_object.nil? + code_object = code_object_map[ref] + include_object.instance_mixins.push code_object_map[ref] if include_object && code_object end store.get_extends(pin.path).each do |ref| extend_object = code_object_at(pin.path, YARD::CodeObjects::ClassObject) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index d41a2a0ae..a0735a273 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -17,7 +17,7 @@ def pins index.pins end - # @param pinsets [Array>] + # @param pinsets [Array>] # @return [Boolean] True if the index was updated def update *pinsets return catalog(pinsets) if pinsets.length != @pinsets.length @@ -50,7 +50,7 @@ def inspect # @param fqns [String] # @param visibility [Array] - # @return [Enumerable] + # @return [Enumerable] def get_constants fqns, visibility = [:public] namespace_children(fqns).select { |pin| !pin.name.empty? && (pin.is_a?(Pin::Namespace) || pin.is_a?(Pin::Constant)) && visibility.include?(pin.visibility) @@ -73,13 +73,13 @@ def get_methods fqns, scope: :instance, visibility: [:public] def get_superclass fq_tag raise "Do not prefix fully qualified tags with '::' - #{fq_tag.inspect}" if fq_tag.start_with?('::') sub = ComplexType.parse(fq_tag) + return sub.simplify_literals.name if sub.literal? + return 'Boolean' if %w[TrueClass FalseClass].include?(fq_tag) fqns = sub.namespace return superclass_references[fq_tag].first if superclass_references.key?(fq_tag) return superclass_references[fqns].first if superclass_references.key?(fqns) return 'Object' if fqns != 'BasicObject' && namespace_exists?(fqns) return 'Object' if fqns == 'Boolean' - simplified_literal_name = ComplexType.parse("#{fqns}").simplify_literals.name - return simplified_literal_name if simplified_literal_name != fqns nil end @@ -117,7 +117,7 @@ def get_instance_variables(fqns, scope = :instance) end # @param fqns [String] - # @return [Enumerable] + # @return [Enumerable] def get_class_variables(fqns) namespace_children(fqns).select { |pin| pin.is_a?(Pin::ClassVariable)} end @@ -133,16 +133,16 @@ def namespace_exists?(fqns) fqns_pins(fqns).any? end - # @return [Set] - def namespaces - index.namespaces - end - # @return [Enumerable] def namespace_pins pins_by_class(Solargraph::Pin::Namespace) end + # @return [Enumerable] + def constant_pins + pins_by_class(Solargraph::Pin::Constant) + end + # @return [Enumerable] def method_pins pins_by_class(Solargraph::Pin::Method) @@ -206,10 +206,12 @@ def index @indexes.last end - # @param pinsets [Array>] - # @return [Boolean] + # @param pinsets [Array>] + # + # @return [void] def catalog pinsets @pinsets = pinsets + # @type [Array] @indexes = [] pinsets.each do |pins| if @indexes.last && pins.empty? diff --git a/lib/solargraph/bench.rb b/lib/solargraph/bench.rb index 3cb19a64b..e6180c933 100644 --- a/lib/solargraph/bench.rb +++ b/lib/solargraph/bench.rb @@ -37,6 +37,7 @@ def source_map_hash .to_h end + # @return [Set] def icebox @icebox ||= (source_maps - [live_map]) end diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index ac9599329..4e7bc4395 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -9,6 +9,7 @@ class ComplexType # include TypeMethods include Equality + autoload :Conformance, 'solargraph/complex_type/conformance' autoload :TypeMethods, 'solargraph/complex_type/type_methods' autoload :UniqueType, 'solargraph/complex_type/unique_type' @@ -17,15 +18,17 @@ def initialize types = [UniqueType::UNDEFINED] # @todo @items here should not need an annotation # @type [Array] items = types.flat_map(&:items).uniq(&:to_s) + + # Canonicalize 'true, false' to the non-runtime-type 'Boolean' if items.any? { |i| i.name == 'false' } && items.any? { |i| i.name == 'true' } items.delete_if { |i| i.name == 'false' || i.name == 'true' } - items.unshift(ComplexType::BOOLEAN) + items.unshift(UniqueType::BOOLEAN) end + items = [UniqueType::UNDEFINED] if items.any?(&:undefined?) @items = items end - # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields [self.class, items] end @@ -76,9 +79,13 @@ def self_to_type dst end # @yieldparam [UniqueType] + # @yieldreturn [UniqueType] # @return [Array] - def map &block - @items.map &block + # @sg-ignore Declared return type + # ::Array<::Solargraph::ComplexType::UniqueType> does not match + # inferred type ::Array<::Proc> for Solargraph::ComplexType#map + def map(&block) + @items.map(&block) end # @yieldparam [UniqueType] @@ -99,12 +106,6 @@ def each_unique_type &block end end - # @param atype [ComplexType] type which may be assigned to this type - # @param api_map [ApiMap] The ApiMap that performs qualification - def can_assign?(api_map, atype) - any? { |ut| ut.can_assign?(api_map, atype) } - end - # @return [Integer] def length @items.length @@ -155,10 +156,12 @@ def to_s map(&:tag).join(', ') end + # @return [String] def tags map(&:tag).join(', ') end + # @return [String] def simple_tags simplify_literals.tags end @@ -172,10 +175,65 @@ def downcast_to_literal_if_possible ComplexType.new(items.map(&:downcast_to_literal_if_possible)) end + # @return [String] def desc rooted_tags end + # @param api_map [ApiMap] + # @param expected [ComplexType, ComplexType::UniqueType] + # @param situation [:method_call, :return_type, :assignment] + # @param allow_subtype_skew [Boolean] if false, check if any + # subtypes of the expected type match the inferred type + # @param allow_reverse_match [Boolean] if true, check if any subtypes + # of the expected type match the inferred type + # @param allow_empty_params [Boolean] if true, allow a general + # inferred type without parameters to conform to a more specific + # expected type + # @param allow_any_match [Boolean] if true, any unique type + # matched in the inferred qualifies as a match + # @param allow_undefined [Boolean] if true, treat undefined as a + # wildcard that matches anything + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic, :allow_unmatched_interface>] + # @param variance [:invariant, :covariant, :contravariant] + # @return [Boolean] + def conforms_to?(api_map, expected, + situation, + rules = [], + variance: erased_variance(situation)) + expected = expected.downcast_to_literal_if_possible + inferred = downcast_to_literal_if_possible + + return duck_types_match?(api_map, expected, inferred) if expected.duck_type? + + if rules.include? :allow_any_match + inferred.any? do |inf| + inf.conforms_to?(api_map, expected, situation, rules, + variance: variance) + end + else + inferred.all? do |inf| + inf.conforms_to?(api_map, expected, situation, rules, + variance: variance) + end + end + end + + # @param api_map [ApiMap] + # @param expected [ComplexType] + # @param inferred [ComplexType] + # @return [Boolean] + def duck_types_match? api_map, expected, inferred + raise ArgumentError, 'Expected type must be duck type' unless expected.duck_type? + expected.each do |exp| + next unless exp.duck_type? + quack = exp.to_s[1..] + return false if api_map.get_method_stack(inferred.namespace, quack, scope: inferred.scope).empty? + end + true + end + + # @return [String] def rooted_tags map(&:rooted_tag).join(', ') end @@ -200,6 +258,7 @@ def generic? any?(&:generic?) end + # @return [ComplexType] def simplify_literals ComplexType.new(map(&:simplify_literals)) end @@ -253,6 +312,13 @@ def all_rooted? all?(&:all_rooted?) end + # @param other [ComplexType, UniqueType] + def erased_version_of?(other) + return false if items.length != 1 || other.items.length != 1 + + @items.first.erased_version_of?(other.items.first) + end + # every top-level type has resolved to be fully qualified; see # #all_rooted? to check their subtypes as well def rooted? diff --git a/lib/solargraph/complex_type/conformance.rb b/lib/solargraph/complex_type/conformance.rb new file mode 100644 index 000000000..fdb962916 --- /dev/null +++ b/lib/solargraph/complex_type/conformance.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +module Solargraph + class ComplexType + # Checks whether a type can be used in a given situation + class Conformance + # @param api_map [ApiMap] + # @param inferred [ComplexType::UniqueType] + # @param expected [ComplexType::UniqueType] + # @param situation [:method_call, :return_type] + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, + # :allow_any_match, :allow_undefined, :allow_unresolved_generic, + # :allow_unmatched_interface>] + # @param variance [:invariant, :covariant, :contravariant] + def initialize api_map, inferred, expected, + situation = :method_call, rules = [], + variance: inferred.erased_variance(situation) + @api_map = api_map + @inferred = inferred + @expected = expected + @situation = situation + @rules = rules + @variance = variance + # :nocov: + unless expected.is_a?(UniqueType) + raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" + end + # :nocov: + return if inferred.is_a?(UniqueType) + # :nocov: + raise "Inferred type must be a UniqueType, got #{inferred.class} in #{inferred.inspect}" + # :nocov: + end + + def conforms_to_unique_type? + unless expected.is_a?(UniqueType) + # :nocov: + raise "Expected type must be a UniqueType, got #{expected.class} in #{expected}" + # :nocov: + end + + downcast_inferred = inferred.downcast_to_literal_if_possible + # First see if we need to turn e.g. NilClass into nil to match expectations + if expected.literal? && (downcast_inferred.name != inferred.name) + return with_new_types(downcast_inferred, expected).conforms_to_unique_type? + elsif use_simplified_inferred_type? + # ...if not, see if we need to do the reverse, or turn a + # more specific literal type (1, 2, 3) into a more general + # one (Integer) + return with_new_types(inferred.simplify_literals, expected).conforms_to_unique_type? + end + return true if ignore_interface? + return true if conforms_via_reverse_match? + + if rules.include?(:allow_subtype_skew) && !expected.all_params.empty? + # parameters are not considered in this case + return with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type? + end + + return with_new_types(inferred.erase_parameters, expected).conforms_to_unique_type? if only_inferred_parameters? + + return conforms_via_stripped_expected_parameters? if can_strip_expected_parameters? + + return true if inferred == expected + + return false unless erased_type_conforms? + + return true if inferred.all_params.empty? && rules.include?(:allow_empty_params) + + # at this point we know the erased type is fine - time to look at parameters + + # there's an implicit 'any' on the expectation parameters + # if there are none specified + return true if expected.all_params.empty? + + return false unless key_types_conform? + + subtypes_conform? + end + + private + + def use_simplified_inferred_type? + inferred.literal? && !expected.literal? + end + + def only_inferred_parameters? + !expected.parameters? && inferred.parameters? + end + + def conforms_via_stripped_expected_parameters? + with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type? + end + + def ignore_interface? + (expected.any?(&:interface?) && rules.include?(:allow_unmatched_interface)) || + (inferred.interface? && rules.include?(:allow_unmatched_interface)) + end + + def can_strip_expected_parameters? + expected.parameters? && !inferred.parameters? && rules.include?(:allow_empty_params) + end + + def conforms_via_reverse_match? + return false unless rules.include? :allow_reverse_match + + expected.conforms_to?(api_map, inferred, situation, + rules - [:allow_reverse_match], + variance: variance) + end + + def erased_type_conforms? + case variance + when :invariant + return false unless inferred.name == expected.name + when :covariant + # covariant: we can pass in a more specific type + # we contain the expected mix-in, or we have a more specific type + return false unless api_map.type_include?(inferred.name, expected.name) || + api_map.super_and_sub?(expected.name, inferred.name) || + inferred.name == expected.name + when :contravariant + # contravariant: we can pass in a more general type + # we contain the expected mix-in, or we have a more general type + return false unless api_map.type_include?(inferred.name, expected.name) || + api_map.super_and_sub?(inferred.name, expected.name) || + inferred.name == expected.name + else + # :nocov: + raise "Unknown variance: #{variance.inspect}" + # :nocov: + end + true + end + + def key_types_conform? + return true if expected.key_types.empty? + + return false if inferred.key_types.empty? + + unless ComplexType.new(inferred.key_types).conforms_to?(api_map, + ComplexType.new(expected.key_types), + situation, + rules, + variance: inferred.parameter_variance(situation)) + return false + end + + true + end + + def subtypes_conform? + return true if expected.subtypes.empty? + + return true if expected.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) + + return true if inferred.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) + + return true if inferred.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic) + + return true if expected.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic) + + return false if inferred.subtypes.empty? + + ComplexType.new(inferred.subtypes).conforms_to?(api_map, + ComplexType.new(expected.subtypes), + situation, + rules, + variance: inferred.parameter_variance(situation)) + end + + # @return [self] + # @param inferred [ComplexType::UniqueType] + # @param expected [ComplexType::UniqueType] + def with_new_types inferred, expected + self.class.new(api_map, inferred, expected, situation, rules, variance: variance) + end + + attr_reader :api_map, :inferred, :expected, :situation, :rules, :variance + end + end +end diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index e6d596244..8b5cfea6b 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -10,11 +10,7 @@ class ComplexType # @name: String # @subtypes: Array # @rooted: boolish - # methods: - # transform() - # all_params() - # rooted?() - # can_root_name?() + # methods: (see @!method declarations below) module TypeMethods # @!method transform(new_name = nil, &transform_type) # @param new_name [String, nil] @@ -24,6 +20,9 @@ module TypeMethods # @!method all_params # @return [Array] # @!method rooted? + # @!method literal? + # @!method simplify_literals + # @return [ComplexType::UniqueType, ComplexType] # @!method can_root_name?(name_to_check = nil) # @param name_to_check [String, nil] @@ -69,6 +68,18 @@ def undefined? name == 'undefined' end + # Variance of the type ignoring any type parameters + # @return [Symbol] + # @param situation [Symbol] The situation in which the variance is being considered. + def erased_variance situation = :method_call + # :nocov: + unless %i[method_call return_type assignment].include?(situation) + raise "Unknown situation: #{situation.inspect}" + end + # :nocov: + :covariant + end + # @param generics_to_erase [Enumerable] # @return [self] def erase_generics(generics_to_erase) @@ -124,12 +135,14 @@ def key_types def namespace # if priority higher than ||=, old implements cause unnecessary check @namespace ||= lambda do - return 'Object' if duck_type? + return simplify_literals.namespace if literal? + return 'Object' if duck_type? || name == 'Boolean' return 'NilClass' if nil_type? return (name == 'Class' || name == 'Module') && !subtypes.empty? ? subtypes.first.name : name end.call end + # @return [ComplexType, UniqueType] def namespace_type return ComplexType.parse('::Object') if duck_type? return ComplexType.parse('::NilClass') if nil_type? diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 63a6ae15b..a074a3547 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -11,7 +11,6 @@ class UniqueType attr_reader :all_params, :subtypes, :key_types - # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields [@name, @all_params, @subtypes, @key_types] end @@ -74,6 +73,8 @@ def initialize(name, key_types = [], subtypes = [], rooted:, parameters_type: ni if parameters_type.nil? raise "You must supply parameters_type if you provide parameters" unless key_types.empty? && subtypes.empty? end + + raise "name must be a String" unless name.is_a?(String) raise "Please remove leading :: and set rooted instead - #{name.inspect}" if name.start_with?('::') @name = name @parameters_type = parameters_type @@ -101,6 +102,7 @@ def to_s tag end + # @return [self] def simplify_literals transform do |t| next t unless t.literal? @@ -112,10 +114,12 @@ def literal? non_literal_name != name end + # @return [String] def non_literal_name @non_literal_name ||= determine_non_literal_name end + # @return [String] def determine_non_literal_name # https://github.com/ruby/rbs/blob/master/docs/syntax.md # @@ -126,7 +130,8 @@ def determine_non_literal_name # | `false` return name if name.empty? return 'NilClass' if name == 'nil' - return 'Boolean' if ['true', 'false'].include?(name) + return 'TrueClass' if name == 'true' + return 'FalseClass' if name == 'false' return 'Symbol' if name[0] == ':' return 'String' if ['"', "'"].include?(name[0]) return 'Integer' if name.match?(/^-?\d+$/) @@ -147,10 +152,86 @@ def ==(other) eql?(other) end + # https://www.playfulpython.com/type-hinting-covariance-contra-variance/ + + # "[Expected] type variables that are COVARIANT can be substituted with + # a more specific [inferred] type without causing errors" + # + # "[Expected] type variables that are CONTRAVARIANT can be substituted + # with a more general [inferred] type without causing errors" + # + # "[Expected] types where neither is possible are INVARIANT" + # + # @param _situation [:method_call] + # @param default [Symbol] The default variance to return if the type is not one of the special cases + # + # @return [:invariant, :covariant, :contravariant] + def parameter_variance _situation, default = :covariant + # @todo RBS can specify variance - maybe we can use that info + # and also let folks specify? + # + # Array/Set: ideally invariant, since we don't know if user is + # going to add new stuff into it or read it. But we don't + # have a way to specify, so we use covariant + # Enumerable: covariant: can't be changed, so we can pass + # in more specific subtypes + # Hash: read-only would be covariant, read-write would be + # invariant if we could distinguish that - should default to + # covariant + # contravariant?: Proc - can be changed, so we can pass + # in less specific super types + if ['Hash', 'Tuple', 'Array', 'Set', 'Enumerable'].include?(name) && fixed_parameters? + :covariant + else + default + end + end + + # Whether this is an RBS interface like _ToAry or _Each. + def interface? + name.start_with?('_') + end + + # @param other [UniqueType] + def erased_version_of?(other) + name == other.name && (all_params.empty? || all_params.all?(&:undefined?)) + end + + # @param api_map [ApiMap] + # @param expected [ComplexType::UniqueType, ComplexType] + # @param situation [:method_call, :assignment, :return] + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic>] + # @param variance [:invariant, :covariant, :contravariant] + def conforms_to?(api_map, expected, situation, rules = [], + variance: erased_variance(situation)) + return true if undefined? && rules.include?(:allow_undefined) + + # @todo teach this to validate duck types as inferred type + return true if duck_type? + + # complex types as expectations are unions - we only need to + # match one of their unique types + expected.any? do |expected_unique_type| + # :nocov: + unless expected_unique_type.instance_of?(UniqueType) + raise "Expected type must be a UniqueType, got #{expected_unique_type.class} in #{expected.inspect}" + end + # :nocov: + conformance = Conformance.new(api_map, self, expected_unique_type, situation, + rules, variance: variance) + conformance.conforms_to_unique_type? + end + end + def hash [self.class, @name, @key_types, @sub_types, @rooted, @all_params, @parameters_type].hash end + # @return [self] + def erase_parameters + UniqueType.new(name, rooted: rooted?, parameters_type: parameters_type) + end + # @return [Array] def items [self] @@ -167,6 +248,7 @@ def rbs_name end end + # @return [String] def desc rooted_tags end @@ -180,7 +262,7 @@ def to_rbs elsif name.downcase == 'nil' 'nil' elsif name == GENERIC_TAG_NAME - all_params.first.name + all_params.first&.name elsif ['Class', 'Module'].include?(name) rbs_name elsif ['Tuple', 'Array'].include?(name) && fixed_parameters? @@ -232,18 +314,6 @@ def generic? name == GENERIC_TAG_NAME || all_params.any?(&:generic?) end - # @param api_map [ApiMap] The ApiMap that performs qualification - # @param atype [ComplexType] type which may be assigned to this type - def can_assign?(api_map, atype) - logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect})" } - downcasted_atype = atype.downcast_to_literal_if_possible - out = downcasted_atype.all? do |autype| - autype.name == name || api_map.super_and_sub?(name, autype.name) - end - logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect}) => #{out}" } - out - end - # @return [UniqueType] def downcast_to_literal_if_possible SINGLE_SUBTYPE.fetch(rooted_tag, self) @@ -251,7 +321,7 @@ def downcast_to_literal_if_possible # @param generics_to_resolve [Enumerable] # @param context_type [UniqueType, nil] - # @param resolved_generic_values [Hash{String => ComplexType}] Added to as types are encountered or resolved + # @param resolved_generic_values [Hash{String => ComplexType, UniqueType}] Added to as types are encountered or resolved # @return [UniqueType, ComplexType] def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {} if name == ComplexType::GENERIC_TAG_NAME @@ -349,9 +419,9 @@ def to_a # @param new_name [String, nil] # @param make_rooted [Boolean, nil] - # @param new_key_types [Array, nil] + # @param new_key_types [Array, nil] # @param rooted [Boolean, nil] - # @param new_subtypes [Array, nil] + # @param new_subtypes [Array, nil] # @return [self] def recreate(new_name: nil, make_rooted: nil, new_key_types: nil, new_subtypes: nil) raise "Please remove leading :: and set rooted instead - #{new_name}" if new_name&.start_with?('::') @@ -433,6 +503,11 @@ def self_to_type dst end end + # @yieldreturn [Boolean] + def any? &block + block.yield self + end + def all_rooted? return true if name == GENERIC_TAG_NAME rooted? && all_params.all?(&:rooted?) diff --git a/lib/solargraph/convention/data_definition/data_assignment_node.rb b/lib/solargraph/convention/data_definition/data_assignment_node.rb index 7aadcf190..0ecfb88eb 100644 --- a/lib/solargraph/convention/data_definition/data_assignment_node.rb +++ b/lib/solargraph/convention/data_definition/data_assignment_node.rb @@ -22,6 +22,7 @@ class << self # s(:def, :foo, # s(:args), # 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? diff --git a/lib/solargraph/convention/data_definition/data_definition_node.rb b/lib/solargraph/convention/data_definition/data_definition_node.rb index dd5929822..5ee79b73d 100644 --- a/lib/solargraph/convention/data_definition/data_definition_node.rb +++ b/lib/solargraph/convention/data_definition/data_definition_node.rb @@ -25,6 +25,7 @@ class << self # s(:def, :foo, # s(:args), # s(:send, nil, :bar))) + # @param node [Parser::AST::Node] def match?(node) return false unless node&.type == :class @@ -46,7 +47,7 @@ def data_definition_node?(data_node) end end - # @return [Parser::AST::Node] + # @param node [Parser::AST::Node] def initialize(node) @node = node end diff --git a/lib/solargraph/convention/struct_definition.rb b/lib/solargraph/convention/struct_definition.rb index 058e2de30..b34ae5494 100644 --- a/lib/solargraph/convention/struct_definition.rb +++ b/lib/solargraph/convention/struct_definition.rb @@ -102,7 +102,7 @@ def process private - # @return [StructDefintionNode, nil] + # @return [StructDefintionNode, StructAssignmentNode, nil] def struct_definition_node @struct_definition_node ||= if StructDefintionNode.match?(node) StructDefintionNode.new(node) diff --git a/lib/solargraph/convention/struct_definition/struct_assignment_node.rb b/lib/solargraph/convention/struct_definition/struct_assignment_node.rb index 04f96d40e..2816de6ed 100644 --- a/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +++ b/lib/solargraph/convention/struct_definition/struct_assignment_node.rb @@ -22,6 +22,8 @@ class << self # s(:def, :foo, # s(:args), # 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? diff --git a/lib/solargraph/convention/struct_definition/struct_definition_node.rb b/lib/solargraph/convention/struct_definition/struct_definition_node.rb index 540320c37..7c3d722d0 100644 --- a/lib/solargraph/convention/struct_definition/struct_definition_node.rb +++ b/lib/solargraph/convention/struct_definition/struct_definition_node.rb @@ -25,6 +25,7 @@ class << self # s(:def, :foo, # s(:args), # s(:send, nil, :bar))) + # @param node [Parser::AST::Node] def match?(node) return false unless node&.type == :class @@ -46,7 +47,7 @@ def struct_definition_node?(struct_node) end end - # @return [Parser::AST::Node] + # @param node [Parser::AST::Node] def initialize(node) @node = node end diff --git a/lib/solargraph/diagnostics/rubocop_helpers.rb b/lib/solargraph/diagnostics/rubocop_helpers.rb index 4eb2c711d..f6f4c82c8 100644 --- a/lib/solargraph/diagnostics/rubocop_helpers.rb +++ b/lib/solargraph/diagnostics/rubocop_helpers.rb @@ -19,8 +19,6 @@ def require_rubocop(version = nil) gem_path = Gem::Specification.find_by_name('rubocop', version).full_gem_path gem_lib_path = File.join(gem_path, 'lib') $LOAD_PATH.unshift(gem_lib_path) unless $LOAD_PATH.include?(gem_lib_path) - # @todo Gem::MissingSpecVersionError is undocumented for some reason - # @sg-ignore rescue Gem::MissingSpecVersionError => e raise InvalidRubocopVersionError, "could not find '#{e.name}' (#{e.requirement}) - "\ diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 43c8768b0..b94bc558e 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -5,122 +5,81 @@ require 'open3' module Solargraph - # A collection of pins generated from required gems. + # A collection of pins generated from specific 'require' statements + # in code. Multiple can be created per workspace, to represent the + # pins available in different files based on their particular + # 'require' lines. # class DocMap include Logging - # @return [Array] - attr_reader :requires - alias required requires + # @return [Workspace] + attr_reader :workspace - # @return [Array] - attr_reader :preferences + # @param requires [Array] + # @param workspace [Workspace] + # @param out [IO, nil] output stream for logging + def initialize requires, workspace, out: $stderr + @provided_requires = requires.compact + @workspace = workspace + @out = out + end - # @return [Array] - attr_reader :pins + # @return [Array] + def requires + @requires ||= @provided_requires + (workspace.global_environ&.requires || []) + end + alias required requires # @return [Array] def uncached_gemspecs - uncached_yard_gemspecs.concat(uncached_rbs_collection_gemspecs) - .sort - .uniq { |gemspec| "#{gemspec.name}:#{gemspec.version}" } + if @uncached_gemspecs.nil? + @uncached_gemspecs = [] + pins # force lazy-loaded pin lookup + end + @uncached_gemspecs end - # @return [Array] - attr_reader :uncached_yard_gemspecs - - # @return [Array] - attr_reader :uncached_rbs_collection_gemspecs - - # @return [String, nil] - attr_reader :rbs_collection_path - - # @return [String, nil] - attr_reader :rbs_collection_config_path - - # @return [Workspace, nil] - attr_reader :workspace - - # @return [Environ] - attr_reader :environ - - # @param requires [Array] - # @param preferences [Array] - # @param workspace [Workspace, nil] - def initialize(requires, preferences, workspace = nil) - @requires = requires.compact - @preferences = preferences.compact - @workspace = workspace - @rbs_collection_path = workspace&.rbs_collection_path - @rbs_collection_config_path = workspace&.rbs_collection_config_path - @environ = Convention.for_global(self) - @requires.concat @environ.requires if @environ - load_serialized_gem_pins - pins.concat @environ.pins + # @return [Array] + def pins + @pins ||= load_serialized_gem_pins + (workspace.global_environ&.pins || []) end - # @param out [IO] # @return [void] - def cache_all!(out) - # if we log at debug level: - if logger.info? - gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ') - logger.info "Caching pins for gems: #{gem_desc}" unless uncached_gemspecs.empty? - end - logger.debug { "Caching for YARD: #{uncached_yard_gemspecs.map(&:name)}" } - logger.debug { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" } - load_serialized_gem_pins - uncached_gemspecs.each do |gemspec| - cache(gemspec, out: out) - end - load_serialized_gem_pins - @uncached_rbs_collection_gemspecs = [] - @uncached_yard_gemspecs = [] + def reset_pins! + @uncached_gemspecs = nil + @pins = nil end - # @param gemspec [Gem::Specification] - # @param out [IO] - # @return [void] - def cache_yard_pins(gemspec, out) - pins = GemPins.build_yard_pins(yard_plugins, gemspec) - PinCache.serialize_yard_gem(gemspec, pins) - logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty? + # @return [Solargraph::PinCache] + def pin_cache + @pin_cache ||= workspace.fresh_pincache end - # @param gemspec [Gem::Specification] - # @param out [IO] - # @return [void] - def cache_rbs_collection_pins(gemspec, out) - rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) - pins = rbs_map.pins - rbs_version_cache_key = rbs_map.cache_key - # cache pins even if result is zero, so we don't retry building pins - pins ||= [] - PinCache.serialize_rbs_collection_gem(gemspec, rbs_version_cache_key, pins) - logger.info { "Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with cache_key #{rbs_version_cache_key.inspect}" unless pins.empty? } + def any_uncached? + uncached_gemspecs.any? end - # @param gemspec [Gem::Specification] - # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # Cache all pins needed for the sources in this doc_map # @param out [IO, nil] output stream for logging # @return [void] - def cache(gemspec, rebuild: false, out: nil) - build_yard = uncached_yard_gemspecs.include?(gemspec) || rebuild - build_rbs_collection = uncached_rbs_collection_gemspecs.include?(gemspec) || rebuild - if build_yard || build_rbs_collection - type = [] - type << 'YARD' if build_yard - type << 'RBS collection' if build_rbs_collection - out.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}") if out + def cache_doc_map_gems! out + unless uncached_gemspecs.empty? + logger.info do + gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ') + "Caching pins for gems: #{gem_desc}" + end end - cache_yard_pins(gemspec, out) if build_yard - cache_rbs_collection_pins(gemspec, out) if build_rbs_collection - end - - # @return [Array] - def gemspecs - @gemspecs ||= required_gems_map.values.compact.flatten + time = Benchmark.measure do + uncached_gemspecs.each do |gemspec| + cache(gemspec, out: out) + end + end + milliseconds = (time.real * 1000).round + if (milliseconds > 500) && uncached_gemspecs.any? && out && uncached_gemspecs.any? + out.puts "Built #{uncached_gemspecs.length} gems in #{milliseconds} ms" + end + reset_pins! end # @return [Array] @@ -128,305 +87,90 @@ def unresolved_requires @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys end - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def self.all_yard_gems_in_memory - @yard_gems_in_memory ||= {} - end - - # @return [Hash{String => Array}] stored by RBS collection path - def self.all_rbs_collection_gems_in_memory - @rbs_collection_gems_in_memory ||= {} - end - - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def yard_pins_in_memory - self.class.all_yard_gems_in_memory - end - - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def rbs_collection_pins_in_memory - self.class.all_rbs_collection_gems_in_memory[rbs_collection_path] ||= {} - end - - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def self.all_combined_pins_in_memory - @combined_pins_in_memory ||= {} + # @param out [IO, nil] output stream for logging + # @return [Set] + # @param out [IO] + def dependencies out: $stderr + @dependencies ||= + begin + all_deps = gemspecs.flat_map { |spec| workspace.fetch_dependencies(spec, out: out) } + existing_gems = gemspecs.map(&:name) + all_deps.reject { |gemspec| existing_gems.include? gemspec.name }.to_set + end end - # @todo this should also include an index by the hash of the RBS collection - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def combined_pins_in_memory - self.class.all_combined_pins_in_memory + # Cache gem documentation if needed for this doc_map + # + # @param gemspec [Gem::Specification] + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # @param out [IO, nil] output stream for logging + # + # @return [void] + def cache gemspec, rebuild: false, out: nil + pin_cache.cache_gem(gemspec: gemspec, + rebuild: rebuild, + out: out) end - # @return [Array] - def yard_plugins - @environ.yard_plugins - end + private - # @return [Set] - def dependencies - @dependencies ||= (gemspecs.flat_map { |spec| fetch_dependencies(spec) } - gemspecs).to_set + # @return [Array] + def gemspecs + @gemspecs ||= required_gems_map.values.compact.flatten end - private - - # @return [void] - def load_serialized_gem_pins - @pins = [] - @uncached_yard_gemspecs = [] - @uncached_rbs_collection_gemspecs = [] + # @param out [IO, nil] + # @return [Array] + def load_serialized_gem_pins out: @out + serialized_pins = [] with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v } + # @sg-ignore Need Hash[] support # @type [Array] - paths = Hash[without_gemspecs].keys + missing_paths = Hash[without_gemspecs].keys # @type [Array] - gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies.to_a - - paths.each do |path| - rbs_pins = deserialize_stdlib_rbs_map path + gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies(out: out).to_a + + missing_paths.each do |path| + # this will load from disk if needed; no need to manage + # uncached_gemspecs to trigger that later + stdlib_name_guess = path.split('/').first + + # try to resolve the stdlib name + deps = workspace.stdlib_dependencies(stdlib_name_guess) || [] + [stdlib_name_guess, *deps].compact.each do |potential_stdlib_name| + rbs_pins = pin_cache.cache_stdlib_rbs_map potential_stdlib_name + serialized_pins.concat rbs_pins if rbs_pins + end end - logger.debug { "DocMap#load_serialized_gem_pins: Combining pins..." } + existing_pin_count = serialized_pins.length time = Benchmark.measure do gemspecs.each do |gemspec| - pins = deserialize_combined_pin_cache gemspec - @pins.concat pins if pins + # only deserializes already-cached gems + gemspec_pins = pin_cache.deserialize_combined_pin_cache gemspec + if gemspec_pins + serialized_pins.concat gemspec_pins + else + uncached_gemspecs << gemspec + end end end - logger.info { "DocMap#load_serialized_gem_pins: Loaded and processed serialized pins together in #{time.real} seconds" } - @uncached_yard_gemspecs.uniq! - @uncached_rbs_collection_gemspecs.uniq! - nil + pins_processed = serialized_pins.length - existing_pin_count + milliseconds = (time.real * 1000).round + if (milliseconds > 500) && out && gemspecs.any? + out.puts "Deserialized #{serialized_pins.length} gem pins from #{PinCache.base_dir} in #{milliseconds} ms" + end + uncached_gemspecs.uniq! { |gemspec| "#{gemspec.name}:#{gemspec.version}" } + serialized_pins end # @return [Hash{String => Array}] def required_gems_map - @required_gems_map ||= requires.to_h { |path| [path, resolve_path_to_gemspecs(path)] } - end - - # @return [Hash{String => Gem::Specification}] - def preference_map - @preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] } - end - - # @param gemspec [Gem::Specification] - # @return [Array] - def deserialize_yard_pin_cache gemspec - if yard_pins_in_memory.key?([gemspec.name, gemspec.version]) - return yard_pins_in_memory[[gemspec.name, gemspec.version]] - end - - cached = PinCache.deserialize_yard_gem(gemspec) - if cached - logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" } - yard_pins_in_memory[[gemspec.name, gemspec.version]] = cached - cached - else - logger.debug "No YARD pin cache for #{gemspec.name}:#{gemspec.version}" - @uncached_yard_gemspecs.push gemspec - nil - end - end - - # @param gemspec [Gem::Specification] - # @return [void] - def deserialize_combined_pin_cache(gemspec) - unless combined_pins_in_memory[[gemspec.name, gemspec.version]].nil? - return combined_pins_in_memory[[gemspec.name, gemspec.version]] - end - - rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) - rbs_version_cache_key = rbs_map.cache_key - - cached = PinCache.deserialize_combined_gem(gemspec, rbs_version_cache_key) - if cached - logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" } - combined_pins_in_memory[[gemspec.name, gemspec.version]] = cached - return combined_pins_in_memory[[gemspec.name, gemspec.version]] - end - - rbs_collection_pins = deserialize_rbs_collection_cache gemspec, rbs_version_cache_key - - yard_pins = deserialize_yard_pin_cache gemspec - - if !rbs_collection_pins.nil? && !yard_pins.nil? - logger.debug { "Combining pins for #{gemspec.name}:#{gemspec.version}" } - combined_pins = GemPins.combine(yard_pins, rbs_collection_pins) - PinCache.serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins) - combined_pins_in_memory[[gemspec.name, gemspec.version]] = combined_pins - logger.info { "Generated #{combined_pins_in_memory[[gemspec.name, gemspec.version]].length} combined pins for #{gemspec.name} #{gemspec.version}" } - return combined_pins - end - - if !yard_pins.nil? - logger.debug { "Using only YARD pins for #{gemspec.name}:#{gemspec.version}" } - combined_pins_in_memory[[gemspec.name, gemspec.version]] = yard_pins - return combined_pins_in_memory[[gemspec.name, gemspec.version]] - elsif !rbs_collection_pins.nil? - logger.debug { "Using only RBS collection pins for #{gemspec.name}:#{gemspec.version}" } - combined_pins_in_memory[[gemspec.name, gemspec.version]] = rbs_collection_pins - return combined_pins_in_memory[[gemspec.name, gemspec.version]] - else - logger.debug { "Pins not yet cached for #{gemspec.name}:#{gemspec.version}" } - return nil - end - end - - # @param path [String] require path that might be in the RBS stdlib collection - # @return [void] - def deserialize_stdlib_rbs_map path - map = RbsMap::StdlibMap.load(path) - if map.resolved? - logger.debug { "Loading stdlib pins for #{path}" } - @pins.concat map.pins - logger.debug { "Loaded #{map.pins.length} stdlib pins for #{path}" } - map.pins - else - # @todo Temporarily ignoring unresolved `require 'set'` - logger.debug { "Require path #{path} could not be resolved in RBS" } unless path == 'set' - nil - end - end - - # @param gemspec [Gem::Specification] - # @param rbs_version_cache_key [String] - # @return [Array, nil] - def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key - return if rbs_collection_pins_in_memory.key?([gemspec, rbs_version_cache_key]) - cached = PinCache.deserialize_rbs_collection_gem(gemspec, rbs_version_cache_key) - if cached - logger.info { "Loaded #{cached.length} pins from RBS collection cache for #{gemspec.name}:#{gemspec.version}" } unless cached.empty? - rbs_collection_pins_in_memory[[gemspec, rbs_version_cache_key]] = cached - cached - else - logger.debug "No RBS collection pin cache for #{gemspec.name} #{gemspec.version}" - @uncached_rbs_collection_gemspecs.push gemspec - nil - end - end - - # @param path [String] - # @return [::Array, nil] - def resolve_path_to_gemspecs path - return nil if path.empty? - return gemspecs_required_from_bundler if path == 'bundler/require' - - # @type [Gem::Specification, nil] - gemspec = Gem::Specification.find_by_path(path) - if gemspec.nil? - gem_name_guess = path.split('/').first - begin - # this can happen when the gem is included via a local path in - # a Gemfile; Gem doesn't try to index the paths in that case. - # - # See if we can make a good guess: - potential_gemspec = Gem::Specification.find_by_name(gem_name_guess) - file = "lib/#{path}.rb" - gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file } - rescue Gem::MissingSpecError - logger.debug { "Require path #{path} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" } - [] - end - end - return nil if gemspec.nil? - [gemspec_or_preference(gemspec)] + @required_gems_map ||= requires.to_h { |require| [require, workspace.resolve_require(require)] } end - # @param gemspec [Gem::Specification] - # @return [Gem::Specification] - def gemspec_or_preference gemspec - return gemspec unless preference_map.key?(gemspec.name) - return gemspec if gemspec.version == preference_map[gemspec.name].version - - change_gemspec_version gemspec, preference_map[by_path.name].version - end - - # @param gemspec [Gem::Specification] - # @param version [Gem::Version] - # @return [Gem::Specification] - def change_gemspec_version gemspec, version - Gem::Specification.find_by_name(gemspec.name, "= #{version}") - rescue Gem::MissingSpecError - Solargraph.logger.info "Gem #{gemspec.name} version #{version} not found. Using #{gemspec.version} instead" - gemspec - end - - # @param gemspec [Gem::Specification] - # @return [Array] - def fetch_dependencies gemspec - # @param spec [Gem::Dependency] - only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps| - Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}" - dep = Gem.loaded_specs[spec.name] - # @todo is next line necessary? - dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement) - deps.merge fetch_dependencies(dep) if deps.add?(dep) - rescue Gem::MissingSpecError - Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for #{gemspec.name} not found in RubyGems." - end.to_a - end - - # @param gemspec [Gem::Specification] - # @return [Array] - def only_runtime_dependencies gemspec - gemspec.dependencies - gemspec.development_dependencies - end - - def inspect self.class.inspect end - - # @return [Array] - def gemspecs_required_from_bundler - # @todo Handle projects with custom Bundler/Gemfile setups - return unless workspace.gemfile? - - if workspace.gemfile? && Bundler.definition&.lockfile&.to_s&.start_with?(workspace.directory) - # Find only the gems bundler is now using - Bundler.definition.locked_gems.specs.flat_map do |lazy_spec| - logger.info "Handling #{lazy_spec.name}:#{lazy_spec.version}" - [Gem::Specification.find_by_name(lazy_spec.name, lazy_spec.version)] - rescue Gem::MissingSpecError => e - logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with find_by_name, falling back to guess") - # can happen in local filesystem references - specs = resolve_path_to_gemspecs lazy_spec.name - logger.warn "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil? - next specs - end.compact - else - logger.info 'Fetching gemspecs required from Bundler (bundler/require)' - gemspecs_required_from_external_bundle - end - end - - # @return [Array] - def gemspecs_required_from_external_bundle - logger.info 'Fetching gemspecs required from external bundle' - return [] unless workspace&.directory - - Solargraph.with_clean_env do - cmd = [ - 'ruby', '-e', - "require 'bundler'; require 'json'; Dir.chdir('#{workspace&.directory}') { puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }.to_h.to_json }" - ] - o, e, s = Open3.capture3(*cmd) - if s.success? - Solargraph.logger.debug "External bundle: #{o}" - hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {} - hash.flat_map do |name, version| - Gem::Specification.find_by_name(name, version) - rescue Gem::MissingSpecError => e - logger.info("Could not find #{name}:#{version} with find_by_name, falling back to guess") - # can happen in local filesystem references - specs = resolve_path_to_gemspecs name - logger.warn "Gem #{name} #{version} from bundle not found: #{e}" if specs.nil? - next specs - end.compact - else - Solargraph.logger.warn "Failed to load gems from bundle at #{workspace&.directory}: #{e}" - end - end - end end end diff --git a/lib/solargraph/environ.rb b/lib/solargraph/environ.rb index 3d24127c6..639442a04 100644 --- a/lib/solargraph/environ.rb +++ b/lib/solargraph/environ.rb @@ -22,7 +22,7 @@ class Environ # @param requires [Array] # @param domains [Array] # @param pins [Array] - # @param yard_plugins[Array] + # @param yard_plugins [Array] def initialize requires: [], domains: [], pins: [], yard_plugins: [] @requires = requires @domains = domains diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index 43422505b..52714b373 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -12,20 +12,22 @@ class << self end # @param pins [Array] - # @return [Array] + # @return [Array] def self.combine_method_pins_by_path(pins) method_pins, alias_pins = pins.partition { |pin| pin.class == Pin::Method } by_path = method_pins.group_by(&:path) - by_path.transform_values! do |pins| + combined_by_path = by_path.transform_values do |pins| GemPins.combine_method_pins(*pins) end - by_path.values + alias_pins + combined_by_path.values + alias_pins end - # @param pins [Pin::Base] - # @return [Pin::Base, nil] + # @param pins [Array] + # @return [Pin::Method, nil] def self.combine_method_pins(*pins) - out = pins.reduce(nil) do |memo, pin| + # @type [Pin::Method, nil] + combined_pin = nil + out = pins.reduce(combined_pin) do |memo, pin| next pin if memo.nil? if memo == pin && memo.source != :combined # @todo we should track down situations where we are handled @@ -39,33 +41,35 @@ def self.combine_method_pins(*pins) out end - # @param yard_plugins [Array] The names of YARD plugins to use. - # @param gemspec [Gem::Specification] - # @return [Array] - def self.build_yard_pins(yard_plugins, gemspec) - Yardoc.cache(yard_plugins, gemspec) unless Yardoc.cached?(gemspec) - yardoc = Yardoc.load!(gemspec) - YardMap::Mapper.new(yardoc, gemspec).map - end - # @param yard_pins [Array] - # @param rbs_map [RbsMap] + # @param rbs_pins [Array] # @return [Array] def self.combine(yard_pins, rbs_pins) in_yard = Set.new - rbs_api_map = Solargraph::ApiMap.new(pins: rbs_pins) + rbs_store = Solargraph::ApiMap::Store.new(rbs_pins) combined = yard_pins.map do |yard_pin| + rbs_pin = rbs_store.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first + + next yard_pin unless rbs_pin && yard_pin.is_a?(Pin::Method) + in_yard.add yard_pin.path - rbs_pin = rbs_api_map.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first - next yard_pin unless rbs_pin && yard_pin.class == Pin::Method unless rbs_pin logger.debug { "GemPins.combine: No rbs pin for #{yard_pin.path} - using YARD's '#{yard_pin.inspect} (return_type=#{yard_pin.return_type}; signatures=#{yard_pin.signatures})" } next yard_pin end + # at this point both yard_pins and rbs_pins are methods or + # method aliases. if not plain methods, prefer the YARD one + next yard_pin if rbs_pin.class != Pin::Method + + next rbs_pin if yard_pin.class != Pin::Method + + # both are method pins out = combine_method_pins(rbs_pin, yard_pin) - logger.debug { "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}" } + logger.debug do + "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}" + end out end in_rbs_only = rbs_pins.select do |pin| diff --git a/lib/solargraph/language_server/host.rb b/lib/solargraph/language_server/host.rb index 1c5831bda..e85fc813a 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -299,6 +299,7 @@ def prepare directory, name = nil end end + # @return [String] def command_path options['commandPath'] || 'solargraph' end @@ -716,7 +717,7 @@ def diagnoser # A hash of client requests by ID. The host uses this to keep track of # pending responses. # - # @return [Hash{Integer => Solargraph::LanguageServer::Host}] + # @return [Hash{Integer => Request}] def requests @requests ||= {} end diff --git a/lib/solargraph/language_server/host/dispatch.rb b/lib/solargraph/language_server/host/dispatch.rb index 1480e20a2..1ff1227b8 100644 --- a/lib/solargraph/language_server/host/dispatch.rb +++ b/lib/solargraph/language_server/host/dispatch.rb @@ -95,6 +95,7 @@ def implicit_library_for uri nil end + # @return [Hash{String => undefined}] def options @options ||= {}.freeze end @@ -118,6 +119,7 @@ def generic_library end # @param library [Solargraph::Library] + # @param progress [Solargraph::LanguageServer::Progress, nil] # @return [void] def update progress progress&.send(self) diff --git a/lib/solargraph/language_server/host/message_worker.rb b/lib/solargraph/language_server/host/message_worker.rb index 482a40e56..ec426b99f 100644 --- a/lib/solargraph/language_server/host/message_worker.rb +++ b/lib/solargraph/language_server/host/message_worker.rb @@ -72,10 +72,12 @@ def tick private + # @return [Hash, nil] def next_message cancel_message || next_priority end + # @return [Hash, nil] def cancel_message # Handle cancellations first idx = messages.find_index { |msg| msg['method'] == '$/cancelRequest' } @@ -86,6 +88,7 @@ def cancel_message msg end + # @return [Hash, nil] def next_priority # Prioritize updates and version-dependent messages for performance idx = messages.find_index do |msg| diff --git a/lib/solargraph/language_server/message/base.rb b/lib/solargraph/language_server/message/base.rb index fbc55ccbd..cc72d99b5 100644 --- a/lib/solargraph/language_server/message/base.rb +++ b/lib/solargraph/language_server/message/base.rb @@ -16,7 +16,7 @@ class Base # @return [String] attr_reader :method - # @return [Hash{String => Array, Hash{String => undefined}, String, Integer}] + # @return [Hash{String => undefined}] attr_reader :params # @return [Hash, Array, nil] @@ -79,6 +79,7 @@ def send_response private + # @return [void] def accept_or_cancel if host.cancel?(id) # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#cancelRequest 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 2e80f40c6..06892ed19 100644 --- a/lib/solargraph/language_server/message/extended/check_gem_version.rb +++ b/lib/solargraph/language_server/message/extended/check_gem_version.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true -# @todo PR the RBS gem to add this -# @!parse -# module ::Gem -# class SpecFetcher; end -# end - module Solargraph module LanguageServer module Message diff --git a/lib/solargraph/language_server/message/text_document/definition.rb b/lib/solargraph/language_server/message/text_document/definition.rb index 47bf7a60d..5f143cc82 100644 --- a/lib/solargraph/language_server/message/text_document/definition.rb +++ b/lib/solargraph/language_server/message/text_document/definition.rb @@ -10,6 +10,7 @@ def process private + # @return [Array] def code_location suggestions = host.definitions_at(params['textDocument']['uri'], @line, @column) return nil if suggestions.empty? @@ -21,6 +22,7 @@ def code_location end end + # @return [Array] def require_location # @todo Terrible hack lib = host.library_for(params['textDocument']['uri']) 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 8143d7710..8c95c231e 100644 --- a/lib/solargraph/language_server/message/text_document/type_definition.rb +++ b/lib/solargraph/language_server/message/text_document/type_definition.rb @@ -10,6 +10,7 @@ def process private + # @return [Array] def code_location suggestions = host.type_definitions_at(params['textDocument']['uri'], @line, @column) return nil if suggestions.empty? diff --git a/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb b/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb index 2e7b4130c..e1e83fc1e 100644 --- a/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb +++ b/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb @@ -9,11 +9,13 @@ def process private + # @return [void] def add_folders return unless params['event'] && params['event']['added'] host.prepare_folders params['event']['added'] end + # @return [void] def remove_folders return unless params['event'] && params['event']['removed'] params['event']['removed'].each do |folder| diff --git a/lib/solargraph/language_server/progress.rb b/lib/solargraph/language_server/progress.rb index 0b2aac5fe..10900a37e 100644 --- a/lib/solargraph/language_server/progress.rb +++ b/lib/solargraph/language_server/progress.rb @@ -39,6 +39,7 @@ def initialize title # @param message [String] # @param percentage [Integer] + # @return [void] def begin message, percentage @kind = 'begin' @message = message @@ -47,6 +48,7 @@ def begin message, percentage # @param message [String] # @param percentage [Integer] + # @return [void] def report message, percentage @kind = 'report' @message = message @@ -54,6 +56,7 @@ def report message, percentage end # @param message [String] + # @return [void] def finish message @kind = 'end' @message = message @@ -62,6 +65,7 @@ def finish message end # @param host [Solargraph::LanguageServer::Host] + # @return [void] def send host return unless host.client_supports_progress? && !finished? @@ -91,6 +95,7 @@ def create host @status = CREATED end + # @return [Hash] def build { token: uuid, @@ -101,6 +106,7 @@ def build } end + # @return [Hash] def build_value case kind when 'begin' @@ -115,6 +121,7 @@ def build_value end # @param host [Host] + # @return [void] def keep_alive host mutex.synchronize { @last = Time.now } @keep_alive ||= Thread.new do @@ -127,6 +134,7 @@ def keep_alive host end end + # @return [Mutex] def mutex @mutex ||= Mutex.new end diff --git a/lib/solargraph/language_server/request.rb b/lib/solargraph/language_server/request.rb index e9aea65eb..dcad7084d 100644 --- a/lib/solargraph/language_server/request.rb +++ b/lib/solargraph/language_server/request.rb @@ -16,6 +16,7 @@ def process result @block.call(result) unless @block.nil? end + # @return [void] def send_response # noop end diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 72224f672..3e4849537 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'rubygems' require 'pathname' require 'observer' require 'open3' @@ -259,11 +260,11 @@ def references_from filename, line, column, strip: false, only: false referenced&.path == pin.path end if pin.path == 'Class#new' - caller = cursor.chain.base.infer(api_map, clip.send(:block), clip.locals).first + caller = cursor.chain.base.infer(api_map, clip.send(:closure), clip.locals).first if caller.defined? found.select! do |loc| clip = api_map.clip_at(loc.filename, loc.range.start) - other = clip.send(:cursor).chain.base.infer(api_map, clip.send(:block), clip.locals).first + other = clip.send(:cursor).chain.base.infer(api_map, clip.send(:closure), clip.locals).first caller == other end else @@ -273,12 +274,12 @@ def references_from filename, line, column, strip: false, only: false # HACK: for language clients that exclude special characters from the start of variable names if strip && match = cursor.word.match(/^[^a-z0-9_]+/i) found.map! do |loc| - Solargraph::Location.new(loc.filename, Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line, loc.range.ending.column)) + Solargraph::Location.new(loc.filename, + Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line, + loc.range.ending.column)) end end - result.concat(found.sort do |a, b| - a.range.start.line <=> b.range.start.line - end) + result.concat(found.sort { |a, b| a.range.start.line <=> b.range.start.line }) end result.uniq end @@ -303,9 +304,7 @@ def locate_ref location return nil if pin.nil? # @param full [String] return_if_match = proc do |full| - if source_map_hash.key?(full) - return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) - end + return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) if source_map_hash.key?(full) end workspace.require_paths.each do |path| full = File.join path, pin.name @@ -402,8 +401,8 @@ def diagnose filename repargs = {} workspace.config.reporters.each do |line| if line == 'all!' - Diagnostics.reporters.each do |reporter| - repargs[reporter] ||= [] + Diagnostics.reporters.each do |reporter_name| + repargs[Diagnostics.reporter(reporter_name)] ||= [] end else args = line.split(':').map(&:strip) @@ -500,7 +499,12 @@ def external_requires private - # @return [Hash{String => Set}] + # @return [PinCache] + def pin_cache + workspace.pin_cache + end + + # @return [Hash{String => Array}] def source_map_external_require_hash @source_map_external_require_hash ||= {} end @@ -508,6 +512,7 @@ def source_map_external_require_hash # @param source_map [SourceMap] # @return [void] def find_external_requires source_map + # @type [Set] new_set = source_map.requires.map(&:name).to_set # return if new_set == source_map_external_require_hash[source_map.filename] _filenames = nil @@ -579,12 +584,13 @@ def cache_errors def cache_next_gemspec return if @cache_progress + # @type [Gem::Specification] spec = cacheable_specs.first return end_cache_progress unless spec pending = api_map.uncached_gemspecs.length - cache_errors.length - 1 - if Yardoc.processing?(spec) + if pin_cache.yardoc_processing?(spec) logger.info "Enqueuing cache of #{spec.name} #{spec.version} (already being processed)" queued_gemspec_cache.push(spec) return if pending - queued_gemspec_cache.length < 1 @@ -595,7 +601,10 @@ def cache_next_gemspec logger.info "Caching #{spec.name} #{spec.version}" Thread.new do report_cache_progress spec.name, pending - _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s) + kwargs = {} + kwargs[:chdir] = workspace.directory.to_s if workspace.directory && !workspace.directory.empty? + _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s, + **kwargs) if s.success? logger.info "Cached #{spec.name} #{spec.version}" else @@ -612,8 +621,7 @@ def cache_next_gemspec # @return [Array] def cacheable_specs - cacheable = api_map.uncached_yard_gemspecs + - api_map.uncached_rbs_collection_gemspecs - + cacheable = api_map.uncached_gemspecs + queued_gemspec_cache - cache_errors.to_a return cacheable unless cacheable.empty? @@ -672,8 +680,7 @@ def sync_catalog source_map_hash.values.each { |map| find_external_requires(map) } api_map.catalog bench logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)" - logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs" - logger.info "#{api_map.uncached_rbs_collection_gemspecs.length} uncached RBS collection gemspecs" + logger.info "#{api_map.uncached_gemspecs.length} uncached gemspecs" cache_next_gemspec @sync_count = 0 end diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index 74d1318df..444fc0659 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -7,29 +7,31 @@ module Solargraph class Location include Equality - # @return [String] + # @return [String, nil] attr_reader :filename # @return [Solargraph::Range] attr_reader :range - # @param filename [String] + # @param filename [String, nil] # @param range [Solargraph::Range] def initialize filename, range @filename = filename @range = range end - # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields [filename, range] end + # @param other [self] def <=>(other) return nil unless other.is_a?(Location) if filename == other.filename range <=> other.range else + return -1 if filename.nil? + return 1 if other.filename.nil? filename <=> other.filename end end @@ -60,6 +62,7 @@ def to_hash end # @param node [Parser::AST::Node, nil] + # @return [self, nil] def self.from_node(node) return nil if node.nil? || node.loc.nil? range = Range.from_node(node) diff --git a/lib/solargraph/logging.rb b/lib/solargraph/logging.rb index a8bc3b3ee..f26867db3 100644 --- a/lib/solargraph/logging.rb +++ b/lib/solargraph/logging.rb @@ -30,9 +30,26 @@ module Logging module_function + # override this in your class to temporarily set a custom + # filtering log level for the class (e.g., suppress any debug + # message by setting it to :info even if it is set elsewhere, or + # show existing debug messages by setting to :debug). + # + # @return [Symbol] + def log_level + :warn + end + # @return [Logger] def logger - @@logger + if LOG_LEVELS[log_level.to_s] == DEFAULT_LOG_LEVEL + @@logger + else + new_log_level = LOG_LEVELS[log_level.to_s] + logger = Logger.new(STDERR, level: new_log_level) + logger.formatter = @@logger.formatter + logger + end end end end diff --git a/lib/solargraph/parser/comment_ripper.rb b/lib/solargraph/parser/comment_ripper.rb index e74fcb259..92373df20 100644 --- a/lib/solargraph/parser/comment_ripper.rb +++ b/lib/solargraph/parser/comment_ripper.rb @@ -3,6 +3,13 @@ module Solargraph module Parser class CommentRipper < Ripper::SexpBuilderPP + # @!override Ripper::SexpBuilder#on_embdoc_beg + # @return [Array(Symbol, String, Array)] + # @!override Ripper::SexpBuilder#on_embdoc + # @return [Array(Symbol, String, Array)] + # @!override Ripper::SexpBuilder#on_embdoc_end + # @return [Array(Symbol, String, Array)] + # @param src [String] # @param filename [String] # @param lineno [Integer] diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 308db214b..4c80d3dea 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -15,7 +15,9 @@ def initialize(locals, enclosing_breakable_pin = nil) # # @return [void] def process_and(and_node, true_ranges = []) + # @type [Parser::AST::Node] lhs = and_node.children[0] + # @type [Parser::AST::Node] rhs = and_node.children[1] before_rhs_loc = rhs.location.expression.adjust(begin_pos: -1) @@ -42,7 +44,9 @@ def process_if(if_node) # s(:send, nil, :bar)) # [4] pry(main)> conditional_node = if_node.children[0] + # @type [Parser::AST::Node] then_clause = if_node.children[1] + # @type [Parser::AST::Node] else_clause = if_node.children[2] true_ranges = [] @@ -156,6 +160,7 @@ def process_facts(facts_by_pin, presences) # @param conditional_node [Parser::AST::Node] # @param true_ranges [Array] + # @return [void] def process_conditional(conditional_node, true_ranges) if conditional_node.type == :send process_isa(conditional_node, true_ranges) diff --git a/lib/solargraph/parser/node_methods.rb b/lib/solargraph/parser/node_methods.rb deleted file mode 100644 index 5d3d1079a..000000000 --- a/lib/solargraph/parser/node_methods.rb +++ /dev/null @@ -1,97 +0,0 @@ -module Solargraph - module Parser - module NodeMethods - module_function - - # @abstract - # @param node [Parser::AST::Node] - # @return [String] - def unpack_name node - raise NotImplementedError - end - - # @abstract - # @todo Temporarily here for testing. Move to Solargraph::Parser. - # @param node [Parser::AST::Node] - # @return [Array] - def call_nodes_from node - raise NotImplementedError - end - - # Find all the nodes within the provided node that potentially return a - # value. - # - # The node parameter typically represents a method's logic, e.g., the - # second child (after the :args node) of a :def node. A simple one-line - # method would typically return itself, while a node with conditions - # would return the resulting node from each conditional branch. Nodes - # that follow a :return node are assumed to be unreachable. Nil values - # are converted to nil node types. - # - # @abstract - # @param node [Parser::AST::Node] - # @return [Array] - def returns_from_method_body node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # - # @return [Array] - def const_nodes_from node - raise NotImplementedError - end - - # @abstract - # @param cursor [Solargraph::Source::Cursor] - # @return [Parser::AST::Node, nil] - def find_recipient_node cursor - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Array] low-level value nodes in - # value position. Does not include explicit return - # statements - def value_position_nodes_only(node) - raise NotImplementedError - end - - # @abstract - # @param nodes [Enumerable] - def any_splatted_call?(nodes) - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [void] - def process node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Hash{Parser::AST::Node => Source::Chain}] - def convert_hash node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Position] - def get_node_start_position(node) - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Position] - def get_node_end_position(node) - raise NotImplementedError - end - end - end -end diff --git a/lib/solargraph/parser/node_processor.rb b/lib/solargraph/parser/node_processor.rb index a55b7120b..347240309 100644 --- a/lib/solargraph/parser/node_processor.rb +++ b/lib/solargraph/parser/node_processor.rb @@ -9,7 +9,7 @@ module NodeProcessor autoload :Base, 'solargraph/parser/node_processor/base' class << self - # @type [Hash>>] + # @type [Hash{Symbol => Array>}] @@processors ||= {} # Register a processor for a node type. You can register multiple processors for the same type. @@ -17,12 +17,15 @@ class << self # # @param type [Symbol] # @param cls [Class] - # @return [Class] + # @return [Array>] def register type, cls @@processors[type] ||= [] @@processors[type] << cls end + # @param type [Symbol] + # @param cls [Class] + # @return [void] def deregister type, cls @@processors[type].delete(cls) end @@ -31,7 +34,7 @@ def deregister type, cls # @param node [Parser::AST::Node] # @param region [Region] # @param pins [Array] - # @param locals [Array] + # @param locals [Array] # @return [Array(Array, Array)] def self.process node, region = Region.new, pins = [], locals = [] if pins.empty? diff --git a/lib/solargraph/parser/node_processor/base.rb b/lib/solargraph/parser/node_processor/base.rb index d87268a91..fad31e95b 100644 --- a/lib/solargraph/parser/node_processor/base.rb +++ b/lib/solargraph/parser/node_processor/base.rb @@ -13,7 +13,7 @@ class Base # @return [Array] attr_reader :pins - # @return [Array] + # @return [Array] attr_reader :locals # @param node [Parser::AST::Node] diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index ddc742bd8..de1110ac9 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -1,15 +1,8 @@ # frozen_string_literal: true require 'prism' - -# Awaiting ability to use a version containing https://github.com/whitequark/parser/pull/1076 -# -# @!parse -# class ::Parser::Base < ::Parser::Builder -# # @return [Integer] -# def version; end -# end -# class ::Parser::CurrentRuby < ::Parser::Base; end +require 'ast' +require 'parser' module Solargraph module Parser @@ -81,9 +74,10 @@ def references source, name end # @param name [String] - # @param top [AST::Node] - # @return [Array] + # @param top [Parser::AST::Node] + # @return [Array] def inner_node_references name, top + # @type [Array] result = [] if top.is_a?(AST::Node) && top.to_s.include?(":#{name}") result.push top if top.children.any? { |c| c.to_s == name } @@ -118,7 +112,7 @@ def version parser.version end - # @param node [BasicObject] + # @param node [Object] # @return [Boolean] def is_ast_node? node node.is_a?(::Parser::AST::Node) @@ -137,9 +131,7 @@ def node_range node def string_ranges node return [] unless is_ast_node?(node) result = [] - if node.type == :str - result.push Range.from_node(node) - end + result.push Range.from_node(node) if node.type == :str node.children.each do |child| result.concat string_ranges(child) end diff --git a/lib/solargraph/parser/parser_gem/flawed_builder.rb b/lib/solargraph/parser/parser_gem/flawed_builder.rb index b5750413d..acf665e16 100644 --- a/lib/solargraph/parser/parser_gem/flawed_builder.rb +++ b/lib/solargraph/parser/parser_gem/flawed_builder.rb @@ -9,6 +9,7 @@ module ParserGem class FlawedBuilder < ::Parser::Builders::Default # @param token [::Parser::AST::Node] # @return [String] + # @sg-ignore 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 646e753d5..d8d46319b 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -99,7 +99,8 @@ def generate_links n elsif [:gvar, :gvasgn].include?(n.type) result.push Chain::GlobalVariable.new(n.children[0].to_s) elsif n.type == :or_asgn - result.concat generate_links n.children[1] + new_node = n.updated(n.children[0].type, n.children[0].children + [n.children[1]]) + result.concat generate_links new_node elsif [:class, :module, :def, :defs].include?(n.type) # @todo Undefined or what? result.push Chain::UNDEFINED_CALL diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index b716b352d..45eb038e2 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -3,20 +3,6 @@ require 'parser' require 'ast' -# Teach AST::Node#children about its generic type -# -# @todo contribute back to https://github.com/ruby/gem_rbs_collection/blob/main/gems/ast/2.4/ast.rbs -# -# @!parse -# module ::AST -# class Node -# # New children -# -# # @return [Array] -# attr_reader :children -# end -# end - # https://github.com/whitequark/parser module Solargraph module Parser @@ -120,7 +106,7 @@ def drill_signature node, signature end # @param node [Parser::AST::Node] - # @return [Hash{Parser::AST::Node => Chain}] + # @return [Hash{Parser::AST::Node, Symbol => Source::Chain}] def convert_hash node return {} unless Parser.is_ast_node?(node) return convert_hash(node.children[0]) if node.type == :kwsplat @@ -179,6 +165,7 @@ def call_nodes_from node node.children[1..-1].each { |child| result.concat call_nodes_from(child) } elsif node.type == :send result.push node + result.concat call_nodes_from(node.children.first) node.children[2..-1].each { |child| result.concat call_nodes_from(child) } elsif [:super, :zsuper].include?(node.type) result.push node @@ -232,6 +219,7 @@ def find_recipient_node cursor else source.tree_at(position.line, position.column - 1) end + # @type [AST::Node, nil] prev = nil tree.each do |node| if node.type == :send @@ -345,7 +333,7 @@ def value_position_nodes_only(node) # Look at known control statements and use them to find # more specific return nodes. # - # @param node [Parser::AST::Node] Statement which is in + # @param node [AST::Node] Statement which is in # value position for a method body # @param include_explicit_returns [Boolean] If true, # include the value nodes of the parameter of the 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 70a2d9e68..d773e8e50 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb @@ -19,7 +19,7 @@ def process else region.closure end - pins.push Solargraph::Pin::Block.new( + block_pin = Solargraph::Pin::Block.new( location: location, closure: parent, node: node, @@ -28,7 +28,8 @@ def process scope: region.scope || region.closure.context.scope, source: :parser ) - process_children region.update(closure: pins.last) + pins.push block_pin + process_children region.update(closure: block_pin) end private 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 5784afcbe..2452b9cc5 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb @@ -11,6 +11,8 @@ def process process_children position = get_node_start_position(node) + # @sg-ignore + # @type [Solargraph::Pin::Breakable, nil] enclosing_breakable_pin = pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location.range.contain?(position)}.last FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_if(node) end diff --git a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb index 8a70a0b3f..8490fd9b0 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb @@ -8,32 +8,41 @@ class SendNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process + # @sg-ignore + # @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? - if [:private, :public, :protected].include?(node.children[1]) + if [:private, :public, :protected].include?(method_name) process_visibility - elsif node.children[1] == :module_function + elsif method_name == :module_function process_module_function - elsif [:attr_reader, :attr_writer, :attr_accessor].include?(node.children[1]) + elsif [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) process_attribute - elsif node.children[1] == :include + elsif method_name == :include process_include - elsif node.children[1] == :extend + elsif method_name == :extend process_extend - elsif node.children[1] == :prepend + elsif method_name == :prepend process_prepend - elsif node.children[1] == :require + elsif method_name == :require process_require - elsif node.children[1] == :autoload + elsif method_name == :autoload process_autoload - elsif node.children[1] == :private_constant + elsif method_name == :private_constant process_private_constant - elsif node.children[1] == :alias_method && node.children[2] && node.children[2] && node.children[2].type == :sym && node.children[3] && node.children[3].type == :sym + elsif method_name == :alias_method && node.children[2] && node.children[2] && node.children[2].type == :sym && node.children[3] && node.children[3].type == :sym process_alias_method - elsif node.children[1] == :private_class_method && node.children[2].is_a?(AST::Node) + elsif method_name == :private_class_method && node.children[2].is_a?(AST::Node) # Processing a private class can potentially handle children on its own return if process_private_class_method end - elsif node.children[1] == :require && node.children[0].to_s == '(const nil :Bundler)' + elsif method_name == :require && node.children[0].to_s == '(const nil :Bundler)' pins.push Pin::Reference::Require.new(Solargraph::Location.new(region.filename, Solargraph::Range.from_to(0, 0, 0, 0)), 'bundler/require', source: :parser) end process_children @@ -45,15 +54,24 @@ def process def process_visibility if (node.children.length > 2) node.children[2..-1].each do |child| + # @sg-ignore + # @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?(AST::Node) && (child.type == :sym || child.type == :str) name = child.children[0].to_s matches = pins.select{ |pin| pin.is_a?(Pin::Method) && pin.name == name && pin.namespace == region.closure.full_context.namespace && pin.context.scope == (region.scope || :instance)} matches.each do |pin| # @todo Smelly instance variable access - pin.instance_variable_set(:@visibility, node.children[1]) + pin.instance_variable_set(:@visibility, visibility) end else - process_children region.update(visibility: node.children[1]) + process_children region.update(visibility: visibility) end end else diff --git a/lib/solargraph/parser/region.rb b/lib/solargraph/parser/region.rb index 8280c99b6..a6559bc8a 100644 --- a/lib/solargraph/parser/region.rb +++ b/lib/solargraph/parser/region.rb @@ -23,8 +23,10 @@ class Region # @param source [Source] # @param namespace [String] + # @param closure [Pin::Closure, nil] # @param scope [Symbol, nil] # @param visibility [Symbol] + # @param lvars [Array] def initialize source: Solargraph::Source.load_string(''), closure: nil, scope: nil, visibility: :public, lvars: [] @source = source @@ -45,6 +47,7 @@ def filename # @param closure [Pin::Closure, nil] # @param scope [Symbol, nil] # @param visibility [Symbol, nil] + # @param lvars [Array, nil] # @return [Region] def update closure: nil, scope: nil, visibility: nil, lvars: nil Region.new( diff --git a/lib/solargraph/parser/snippet.rb b/lib/solargraph/parser/snippet.rb index d28c57c8c..1ea6bd6d9 100644 --- a/lib/solargraph/parser/snippet.rb +++ b/lib/solargraph/parser/snippet.rb @@ -1,11 +1,13 @@ module Solargraph module Parser class Snippet - # @return [Range] + # @return [Solargraph::Range] attr_reader :range # @return [String] attr_reader :text + # @param range [Solargraph::Range] + # @param text [String] def initialize range, text @range = range @text = text diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index fb3274dab..0ce9902f5 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -69,8 +69,15 @@ def assert_location_provided Solargraph.assert_or_log(:best_location, "Neither location nor type_location provided - #{path} #{source} #{self.class}") end + # @return [Pin::Closure, nil] + def closure + Solargraph.assert_or_log(:closure, "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") unless @closure + # @type [Pin::Closure, nil] + @closure + end + # @param other [self] - # @param attrs [Hash{Symbol => Object}] + # @param attrs [Hash{::Symbol => Object}] # # @return [self] def combine_with(other, attrs={}) @@ -182,15 +189,19 @@ def combine_return_type(other) other.return_type elsif other.return_type.undefined? return_type + elsif return_type.erased_version_of?(other.return_type) + other.return_type + elsif other.return_type.erased_version_of?(return_type) + return_type elsif dodgy_return_type_source? && !other.dodgy_return_type_source? other.return_type elsif other.dodgy_return_type_source? && !dodgy_return_type_source? return_type else all_items = return_type.items + other.return_type.items - if all_items.any? { |item| item.selfy? } && all_items.any? { |item| item.rooted_tag == context.rooted_tag } + if all_items.any? { |item| item.selfy? } && all_items.any? { |item| item.rooted_namespace == context.rooted_namespace } # assume this was a declaration that should have said 'self' - all_items.delete_if { |item| item.rooted_tag == context.rooted_tag } + all_items.delete_if { |item| item.rooted_namespace == context.rooted_namespace } end ComplexType.new(all_items) end @@ -218,7 +229,7 @@ def choose(other, attr) end # @param other [self] - # @param attr [Symbol] + # @param attr [::Symbol] # @sg-ignore # @return [undefined] def choose_node(other, attr) @@ -298,9 +309,15 @@ def assert_same_count(other, attr) # @param other [self] # @param attr [::Symbol] # - # @return [Object, nil] + # @sg-ignore + # @return [undefined] def assert_same(other, attr) - return false if other.nil? + # :nocov: + if other.nil? + Solargraph.assert_or_log("combine_with_#{attr}".to_sym, "Sent nil for comparison") + return send(attr) + end + # :nocov: val1 = send(attr) val2 = other.send(attr) return val1 if val1 == val2 @@ -329,6 +346,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] def choose_pin_attr(other, attr) # @type [Pin::Base, nil] @@ -336,11 +355,14 @@ def choose_pin_attr(other, attr) # @type [Pin::Base, nil] val2 = other.send(attr) if val1.class != val2.class + # :nocov: Solargraph.assert_or_log("combine_with_#{attr}_class".to_sym, "Inconsistent #{attr.inspect} class values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") return val1 + # :nocov: end # arbitrary way of choosing a pin + # @sg-ignore Need _1 support [val1, val2].compact.min_by { _1.best_location.to_s } end diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index 20d2301eb..4b0f45fdf 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -21,10 +21,13 @@ def initialize block: nil, return_type: nil, parameters: [], **splat @parameters = parameters end + # @return [String] def method_namespace closure.namespace end + # @param other [self] + # @return [Solargraph::Pin::Signature, nil] def combine_blocks(other) if block.nil? other.block @@ -57,6 +60,8 @@ def generics [] end + # @param other [self] + # @return [::Array] def choose_parameters(other) raise "Trying to combine two pins with different arities - \nself =#{inspect}, \nother=#{other.inspect}, \n\n self.arity=#{self.arity}, \nother.arity=#{other.arity}" if other.arity != arity parameters.zip(other.parameters).map do |param, other_param| @@ -70,6 +75,7 @@ def choose_parameters(other) end end + # @return [Array] def blockless_parameters if parameters.last&.block? parameters[0..-2] @@ -78,6 +84,7 @@ def blockless_parameters end end + # @return [Array] def arity [generics, blockless_parameters.map(&:arity_decl), block&.arity] end @@ -125,6 +132,7 @@ def typify api_map end end + # @return [String] def method_name raise "closure was nil in #{self.inspect}" if closure.nil? @method_name ||= closure.name @@ -197,6 +205,12 @@ def arity_matches? arguments, with_block true end + def reset_generated! + super + @parameters.each(&:reset_generated!) + end + + # @return [Integer] def mandatory_positional_param_count parameters.count(&:arg?) end diff --git a/lib/solargraph/pin/closure.rb b/lib/solargraph/pin/closure.rb index 551ba5522..2d87bad07 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -8,6 +8,7 @@ class Closure < Base # @param scope [::Symbol] :class or :instance # @param generics [::Array, nil] + # @param generic_defaults [Hash{String => ComplexType}] def initialize scope: :class, generics: nil, generic_defaults: {}, **splat super(**splat) @scope = scope @@ -15,6 +16,7 @@ def initialize scope: :class, generics: nil, generic_defaults: {}, **splat @generic_defaults = generic_defaults end + # @return [Hash{String => ComplexType}] def generic_defaults @generic_defaults ||= {} end @@ -46,6 +48,10 @@ def binder @binder || context end + # @param api_map [Solargraph::ApiMap] + # @return [void] + def rebind api_map; end + # @return [::Array] def gates # @todo This check might not be necessary. There should always be a diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index 829dbe496..f7fbf4573 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -3,12 +3,22 @@ module Solargraph module Pin module Common + # @!method closure + # @abstract + # @return [Pin::Closure, nil] + # @!method source + # @abstract + # @return [String, nil] + # @return [Location] attr_reader :location + # @sg-ignore Solargraph::Pin::Common#closure return type could not be inferred # @return [Pin::Closure, nil] def closure - Solargraph.assert_or_log(:closure, "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") unless @closure + unless @closure + Solargraph.assert_or_log(:closure, "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") + end @closure end diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index c680bebd0..36b75773c 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -26,7 +26,7 @@ def combine_with(other, attrs={}) assignment: assert_same(other, :assignment), presence_certain: assert_same(other, :presence_certain?), }.merge(attrs) - new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence) + new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence) super(other, new_attrs) end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 6309cb55a..0a81d0c93 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -51,7 +51,7 @@ def combine_all_signature_pins(*signature_pins) end # @param other [Pin::Method] - # @return [Symbol] + # @return [::Symbol] def combine_visibility(other) if dodgy_visibility_source? && !other.dodgy_visibility_source? other.visibility @@ -388,7 +388,7 @@ def probe api_map attribute? ? infer_from_iv(api_map) : infer_from_return_nodes(api_map) end - # @return [::Array] + # @return [::Array] def overloads # Ignore overload tags with nil parameters. If it's not an array, the # tag's source is likely malformed. diff --git a/lib/solargraph/pin/method_alias.rb b/lib/solargraph/pin/method_alias.rb index 28be6d8b1..feb6baccc 100644 --- a/lib/solargraph/pin/method_alias.rb +++ b/lib/solargraph/pin/method_alias.rb @@ -26,6 +26,14 @@ def visibility :public end + def to_rbs + if scope == :class + "alias self.#{name} self.#{original}" + else + "alias #{name} #{original}" + end + end + def path @path ||= namespace + (scope == :instance ? '#' : '.') + name end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index e298ba20a..b4405de40 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -135,6 +135,11 @@ def full end end + def reset_generated! + super + @return_type = nil if @return_type&.undefined? + end + # @return [ComplexType] def return_type if @return_type.nil? @@ -176,7 +181,13 @@ def compatible_arg?(atype, api_map) # make sure we get types from up the method # inheritance chain if we don't have them on this pin ptype = typify api_map - ptype.undefined? || ptype.can_assign?(api_map, atype) || ptype.generic? + return true if ptype.undefined? + + return true if atype.conforms_to?(api_map, + ptype, + :method_call, + [:allow_empty_params, :allow_undefined]) + ptype.generic? end def documentation diff --git a/lib/solargraph/pin/proxy_type.rb b/lib/solargraph/pin/proxy_type.rb index 819c97481..2323489a7 100644 --- a/lib/solargraph/pin/proxy_type.rb +++ b/lib/solargraph/pin/proxy_type.rb @@ -4,6 +4,7 @@ module Solargraph module Pin class ProxyType < Base # @param return_type [ComplexType] + # @param binder [ComplexType, ComplexType::UniqueType, nil] def initialize return_type: ComplexType::UNDEFINED, binder: nil, **splat super(**splat) @return_type = return_type diff --git a/lib/solargraph/pin/reference/override.rb b/lib/solargraph/pin/reference/override.rb index d547e3caf..878c309db 100644 --- a/lib/solargraph/pin/reference/override.rb +++ b/lib/solargraph/pin/reference/override.rb @@ -14,16 +14,30 @@ def closure nil end + # @param location [Location, nil] + # @param name [String] + # @param tags [::Array] + # @param delete [::Array] + # @param splat [Hash] def initialize location, name, tags, delete = [], **splat super(location: location, name: name, **splat) @tags = tags @delete = delete end + # @param name [String] + # @param tags [::Array] + # @param delete [::Array] + # @param splat [Hash] + # @return [Solargraph::Pin::Reference::Override] def self.method_return name, *tags, delete: [], **splat - new(nil, name, [YARD::Tags::Tag.new('return', nil, tags)], delete, **splat) + new(nil, name, [YARD::Tags::Tag.new('return', '', tags)], delete, **splat) end + # @param name [String] + # @param comment [String] + # @param splat [Hash] + # @return [Solargraph::Pin::Reference::Override] def self.from_comment name, comment, **splat new(nil, name, Solargraph::Source.parse_docstring(comment).to_docstring.tags, **splat) end diff --git a/lib/solargraph/pin/search.rb b/lib/solargraph/pin/search.rb index fc0f000cd..33f02e027 100644 --- a/lib/solargraph/pin/search.rb +++ b/lib/solargraph/pin/search.rb @@ -12,6 +12,8 @@ class Result # @return [Pin::Base] attr_reader :pin + # @param match [Float] The match score for the pin + # @param pin [Pin::Base] def initialize match, pin @match = match @pin = pin diff --git a/lib/solargraph/pin/signature.rb b/lib/solargraph/pin/signature.rb index 818d66411..4c25e028b 100644 --- a/lib/solargraph/pin/signature.rb +++ b/lib/solargraph/pin/signature.rb @@ -39,6 +39,8 @@ def typify api_map end return ComplexType::UNDEFINED if closure.nil? return ComplexType::UNDEFINED unless closure.is_a?(Pin::Method) + # @sg-ignore need is_a? support + # @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| diff --git a/lib/solargraph/pin/symbol.rb b/lib/solargraph/pin/symbol.rb index 9e11c3d7d..294363f5f 100644 --- a/lib/solargraph/pin/symbol.rb +++ b/lib/solargraph/pin/symbol.rb @@ -20,6 +20,10 @@ def path '' end + def closure + @closure ||= Pin::ROOT_PIN + end + def completion_item_kind Solargraph::LanguageServer::CompletionItemKinds::KEYWORD end @@ -36,6 +40,7 @@ def directives [] end + # @return [::Symbol] def visibility :public end diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 2a0ec4639..7e769b03b 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -1,12 +1,429 @@ -require 'yard-activesupport-concern' require 'fileutils' require 'rbs' +require 'rubygems' module Solargraph - module PinCache + class PinCache + include Logging + + attr_reader :directory, :rbs_collection_path, :rbs_collection_config_path, :yard_plugins + + # @param rbs_collection_path [String, nil] + # @param rbs_collection_config_path [String, nil] + # @param directory [String, nil] + # @param yard_plugins [Array] + def initialize rbs_collection_path:, rbs_collection_config_path:, + directory:, + yard_plugins: + @rbs_collection_path = rbs_collection_path + @rbs_collection_config_path = rbs_collection_config_path + @directory = directory + @yard_plugins = yard_plugins + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + def cached? gemspec + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + combined_gem?(gemspec, rbs_version_cache_key) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rebuild [Boolean] whether to rebuild the cache regardless of whether it already exists + # @param out [IO, nil] output stream for logging + # @return [void] + def cache_gem gemspec:, rebuild: false, out: nil + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + + build_yard, build_rbs_collection, build_combined = + calculate_build_needs(gemspec, + rebuild: rebuild, + rbs_version_cache_key: rbs_version_cache_key) + + return unless build_yard || build_rbs_collection || build_combined + + build_combine_and_cache(gemspec, + rbs_version_cache_key, + build_yard: build_yard, + build_rbs_collection: build_rbs_collection, + build_combined: build_combined, + out: out) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String] + def suppress_yard_cache? gemspec, rbs_version_cache_key + if gemspec.name == 'parser' && rbs_version_cache_key != RbsMap::CACHE_KEY_UNRESOLVED + # parser takes forever to build YARD pins, but has excellent RBS collection pins + return true + end + false + end + + # @param out [IO, nil] output stream for logging + # + # @return [void] + def cache_all_stdlibs out: $stderr + possible_stdlibs.each do |stdlib| + RbsMap::StdlibMap.new(stdlib, out: out) + end + end + + # @param path [String] require path that might be in the RBS stdlib collection + # @return [void] + def cache_stdlib_rbs_map path + # these are held in memory in RbsMap::StdlibMap + map = RbsMap::StdlibMap.load(path) + if map.resolved? + logger.debug { "Loading stdlib pins for #{path}" } + pins = map.pins + logger.debug { "Loaded #{pins.length} stdlib pins for #{path}" } + pins + else + # @todo Temporarily ignoring unresolved `require 'set'` + logger.debug { "Require path #{path} could not be resolved in RBS" } unless path == 'set' + nil + end + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # + # @return [String] + def lookup_rbs_version_cache_key gemspec + rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) + rbs_map.cache_key + end + + # @param gemspec [Gem::Specification] + # @param rbs_version_cache_key [String] + # @param yard_pins [Array] + # @param rbs_collection_pins [Array] + # @return [void] + def cache_combined_pins gemspec, rbs_version_cache_key, yard_pins, rbs_collection_pins + combined_pins = GemPins.combine(yard_pins, rbs_collection_pins) + serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins) + end + + # @param gemspec [Gem::Specification] + # @return [Array] + def deserialize_combined_pin_cache gemspec + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + + load_combined_gem(gemspec, rbs_version_cache_key) + end + + # @param gemspec [Gem::Specification] + # @param out [IO, nil] + # @return [void] + def uncache_gem gemspec, out: nil + PinCache.uncache(yardoc_path(gemspec), out: out) + PinCache.uncache(yard_gem_path(gemspec), out: out) + uncache_by_prefix(rbs_collection_pins_path_prefix(gemspec), out: out) + uncache_by_prefix(combined_path_prefix(gemspec), out: out) + combined_pins_in_memory.delete([gemspec.name, gemspec.version]) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + def yardoc_processing? gemspec + Yardoc.processing?(yardoc_path(gemspec)) + end + + # @return [Array] a list of possible standard library names + def possible_stdlibs + # all dirs and .rb files in Gem::RUBYGEMS_DIR + Dir.glob(File.join(Gem::RUBYGEMS_DIR, '*')).map do |file_or_dir| + basename = File.basename(file_or_dir) + # remove .rb + basename = basename[0..-4] if basename.end_with?('.rb') + basename + end.sort.uniq + rescue StandardError => e + logger.info { "Failed to get possible stdlibs: #{e.message}" } + logger.debug { e.backtrace.join("\n") } + [] + end + + private + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rebuild [Boolean] whether to rebuild the cache regardless of whether it already exists + # @param rbs_version_cache_key [String, nil] the cache key for the gem in the RBS collection + # + # @return [Array(Boolean, Boolean, Boolean)] whether to build YARD + # pins, RBS collection pins, and combined pins + def calculate_build_needs gemspec, rebuild:, rbs_version_cache_key: + if rebuild + build_yard = true + build_rbs_collection = true + build_combined = true + else + build_yard = !yard_gem?(gemspec) + build_rbs_collection = !rbs_collection_pins?(gemspec, rbs_version_cache_key) + build_combined = !combined_gem?(gemspec, rbs_version_cache_key) || build_yard || build_rbs_collection + end + + build_yard = false if suppress_yard_cache?(gemspec, rbs_version_cache_key) + + [build_yard, build_rbs_collection, build_combined] + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String, nil] + # @param build_yard [Boolean] + # @param build_rbs_collection [Boolean] + # @param build_combined [Boolean] + # @param out [IO, nil] + # + # @return [void] + def build_combine_and_cache gemspec, + rbs_version_cache_key, + build_yard:, + build_rbs_collection:, + build_combined:, + out: + log_cache_info(gemspec, rbs_version_cache_key, + build_yard: build_yard, + build_rbs_collection: build_rbs_collection, + build_combined: build_combined, + out: out) + cache_yard_pins(gemspec, out) if build_yard + # this can be nil even if we aren't told to build it - see suppress_yard_cache? + yard_pins = deserialize_yard_pin_cache(gemspec) || [] + cache_rbs_collection_pins(gemspec, out) if build_rbs_collection + rbs_collection_pins = deserialize_rbs_collection_cache(gemspec, rbs_version_cache_key) || [] + cache_combined_pins(gemspec, rbs_version_cache_key, yard_pins, rbs_collection_pins) if build_combined + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String, nil] + # @param build_yard [Boolean] + # @param build_rbs_collection [Boolean] + # @param build_combined [Boolean] + # @param out [IO, nil] + # + # @return [void] + def log_cache_info gemspec, + rbs_version_cache_key, + build_yard:, + build_rbs_collection:, + build_combined:, + out: + type = [] + type << 'YARD' if build_yard + rbs_source_desc = RbsMap.rbs_source_desc(rbs_version_cache_key) + type << rbs_source_desc if build_rbs_collection && !rbs_source_desc.nil? + # we'll build it anyway, but it won't take long to build with + # only a single source + + # 'combining' is awkward terminology in this case + just_yard = build_yard && rbs_source_desc.nil? + + type << 'combined' if build_combined && !just_yard + out&.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}") + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [IO, nil] + # @return [Array] + def cache_yard_pins gemspec, out + gem_yardoc_path = yardoc_path(gemspec) + Yardoc.build_docs(gem_yardoc_path, yard_plugins, gemspec) unless Yardoc.docs_built?(gem_yardoc_path) + pins = Yardoc.build_pins(gem_yardoc_path, gemspec, out: out) + serialize_yard_gem(gemspec, pins) + logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty? + pins + end + + # @return [Hash{Array(String, String, String) => Array}] + def combined_pins_in_memory + PinCache.all_combined_pins_in_memory[yard_plugins] ||= {} + end + + # @param gemspec [Gem::Specification] + # @param _out [IO, nil] + # @return [Array] + def cache_rbs_collection_pins gemspec, _out + rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) + pins = rbs_map.pins + rbs_version_cache_key = rbs_map.cache_key + # cache pins even if result is zero, so we don't retry building pins + pins ||= [] + serialize_rbs_collection_pins(gemspec, rbs_version_cache_key, pins) + logger.info do + unless pins.empty? + "Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with " \ + "cache_key #{rbs_version_cache_key.inspect}" + end + end + pins + end + + # @param gemspec [Gem::Specification] + # @return [Array] + def deserialize_yard_pin_cache gemspec + cached = load_yard_gem(gemspec) + if cached + cached + else + logger.debug "No YARD pin cache for #{gemspec.name}:#{gemspec.version}" + nil + end + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String] + # @return [Array] + def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key + cached = load_rbs_collection_pins(gemspec, rbs_version_cache_key) + Solargraph.assert_or_log(:pin_cache_rbs_collection, 'Asked for non-existent rbs collection') if cached.nil? + logger.info do + "Loaded #{cached&.length} pins from RBS collection cache for #{gemspec.name}:#{gemspec.version}" + end + cached + end + + # @return [Array] + def yard_path_components + ["yard-#{YARD::VERSION}", + yard_plugins.sort.uniq.join('-')] + end + + # @param gemspec [Gem::Specification] + # @return [String] + def yardoc_path gemspec + File.join(PinCache.base_dir, + *yard_path_components, + "#{gemspec.name}-#{gemspec.version}.yardoc") + end + + # @param gemspec [Gem::Specification] + # @return [String] + def yard_gem_path gemspec + File.join(PinCache.work_dir, *yard_path_components, "#{gemspec.name}-#{gemspec.version}.ser") + end + + # @param gemspec [Gem::Specification] + # @return [Array, nil] + def load_yard_gem gemspec + PinCache.load(yard_gem_path(gemspec)) + end + + # @param gemspec [Gem::Specification] + # @param pins [Array] + # @return [void] + def serialize_yard_gem gemspec, pins + PinCache.save(yard_gem_path(gemspec), pins) + end + + # @param gemspec [Gem::Specification] + # @return [Boolean] + def yard_gem? gemspec + exist?(yard_gem_path(gemspec)) + end + + # @param gemspec [Gem::Specification] + # @param hash [String, nil] + # @return [String] + def rbs_collection_pins_path gemspec, hash + rbs_collection_pins_path_prefix(gemspec) + "#{hash || 0}.ser" + end + + # @param gemspec [Gem::Specification] + # @return [String] + def rbs_collection_pins_path_prefix gemspec + File.join(PinCache.work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-") + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String] + # + # @return [Array, nil] + def load_rbs_collection_pins gemspec, hash + PinCache.load(rbs_collection_pins_path(gemspec, hash)) + end + + # @param gemspec [Gem::Specification] + # @param hash [String, nil] + # @param pins [Array] + # @return [void] + def serialize_rbs_collection_pins gemspec, hash, pins + PinCache.save(rbs_collection_pins_path(gemspec, hash), pins) + end + + # @param gemspec [Gem::Specification] + # @param hash [String, nil] + # @return [String] + def combined_path gemspec, hash + File.join(combined_path_prefix(gemspec) + "-#{hash || 0}.ser") + end + + # @param gemspec [Gem::Specification] + # @return [String] + def combined_path_prefix gemspec + File.join(PinCache.work_dir, 'combined', yard_plugins.sort.join('-'), "#{gemspec.name}-#{gemspec.version}") + end + + # @param gemspec [Gem::Specification] + # @param hash [String, nil] + # @param pins [Array] + # @return [void] + def serialize_combined_gem gemspec, hash, pins + PinCache.save(combined_path(gemspec, hash), pins) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String] + def combined_gem? gemspec, hash + exist?(combined_path(gemspec, hash)) + end + + # @param gemspec [Gem::Specification] + # @param hash [String, nil] + # @return [Array, nil] + def load_combined_gem gemspec, hash + PinCache.load(combined_path(gemspec, hash)) + end + + # @param gemspec [Gem::Specification] + # @param hash [String] + def rbs_collection_pins? gemspec, hash + exist?(rbs_collection_pins_path(gemspec, hash)) + end + + include Logging + + # @param path [String] + def exist? *path + File.file? File.join(*path) + end + + # @return [void] + # @param path_segments [Array] + def uncache_by_prefix *path_segments, out: nil + path = File.join(*path_segments) + glob = "#{path}*" + out&.puts "Clearing pin cache in #{glob}" + Dir.glob(glob).each do |file| + next unless File.file?(file) + FileUtils.rm_rf file, secure: true + out&.puts "Clearing pin cache in #{file}" + end + end + class << self include Logging + # @param path [String] + def exist? *path + File.file? File.join(*path) + end + + # @return [Hash{Array => Hash{Array(String, String) => + # Array}}] yard plugins, then gemspec name and + # version + def all_combined_pins_in_memory + @all_combined_pins_in_memory ||= {} + end + # The base directory where cached YARD documentation and serialized pins are serialized # # @return [String] @@ -18,6 +435,32 @@ def base_dir File.join(Dir.home, '.cache', 'solargraph') end + # @param path_segments [Array] + # @return [void] + def uncache *path_segments, out: nil + path = File.join(*path_segments) + if File.exist?(path) + FileUtils.rm_rf path, secure: true + out&.puts "Clearing pin cache in #{path}" + else + out&.puts "Pin cache file #{path} does not exist" + end + end + + # @param out [IO, nil] + # @return [void] + def uncache_core out: nil + uncache(core_path, out: out) + # ApiMap keep this in memory + ApiMap.reset_core(out: out) + end + + # @param out [IO, nil] + # @return [void] + def uncache_stdlib out: nil + uncache(stdlib_path, out: out) + end + # The working directory for the current Ruby, RBS, and Solargraph versions. # # @return [String] @@ -27,15 +470,6 @@ def work_dir File.join(base_dir, "ruby-#{RUBY_VERSION}", "rbs-#{RBS::VERSION}", "solargraph-#{Solargraph::VERSION}") end - # @param gemspec [Gem::Specification] - # @return [String] - def yardoc_path gemspec - File.join(base_dir, - "yard-#{YARD::VERSION}", - "yard-activesupport-concern-#{YARD::ActiveSupport::Concern::VERSION}", - "#{gemspec.name}-#{gemspec.version}.yardoc") - end - # @return [String] def stdlib_path File.join(work_dir, 'stdlib') @@ -164,33 +598,11 @@ def has_rbs_collection?(gemspec, hash) exist?(rbs_collection_path(gemspec, hash)) end - # @return [void] - def uncache_core - uncache(core_path) - end - - # @return [void] - def uncache_stdlib - uncache(stdlib_path) - end - - # @param gemspec [Gem::Specification] - # @param out [IO, nil] - # @return [void] - def uncache_gem(gemspec, out: nil) - uncache(yardoc_path(gemspec), out: out) - uncache_by_prefix(rbs_collection_path_prefix(gemspec), out: out) - uncache(yard_gem_path(gemspec), out: out) - uncache_by_prefix(combined_path_prefix(gemspec), out: out) - end - # @return [void] def clear FileUtils.rm_rf base_dir, secure: true end - private - # @param file [String] # @return [Array, nil] def load file @@ -202,11 +614,6 @@ def load file nil end - # @param path [String] - def exist? *path - File.file? File.join(*path) - end - # @param file [String] # @param pins [Array] # @return [void] @@ -218,27 +625,14 @@ def save file, pins logger.debug { "Cache#save: Saved #{pins.length} pins to #{file}" } end - # @param path_segments [Array] - # @return [void] - def uncache *path_segments, out: nil - path = File.join(*path_segments) - if File.exist?(path) - FileUtils.rm_rf path, secure: true - out.puts "Clearing pin cache in #{path}" unless out.nil? - end + def core? + File.file?(core_path) end - # @return [void] - # @param path_segments [Array] - def uncache_by_prefix *path_segments, out: nil - path = File.join(*path_segments) - glob = "#{path}*" - out.puts "Clearing pin cache in #{glob}" unless out.nil? - Dir.glob(glob).each do |file| - next unless File.file?(file) - FileUtils.rm_rf file, secure: true - out.puts "Clearing pin cache in #{file}" unless out.nil? - end + # @param out [IO, nil] + # @return [Array] + def cache_core out: $stderr + RbsMap::CoreMap.new.cache_core(out: out) end end end diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index 1bd31e0f5..ec8605d18 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -21,11 +21,11 @@ def initialize line, character @character = character end - # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields [line, character] end + # @param other [Position] def <=>(other) return nil unless other.is_a?(Position) if line == other.line diff --git a/lib/solargraph/range.rb b/lib/solargraph/range.rb index 615f180af..2bea62797 100644 --- a/lib/solargraph/range.rb +++ b/lib/solargraph/range.rb @@ -19,11 +19,11 @@ def initialize start, ending @ending = ending end - # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields [start, ending] end + # @param other [Object] def <=>(other) return nil unless other.is_a?(Range) if start == other.start @@ -78,7 +78,7 @@ def self.from_to l1, c1, l2, c2 # Get a range from a node. # - # @param node [Parser::AST::Node] + # @param node [AST::Node] # @return [Range, nil] def self.from_node node if node&.loc && node.loc.expression diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index 2f36ce991..1dad9e4a8 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -23,10 +23,11 @@ class RbsMap attr_reader :rbs_collection_config_path # @param library [String] - # @param version [String, nil + # @param version [String, nil] # @param rbs_collection_config_path [String, Pathname, nil] # @param rbs_collection_paths [Array] - def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: [] + # @param out [IO, nil] where to log messages + def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: [], out: $stderr if rbs_collection_config_path.nil? && !rbs_collection_paths.empty? raise 'Please provide rbs_collection_config_path if you provide rbs_collection_paths' end @@ -37,19 +38,44 @@ def initialize library, version = nil, rbs_collection_config_path: nil, rbs_coll add_library loader, library, version end + CACHE_KEY_GEM_EXPORT = 'gem-export' + CACHE_KEY_UNRESOLVED = 'unresolved' + CACHE_KEY_STDLIB = 'stdlib' + CACHE_KEY_LOCAL = 'local' + + # @param cache_key [String] + # @return [String, nil] a description of the source of the RBS info + def self.rbs_source_desc cache_key + case cache_key + when CACHE_KEY_GEM_EXPORT + 'RBS gem export' + when CACHE_KEY_UNRESOLVED + nil + when CACHE_KEY_STDLIB + 'RBS standard library' + when CACHE_KEY_LOCAL + 'local RBS shims' + else + 'RBS collection' + end + end + # @return [RBS::EnvironmentLoader] def loader @loader ||= RBS::EnvironmentLoader.new(core_root: nil, repository: repository) end - # @sg-ignore # @return [String] representing the version of the RBS info fetched # for the given library. Must change when the RBS info is # updated upstream for the same library and version. May change # if the config for where information comes form changes. def cache_key + return CACHE_KEY_UNRESOLVED unless resolved? + @hextdigest ||= begin data = nil + # @type gem_config [nil, Hash{String => Hash{String => String}}] + gem_config = nil if rbs_collection_config_path lockfile_path = RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path)) if lockfile_path.exist? @@ -58,21 +84,26 @@ def cache_key data = gem_config&.to_s end end - if data.nil? || data.empty? - if resolved? - # definitely came from the gem itself and not elsewhere - - # only one version per gem - 'gem-export' + if gem_config.nil? + CACHE_KEY_STDLIB + else + # @type [String] + source = gem_config.dig('source', 'type') + case source + when 'rubygems' + CACHE_KEY_GEM_EXPORT + when 'local' + CACHE_KEY_LOCAL + when 'stdlib' + CACHE_KEY_STDLIB else - 'unresolved' + Digest::SHA1.hexdigest(data) end - else - Digest::SHA1.hexdigest(data) end end end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @param rbs_collection_path [String, Pathname, nil] # @param rbs_collection_config_path [String, Pathname, nil] # @return [RbsMap] @@ -83,14 +114,24 @@ def self.from_gemspec gemspec, rbs_collection_path, rbs_collection_config_path return rbs_map if rbs_map.resolved? # try any version of the gem in the collection - RbsMap.new(gemspec.name, nil, - rbs_collection_paths: [rbs_collection_path].compact, - rbs_collection_config_path: rbs_collection_config_path) + rbs_map = RbsMap.new(gemspec.name, nil, + rbs_collection_paths: [rbs_collection_path].compact, + rbs_collection_config_path: rbs_collection_config_path) + + return rbs_map if rbs_map.resolved? + + StdlibMap.new(gemspec.name) end + # @param out [IO, nil] where to log messages # @return [Array] - def pins - @pins ||= resolved? ? conversions.pins : [] + def pins out: $stderr + @pins ||= if resolved? + loader.libs.each { |lib| log_caching(lib, out: out) } + conversions.pins + else + [] + end end # @generic T @@ -140,15 +181,22 @@ def conversions @conversions ||= Conversions.new(loader: loader) end + # @param lib [RBS::EnvironmentLoader::Library] + # @param out [IO, nil] where to log messages + # @return [void] + def log_caching lib, out:; end + # @param loader [RBS::EnvironmentLoader] # @param library [String] - # @param version [String, nil] + # @param version [String, nil] the version of the library to load, or nil for any + # @param out [IO, nil] where to log messages # @return [Boolean] true if adding the library succeeded - def add_library loader, library, version + def add_library loader, library, version, out: $stderr @resolved = if loader.has_library?(library: library, version: version) - loader.add library: library, version: version - logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" } - true + # we find our own dependencies from gemfile.lock + loader.add library: library, version: version, resolve_dependencies: false + logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" } + true else logger.info { "#{short_name} did not find data for library #{library}:#{version}" } false diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 6e50c022a..851ce7560 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -95,7 +95,7 @@ def convert_self_type_to_pins decl, closure type = build_type(decl.name, decl.args) generic_values = type.all_params.map(&:to_s) include_pin = Solargraph::Pin::Reference::Include.new( - name: decl.name.relative!.to_s, + name: type.rooted_name, type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure, @@ -184,7 +184,7 @@ def class_decl_to_pin decl type_location: location_decl_to_pin_location(decl.super_class.location), closure: class_pin, generic_values: generic_values, - name: decl.super_class.name.relative!.to_s, + name: type.rooted_name, source: :rbs ) end @@ -229,6 +229,8 @@ def module_decl_to_pin decl convert_self_types_to_pins decl, module_pin convert_members_to_pins decl, module_pin + raise "Invalid type for module declaration: #{module_pin.class}" unless module_pin.is_a?(Pin::Namespace) + add_mixins decl, module_pin.closure end @@ -240,6 +242,7 @@ def module_decl_to_pin decl # # @return [Solargraph::Pin::Constant] def create_constant(name, tag, comments, decl, base = nil) + tag = "#{base}<#{tag}>" if base parts = name.split('::') if parts.length > 1 name = parts.last @@ -255,7 +258,6 @@ def create_constant(name, tag, comments, decl, base = nil) comments: comments, source: :rbs ) - tag = "#{base}<#{tag}>" if base rooted_tag = ComplexType.parse(tag).force_rooted.rooted_tags constant_pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) constant_pin @@ -345,7 +347,7 @@ def global_decl_to_pin decl } # @param decl [RBS::AST::Members::MethodDefinition, RBS::AST::Members::AttrReader, RBS::AST::Members::AttrAccessor] - # @param closure [Pin::Namespace] + # @param closure [Pin::Closure] # @param context [Context] # @param scope [Symbol] :instance or :class # @param name [String] The name of the method @@ -476,7 +478,15 @@ def parts_of_function type, pin end if type.type.rest_positionals name = type.type.rest_positionals.name ? type.type.rest_positionals.name.to_s : "arg_#{arg_num += 1}" - parameters.push Solargraph::Pin::Parameter.new(decl: :restarg, name: name, closure: pin, source: :rbs, type_location: type_location) + inner_rest_positional_type = + ComplexType.try_parse(other_type_to_tag(type.type.rest_positionals.type)) + rest_positional_type = ComplexType::UniqueType.new('Array', + [], + [inner_rest_positional_type], + rooted: true, parameters_type: :list) + parameters.push Solargraph::Pin::Parameter.new(decl: :restarg, name: name, closure: pin, + source: :rbs, type_location: type_location, + return_type: rest_positional_type,) end type.type.trailing_positionals.each do |param| name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" @@ -699,13 +709,13 @@ def method_type_to_tag type # @return [ComplexType::UniqueType] def build_type(type_name, type_args = []) base = RBS_TO_YARD_TYPE[type_name.relative!.to_s] || type_name.relative!.to_s - params = type_args.map { |a| other_type_to_tag(a) }.reject { |t| t == 'undefined' }.map do |t| + params = type_args.map { |a| other_type_to_tag(a) }.map do |t| ComplexType.try_parse(t).force_rooted end if base == 'Hash' && params.length == 2 ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: true, parameters_type: :hash) else - ComplexType::UniqueType.new(base, [], params, rooted: true, parameters_type: :list) + ComplexType::UniqueType.new(base, [], params.reject(&:undefined?), rooted: true, parameters_type: :list) end end diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index 0d265d773..cb530cbec 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -5,44 +5,59 @@ class RbsMap # Ruby core pins # class CoreMap + include Logging def resolved? true end - FILLS_DIRECTORY = File.join(File.dirname(__FILE__), '..', '..', '..', 'rbs', 'fills') + FILLS_DIRECTORY = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'rbs', 'fills')) def initialize; end - def pins + # @param out [IO, nil] output stream for logging + # @return [Array] + def pins out: $stderr return @pins if @pins + @pins = cache_core(out: out) + end - @pins = [] + # @param out [IO, nil] output stream for logging + # @return [Array] + def cache_core out: $stderr + new_pins = [] cache = PinCache.deserialize_core - if cache - @pins.replace cache - else - loader.add(path: Pathname(FILLS_DIRECTORY)) - @pins = conversions.pins - @pins.concat RbsMap::CoreFills::ALL - processed = ApiMap::Store.new(pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } - @pins.replace processed - - PinCache.serialize_core @pins - end - @pins - end + return cache if cache - def loader - @loader ||= RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false)) + new_pins.concat conversions.pins + + # Avoid RBS::DuplicatedDeclarationError by loading in a different EnvironmentLoader + fill_loader = RBS::EnvironmentLoader.new(core_root: nil, repository: RBS::Repository.new(no_stdlib: false)) + fill_loader.add(path: Pathname(FILLS_DIRECTORY)) + out&.puts 'Caching RBS pins for Ruby core' + fill_conversions = Conversions.new(loader: fill_loader) + new_pins.concat fill_conversions.pins + + # add some overrides + new_pins.concat RbsMap::CoreFills::ALL + + # process overrides, then remove any which couldn't be resolved + processed = ApiMap::Store.new(new_pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } + new_pins.replace processed + + PinCache.serialize_core new_pins + + new_pins end private + # @return [RBS::EnvironmentLoader] def loader @loader ||= RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false)) end + # @return [Conversions] def conversions @conversions ||= Conversions.new(loader: loader) end diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index b6804157f..e7891bfe3 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -12,8 +12,13 @@ class StdlibMap < RbsMap # @type [Hash{String => RbsMap}] @stdlib_maps_hash = {} + def log_caching lib, out: $stderr + out&.puts("Caching RBS pins for standard library #{lib.name}") + end + # @param library [String] - def initialize library + # @param out [IO, nil] where to log messages + def initialize library, out: $stderr cached_pins = PinCache.deserialize_stdlib_require library if cached_pins @pins = cached_pins @@ -24,7 +29,7 @@ def initialize library super unless resolved? @pins = [] - logger.info { "Could not resolve #{library.inspect}" } + logger.debug { "StdlibMap could not resolve #{library.inspect}" } return end generated_pins = pins @@ -33,6 +38,22 @@ def initialize library end end + # @return [RBS::Collection::Sources::Stdlib] + def self.source + @source ||= RBS::Collection::Sources::Stdlib.instance + end + + # @param name [String] + # @param version [String, nil] + # @return [Array String}>, nil] + def self.stdlib_dependencies name, version = nil + if source.has?(name, version) + source.dependencies_of(name, version) + else + [] + end + end + # @param library [String] # @return [StdlibMap] def self.load library diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index afea86a92..2111c1821 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -3,10 +3,14 @@ require 'benchmark' require 'thor' require 'yard' +require 'sord' +require 'tmpdir' +require 'yaml' module Solargraph class Shell < Thor include Solargraph::ServerMethods + include ApiMap::SourceToYard # Tell Thor to ensure the process exits with status 1 if any error happens. def self.exit_on_failure? @@ -15,7 +19,7 @@ def self.exit_on_failure? map %w[--version -v] => :version - desc "--version, -v", "Print the version" + desc '--version, -v', 'Print the version' # @return [void] def version puts Solargraph::VERSION @@ -36,6 +40,7 @@ def socket Signal.trap("TERM") do Backport.stop end + # @sg-ignore https://github.com/castwide/backport/pull/5 Backport.prepare_tcp_server host: options[:host], port: port, adapter: Solargraph::LanguageServer::Transport::Adapter STDERR.puts "Solargraph is listening PORT=#{port} PID=#{Process.pid}" end @@ -52,6 +57,7 @@ def stdio Signal.trap("TERM") do Backport.stop end + # @sg-ignore https://github.com/castwide/backport/pull/5 Backport.prepare_stdio_server adapter: Solargraph::LanguageServer::Transport::Adapter STDERR.puts "Solargraph is listening on stdio PID=#{Process.pid}" end @@ -101,12 +107,63 @@ def clear # @param gem [String] # @param version [String, nil] def cache gem, version = nil - api_map = Solargraph::ApiMap.load(Dir.pwd) - spec = Gem::Specification.find_by_name(gem, version) - api_map.cache_gem(spec, rebuild: options[:rebuild], out: $stdout) + gems(gem + (version ? "=#{version}" : '')) + # ' end - desc 'uncache GEM [...GEM]', "Delete specific cached gem documentation" + desc 'gems [GEM[=VERSION]...] [STDLIB...] [core]', 'Cache documentation for + installed libraries' + long_desc %( This command will cache the + generated type documentation for the specified libraries. While + Solargraph will generate this on the fly when needed, it takes + time. This command will generate it in advance, which can be + useful for CI scenarios. + + With no arguments, it will cache all libraries in the current + workspace. If a gem or standard library name is specified, it + will cache that library's type documentation. + + An equals sign after a gem will allow a specific gem version + to be cached. + + The 'core' argument can be used to cache the type + documentation for the core Ruby libraries. + + If the library is already cached, it will be rebuilt if the + --rebuild option is set. + + Cached documentation is stored in #{PinCache.base_dir}, which + can be stored between CI runs. + ) + option :rebuild, type: :boolean, desc: 'Rebuild existing documentation', default: false + # @param names [Array] + # @return [void] + def gems *names + # print time with ms + workspace = Solargraph::Workspace.new('.') + + if names.empty? + workspace.cache_all_for_workspace!($stdout, rebuild: options[:rebuild]) + else + $stderr.puts("Caching these gems: #{names}") + names.each do |name| + if name == 'core' + PinCache.cache_core(out: $stdout) + next + end + + gemspec = workspace.find_gem(*name.split('=')) + if gemspec.nil? + warn "Gem '#{name}' not found" + else + workspace.cache_gem(gemspec, rebuild: options[:rebuild], out: $stdout) + end + end + $stderr.puts "Documentation cached for #{names.count} gems." + end + end + + desc 'uncache GEM [...GEM]', 'Delete specific cached gem documentation' long_desc %( Specify one or more gem names to clear. 'core' or 'stdlib' may also be specified to clear cached system documentation. @@ -116,39 +173,20 @@ def cache gem, version = nil # @return [void] def uncache *gems raise ArgumentError, 'No gems specified.' if gems.empty? + workspace = Workspace.new('.') gems.each do |gem| if gem == 'core' - PinCache.uncache_core + PinCache.uncache_core(out: $stdout) next end if gem == 'stdlib' - PinCache.uncache_stdlib + PinCache.uncache_stdlib(out: $stdout) next end spec = Gem::Specification.find_by_name(gem) - PinCache.uncache_gem(spec, out: $stdout) - end - end - - desc 'gems [GEM[=VERSION]]', 'Cache documentation for installed gems' - option :rebuild, type: :boolean, desc: 'Rebuild existing documentation', default: false - # @param names [Array] - # @return [void] - def gems *names - api_map = ApiMap.load('.') - if names.empty? - Gem::Specification.to_a.each { |spec| do_cache spec, api_map } - STDERR.puts "Documentation cached for all #{Gem::Specification.count} gems." - else - names.each do |name| - spec = Gem::Specification.find_by_name(*name.split('=')) - do_cache spec, api_map - rescue Gem::MissingSpecError - warn "Gem '#{name}' not found" - end - STDERR.puts "Documentation cached for #{names.count} gems." + workspace.uncache_gem(spec, out: $stdout) end end @@ -189,7 +227,6 @@ def typecheck *files filecount += 1 probcount += problems.length end - # " } puts "Typecheck finished in #{time.real} seconds." puts "#{probcount} problem#{probcount != 1 ? 's' : ''} found#{files.length != 1 ? " in #{filecount} of #{files.length} files" : ''}." @@ -212,7 +249,7 @@ def scan # @type [Solargraph::ApiMap, nil] api_map = nil time = Benchmark.measure { - api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout) + api_map = Solargraph::ApiMap.load_with_cache(directory, out: $stdout) api_map.pins.each do |pin| begin puts pin_description(pin) if options[:verbose] @@ -239,6 +276,77 @@ def list puts "#{workspace.filenames.length} files total." end + 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) + store = Solargraph::ApiMap::Store.new(pins) + if options[:inference] + store.method_pins.each do |pin| + next unless pin.return_type.undefined? + + type = pin.typify(api_map) + type = pin.probe(api_map) if type.undefined? + pin.docstring.add_tag YARD::Tags::Tag.new('return', '', type.items.map(&:to_s)) + pin.instance_variable_set(:@return_type, type) + end + end + rake_yard(store) + work_dir = Dir.pwd + Dir.mktmpdir do |tmpdir| + Dir.chdir tmpdir do + yardoc = File.join(tmpdir, '.yardoc') + YARD::Registry.save(false, yardoc) + YARD::Registry.load(yardoc) + target = File.join(work_dir, 'sig', options[:filename]) + FileUtils.mkdir_p(File.join(work_dir, 'sig')) + `sord #{target} --rbs --no-regenerate` + end + end + end + + desc 'method_pin [PATH]', 'Describe a method pin' + option :rbs, type: :boolean, desc: 'Output the pin as RBS', default: false + option :typify, type: :boolean, desc: 'Output the calculated return type of the pin from annotations', default: false + option :probe, type: :boolean, desc: 'Output the calculated return type of the pin from annotations and inference', default: false + option :stack, type: :boolean, desc: 'Show entire stack by including definitions in superclasses', default: false + # @param path [String] The path to the method pin, e.g. 'Class#method' or 'Class.method' + # @return [void] + def method_pin path + api_map = Solargraph::ApiMap.load_with_cache('.', out: $stderr) + + # @type [Array] + pins = if options[:stack] + scope, ns, meth = if path.include? '#' + [:instance, *path.split('#', 2)] + else + [:class, *path.split('.', 2)] + end + # @sg-ignore need better splat destructuring support + api_map.get_method_stack(ns, meth, scope: scope) + else + api_map.get_path_pins path + end + if pins.empty? + $stderr.puts "Pin not found for path '#{path}'" + exit 1 + end + pins.each do |pin| + if options[:typify] || options[:probe] + type = ComplexType::UNDEFINED + type = pin.typify(api_map) if options[:typify] + type = pin.probe(api_map) if options[:probe] && type.undefined? + print_type(type) + next + end + + print_pin(pin) + end + end + private # @param pin [Solargraph::Pin::Base] @@ -257,13 +365,24 @@ def pin_description pin desc end - # @param gemspec [Gem::Specification] - # @param api_map [ApiMap] + # @param type [ComplexType] + # @return [void] + def print_type(type) + if options[:rbs] + puts type.to_rbs + else + puts type.rooted_tag + end + end + + # @param pin [Solargraph::Pin::Base] # @return [void] - def do_cache gemspec, api_map - # @todo if the rebuild: option is passed as a positional arg, - # typecheck doesn't complain on the below line - api_map.cache_gem(gemspec, rebuild: options.rebuild, out: $stdout) + def print_pin(pin) + if options[:rbs] + puts pin.to_rbs + else + puts pin.inspect + end end end end diff --git a/lib/solargraph/source.rb b/lib/solargraph/source.rb index d2b24cc61..0af8b0cdf 100644 --- a/lib/solargraph/source.rb +++ b/lib/solargraph/source.rb @@ -187,7 +187,7 @@ def code_for(node) frag.strip.gsub(/,$/, '') end - # @param node [Parser::AST::Node] + # @param node [AST::Node] # @return [String, nil] def comments_for node rng = Range.from_node(node) @@ -318,7 +318,7 @@ def string_nodes @string_nodes ||= string_nodes_in(node) end - # @return [Array] + # @return [Array] def comment_ranges @comment_ranges ||= comments.values.map(&:range) end @@ -387,6 +387,7 @@ def changes # @return [Integer] attr_writer :version + # @return [void] def finalize return if @finalized && changes.empty? @@ -441,6 +442,7 @@ def code=(val) # @return [String] attr_writer :repaired + # @return [String] def repaired finalize @repaired diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 065c3bf10..5850364a4 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -17,6 +17,10 @@ class Chain include Equality include Logging + # + # A chain of constants, variables, and method calls for inferring types of + # values. + # autoload :Link, 'solargraph/source/chain/link' autoload :Call, 'solargraph/source/chain/call' autoload :QCall, 'solargraph/source/chain/q_call' @@ -269,7 +273,10 @@ def infer_from_definitions pins, context, api_map, locals else ComplexType.new(types) end - return type if context.nil? || context.return_type.undefined? + if context.nil? || context.return_type.undefined? + # up to downstream to resolve self type + return type + end type.self_to_type(context.return_type) end diff --git a/lib/solargraph/source/chain/if.rb b/lib/solargraph/source/chain/if.rb index c14d00ddf..3a7fa0ca9 100644 --- a/lib/solargraph/source/chain/if.rb +++ b/lib/solargraph/source/chain/if.rb @@ -8,7 +8,7 @@ def word '' end - # @param links [::Array] + # @param links [::Array] def initialize links @links = links end diff --git a/lib/solargraph/source/chain/or.rb b/lib/solargraph/source/chain/or.rb index 1e3a70f40..9264d4107 100644 --- a/lib/solargraph/source/chain/or.rb +++ b/lib/solargraph/source/chain/or.rb @@ -8,7 +8,7 @@ def word '' end - # @param links [::Array] + # @param links [::Array] def initialize links @links = links end diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index 84b3a4bcc..1fea14870 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -32,11 +32,13 @@ def initialize source environ.merge Convention.for_local(self) unless filename.nil? self.convention_pins = environ.pins + # @type [Hash{Class> => Array>}] @pin_select_cache = {} end - # @param klass [Class] - # @return [Array] + # @generic T + # @param klass [Class>] + # @return [Array>] def pins_by_class klass @pin_select_cache[klass] ||= pin_class_hash.select { |key, _| key <= klass }.values.flatten end @@ -114,10 +116,13 @@ def locate_named_path_pin line, character # @param line [Integer] # @param character [Integer] # @return [Pin::Namespace,Pin::Method,Pin::Block] - def locate_block_pin line, character - _locate_pin line, character, Pin::Namespace, Pin::Method, Pin::Block + def locate_closure_pin line, character + _locate_pin line, character, Pin::Closure end + # @deprecated Please use locate_closure_pin instead + alias locate_block_pin locate_closure_pin + # @param name [String] # @return [Array] def references name @@ -158,10 +163,12 @@ def map source private + # @return [Hash{Class => Array}] def pin_class_hash @pin_class_hash ||= pins.to_set.classify(&:class).transform_values(&:to_a) end + # @return [Data] def data @data ||= Data.new(source) end diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index ba69b1b93..3d198ac1e 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -11,15 +11,14 @@ class Clip def initialize api_map, cursor @api_map = api_map @cursor = cursor - block_pin = block - block_pin.rebind(api_map) if block_pin.is_a?(Pin::Block) && !Solargraph::Range.from_node(block_pin.receiver).contain?(cursor.range.start) - @in_block = nil + closure_pin = closure + closure_pin.rebind(api_map) if closure_pin.is_a?(Pin::Block) && !Solargraph::Range.from_node(closure_pin.receiver).contain?(cursor.range.start) end # @return [Array] Relevant pins for infering the type of the Cursor's position def define return [] if cursor.comment? || cursor.chain.literal? - result = cursor.chain.define(api_map, block, locals) + result = cursor.chain.define(api_map, closure, locals) result.concat file_global_methods result.concat((source_map.pins + source_map.locals).select{ |p| p.name == cursor.word && p.location.range.contain?(cursor.position) }) if result.empty? result @@ -51,43 +50,36 @@ def signify # @return [ComplexType] def infer - result = cursor.chain.infer(api_map, block, locals) + result = cursor.chain.infer(api_map, closure, locals) if result.tag == 'Class' # HACK: Exception to return BasicObject from Class#new - dfn = cursor.chain.define(api_map, block, locals).first + dfn = cursor.chain.define(api_map, closure, locals).first return ComplexType.try_parse('::BasicObject') if dfn && dfn.path == 'Class#new' end - return result unless result.tag == 'self' - cursor.chain.base.infer(api_map, block, locals) + # should receive result with selfs resolved from infer() + Solargraph.assert_or_log(:clip_infer_self, 'Received selfy inference') if result.selfy? + result end # Get an array of all the locals that are visible from the cursors's # position. Locals can be local variables, method parameters, or block # parameters. The array starts with the nearest local pin. # - # @return [::Array] + # @return [::Array] def locals @locals ||= source_map.locals_at(location) end # @return [::Array] def gates - block.gates - end - - def in_block? - return @in_block unless @in_block.nil? - @in_block = begin - tree = cursor.source.tree_at(cursor.position.line, cursor.position.column) - Parser.is_ast_node?(tree[1]) && [:block, :ITER].include?(tree[1].type) - end + closure.gates end # @param phrase [String] # @return [Array] def translate phrase chain = Parser.chain(Parser.parse(phrase)) - chain.define(api_map, block, locals) + chain.define(api_map, closure, locals) end private @@ -109,8 +101,8 @@ def location end # @return [Solargraph::Pin::Closure] - def block - @block ||= source_map.locate_block_pin(cursor.node_position.line, cursor.node_position.character) + def closure + @closure ||= source_map.locate_closure_pin(cursor.node_position.line, cursor.node_position.character) end # The context at the current position. @@ -201,20 +193,20 @@ def code_complete result.concat api_map.get_constants(type.namespace, cursor.start_of_constant? ? '' : context_pin.full_context.namespace, *gates) end else - type = cursor.chain.base.infer(api_map, block, locals) - result.concat api_map.get_complex_type_methods(type, block.binder.namespace, cursor.chain.links.length == 1) + type = cursor.chain.base.infer(api_map, closure, locals) + result.concat api_map.get_complex_type_methods(type, closure.binder.namespace, cursor.chain.links.length == 1) if cursor.chain.links.length == 1 if cursor.word.start_with?('@@') return package_completions(api_map.get_class_variable_pins(context_pin.full_context.namespace)) elsif cursor.word.start_with?('@') - return package_completions(api_map.get_instance_variable_pins(block.binder.namespace, block.binder.scope)) + return package_completions(api_map.get_instance_variable_pins(closure.binder.namespace, closure.binder.scope)) elsif cursor.word.start_with?('$') return package_completions(api_map.get_global_variable_pins) end result.concat locals - result.concat file_global_methods unless block.binder.namespace.empty? + result.concat file_global_methods unless closure.binder.namespace.empty? result.concat api_map.get_constants(context_pin.context.namespace, *gates) - result.concat api_map.get_methods(block.binder.namespace, scope: block.binder.scope, visibility: [:public, :private, :protected]) + result.concat api_map.get_methods(closure.binder.namespace, scope: closure.binder.scope, visibility: [:public, :private, :protected]) result.concat api_map.get_methods('Kernel') result.concat api_map.keyword_pins.to_a end diff --git a/lib/solargraph/source_map/data.rb b/lib/solargraph/source_map/data.rb index 9d1b30bac..453520414 100644 --- a/lib/solargraph/source_map/data.rb +++ b/lib/solargraph/source_map/data.rb @@ -3,15 +3,18 @@ module Solargraph class SourceMap class Data + # @param source [Solargraph::Source] def initialize source @source = source end + # @return [Array] def pins generate @pins || [] end + # @return [Array] def locals generate @locals || [] @@ -19,6 +22,7 @@ def locals private + # @return [void] def generate return if @generated diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index e99f99195..7c3a74829 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -5,11 +5,8 @@ module Solargraph # class TypeChecker autoload :Problem, 'solargraph/type_checker/problem' - autoload :ParamDef, 'solargraph/type_checker/param_def' autoload :Rules, 'solargraph/type_checker/rules' - autoload :Checks, 'solargraph/type_checker/checks' - include Checks include Parser::NodeMethods # @return [String] @@ -43,6 +40,39 @@ def source @source_map.source end + # @param inferred [ComplexType] + # @param expected [ComplexType] + def return_type_conforms_to?(inferred, expected) + conforms_to?(inferred, expected, :return_type) + end + + # @param inferred [ComplexType] + # @param expected [ComplexType] + def arg_conforms_to?(inferred, expected) + conforms_to?(inferred, expected, :method_call) + end + + # @param inferred [ComplexType] + # @param expected [ComplexType] + def assignment_conforms_to?(inferred, expected) + conforms_to?(inferred, expected, :assignment) + end + + # @param inferred [ComplexType] + # @param expected [ComplexType] + # @param scenario [Symbol] + def conforms_to?(inferred, expected, scenario) + rules_arr = [] + rules_arr << :allow_empty_params unless rules.require_inferred_type_params? + rules_arr << :allow_any_match unless rules.require_all_unique_types_match_declared? + rules_arr << :allow_undefined unless rules.require_no_undefined_args? + rules_arr << :allow_unresolved_generic unless rules.require_generics_resolved? + rules_arr << :allow_unmatched_interface unless rules.require_interfaces_resolved? + rules_arr << :allow_reverse_match unless rules.require_downcasts? + inferred.conforms_to?(api_map, expected, scenario, + rules_arr) + end + # @return [Array] def problems @problems ||= begin @@ -69,10 +99,10 @@ def load filename, level = :normal # @param code [String] # @param filename [String, nil] # @param level [Symbol] + # @param api_map [Solargraph::ApiMap] # @return [self] - def load_string code, filename = nil, level = :normal + def load_string code, filename = nil, level = :normal, api_map: Solargraph::ApiMap.new source = Solargraph::Source.load_string(code, filename) - api_map = Solargraph::ApiMap.new api_map.map(source) new(filename, api_map: api_map, level: level) end @@ -118,7 +148,7 @@ def method_return_type_problems_for pin result.push Problem.new(pin.location, "#{pin.path} return type could not be inferred", pin: pin) end else - unless (rules.require_all_return_types_match_inferred? ? all_types_match?(api_map, inferred, declared) : any_types_match?(api_map, declared, inferred)) + unless return_type_conforms_to?(inferred, declared) result.push Problem.new(pin.location, "Declared return type #{declared.rooted_tags} does not match inferred type #{inferred.rooted_tags} for #{pin.path}", pin: pin) end end @@ -152,33 +182,33 @@ def virtual_pin? pin # @return [Array] def method_param_type_problems_for pin stack = api_map.get_method_stack(pin.namespace, pin.name, scope: pin.scope) - params = first_param_hash(stack) result = [] - if rules.require_type_tags? - pin.signatures.each do |sig| - sig.parameters.each do |par| - break if par.decl == :restarg || par.decl == :kwrestarg || par.decl == :blockarg - unless params[par.name] - if pin.attribute? - inferred = pin.probe(api_map).self_to_type(pin.full_context) - if inferred.undefined? + pin.signatures.each do |sig| + params = param_details_from_stack(sig, stack) + if rules.require_type_tags? + sig.parameters.each do |par| + break if par.decl == :restarg || par.decl == :kwrestarg || par.decl == :blockarg + unless params[par.name] + if pin.attribute? + inferred = pin.probe(api_map).self_to_type(pin.full_context) + if inferred.undefined? + result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin) + end + else result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin) end - else - result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin) end end - end end - end - # @todo Should be able to probe type of name and data here - # @param name [String] - # @param data [Hash{Symbol => BasicObject}] - params.each_pair do |name, data| - # @type [ComplexType] - type = data[:qualified] - if type.undefined? - result.push Problem.new(pin.location, "Unresolved type #{data[:tagged]} for #{name} param on #{pin.path}", pin: pin) + # @todo Should be able to probe type of name and data here + # @param name [String] + # @param data [Hash{Symbol => BasicObject}] + params.each_pair do |name, data| + # @type [ComplexType] + type = data[:qualified] + if type.undefined? + result.push Problem.new(pin.location, "Unresolved type #{data[:tagged]} for #{name} param on #{pin.path}", pin: pin) + end end end result @@ -207,7 +237,7 @@ def variable_type_tag_problems result.push Problem.new(pin.location, "Variable type could not be inferred for #{pin.name}", pin: pin) end else - unless any_types_match?(api_map, declared, inferred) + unless assignment_conforms_to?(inferred, declared) result.push Problem.new(pin.location, "Declared type #{declared} does not match inferred type #{inferred} for variable #{pin.name}", pin: pin) end end @@ -239,10 +269,11 @@ 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) - block_pin = source_map.locate_block_pin(rng.start.line, rng.start.column) + closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) + closure_pin.rebind(api_map) location = Location.new(filename, rng) locals = source_map.locals_at(location) - pins = chain.define(api_map, block_pin, locals) + pins = chain.define(api_map, closure_pin, locals) if pins.empty? result.push Problem.new(location, "Unresolved constant #{Solargraph::Parser::NodeMethods.unpack_name(const)}") @marked_ranges.push location.range @@ -258,17 +289,25 @@ def call_problems rng = Solargraph::Range.from_node(call) next if @marked_ranges.any? { |d| d.contain?(rng.start) } chain = Solargraph::Parser.chain(call, filename) - block_pin = source_map.locate_block_pin(rng.start.line, rng.start.column) + closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) + namespace_pin = closure_pin + if call.type == :block + # blocks in the AST include the method call as well, so the + # node returned by #call_nodes_from needs to be backed out + # one closure + closure_pin = closure_pin.closure + end + closure_pin.rebind(api_map) location = Location.new(filename, rng) locals = source_map.locals_at(location) - type = chain.infer(api_map, block_pin, locals) + type = chain.infer(api_map, closure_pin, locals) if type.undefined? && !rules.ignore_all_undefined? base = chain missing = chain found = nil closest = ComplexType::UNDEFINED until base.links.first.undefined? - found = base.define(api_map, block_pin, locals).first + found = base.define(api_map, closure_pin, locals).first break if found missing = base base = base.base @@ -282,140 +321,150 @@ def call_problems end end end - result.concat argument_problems_for(chain, api_map, block_pin, locals, location) + result.concat argument_problems_for(chain, api_map, closure_pin, locals, location) end result end # @param chain [Solargraph::Source::Chain] # @param api_map [Solargraph::ApiMap] - # @param block_pin [Solargraph::Pin::Base] - # @param locals [Array] + # @param closure_pin [Solargraph::Pin::Closure] + # @param locals [Array] # @param location [Solargraph::Location] # @return [Array] - def argument_problems_for chain, api_map, block_pin, locals, location + def argument_problems_for chain, api_map, closure_pin, locals, location result = [] base = chain - until base.links.length == 1 && base.undefined? - last_base_link = base.links.last - break unless last_base_link.is_a?(Solargraph::Source::Chain::Call) - - arguments = last_base_link.arguments - - pins = base.define(api_map, block_pin, locals) - - first_pin = pins.first - if first_pin.is_a?(Pin::DelegatedMethod) && !first_pin.resolvable?(api_map) - # Do nothing, as we can't find the actual method implementation - elsif first_pin.is_a?(Pin::Method) - # @type [Pin::Method] - pin = first_pin - ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper) - arity_problems_for(pin, fake_args_for(block_pin), location) - elsif pin.path == 'Class#new' - fqns = if base.links.one? - block_pin.namespace + # @type last_base_link [Solargraph::Source::Chain::Call] + last_base_link = base.links.last + return [] unless last_base_link.is_a?(Solargraph::Source::Chain::Call) + + arguments = last_base_link.arguments + + pins = base.define(api_map, closure_pin, locals) + + first_pin = pins.first + unresolvable = first_pin.is_a?(Pin::DelegatedMethod) && !first_pin.resolvable?(api_map) + if !unresolvable && first_pin.is_a?(Pin::Method) + # @type [Pin::Method] + pin = first_pin + ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper) + arity_problems_for(pin, fake_args_for(closure_pin), location) + elsif pin.path == 'Class#new' + fqns = if base.links.one? + closure_pin.namespace + else + base.base.infer(api_map, closure_pin, locals).namespace + end + init = api_map.get_method_stack(fqns, 'initialize').first + + init ? arity_problems_for(init, arguments, location) : [] + else + arity_problems_for(pin, arguments, location) + end + return ap unless ap.empty? + return [] if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper) + + all_errors = [] + pin.signatures.sort { |sig| sig.parameters.length }.each do |sig| + signature_errors = signature_argument_problems_for location, locals, closure_pin, arguments, sig, pin, pins + if signature_errors.empty? + # we found a signature that works - meaning errors from + # other signatures don't matter. + return [] + end + all_errors.concat signature_errors + end + result.concat all_errors + end + result + end + + # @param location [Location] + # @param locals [Array] + # @param closure_pin [Pin::Closure] + # @param arguments [Array] + # @param sig [Pin::Signature] + # @param pin [Pin::Method] + # @param pins [Array] + # + # @return [Array] + def signature_argument_problems_for location, locals, closure_pin, arguments, sig, pin, pins + params = param_details_from_stack(sig, pins) + + errors = [] + # @todo add logic mapping up restarg parameters with + # arguments (including restarg arguments). Use tuples + # when possible, and when not, ensure provably + # incorrect situations are detected. + sig.parameters.each_with_index do |par, idx| + return errors if par.decl == :restarg # bail out and assume the rest is valid pending better arg processing + argchain = arguments[idx] + if argchain.nil? + if par.decl == :arg + final_arg = arguments.last + if final_arg && final_arg.node.type == :splat + argchain = final_arg + return errors else - base.base.infer(api_map, block_pin, locals).namespace + errors.push Problem.new(location, "Not enough arguments to #{pin.path}") end - init = api_map.get_method_stack(fqns, 'initialize').first - init ? arity_problems_for(init, arguments, location) : [] else - arity_problems_for(pin, arguments, location) + final_arg = arguments.last + argchain = final_arg if final_arg && [:kwsplat, :hash].include?(final_arg.node.type) end - unless ap.empty? - result.concat ap - break - end - break if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper) - - params = first_param_hash(pins) - - all_errors = [] - pin.signatures.sort { |sig| sig.parameters.length }.each do |sig| - errors = [] - sig.parameters.each_with_index do |par, idx| - # @todo add logic mapping up restarg parameters with - # arguments (including restarg arguments). Use tuples - # when possible, and when not, ensure provably - # incorrect situations are detected. - break if par.decl == :restarg # bail out pending better arg processing - argchain = arguments[idx] - if argchain.nil? - if par.decl == :arg - final_arg = arguments.last - if final_arg && final_arg.node.type == :splat - argchain = final_arg - next # don't try to apply the type of the splat - unlikely to be specific enough - else - errors.push Problem.new(location, "Not enough arguments to #{pin.path}") - next - end - else - final_arg = arguments.last - argchain = final_arg if final_arg && [:kwsplat, :hash].include?(final_arg.node.type) - end - end - if argchain - if par.decl != :arg - errors.concat kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, params, idx - next - else - if argchain.node.type == :splat && argchain == arguments.last - final_arg = argchain - end - if (final_arg && final_arg.node.type == :splat) - # The final argument given has been seen and was a - # splat, which doesn't give us useful types or - # arities against positional parameters, so let's - # continue on in case there are any required - # kwargs we should warn about - next - end - - if argchain.node.type == :splat && par != sig.parameters.last - # we have been given a splat and there are more - # arguments to come. + end + if argchain + if par.decl != :arg + errors.concat kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx + next + else + if argchain.node.type == :splat && argchain == arguments.last + final_arg = argchain + end + if (final_arg && final_arg.node.type == :splat) + # The final argument given has been seen and was a + # splat, which doesn't give us useful types or + # arities against positional parameters, so let's + # continue on in case there are any required + # kwargs we should warn about + next + end + if argchain.node.type == :splat && par != sig.parameters.last + # we have been given a splat and there are more + # arguments to come. + + # @todo Improve this so that we can skip past the + # rest of the positional parameters here but still + # process the kwargs + return errors + end + ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED + ptype = ptype.self_to_type(par.context) + if ptype.nil? + # @todo Some level (strong, I guess) should require the param here + else + argtype = argchain.infer(api_map, closure_pin, locals) + argtype = argtype.self_to_type(closure_pin.context) - # @todo Improve this so that we can skip past the - # rest of the positional parameters here but still - # process the kwargs - break - end - ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED - ptype = ptype.self_to_type(par.context) - if ptype.nil? - # @todo Some level (strong, I guess) should require the param here - else - argtype = argchain.infer(api_map, block_pin, locals) - if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype) - errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") - next - end - end - end - elsif par.decl == :kwarg - errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}") - next + if argtype.defined? && ptype.defined? && !arg_conforms_to?(argtype, ptype) + errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") + return errors end end - if errors.empty? - all_errors.clear - break - end - all_errors.concat errors end - result.concat all_errors + elsif par.decl == :kwarg + errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}") + next end - base = base.base end - result + errors end # @param sig [Pin::Signature] # @param argchain [Source::Chain] # @param api_map [ApiMap] - # @param block_pin [Pin::Block] + # @param closure_pin [Pin::Closure] # @param locals [Array] # @param location [Location] # @param pin [Pin::Method] @@ -423,13 +472,13 @@ def argument_problems_for chain, api_map, block_pin, locals, location # @param idx [Integer] # # @return [Array] - def kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, params, idx + 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] 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, block_pin, locals, location, pin, params, kwargs) + result.concat kwrestarg_problems_for(api_map, closure_pin, locals, location, pin, params, kwargs) else if argchain data = params[par.name] @@ -438,8 +487,10 @@ def kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, else ptype = data[:qualified] unless ptype.undefined? - argtype = argchain.infer(api_map, block_pin, locals) - if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype) + argtype = argchain.infer(api_map, closure_pin, locals) + argtype = argtype.self_to_type(closure_pin.context) + + if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") end end @@ -452,29 +503,48 @@ def kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, end # @param api_map [ApiMap] - # @param block_pin [Pin::Block] + # @param closure_pin [Pin::Closure] # @param locals [Array] # @param location [Location] # @param pin [Pin::Method] # @param params [Hash{String => [nil, Hash]}] # @param kwargs [Hash{Symbol => Source::Chain}] # @return [Array] - def kwrestarg_problems_for(api_map, block_pin, locals, location, pin, params, kwargs) + def kwrestarg_problems_for(api_map, closure_pin, locals, location, pin, params, kwargs) result = [] kwargs.each_pair do |pname, argchain| next unless params.key?(pname.to_s) ptype = params[pname.to_s][:qualified] - argtype = argchain.infer(api_map, block_pin, locals) - if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype) + ptype = ptype.self_to_type(pin.context) + argtype = argchain.infer(api_map, closure_pin, locals) + argtype = argtype.self_to_type(closure_pin.context) + if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}") end end result end - # @param pin [Pin::Method] + # @param param_details [Hash{String => Hash{Symbol => String, ComplexType}}] + # @param pin [Pin::Method, Pin::Signature] + # @return [void] + def add_restkwarg_param_tag_details(param_details, pin) + # see if we have additional tags to pay attention to from YARD - + # e.g., kwargs in a **restkwargs splat + tags = pin.docstring.tags(:param) + tags.each do |tag| + next if param_details.key? tag.name.to_s + next if tag.types.nil? + param_details[tag.name.to_s] = { + tagged: tag.types.join(', '), + qualified: Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, pin.full_context.namespace) + } + end + end + + # @param pin [Pin::Signature] # @return [Hash{String => Hash{Symbol => String, ComplexType}}] - def param_hash(pin) + def signature_param_details(pin) # @type [Hash{String => Hash{Symbol => String, ComplexType}}] result = {} pin.parameters.each do |param| @@ -485,44 +555,49 @@ def param_hash(pin) qualified: type } end - # see if we have additional tags to pay attention to from YARD - - # e.g., kwargs in a **restkwargs splat - tags = pin.docstring.tags(:param) - tags.each do |tag| - next if result.key? tag.name.to_s - next if tag.types.nil? - result[tag.name.to_s] = { - tagged: tag.types.join(', '), - qualified: Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, pin.full_context.namespace) - } - end result end + # The original signature defines the parameters, but other + # signatures and method pins can help by adding type information + # + # @param param_details [Hash{String => Hash{Symbol => String, ComplexType}}] + # @param param_names [Array] + # @param new_param_details [Hash{String => Hash{Symbol => String, ComplexType}}] + # + # @return [void] + def add_to_param_details(param_details, param_names, new_param_details) + new_param_details.each do |param_name, details| + next unless param_names.include?(param_name) + + param_details[param_name] ||= {} + param_details[param_name][:tagged] ||= details[:tagged] + param_details[param_name][:qualified] ||= details[:qualified] + end + end + + # @param signature [Pin::Signature] # @param pins [Array] + # @param method_pin_stack [Array] + # # @return [Hash{String => Hash{Symbol => String, ComplexType}}] - def first_param_hash(pins) - return {} if pins.empty? - first_pin_type = pins.first.typify(api_map) - first_pin = pins.first.proxy first_pin_type - param_names = first_pin.parameter_names - results = param_hash(first_pin) - pins[1..].each do |pin| - # @todo this assignment from parametric use of Hash should not lose its generic - # @type [Hash{String => Hash{Symbol => BasicObject}}] + def param_details_from_stack(signature, method_pin_stack) + signature_type = signature.typify(api_map) + signature = signature.proxy signature_type + param_details = signature_param_details(signature) + param_names = signature.parameter_names + + method_pin_stack.each do |method_pin| + add_restkwarg_param_tag_details(param_details, method_pin) # documentation of types in superclasses should fail back to # subclasses if the subclass hasn't documented something - superclass_results = param_hash(pin) - superclass_results.each do |param_name, details| - next unless param_names.include?(param_name) - - results[param_name] ||= {} - results[param_name][:tagged] ||= details[:tagged] - results[param_name][:qualified] ||= details[:qualified] + method_pin.signatures.each do |sig| + add_restkwarg_param_tag_details(param_details, sig) + add_to_param_details param_details, param_names, signature_param_details(sig) end end - results + param_details end # @param pin [Pin::Base] @@ -548,17 +623,17 @@ def declared_externally? pin return true if pin.assignment.nil? chain = Solargraph::Parser.chain(pin.assignment, filename) rng = Solargraph::Range.from_node(pin.assignment) - block_pin = source_map.locate_block_pin(rng.start.line, rng.start.column) + closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) location = Location.new(filename, Range.from_node(pin.assignment)) locals = source_map.locals_at(location) - type = chain.infer(api_map, block_pin, locals) + type = chain.infer(api_map, closure_pin, locals) if type.undefined? && !rules.ignore_all_undefined? base = chain missing = chain found = nil closest = ComplexType::UNDEFINED until base.links.first.undefined? - found = base.define(api_map, block_pin, locals).first + found = base.define(api_map, closure_pin, locals).first break if found missing = base base = base.base diff --git a/lib/solargraph/type_checker/checks.rb b/lib/solargraph/type_checker/checks.rb deleted file mode 100644 index de402978b..000000000 --- a/lib/solargraph/type_checker/checks.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - class TypeChecker - # Helper methods for performing type checks - # - module Checks - module_function - - # Compare an expected type with an inferred type. Common usage is to - # check if the type declared in a method's @return tag matches the type - # inferred from static analysis of the code. - # - # @param api_map [ApiMap] - # @param expected [ComplexType] - # @param inferred [ComplexType] - # @return [Boolean] - def types_match? api_map, expected, inferred - return true if expected.to_s == inferred.to_s - matches = [] - expected.each do |exp| - found = false - inferred.each do |inf| - # if api_map.super_and_sub?(fuzz(inf), fuzz(exp)) - if either_way?(api_map, inf, exp) - found = true - matches.push inf - break - end - end - return false unless found - end - inferred.each do |inf| - next if matches.include?(inf) - found = false - expected.each do |exp| - # if api_map.super_and_sub?(fuzz(inf), fuzz(exp)) - if either_way?(api_map, inf, exp) - found = true - break - end - end - return false unless found - end - true - end - - # @param api_map [ApiMap] - # @param expected [ComplexType] - # @param inferred [ComplexType] - # @return [Boolean] - def any_types_match? api_map, expected, inferred - expected = expected.downcast_to_literal_if_possible - inferred = inferred.downcast_to_literal_if_possible - return duck_types_match?(api_map, expected, inferred) if expected.duck_type? - # walk through the union expected type and see if any members - # of the union match the inferred type - expected.each do |exp| - next if exp.duck_type? - # @todo: there should be a level of typechecking where all - # unique types in the inferred must match one of the - # expected unique types - inferred.each do |inf| - # return true if exp == inf || api_map.super_and_sub?(fuzz(inf), fuzz(exp)) - return true if exp == inf || either_way?(api_map, inf, exp) - end - end - false - end - - # @param api_map [ApiMap] - # @param inferred [ComplexType] - # @param expected [ComplexType] - # @return [Boolean] - def all_types_match? api_map, inferred, expected - expected = expected.downcast_to_literal_if_possible - inferred = inferred.downcast_to_literal_if_possible - return duck_types_match?(api_map, expected, inferred) if expected.duck_type? - inferred.each do |inf| - next if inf.duck_type? - return false unless expected.any? { |exp| exp == inf || either_way?(api_map, inf, exp) } - end - true - end - - # @param api_map [ApiMap] - # @param expected [ComplexType] - # @param inferred [ComplexType] - # @return [Boolean] - def duck_types_match? api_map, expected, inferred - raise ArgumentError, 'Expected type must be duck type' unless expected.duck_type? - expected.each do |exp| - next unless exp.duck_type? - quack = exp.to_s[1..-1] - return false if api_map.get_method_stack(inferred.namespace, quack, scope: inferred.scope).empty? - end - true - end - - # @param type [ComplexType::UniqueType] - # @return [String] - def fuzz type - if type.parameters? - type.name - else - type.tag - end - end - - # @param api_map [ApiMap] - # @param cls1 [ComplexType::UniqueType] - # @param cls2 [ComplexType::UniqueType] - # @return [Boolean] - def either_way?(api_map, cls1, cls2) - # @todo there should be a level of typechecking which uses the - # full tag with parameters to determine compatibility - f1 = cls1.name - f2 = cls2.name - api_map.type_include?(f1, f2) || api_map.super_and_sub?(f1, f2) || api_map.super_and_sub?(f2, f1) - # api_map.type_include?(f1, f2) || api_map.super_and_sub?(f1, f2) || api_map.super_and_sub?(f2, f1) - end - end - end -end diff --git a/lib/solargraph/type_checker/param_def.rb b/lib/solargraph/type_checker/param_def.rb deleted file mode 100644 index 2c626270a..000000000 --- a/lib/solargraph/type_checker/param_def.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - class TypeChecker - # Data about a method parameter definition. This is the information from - # the args list in the def call, not the `@param` tags. - # - class ParamDef - # @return [String] - attr_reader :name - - # @return [Symbol] - attr_reader :type - - def initialize name, type - @name = name - @type = type - end - - class << self - # Get an array of ParamDefs from a method pin. - # - # @param pin [Solargraph::Pin::Method] - # @return [Array] - def from pin - result = [] - pin.parameters.each do |par| - result.push ParamDef.new(par.name, par.decl) - end - result - end - end - end - end -end diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 8f15037d5..5290c8c12 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -54,11 +54,31 @@ def validate_tags? rank > LEVELS[:normal] end - def require_all_return_types_match_inferred? + def require_inferred_type_params? rank >= LEVELS[:alpha] end - # We keep this at strong because if you added an @sg-ignore to + def require_all_unique_types_match_declared? + rank >= LEVELS[:alpha] + end + + def require_no_undefined_args? + rank >= LEVELS[:alpha] + end + + def require_generics_resolved? + rank >= LEVELS[:alpha] + end + + def require_interfaces_resolved? + rank >= LEVELS[:alpha] + end + + def require_downcasts? + rank >= LEVELS[:alpha] + end + + # We keep this at strong because if you added an @ sg-ignore to # address a strong-level issue, then ran at a lower level, you'd # get a false positive - we don't run stronger level checks than # requested for performance reasons diff --git a/lib/solargraph/version.rb b/lib/solargraph/version.rb index 94cc1b851..7473b20b9 100755 --- a/lib/solargraph/version.rb +++ b/lib/solargraph/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Solargraph - VERSION = '0.56.2' + VERSION = '0.57.alpha' end diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index ffd653d96..8b57177c9 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -2,6 +2,7 @@ require 'open3' require 'json' +require 'yaml' module Solargraph # A workspace consists of the files in a project's directory and the @@ -9,38 +10,107 @@ module Solargraph # in an associated Library or ApiMap. # class Workspace + include Logging + autoload :Config, 'solargraph/workspace/config' + autoload :Gemspecs, 'solargraph/workspace/gemspecs' + autoload :RequirePaths, 'solargraph/workspace/require_paths' # @return [String] attr_reader :directory - # The require paths associated with the workspace. - # - # @return [Array] - attr_reader :require_paths - - # @return [Array] - attr_reader :gemnames - alias source_gems gemnames - - # @param directory [String] + # @param directory [String] TODO: Document and test '' and '*' semantics # @param config [Config, nil] # @param server [Hash] def initialize directory = '', config = nil, server = {} - @directory = directory + raise ArgumentError, 'directory must be a String' unless directory.is_a?(String) + + @directory = if ['*', ''].include?(directory) + directory + else + File.absolute_path(directory) + end @config = config @server = server load_sources - @gemnames = [] - @require_paths = generate_require_paths require_plugins end + # The require paths associated with the workspace. + # + # @return [Array] + def require_paths + # @todo are the semantics of '*' the same as '', meaning 'don't send back any require paths'? + @require_paths ||= RequirePaths.new(directory_or_nil, config).generate + end + # @return [Solargraph::Workspace::Config] def config @config ||= Solargraph::Workspace::Config.new(directory) end + # @param out [IO, nil] output stream for logging + # @param gemspec [Gem::Specification] + # @return [Array] + def fetch_dependencies gemspec, out: $stderr + gemspecs.fetch_dependencies(gemspec, out: out) + end + + # @return [Solargraph::PinCache] + def pin_cache + @pin_cache ||= fresh_pincache + end + + # @param require [String] The string sent to 'require' in the code to resolve, e.g. 'rails', 'bundler/require' + # @return [Array] + def resolve_require require + gemspecs.resolve_require(require) + end + + # @param stdlib_name [String] + # + # @return [Array] + def stdlib_dependencies stdlib_name + gemspecs.stdlib_dependencies(stdlib_name) + end + + # @return [Environ] + def global_environ + # empty docmap, since the result needs to work in any possible + # context here + @global_environ ||= Convention.for_global(DocMap.new([], self, out: nil)) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [IO, nil] output stream for logging + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # + # @return [void] + def cache_gem gemspec, out: nil, rebuild: false + pin_cache.cache_gem(gemspec: gemspec, out: out, rebuild: rebuild) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [IO, nil] output stream for logging + # + # @return [void] + def uncache_gem gemspec, out: nil + pin_cache.uncache_gem(gemspec, out: out) + end + + # @return [Solargraph::PinCache] + def fresh_pincache + PinCache.new(rbs_collection_path: rbs_collection_path, + rbs_collection_config_path: rbs_collection_config_path, + yard_plugins: yard_plugins, + directory: directory) + end + + # @return [Array] + def yard_plugins + @yard_plugins ||= global_environ.yard_plugins.sort.uniq + end + # Merge the source. A merge will update the existing source for the file # or add it to the sources if the workspace is configured to include it. # The source is ignored if the configuration excludes it. @@ -111,23 +181,6 @@ def would_require? path false end - # True if the workspace contains at least one gemspec file. - # - # @return [Boolean] - def gemspec? - !gemspecs.empty? - end - - # Get an array of all gemspec files in the workspace. - # - # @return [Array] - def gemspecs - return [] if directory.empty? || directory == '*' - @gemspecs ||= Dir[File.join(directory, '**/*.gemspec')].select do |gs| - config.allow? gs - end - end - # @return [String, nil] def rbs_collection_path @gem_rbs_collection ||= read_rbs_collection_path @@ -135,12 +188,44 @@ def rbs_collection_path # @return [String, nil] def rbs_collection_config_path - @rbs_collection_config_path ||= begin - unless directory.empty? || directory == '*' - yaml_file = File.join(directory, 'rbs_collection.yaml') - yaml_file if File.file?(yaml_file) + @rbs_collection_config_path ||= + begin + unless directory.empty? || directory == '*' + yaml_file = File.join(directory, 'rbs_collection.yaml') + yaml_file if File.file?(yaml_file) + end end + end + + # @param name [String] + # @param version [String, nil] + # + # @return [Gem::Specification, nil] + def find_gem name, version = nil + gemspecs.find_gem(name, version) + end + + # @param out [IO, nil] output stream for logging + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # @return [void] + def cache_all_for_workspace! out, rebuild: false + PinCache.cache_core(out: out) unless PinCache.core? + + # @type [Array] + gem_specs = gemspecs.all_gemspecs_from_bundle + # try any possible standard libraries, but be quiet about it + stdlib_specs = pin_cache.possible_stdlibs.map { |stdlib| gemspecs.find_gem(stdlib, out: nil) }.compact + specs = (gem_specs + stdlib_specs) + specs.each do |spec| + pin_cache.cache_gem(gemspec: spec, rebuild: rebuild, out: out) unless pin_cache.cached?(spec) end + out&.puts "Documentation cached for all #{specs.length} gems." + + # do this after so that we prefer stdlib requires from gems, + # which are likely to be newer and have more pins + pin_cache.cache_all_stdlibs(out: out) + + out&.puts "Documentation cached for core, standard library and gems." end # Synchronize the workspace from the provided updater. @@ -156,12 +241,15 @@ def command_path server['commandPath'] || 'solargraph' end - # True if the workspace has a root Gemfile. - # - # @todo Handle projects with custom Bundler/Gemfile setups (see DocMap#gemspecs_required_from_bundler) - # - def gemfile? - directory && File.file?(File.join(directory, 'Gemfile')) + # @return [String, nil] + def directory_or_nil + return nil if directory.empty? || directory == '*' + directory + end + + # @return [Solargraph::Workspace::Gemspecs] + def gemspecs + @gemspecs ||= Solargraph::Workspace::Gemspecs.new(directory_or_nil) end private @@ -182,7 +270,10 @@ def load_sources source_hash.clear unless directory.empty? || directory == '*' size = config.calculated.length - raise WorkspaceTooLargeError, "The workspace is too large to index (#{size} files, #{config.max_files} max)" if config.max_files > 0 and size > config.max_files + if config.max_files > 0 and size > config.max_files + raise WorkspaceTooLargeError, + "The workspace is too large to index (#{size} files, #{config.max_files} max)" + end config.calculated.each do |filename| begin source_hash[filename] = Solargraph::Source.load(filename) @@ -193,48 +284,6 @@ def load_sources end end - # Generate require paths from gemspecs if they exist or assume the default - # lib directory. - # - # @return [Array] - def generate_require_paths - return configured_require_paths unless gemspec? - result = [] - gemspecs.each do |file| - base = File.dirname(file) - # HACK: Evaluating gemspec files violates the goal of not running - # workspace code, but this is how Gem::Specification.load does it - # anyway. - cmd = ['ruby', '-e', "require 'rubygems'; require 'json'; spec = eval(File.read('#{file}'), TOPLEVEL_BINDING, '#{file}'); return unless Gem::Specification === spec; puts({name: spec.name, paths: spec.require_paths}.to_json)"] - o, e, s = Open3.capture3(*cmd) - if s.success? - begin - hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {} - next if hash.empty? - @gemnames.push hash['name'] - result.concat(hash['paths'].map { |path| File.join(base, path) }) - rescue StandardError => e - Solargraph.logger.warn "Error reading #{file}: [#{e.class}] #{e.message}" - end - else - Solargraph.logger.warn "Error reading #{file}" - Solargraph.logger.warn e - end - end - result.concat(config.require_paths.map { |p| File.join(directory, p) }) - result.push File.join(directory, 'lib') if result.empty? - result - end - - # Get additional require paths defined in the configuration. - # - # @return [Array] - def configured_require_paths - return ['lib'] if directory.empty? - return [File.join(directory, 'lib')] if config.require_paths.empty? - config.require_paths.map { |p| File.join(directory, p) } - end - # @return [void] def require_plugins config.plugins.each do |plugin| diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb new file mode 100644 index 000000000..be765127a --- /dev/null +++ b/lib/solargraph/workspace/gemspecs.rb @@ -0,0 +1,358 @@ +# frozen_string_literal: true + +require 'rubygems' +require 'bundler' + +module Solargraph + class Workspace + # Manages determining which gemspecs are available in a workspace + class Gemspecs + include Logging + + attr_reader :directory, :preferences + + # @param directory [String, nil] If nil, assume no bundle is present + # @param preferences [Array] + def initialize directory, preferences: [] + # @todo an issue with both external bundles and the potential + # preferences feature is that bundler gives you a 'clean' + # rubygems environment with only the specified versions + # installed. Possible alternatives: + # + # *) prompt the user to run solargraph outside of bundler + # and treat all bundles as external + # *) reinstall the needed gems dynamically each time + # *) manipulate the rubygems/bundler environment + @directory = directory && File.absolute_path(directory) + # @todo implement preferences as a config-exposed feature + @preferences = preferences + end + + # Take the path given to a 'require' statement in a source file + # and return the Gem::Specifications which will be brought into + # scope with it, so we can load pins for them. + # + # @param require [String] The string sent to 'require' in the code to resolve, e.g. 'rails', 'bundler/require' + # @return [::Array, nil] + def resolve_require require + return nil if require.empty? + + # This is added in the parser when it sees 'Bundler.require' - + # see https://bundler.io/guides/bundler_setup.html ' + # + # @todo handle different arguments to Bundler.require + return auto_required_gemspecs_from_bundler if require == 'bundler/require' + + # Determine gem name based on the require path + file = "lib/#{require}.rb" + spec_with_path = Gem::Specification.find_by_path(file) + + all_gemspecs = all_gemspecs_from_bundle + + gem_names_to_try = [ + spec_with_path&.name, + require.tr('/', '-'), + require.split('/').first + ].compact.uniq + gem_names_to_try.each do |gem_name| + gemspec = all_gemspecs.find { |gemspec| gemspec.name == gem_name } + return [gemspec_or_preference(gemspec)] if gemspec + + begin + gemspec = Gem::Specification.find_by_name(gem_name) + return [gemspec_or_preference(gemspec)] if gemspec + rescue Gem::MissingSpecError + logger.debug "Gem #{gem_name} not found in the current Ruby environment" + end + + # look ourselves just in case this is hanging out somewhere + # that find_by_path doesn't index' + gemspec = all_gemspecs.find do |spec| + spec = to_gem_specification(spec) unless spec.respond_to?(:files) + + spec&.files&.any? { |gemspec_file| file == gemspec_file } + end + return [gemspec_or_preference(gemspec)] if gemspec + end + + nil + end + + # @param stdlib_name [String] + # + # @return [Array] + def stdlib_dependencies stdlib_name + deps = RbsMap::StdlibMap.stdlib_dependencies(stdlib_name, nil) || [] + deps.map { |dep| dep['name'] }.compact + end + + # @param name [String] + # @param version [String, nil] + # @param out [IO, nil] output stream for logging + # + # @return [Gem::Specification, nil] + def find_gem name, version = nil, out: $stderr + gemspec = all_gemspecs_from_bundle.find { |gemspec| gemspec.name == name && gemspec.version == version } + return gemspec if gemspec + + gemspec = all_gemspecs_from_bundle.find { |gemspec| gemspec.name == name } + return gemspec if gemspec + + resolve_gem_ignoring_local_bundle name, version, out: out + end + + # @param gemspec [Gem::Specification] + # @param out[IO, nil] output stream for logging + # + # @return [Array] + def fetch_dependencies gemspec, out: $stderr + gemspecs = all_gemspecs_from_bundle + + # @type [Hash{String => Gem::Specification}] + deps_so_far = {} + + # @param runtime_dep [Gem::Dependency] + # @param deps [Hash{String => Gem::Specification}] + gem_dep_gemspecs = only_runtime_dependencies(gemspec).each_with_object(deps_so_far) do |runtime_dep, deps| + next if deps[runtime_dep.name] + + Solargraph.logger.info "Adding #{runtime_dep.name} dependency for #{gemspec.name}" + dep = gemspecs.find { |dep| dep.name == runtime_dep.name } + dep ||= Gem::Specification.find_by_name(runtime_dep.name, runtime_dep.requirement) + rescue Gem::MissingSpecError + dep = resolve_gem_ignoring_local_bundle runtime_dep.name, out: out + ensure + next unless dep + + fetch_dependencies(dep, out: out).each { |sub_dep| deps[sub_dep.name] ||= sub_dep } + deps[dep.name] ||= dep + end + # RBS tracks implicit dependencies, like how the YAML standard + # library implies pulling in the psych library. + stdlib_deps = RbsMap::StdlibMap.stdlib_dependencies(gemspec.name, gemspec.version) || [] + stdlib_dep_gemspecs = stdlib_deps.map { |dep| find_gem(dep['name'], dep['version']) }.compact + (gem_dep_gemspecs.values.compact + stdlib_dep_gemspecs).uniq(&:name) + end + + # Returns all gemspecs directly depended on by this workspace's + # bundle (does not include transitive dependencies). + # + # @return [Array] + def all_gemspecs_from_bundle + return [] unless directory + + @all_gemspecs_from_bundle ||= + if in_this_bundle? + all_gemspecs_from_this_bundle + else + all_gemspecs_from_external_bundle + end + end + + # @return [Hash{Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification => Gem::Specification}] + def self.gem_specification_cache + @gem_specification_cache ||= {} + end + + private + + # @param specish [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification] + # @return [Gem::Specification, nil] + def to_gem_specification specish + # print time including milliseconds + self.class.gem_specification_cache[specish] ||= case specish + when Gem::Specification + # yay! + specish + when Bundler::LazySpecification + # materializing didn't work. Let's look in the local + # rubygems without bundler's help + resolve_gem_ignoring_local_bundle specish.name, + specish.version + when Bundler::StubSpecification + # turns a Bundler::StubSpecification into a + # Gem::StubSpecification into a Gem::Specification + specish = specish.stub + if specish.respond_to?(:spec) + specish.spec + else + # turn the crank again + to_gem_specification(specish) + end + else + @@warned_on_gem_type ||= + false + unless @@warned_on_gem_type + logger.warn 'Unexpected type while resolving gem: ' \ + "#{specish.class}" + @@warned_on_gem_type = true + end + nil + end + end + + # @param command [String] The expression to evaluate in the external bundle + # @sg-ignore Need a JSON type + # @yield [undefined, nil] + def query_external_bundle command + Solargraph.with_clean_env do + cmd = [ + 'ruby', '-e', + "require 'bundler'; require 'json'; Dir.chdir('#{directory}') { puts begin; #{command}; end.to_json }" + ] + o, e, s = Open3.capture3(*cmd) + if s.success? + Solargraph.logger.debug "External bundle: #{o}" + o && !o.empty? ? JSON.parse(o.split("\n").last) : nil + else + Solargraph.logger.warn e + raise BundleNotFoundError, "Failed to load gems from bundle at #{directory}" + end + end + end + + def in_this_bundle? + Bundler.definition&.lockfile&.to_s&.start_with?(directory) # rubocop:disable Style/SafeNavigationChainLength + end + + # @return [Array] + def all_gemspecs_from_this_bundle + # Find only the gems bundler is now using + specish_objects = Bundler.definition.locked_gems.specs + if specish_objects.first.respond_to?(:materialize_for_installation) + specish_objects = specish_objects.map(&:materialize_for_installation) + end + specish_objects.map do |specish| + if specish.respond_to?(:name) && specish.respond_to?(:version) && specish.respond_to?(:gem_dir) + # duck type is good enough for outside uses! + specish + else + to_gem_specification(specish) + end + end.compact + end + + # @return [Array] + def auto_required_gemspecs_from_bundler + return [] unless directory + + logger.info 'Fetching gemspecs autorequired from Bundler (bundler/require)' + @auto_required_gemspecs_from_bundler ||= + if in_this_bundle? + auto_required_gemspecs_from_this_bundle + else + auto_required_gemspecs_from_external_bundle + end + end + + # @return [Array] + def auto_required_gemspecs_from_this_bundle + # Adapted from require() in lib/bundler/runtime.rb + dep_names = Bundler.definition.dependencies.select do |dep| + dep.groups.include?(:default) && dep.should_include? + end.map(&:name) + + all_gemspecs_from_bundle.select { |gemspec| dep_names.include?(gemspec.name) } + end + + # @return [Array] + def auto_required_gemspecs_from_external_bundle + @auto_required_gemspecs_from_external_bundle ||= + begin + logger.info 'Fetching auto-required gemspecs from Bundler (bundler/require)' + command = + 'Bundler.definition.dependencies' \ + '.select { |dep| dep.groups.include?(:default) && dep.should_include? }' \ + '.map(&:name)' + # @sg-ignore + # @type [Array] + dep_names = query_external_bundle command + + all_gemspecs_from_bundle.select { |gemspec| dep_names.include?(gemspec.name) } + end + end + + # @param gemspec [Gem::Specification] + # @return [Array] + def only_runtime_dependencies gemspec + unless gemspec.respond_to?(:dependencies) && gemspec.respond_to?(:development_dependencies) + gemspec = to_gem_specification(gemspec) + end + return [] if gemspec.nil? + + gemspec.dependencies - gemspec.development_dependencies + end + + # @todo Should this be using Gem::SpecFetcher and pull them automatically? + # + # @param name [String] + # @param version [String, nil] + # @param out [IO, nil] output stream for logging + # + # @return [Gem::Specification, nil] + def resolve_gem_ignoring_local_bundle name, version = nil, out: $stderr + Gem::Specification.find_by_name(name, version) + rescue Gem::MissingSpecError + begin + Gem::Specification.find_by_name(name) + rescue Gem::MissingSpecError + stdlibmap = RbsMap::StdlibMap.new(name) + unless stdlibmap.resolved? + gem_desc = name + gem_desc += ":#{version}" if version + out&.puts "Please install the gem #{gem_desc} in Solargraph's Ruby environment" + end + nil # either not here or in stdlib + end + end + + # @return [Array] + # @sg-ignore + def all_gemspecs_from_external_bundle + @all_gemspecs_from_external_bundle ||= + begin + logger.info 'Fetching gemspecs required from external bundle' + + command = 'specish_objects = Bundler.definition.locked_gems&.specs; ' \ + 'if specish_objects.first.respond_to?(:materialize_for_installation);' \ + 'specish_objects = specish_objects.map(&:materialize_for_installation);' \ + 'end;' \ + 'specish_objects.map { |specish| [specish.name, specish.version] }' + query_external_bundle(command).map do |name, version| + resolve_gem_ignoring_local_bundle(name, version) + end.compact + rescue Solargraph::BundleNotFoundError => e + Solargraph.logger.info e.message + Solargraph.logger.debug e.backtrace.join("\n") + [] + end + end + + # @return [Hash{String => Gem::Specification}] + def preference_map + @preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] } + end + + # @param gemspec [Gem::Specification] + # + # @return [Gem::Specification] + def gemspec_or_preference gemspec + return gemspec unless preference_map.key?(gemspec.name) + return gemspec if gemspec.version == preference_map[gemspec.name].version + + change_gemspec_version gemspec, preference_map[gemspec.name].version + end + + # @param gemspec [Gem::Specification] + # @param version [String] + # @return [Gem::Specification] + def change_gemspec_version gemspec, version + Gem::Specification.find_by_name(gemspec.name, "= #{version}") + rescue Gem::MissingSpecError + Solargraph.logger.info "Gem #{gemspec.name} version #{version.inspect} not found. " \ + "Using #{gemspec.version} instead" + gemspec + end + end + end +end diff --git a/lib/solargraph/workspace/require_paths.rb b/lib/solargraph/workspace/require_paths.rb new file mode 100644 index 000000000..c8eea161b --- /dev/null +++ b/lib/solargraph/workspace/require_paths.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'open3' + +module Solargraph + # A workspace consists of the files in a project's directory and the + # project's configuration. It provides a Source for each file to be used + # in an associated Library or ApiMap. + # + class Workspace + # Manages determining which gemspecs are available in a workspace + class RequirePaths + attr_reader :directory, :config + + # @param directory [String, nil] + # @param config [Config, nil] + def initialize directory, config + @directory = directory + @config = config + end + + # Generate require paths from gemspecs if they exist or assume the default + # lib directory. + # + # @return [Array] + def generate + result = require_paths_from_gemspec_files + return configured_require_paths if result.empty? + result.concat(config.require_paths.map { |p| File.join(directory, p) }) if config + result + end + + private + + # @return [Array] + def require_paths_from_gemspec_files + results = [] + gemspec_file_paths.each do |gemspec_file_path| + results.concat require_path_from_gemspec_file(gemspec_file_path) + end + results + end + + # Get an array of all gemspec files in the workspace. + # + # @return [Array] + def gemspec_file_paths + return [] if directory.nil? + @gemspec_file_paths ||= Dir[File.join(directory, '**/*.gemspec')].select do |gs| + config.nil? || config.allow?(gs) + end + end + + # Get additional require paths defined in the configuration. + # + # @return [Array] + def configured_require_paths + return ['lib'] unless directory + return [File.join(directory, 'lib')] if !config || config.require_paths.empty? + config.require_paths.map { |p| File.join(directory, p) } + end + + # Generate require paths from gemspecs if they exist or assume the default + # lib directory. + # + # @param gemspec_file_path [String] + # @return [Array] + def require_path_from_gemspec_file gemspec_file_path + base = File.dirname(gemspec_file_path) + # HACK: Evaluating gemspec files violates the goal of not running + # workspace code, but this is how Gem::Specification.load does it + # anyway. + cmd = ['ruby', '-e', + "require 'rubygems'; " \ + "require 'json'; " \ + "spec = eval(File.read('#{gemspec_file_path}'), TOPLEVEL_BINDING, '#{gemspec_file_path}'); " \ + 'return unless Gem::Specification === spec; ' \ + 'puts({name: spec.name, paths: spec.require_paths}.to_json)'] + o, e, s = Open3.capture3(*cmd) + if s.success? + begin + hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {} + return [] if hash.empty? + hash['paths'].map { |path| File.join(base, path) } + rescue StandardError => e + Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}" + [] + end + else + Solargraph.logger.warn "Error reading #{gemspec_file_path}" + Solargraph.logger.warn e + [] + end + end + end + end +end diff --git a/lib/solargraph/yard_map/mapper/to_method.rb b/lib/solargraph/yard_map/mapper/to_method.rb index df431bb3c..d8e3b8b43 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -27,8 +27,8 @@ def self.make code_object, name = nil, scope = nil, visibility = nil, closure = final_scope = scope || code_object.scope override_key = [closure.path, final_scope, name] final_visibility = VISIBILITY_OVERRIDE[override_key] - final_visibility ||= VISIBILITY_OVERRIDE[override_key[0..-2]] - final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name) + final_visibility ||= VISIBILITY_OVERRIDE[[closure.path, final_scope]] + final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name.to_sym) final_visibility ||= visibility final_visibility ||= :private if code_object.module_function? && final_scope == :instance final_visibility ||= :public if code_object.module_function? && final_scope == :class diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index d8e597978..50f212f13 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -8,44 +8,54 @@ module Solargraph module Yardoc module_function - # Build and cache a gem's yardoc and return the path. If the cache already - # exists, do nothing and return the path. + # Build and save a gem's yardoc into a given path. # - # @param yard_plugins [Array] The names of YARD plugins to use. + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem + # @param yard_plugins [Array] # @param gemspec [Gem::Specification] - # @return [String] The path to the cached yardoc. - def cache(yard_plugins, gemspec) - path = PinCache.yardoc_path gemspec - return path if cached?(gemspec) + # + # @return [void] + def build_docs gem_yardoc_path, yard_plugins, gemspec + return if docs_built?(gem_yardoc_path) - Solargraph.logger.info "Caching yardoc for #{gemspec.name} #{gemspec.version}" - cmd = "yardoc --db #{path} --no-output --plugin solargraph" + Solargraph.logger.info "Saving yardoc for #{gemspec.name} #{gemspec.version} into #{gem_yardoc_path}" + cmd = "yardoc --db #{gem_yardoc_path} --no-output --plugin solargraph" yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" } Solargraph.logger.debug { "Running: #{cmd}" } # @todo set these up to run in parallel - # - # @sg-ignore RBS gem doesn't reflect that Open3.* also include - # kwopts from Process.spawn() - stdout_and_stderr_str, status = Open3.capture2e(cmd, chdir: gemspec.gem_dir) - unless status.success? - Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } - Solargraph.logger.info stdout_and_stderr_str + unless File.exist?(gemspec.gem_dir) + Solargraph.logger.info { "Bad info from gemspec - #{gemspec.gem_dir} does not exist" } + return end - path + + stdout_and_stderr_str, status = Open3.capture2e(current_bundle_env_tweaks, cmd, chdir: gemspec.gem_dir) + return if status.success? + Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } + Solargraph.logger.info stdout_and_stderr_str + end + + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem + # @param gemspec [Gem::Specification] + # @param out [IO, nil] where to log messages + # @return [Array] + def build_pins gem_yardoc_path, gemspec, out: $stderr + yardoc = load!(gem_yardoc_path) + YardMap::Mapper.new(yardoc, gemspec).map end # True if the gem yardoc is cached. # - # @param gemspec [Gem::Specification] - def cached?(gemspec) - yardoc = File.join(PinCache.yardoc_path(gemspec), 'complete') + # @param gem_yardoc_path [String] + def docs_built? gem_yardoc_path + yardoc = File.join(gem_yardoc_path, 'complete') File.exist?(yardoc) end # True if another process is currently building the yardoc cache. # - def processing?(gemspec) - yardoc = File.join(PinCache.yardoc_path(gemspec), 'processing') + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem + def processing? gem_yardoc_path + yardoc = File.join(gem_yardoc_path, 'processing') File.exist?(yardoc) end @@ -53,11 +63,28 @@ def processing?(gemspec) # # @note This method modifies the global YARD registry. # - # @param gemspec [Gem::Specification] + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem # @return [Array] - def load!(gemspec) - YARD::Registry.load! PinCache.yardoc_path gemspec + def load! gem_yardoc_path + YARD::Registry.load! gem_yardoc_path YARD::Registry.all end + + # If the BUNDLE_GEMFILE environment variable is set, we need to + # make sure it's an absolute path, as we'll be changing + # directories. + # + # 'bundle exec' sets an absolute path here, but at least the + # overcommit gem does not, breaking on-the-fly documention with a + # spawned yardoc command from our current bundle + # + # @return [Hash{String => String}] a hash of environment variables to override + def current_bundle_env_tweaks + tweaks = {} + if ENV['BUNDLE_GEMFILE'] && !ENV['BUNDLE_GEMFILE'].empty? + tweaks['BUNDLE_GEMFILE'] = File.expand_path(ENV['BUNDLE_GEMFILE']) + end + tweaks + end end end diff --git a/rbs/fills/bundler/0/bundler.rbs b/rbs/fills/bundler/0/bundler.rbs new file mode 100644 index 000000000..8b23710d4 --- /dev/null +++ b/rbs/fills/bundler/0/bundler.rbs @@ -0,0 +1,4271 @@ +# [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) provides a +# consistent environment for Ruby projects by tracking and installing the exact +# gems and versions that are needed. +# +# Since Ruby 2.6, [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) +# is a part of Ruby's standard library. +# +# Bunder is used by creating *gemfiles* listing all the project dependencies and +# (optionally) their versions and then using +# +# ```ruby +# require 'bundler/setup' +# ``` +# +# or +# [`Bundler.setup`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html#method-c-setup) +# to setup environment where only specified gems and their specified versions +# could be used. +# +# See [Bundler website](https://bundler.io/docs.html) for extensive +# documentation on gemfiles creation and +# [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) usage. +# +# As a standard library inside project, +# [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) could be used +# for introspection of loaded and required modules. +module Bundler + def self.app_cache: (?untyped custom_path) -> untyped + + def self.app_config_path: () -> untyped + + # Returns absolute location of where binstubs are installed to. + def self.bin_path: () -> untyped + + # Returns absolute path of where gems are installed on the filesystem. + def self.bundle_path: () -> untyped + + def self.bundler_major_version: () -> untyped + + # @deprecated Use `unbundled\_env` instead + def self.clean_env: () -> untyped + + def self.clean_exec: (*untyped args) -> untyped + + def self.clean_system: (*untyped args) -> untyped + + def self.clear_gemspec_cache: () -> untyped + + def self.configure: () -> untyped + + def self.configured_bundle_path: () -> untyped + + # Returns current version of Ruby + # + # @return [CurrentRuby] Current version of Ruby + def self.current_ruby: () -> untyped + + def self.default_bundle_dir: () -> untyped + + def self.default_gemfile: () -> untyped + + def self.default_lockfile: () -> untyped + + def self.definition: (?(::Hash[String, Boolean | nil] | Boolean | nil) unlock) -> Bundler::Definition + + def self.environment: () -> untyped + + def self.feature_flag: () -> untyped + + def self.frozen_bundle?: () -> untyped + + def self.git_present?: () -> untyped + + def self.home: () -> untyped + + def self.install_path: () -> untyped + + def self.load: () -> untyped + + def self.load_gemspec: (untyped file, ?untyped validate) -> untyped + + def self.load_gemspec_uncached: (untyped file, ?untyped validate) -> untyped + + def self.load_marshal: (untyped data) -> untyped + + def self.local_platform: () -> untyped + + def self.locked_gems: () -> untyped + + def self.mkdir_p: (untyped path, ?untyped options) -> untyped + + # @return [Hash] Environment present before + # [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) was activated + def self.original_env: () -> untyped + + def self.read_file: (untyped file) -> untyped + + def self.require: (*untyped groups) -> untyped + + def self.require_thor_actions: () -> untyped + + def self.requires_sudo?: () -> untyped + + def self.reset!: () -> untyped + + def self.reset_paths!: () -> untyped + + def self.reset_rubygems!: () -> untyped + + def self.rm_rf: (untyped path) -> untyped + + def self.root: () -> untyped + + def self.ruby_scope: () -> untyped + + def self.rubygems: () -> untyped + + def self.settings: () -> untyped + + def self.setup: (*untyped groups) -> untyped + + def self.specs_path: () -> untyped + + def self.sudo: (untyped str) -> untyped + + def self.system_bindir: () -> untyped + + def self.tmp: (?untyped name) -> untyped + + def self.tmp_home_path: (untyped login, untyped warning) -> untyped + + def self.ui: () -> untyped + + def self.ui=: (untyped ui) -> untyped + + def self.use_system_gems?: () -> untyped + + def self.user_bundle_path: (?untyped dir) -> untyped + + def self.user_cache: () -> untyped + + def self.user_home: () -> untyped + + def self.which: (untyped executable) -> untyped + + # @deprecated Use `with\_unbundled\_env` instead + def self.with_clean_env: () { () -> untyped } -> untyped + + def self.with_unbundled_env: () { () -> untyped } -> untyped + + # Run block with environment present before + # [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) was activated + def self.with_original_env: () { () -> untyped } -> untyped +end + +Bundler::FREEBSD: untyped + +Bundler::NULL: untyped + +Bundler::ORIGINAL_ENV: untyped + +Bundler::SUDO_MUTEX: untyped + +Bundler::VERSION: untyped + +Bundler::WINDOWS: untyped + +class Bundler::APIResponseMismatchError < Bundler::BundlerError + def status_code: () -> untyped +end + +# Represents metadata from when the +# [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) gem was built. +module Bundler::BuildMetadata + # A string representing the date the bundler gem was built. + def self.built_at: () -> untyped + + # The SHA for the git commit the bundler gem was built from. + def self.git_commit_sha: () -> untyped + + # Whether this is an official release build of + # [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html). + def self.release?: () -> untyped + + # A hash representation of the build metadata. + def self.to_h: () -> untyped +end + +class Bundler::BundlerError < StandardError + def self.all_errors: () -> untyped + + def self.status_code: (untyped code) -> untyped +end + +class Bundler::CurrentRuby + def jruby?: () -> untyped + + def jruby_18?: () -> untyped + + def jruby_19?: () -> untyped + + def jruby_1?: () -> untyped + + def jruby_20?: () -> untyped + + def jruby_21?: () -> untyped + + def jruby_22?: () -> untyped + + def jruby_23?: () -> untyped + + def jruby_24?: () -> untyped + + def jruby_25?: () -> untyped + + def jruby_26?: () -> untyped + + def jruby_27?: () -> untyped + + def jruby_2?: () -> untyped + + def maglev?: () -> untyped + + def maglev_18?: () -> untyped + + def maglev_19?: () -> untyped + + def maglev_1?: () -> untyped + + def maglev_20?: () -> untyped + + def maglev_21?: () -> untyped + + def maglev_22?: () -> untyped + + def maglev_23?: () -> untyped + + def maglev_24?: () -> untyped + + def maglev_25?: () -> untyped + + def maglev_26?: () -> untyped + + def maglev_27?: () -> untyped + + def maglev_2?: () -> untyped + + def mingw?: () -> untyped + + def mingw_18?: () -> untyped + + def mingw_19?: () -> untyped + + def mingw_1?: () -> untyped + + def mingw_20?: () -> untyped + + def mingw_21?: () -> untyped + + def mingw_22?: () -> untyped + + def mingw_23?: () -> untyped + + def mingw_24?: () -> untyped + + def mingw_25?: () -> untyped + + def mingw_26?: () -> untyped + + def mingw_27?: () -> untyped + + def mingw_2?: () -> untyped + + def mri?: () -> untyped + + def mri_18?: () -> untyped + + def mri_19?: () -> untyped + + def mri_1?: () -> untyped + + def mri_20?: () -> untyped + + def mri_21?: () -> untyped + + def mri_22?: () -> untyped + + def mri_23?: () -> untyped + + def mri_24?: () -> untyped + + def mri_25?: () -> untyped + + def mri_26?: () -> untyped + + def mri_27?: () -> untyped + + def mri_2?: () -> untyped + + def mswin64?: () -> untyped + + def mswin64_18?: () -> untyped + + def mswin64_19?: () -> untyped + + def mswin64_1?: () -> untyped + + def mswin64_20?: () -> untyped + + def mswin64_21?: () -> untyped + + def mswin64_22?: () -> untyped + + def mswin64_23?: () -> untyped + + def mswin64_24?: () -> untyped + + def mswin64_25?: () -> untyped + + def mswin64_26?: () -> untyped + + def mswin64_27?: () -> untyped + + def mswin64_2?: () -> untyped + + def mswin?: () -> untyped + + def mswin_18?: () -> untyped + + def mswin_19?: () -> untyped + + def mswin_1?: () -> untyped + + def mswin_20?: () -> untyped + + def mswin_21?: () -> untyped + + def mswin_22?: () -> untyped + + def mswin_23?: () -> untyped + + def mswin_24?: () -> untyped + + def mswin_25?: () -> untyped + + def mswin_26?: () -> untyped + + def mswin_27?: () -> untyped + + def mswin_2?: () -> untyped + + def on_18?: () -> untyped + + def on_19?: () -> untyped + + def on_1?: () -> untyped + + def on_20?: () -> untyped + + def on_21?: () -> untyped + + def on_22?: () -> untyped + + def on_23?: () -> untyped + + def on_24?: () -> untyped + + def on_25?: () -> untyped + + def on_26?: () -> untyped + + def on_27?: () -> untyped + + def on_2?: () -> untyped + + def rbx?: () -> untyped + + def rbx_18?: () -> untyped + + def rbx_19?: () -> untyped + + def rbx_1?: () -> untyped + + def rbx_20?: () -> untyped + + def rbx_21?: () -> untyped + + def rbx_22?: () -> untyped + + def rbx_23?: () -> untyped + + def rbx_24?: () -> untyped + + def rbx_25?: () -> untyped + + def rbx_26?: () -> untyped + + def rbx_27?: () -> untyped + + def rbx_2?: () -> untyped + + def ruby?: () -> untyped + + def ruby_18?: () -> untyped + + def ruby_19?: () -> untyped + + def ruby_1?: () -> untyped + + def ruby_20?: () -> untyped + + def ruby_21?: () -> untyped + + def ruby_22?: () -> untyped + + def ruby_23?: () -> untyped + + def ruby_24?: () -> untyped + + def ruby_25?: () -> untyped + + def ruby_26?: () -> untyped + + def ruby_27?: () -> untyped + + def ruby_2?: () -> untyped + + def truffleruby?: () -> untyped + + def truffleruby_18?: () -> untyped + + def truffleruby_19?: () -> untyped + + def truffleruby_1?: () -> untyped + + def truffleruby_20?: () -> untyped + + def truffleruby_21?: () -> untyped + + def truffleruby_22?: () -> untyped + + def truffleruby_23?: () -> untyped + + def truffleruby_24?: () -> untyped + + def truffleruby_25?: () -> untyped + + def truffleruby_26?: () -> untyped + + def truffleruby_27?: () -> untyped + + def truffleruby_2?: () -> untyped + + def x64_mingw?: () -> untyped + + def x64_mingw_18?: () -> untyped + + def x64_mingw_19?: () -> untyped + + def x64_mingw_1?: () -> untyped + + def x64_mingw_20?: () -> untyped + + def x64_mingw_21?: () -> untyped + + def x64_mingw_22?: () -> untyped + + def x64_mingw_23?: () -> untyped + + def x64_mingw_24?: () -> untyped + + def x64_mingw_25?: () -> untyped + + def x64_mingw_26?: () -> untyped + + def x64_mingw_27?: () -> untyped + + def x64_mingw_2?: () -> untyped +end + +Bundler::CurrentRuby::KNOWN_MAJOR_VERSIONS: untyped + +Bundler::CurrentRuby::KNOWN_MINOR_VERSIONS: untyped + +Bundler::CurrentRuby::KNOWN_PLATFORMS: untyped + +class Bundler::CyclicDependencyError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::Definition + include ::Bundler::GemHelpers + + def add_current_platform: () -> untyped + + def add_platform: (untyped platform) -> untyped + + def current_dependencies: () -> untyped + + def dependencies: () -> Array[::Bundler::Dependency] + + def ensure_equivalent_gemfile_and_lockfile: (?untyped explicit_flag) -> untyped + + def find_indexed_specs: (untyped current_spec) -> untyped + + def find_resolved_spec: (untyped current_spec) -> untyped + + def gem_version_promoter: () -> untyped + + def gemfiles: () -> untyped + + def groups: () -> untyped + + def has_local_dependencies?: () -> untyped + + def has_rubygems_remotes?: () -> untyped + + def index: () -> untyped + + def initialize: (untyped lockfile, untyped dependencies, untyped sources, untyped unlock, ?untyped ruby_version, ?untyped optional_groups, ?untyped gemfiles) -> void + + def lock: (untyped file, ?untyped preserve_unknown_sections) -> untyped + + def locked_bundler_version: () -> untyped + + def locked_deps: () -> untyped + + def locked_gems: () -> Bundler::LockfileParser + + def locked_ruby_version: () -> untyped + + def locked_ruby_version_object: () -> untyped + + def lockfile: () -> Pathname + + def missing_specs: () -> untyped + + def missing_specs?: () -> untyped + + def new_platform?: () -> untyped + + def new_specs: () -> untyped + + def nothing_changed?: () -> untyped + + def platforms: () -> untyped + + def remove_platform: (untyped platform) -> untyped + + def removed_specs: () -> untyped + + def requested_specs: () -> untyped + + def requires: () -> untyped + + # Resolve all the dependencies specified in Gemfile. It ensures that + # dependencies that have been already resolved via locked file and are fresh + # are reused when resolving dependencies + # + # @return [SpecSet] resolved dependencies + def resolve: () -> untyped + + def resolve_remotely!: () -> untyped + + def resolve_with_cache!: () -> untyped + + def ruby_version: () -> untyped + + def spec_git_paths: () -> untyped + + # For given dependency list returns a SpecSet with Gemspec of all the required + # dependencies. + # + # ``` + # 1. The method first resolves the dependencies specified in Gemfile + # 2. After that it tries and fetches gemspec of resolved dependencies + # ``` + # + # @return [Bundler::SpecSet] + def specs: () -> untyped + + def specs_for: (untyped groups) -> untyped + + def to_lock: () -> untyped + + def unlocking?: () -> untyped + + def validate_platforms!: () -> untyped + + def validate_ruby!: () -> untyped + + def validate_runtime!: () -> untyped + + def self.build: (untyped gemfile, untyped lockfile, untyped unlock) -> untyped +end + +class Bundler::DepProxy + def ==: (untyped other) -> untyped + + def __platform: () -> untyped + + def dep: () -> untyped + + def eql?: (untyped other) -> untyped + + def hash: () -> untyped + + def initialize: (untyped dep, untyped platform) -> void + + def name: () -> untyped + + def requirement: () -> untyped + + def to_s: () -> untyped + + def type: () -> untyped +end + +class Bundler::Dependency < Gem::Dependency + def autorequire: () -> untyped + + def current_env?: () -> untyped + + def current_platform?: () -> untyped + + def gem_platforms: (untyped valid_platforms) -> untyped + + def gemfile: () -> untyped + + def groups: () -> untyped + + def initialize: (untyped name, untyped version, ?untyped options) { () -> untyped } -> void + + def platforms: () -> untyped + + def should_include?: () -> untyped + + def specific?: () -> untyped + + def to_lock: () -> untyped +end + +Bundler::Dependency::PLATFORM_MAP: untyped + +Bundler::Dependency::REVERSE_PLATFORM_MAP: untyped + +class Bundler::DeprecatedError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::Dsl + @source: untyped + + @sources: untyped + + @git_sources: untyped + + @dependencies: untyped + + @groups: untyped + + @install_conditionals: untyped + + @optional_groups: untyped + + @platforms: untyped + + @env: untyped + + @ruby_version: untyped + + @gemspecs: untyped + + @gemfile: untyped + + @gemfiles: untyped + + @valid_keys: untyped + + include RubyDsl + + def self.evaluate: (untyped gemfile, untyped lockfile, untyped unlock) -> untyped + + VALID_PLATFORMS: untyped + + VALID_KEYS: ::Array["group" | "groups" | "git" | "path" | "glob" | "name" | "branch" | "ref" | "tag" | "require" | "submodules" | "platform" | "platforms" | "source" | "install_if" | "force_ruby_platform"] + + GITHUB_PULL_REQUEST_URL: ::Regexp + + GITLAB_MERGE_REQUEST_URL: ::Regexp + + attr_reader gemspecs: untyped + + attr_reader gemfile: untyped + + attr_accessor dependencies: untyped + + def initialize: () -> void + + def eval_gemfile: (untyped gemfile, ?untyped? contents) -> untyped + + def gemspec: (?path: ::String, ?glob: ::String, ?name: ::String, ?development_group: ::Symbol) -> void + + def gem: (untyped name, *untyped args) -> void + + def source: (::String source, ?type: ::Symbol) ?{ (?) -> untyped } -> void + + def git_source: (untyped name) ?{ (?) -> untyped } -> untyped + + def path: (untyped path, ?::Hash[untyped, untyped] options) ?{ (?) -> untyped } -> untyped + + def git: (untyped uri, ?::Hash[untyped, untyped] options) ?{ (?) -> untyped } -> untyped + + def github: (untyped repo, ?::Hash[untyped, untyped] options) ?{ () -> untyped } -> untyped + + def to_definition: (untyped lockfile, untyped unlock) -> untyped + + def group: (*untyped args) { () -> untyped } -> untyped + + def install_if: (*untyped args) { () -> untyped } -> untyped + + def platforms: (*untyped platforms) { () -> untyped } -> untyped + + alias platform platforms + + def env: (untyped name) { () -> untyped } -> untyped + + def plugin: (*untyped args) -> nil + + def method_missing: (untyped name, *untyped args) -> untyped + + def check_primary_source_safety: () -> untyped + + private + + def add_dependency: (untyped name, ?untyped? version, ?::Hash[untyped, untyped] options) -> (nil | untyped) + + def with_gemfile: (untyped gemfile) { (untyped) -> untyped } -> untyped + + def add_git_sources: () -> untyped + + def with_source: (untyped source) ?{ () -> untyped } -> untyped + + def normalize_hash: (untyped opts) -> untyped + + def valid_keys: () -> untyped + + def normalize_options: (untyped name, untyped version, untyped opts) -> untyped + + def normalize_group_options: (untyped opts, untyped groups) -> untyped + + def validate_keys: (untyped command, untyped opts, untyped valid_keys) -> (true | untyped) + + def normalize_source: (untyped source) -> untyped + + def deprecate_legacy_windows_platforms: (untyped platforms) -> (nil | untyped) + + def check_path_source_safety: () -> (nil | untyped) + + def check_rubygems_source_safety: () -> (untyped | nil) + + def multiple_global_source_warning: () -> untyped + + class DSLError < GemfileError + @status_code: untyped + + @description: untyped + + @dsl_path: untyped + + @backtrace: untyped + + @contents: untyped + + @to_s: untyped + + # @return [::String] the description that should be presented to the user. + # + attr_reader description: ::String + + # @return [::String] the path of the dsl file that raised the exception. + # + attr_reader dsl_path: ::String + + # @return [::Exception] the backtrace of the exception raised by the + # evaluation of the dsl file. + # + attr_reader backtrace: ::Exception + + # @param [::Exception] backtrace @see backtrace + # @param [::String] dsl_path @see dsl_path + # + def initialize: (untyped description, ::String dsl_path, ::Exception backtrace, ?untyped? contents) -> void + + def status_code: () -> untyped + + # @return [::String] the contents of the DSL that cause the exception to + # be raised. + # + def contents: () -> ::String + + # The message of the exception reports the content of podspec for the + # line that generated the original exception. + # + # @example Output + # + # Invalid podspec at `RestKit.podspec` - undefined method + # `exclude_header_search_paths=' for # + # + # from spec-repos/master/RestKit/0.9.3/RestKit.podspec:36 + # ------------------------------------------- + # # because it would break: #import + # > ns.exclude_header_search_paths = 'Code/RestKit.h' + # end + # ------------------------------------------- + # + # @return [::String] the message of the exception. + # + def to_s: () -> ::String + + private + + def parse_line_number_from_description: () -> ::Array[untyped] + end + + def gemfile_root: () -> untyped +end + +# used for Creating Specifications from the Gemcutter Endpoint +class Bundler::EndpointSpecification < Gem::Specification + def __swap__: (untyped spec) -> untyped + + def _local_specification: () -> untyped + + # needed for bundle clean + def bindir: () -> untyped + + def checksum: () -> untyped + + def dependencies: () -> untyped + + def dependencies=: (untyped dependencies) -> untyped + + # needed for binstubs + def executables: () -> untyped + + # needed for "with native extensions" during install + def extensions: () -> untyped + + def fetch_platform: () -> untyped + + def initialize: (untyped name, untyped version, untyped platform, untyped dependencies, ?untyped metadata) -> void + + # needed for inline + def load_paths: () -> untyped + + def name: () -> untyped + + def platform: () -> untyped + + # needed for post\_install\_messages during install + def post_install_message: () -> untyped + + def remote: () -> untyped + + def remote=: (untyped remote) -> untyped + + # needed for standalone, load required\_paths from local gemspec after the gem + # is installed + def require_paths: () -> untyped + + def required_ruby_version: () -> untyped + + def required_rubygems_version: () -> untyped + + def source: () -> untyped + + def source=: (untyped source) -> untyped + + def version: () -> untyped +end + +Bundler::EndpointSpecification::Elem: untyped + +Bundler::EndpointSpecification::ILLFORMED_MESSAGE: untyped + +class Bundler::EnvironmentPreserver + # @return [Hash] + def backup: () -> untyped + + def initialize: (untyped env, untyped keys) -> void + + # @return [Hash] + def restore: () -> untyped +end + +Bundler::EnvironmentPreserver::BUNDLER_KEYS: untyped + +Bundler::EnvironmentPreserver::BUNDLER_PREFIX: untyped + +Bundler::EnvironmentPreserver::INTENTIONALLY_NIL: untyped + +class Bundler::FeatureFlag + def allow_bundler_dependency_conflicts?: () -> untyped + + def allow_offline_install?: () -> untyped + + def auto_clean_without_path?: () -> untyped + + def auto_config_jobs?: () -> untyped + + def bundler_10_mode?: () -> untyped + + def bundler_1_mode?: () -> untyped + + def bundler_2_mode?: () -> untyped + + def bundler_3_mode?: () -> untyped + + def bundler_4_mode?: () -> untyped + + def bundler_5_mode?: () -> untyped + + def bundler_6_mode?: () -> untyped + + def bundler_7_mode?: () -> untyped + + def bundler_8_mode?: () -> untyped + + def bundler_9_mode?: () -> untyped + + def cache_all?: () -> untyped + + def cache_command_is_package?: () -> untyped + + def console_command?: () -> untyped + + def default_cli_command: () -> untyped + + def default_install_uses_path?: () -> untyped + + def deployment_means_frozen?: () -> untyped + + def disable_multisource?: () -> untyped + + def error_on_stderr?: () -> untyped + + def forget_cli_options?: () -> untyped + + def global_gem_cache?: () -> untyped + + def init_gems_rb?: () -> untyped + + def initialize: (untyped bundler_version) -> void + + def list_command?: () -> untyped + + def lockfile_uses_separate_rubygems_sources?: () -> untyped + + def only_update_to_newer_versions?: () -> untyped + + def path_relative_to_cwd?: () -> untyped + + def plugins?: () -> untyped + + def prefer_gems_rb?: () -> untyped + + def print_only_version_number?: () -> untyped + + def setup_makes_kernel_gem_public?: () -> untyped + + def skip_default_git_sources?: () -> untyped + + def specific_platform?: () -> untyped + + def suppress_install_using_messages?: () -> untyped + + def unlock_source_unlocks_spec?: () -> untyped + + def update_requires_all_flag?: () -> untyped + + def use_gem_version_promoter_for_major_updates?: () -> untyped + + def viz_command?: () -> untyped +end + +# # fileutils.rb +# +# Copyright (c) 2000-2007 Minero Aoki +# +# This program is free software. You can distribute/modify this program under +# the same terms of ruby. +# +# ## module [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html) +# +# Namespace for several file utility methods for copying, moving, removing, etc. +# +# ### [`Module`](https://docs.ruby-lang.org/en/2.7.0/Module.html) Functions +# +# ```ruby +# require 'bundler/vendor/fileutils/lib/fileutils' +# +# Bundler::FileUtils.cd(dir, **options) +# Bundler::FileUtils.cd(dir, **options) {|dir| block } +# Bundler::FileUtils.pwd() +# Bundler::FileUtils.mkdir(dir, **options) +# Bundler::FileUtils.mkdir(list, **options) +# Bundler::FileUtils.mkdir_p(dir, **options) +# Bundler::FileUtils.mkdir_p(list, **options) +# Bundler::FileUtils.rmdir(dir, **options) +# Bundler::FileUtils.rmdir(list, **options) +# Bundler::FileUtils.ln(target, link, **options) +# Bundler::FileUtils.ln(targets, dir, **options) +# Bundler::FileUtils.ln_s(target, link, **options) +# Bundler::FileUtils.ln_s(targets, dir, **options) +# Bundler::FileUtils.ln_sf(target, link, **options) +# Bundler::FileUtils.cp(src, dest, **options) +# Bundler::FileUtils.cp(list, dir, **options) +# Bundler::FileUtils.cp_r(src, dest, **options) +# Bundler::FileUtils.cp_r(list, dir, **options) +# Bundler::FileUtils.mv(src, dest, **options) +# Bundler::FileUtils.mv(list, dir, **options) +# Bundler::FileUtils.rm(list, **options) +# Bundler::FileUtils.rm_r(list, **options) +# Bundler::FileUtils.rm_rf(list, **options) +# Bundler::FileUtils.install(src, dest, **options) +# Bundler::FileUtils.chmod(mode, list, **options) +# Bundler::FileUtils.chmod_R(mode, list, **options) +# Bundler::FileUtils.chown(user, group, list, **options) +# Bundler::FileUtils.chown_R(user, group, list, **options) +# Bundler::FileUtils.touch(list, **options) +# ``` +# +# Possible `options` are: +# +# `:force` +# : forced operation (rewrite files if exist, remove directories if not empty, +# etc.); +# `:verbose` +# : print command to be run, in bash syntax, before performing it; +# `:preserve` +# : preserve object's group, user and modification time on copying; +# `:noop` +# : no changes are made (usable in combination with `:verbose` which will +# print the command to run) +# +# +# Each method documents the options that it honours. See also +# [`::commands`](https://docs.ruby-lang.org/en/2.7.0/FileUtils.html#method-c-commands), +# [`::options`](https://docs.ruby-lang.org/en/2.7.0/FileUtils.html#method-c-options) +# and +# [`::options_of`](https://docs.ruby-lang.org/en/2.7.0/FileUtils.html#method-c-options_of) +# methods to introspect which command have which options. +# +# All methods that have the concept of a "source" file or directory can take +# either one file or a list of files in that argument. See the method +# documentation for examples. +# +# There are some 'low level' methods, which do not accept keyword arguments: +# +# ```ruby +# Bundler::FileUtils.copy_entry(src, dest, preserve = false, dereference_root = false, remove_destination = false) +# Bundler::FileUtils.copy_file(src, dest, preserve = false, dereference = true) +# Bundler::FileUtils.copy_stream(srcstream, deststream) +# Bundler::FileUtils.remove_entry(path, force = false) +# Bundler::FileUtils.remove_entry_secure(path, force = false) +# Bundler::FileUtils.remove_file(path, force = false) +# Bundler::FileUtils.compare_file(path_a, path_b) +# Bundler::FileUtils.compare_stream(stream_a, stream_b) +# Bundler::FileUtils.uptodate?(file, cmp_list) +# ``` +# +# ## module [`Bundler::FileUtils::Verbose`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils/Verbose.html) +# +# This module has all methods of +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html) +# module, but it outputs messages before acting. This equates to passing the +# `:verbose` flag to methods in +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html). +# +# ## module [`Bundler::FileUtils::NoWrite`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils/NoWrite.html) +# +# This module has all methods of +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html) +# module, but never changes files/directories. This equates to passing the +# `:noop` flag to methods in +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html). +# +# ## module [`Bundler::FileUtils::DryRun`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils/DryRun.html) +# +# This module has all methods of +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html) +# module, but never changes files/directories. This equates to passing the +# `:noop` and `:verbose` flags to methods in +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html). +module Bundler::FileUtils + include ::Bundler::FileUtils::StreamUtils_ + + extend ::Bundler::FileUtils::StreamUtils_ + + def self.cd: (untyped dir, ?verbose: untyped verbose) { () -> untyped } -> untyped + + def self.chdir: (untyped dir, ?verbose: untyped verbose) { () -> untyped } -> untyped + + def self.chmod: (untyped mode, untyped list, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.chmod_R: (untyped mode, untyped list, ?noop: untyped noop, ?verbose: untyped verbose, ?force: untyped force) -> untyped + + def self.chown: (untyped user, untyped group, untyped list, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.chown_R: (untyped user, untyped group, untyped list, ?noop: untyped noop, ?verbose: untyped verbose, ?force: untyped force) -> untyped + + def self.cmp: (untyped a, untyped b) -> untyped + + def self.collect_method: (untyped opt) -> untyped + + # Returns an [`Array`](https://docs.ruby-lang.org/en/2.7.0/Array.html) of + # names of high-level methods that accept any keyword arguments. + # + # ```ruby + # p Bundler::FileUtils.commands #=> ["chmod", "cp", "cp_r", "install", ...] + # ``` + def self.commands: () -> untyped + + def self.compare_file: (untyped a, untyped b) -> untyped + + def self.compare_stream: (untyped a, untyped b) -> untyped + + def self.copy: (untyped src, untyped dest, ?preserve: untyped preserve, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.copy_entry: (untyped src, untyped dest, ?untyped preserve, ?untyped dereference_root, ?untyped remove_destination) -> untyped + + def self.copy_file: (untyped src, untyped dest, ?untyped preserve, ?untyped dereference) -> untyped + + def self.copy_stream: (untyped src, untyped dest) -> untyped + + def self.cp: (untyped src, untyped dest, ?preserve: untyped preserve, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.cp_r: (untyped src, untyped dest, ?preserve: untyped preserve, ?noop: untyped noop, ?verbose: untyped verbose, ?dereference_root: untyped dereference_root, ?remove_destination: untyped remove_destination) -> untyped + + # Alias for: + # [`pwd`](https://docs.ruby-lang.org/en/2.7.0/FileUtils.html#method-i-pwd) + def self.getwd: () -> untyped + + def self.have_option?: (untyped mid, untyped opt) -> untyped + + def self.identical?: (untyped a, untyped b) -> untyped + + def self.install: (untyped src, untyped dest, ?mode: untyped mode, ?owner: untyped owner, ?group: untyped group, ?preserve: untyped preserve, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.link: (untyped src, untyped dest, ?force: untyped force, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.ln: (untyped src, untyped dest, ?force: untyped force, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.ln_s: (untyped src, untyped dest, ?force: untyped force, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.ln_sf: (untyped src, untyped dest, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.makedirs: (untyped list, ?mode: untyped mode, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.mkdir: (untyped list, ?mode: untyped mode, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.mkdir_p: (untyped list, ?mode: untyped mode, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.mkpath: (untyped list, ?mode: untyped mode, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.move: (untyped src, untyped dest, ?force: untyped force, ?noop: untyped noop, ?verbose: untyped verbose, ?secure: untyped secure) -> untyped + + def self.mv: (untyped src, untyped dest, ?force: untyped force, ?noop: untyped noop, ?verbose: untyped verbose, ?secure: untyped secure) -> untyped + + # Returns an [`Array`](https://docs.ruby-lang.org/en/2.7.0/Array.html) of + # option names. + # + # ```ruby + # p Bundler::FileUtils.options #=> ["noop", "force", "verbose", "preserve", "mode"] + # ``` + def self.options: () -> untyped + + def self.options_of: (untyped mid) -> untyped + + def self.private_module_function: (untyped name) -> untyped + + # Returns the name of the current directory. + # + # Also aliased as: + # [`getwd`](https://docs.ruby-lang.org/en/2.7.0/FileUtils.html#method-c-getwd) + def self.pwd: () -> untyped + + def self.remove: (untyped list, ?force: untyped force, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.remove_dir: (untyped path, ?untyped force) -> untyped + + def self.remove_entry: (untyped path, ?untyped force) -> untyped + + def self.remove_entry_secure: (untyped path, ?untyped force) -> untyped + + def self.remove_file: (untyped path, ?untyped force) -> untyped + + def self.rm: (untyped list, ?force: untyped force, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.rm_f: (untyped list, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.rm_r: (untyped list, ?force: untyped force, ?noop: untyped noop, ?verbose: untyped verbose, ?secure: untyped secure) -> untyped + + def self.rm_rf: (untyped list, ?noop: untyped noop, ?verbose: untyped verbose, ?secure: untyped secure) -> untyped + + def self.rmdir: (untyped list, ?parents: untyped parents, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.rmtree: (untyped list, ?noop: untyped noop, ?verbose: untyped verbose, ?secure: untyped secure) -> untyped + + def self.safe_unlink: (untyped list, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.symlink: (untyped src, untyped dest, ?force: untyped force, ?noop: untyped noop, ?verbose: untyped verbose) -> untyped + + def self.touch: (untyped list, ?noop: untyped noop, ?verbose: untyped verbose, ?mtime: untyped mtime, ?nocreate: untyped nocreate) -> untyped + + def self.uptodate?: (untyped new, untyped old_list) -> untyped +end + +Bundler::FileUtils::LOW_METHODS: untyped + +Bundler::FileUtils::METHODS: untyped + +Bundler::FileUtils::OPT_TABLE: untyped + +# This module has all methods of +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html) +# module, but never changes files/directories, with printing message before +# acting. This equates to passing the `:noop` and `:verbose` flag to methods in +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html). +module Bundler::FileUtils::DryRun + include ::Bundler::FileUtils::LowMethods + + include ::Bundler::FileUtils + + include ::Bundler::FileUtils::StreamUtils_ + + extend ::Bundler::FileUtils::DryRun + + extend ::Bundler::FileUtils::LowMethods + + extend ::Bundler::FileUtils + + extend ::Bundler::FileUtils::StreamUtils_ + + def self.cd: (*untyped _) -> untyped + + def self.chdir: (*untyped _) -> untyped + + def self.chmod: (*untyped args, **untyped options) -> untyped + + def self.chmod_R: (*untyped args, **untyped options) -> untyped + + def self.chown: (*untyped args, **untyped options) -> untyped + + def self.chown_R: (*untyped args, **untyped options) -> untyped + + def self.cmp: (*untyped _) -> untyped + + def self.compare_file: (*untyped _) -> untyped + + def self.compare_stream: (*untyped _) -> untyped + + def self.copy: (*untyped args, **untyped options) -> untyped + + def self.copy_entry: (*untyped _) -> untyped + + def self.copy_file: (*untyped _) -> untyped + + def self.copy_stream: (*untyped _) -> untyped + + def self.cp: (*untyped args, **untyped options) -> untyped + + def self.cp_r: (*untyped args, **untyped options) -> untyped + + def self.getwd: (*untyped _) -> untyped + + def self.identical?: (*untyped _) -> untyped + + def self.install: (*untyped args, **untyped options) -> untyped + + def self.link: (*untyped args, **untyped options) -> untyped + + def self.ln: (*untyped args, **untyped options) -> untyped + + def self.ln_s: (*untyped args, **untyped options) -> untyped + + def self.ln_sf: (*untyped args, **untyped options) -> untyped + + def self.makedirs: (*untyped args, **untyped options) -> untyped + + def self.mkdir: (*untyped args, **untyped options) -> untyped + + def self.mkdir_p: (*untyped args, **untyped options) -> untyped + + def self.mkpath: (*untyped args, **untyped options) -> untyped + + def self.move: (*untyped args, **untyped options) -> untyped + + def self.mv: (*untyped args, **untyped options) -> untyped + + def self.pwd: (*untyped _) -> untyped + + def self.remove: (*untyped args, **untyped options) -> untyped + + def self.remove_dir: (*untyped _) -> untyped + + def self.remove_entry: (*untyped _) -> untyped + + def self.remove_entry_secure: (*untyped _) -> untyped + + def self.remove_file: (*untyped _) -> untyped + + def self.rm: (*untyped args, **untyped options) -> untyped + + def self.rm_f: (*untyped args, **untyped options) -> untyped + + def self.rm_r: (*untyped args, **untyped options) -> untyped + + def self.rm_rf: (*untyped args, **untyped options) -> untyped + + def self.rmdir: (*untyped args, **untyped options) -> untyped + + def self.rmtree: (*untyped args, **untyped options) -> untyped + + def self.safe_unlink: (*untyped args, **untyped options) -> untyped + + def self.symlink: (*untyped args, **untyped options) -> untyped + + def self.touch: (*untyped args, **untyped options) -> untyped + + def self.uptodate?: (*untyped _) -> untyped +end + +class Bundler::FileUtils::Entry_ + include ::Bundler::FileUtils::StreamUtils_ + + def blockdev?: () -> untyped + + def chardev?: () -> untyped + + def chmod: (untyped mode) -> untyped + + def chown: (untyped uid, untyped gid) -> untyped + + def copy: (untyped dest) -> untyped + + def copy_file: (untyped dest) -> untyped + + def copy_metadata: (untyped path) -> untyped + + def dereference?: () -> untyped + + def directory?: () -> untyped + + def door?: () -> untyped + + def entries: () -> untyped + + def exist?: () -> untyped + + def file?: () -> untyped + + def initialize: (untyped a, ?untyped b, ?untyped deref) -> void + + def inspect: () -> untyped + + def lstat: () -> untyped + + def lstat!: () -> untyped + + def path: () -> untyped + + def pipe?: () -> untyped + + def platform_support: () -> untyped + + def postorder_traverse: () -> untyped + + def prefix: () -> untyped + + def preorder_traverse: () -> untyped + + def rel: () -> untyped + + def remove: () -> untyped + + def remove_dir1: () -> untyped + + def remove_file: () -> untyped + + def socket?: () -> untyped + + def stat: () -> untyped + + def stat!: () -> untyped + + def symlink?: () -> untyped + + def traverse: () -> untyped + + def wrap_traverse: (untyped pre, untyped post) -> untyped +end + +Bundler::FileUtils::Entry_::DIRECTORY_TERM: untyped + +Bundler::FileUtils::Entry_::SYSCASE: untyped + +Bundler::FileUtils::Entry_::S_IF_DOOR: untyped + +module Bundler::FileUtils::LowMethods +end + +# This module has all methods of +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html) +# module, but never changes files/directories. This equates to passing the +# `:noop` flag to methods in +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html). +module Bundler::FileUtils::NoWrite + include ::Bundler::FileUtils::LowMethods + + include ::Bundler::FileUtils + + include ::Bundler::FileUtils::StreamUtils_ + + extend ::Bundler::FileUtils::NoWrite + + extend ::Bundler::FileUtils::LowMethods + + extend ::Bundler::FileUtils + + extend ::Bundler::FileUtils::StreamUtils_ + + def self.cd: (*untyped _) -> untyped + + def self.chdir: (*untyped _) -> untyped + + def self.chmod: (*untyped args, **untyped options) -> untyped + + def self.chmod_R: (*untyped args, **untyped options) -> untyped + + def self.chown: (*untyped args, **untyped options) -> untyped + + def self.chown_R: (*untyped args, **untyped options) -> untyped + + def self.cmp: (*untyped _) -> untyped + + def self.compare_file: (*untyped _) -> untyped + + def self.compare_stream: (*untyped _) -> untyped + + def self.copy: (*untyped args, **untyped options) -> untyped + + def self.copy_entry: (*untyped _) -> untyped + + def self.copy_file: (*untyped _) -> untyped + + def self.copy_stream: (*untyped _) -> untyped + + def self.cp: (*untyped args, **untyped options) -> untyped + + def self.cp_r: (*untyped args, **untyped options) -> untyped + + def self.getwd: (*untyped _) -> untyped + + def self.identical?: (*untyped _) -> untyped + + def self.install: (*untyped args, **untyped options) -> untyped + + def self.link: (*untyped args, **untyped options) -> untyped + + def self.ln: (*untyped args, **untyped options) -> untyped + + def self.ln_s: (*untyped args, **untyped options) -> untyped + + def self.ln_sf: (*untyped args, **untyped options) -> untyped + + def self.makedirs: (*untyped args, **untyped options) -> untyped + + def self.mkdir: (*untyped args, **untyped options) -> untyped + + def self.mkdir_p: (*untyped args, **untyped options) -> untyped + + def self.mkpath: (*untyped args, **untyped options) -> untyped + + def self.move: (*untyped args, **untyped options) -> untyped + + def self.mv: (*untyped args, **untyped options) -> untyped + + def self.pwd: (*untyped _) -> untyped + + def self.remove: (*untyped args, **untyped options) -> untyped + + def self.remove_dir: (*untyped _) -> untyped + + def self.remove_entry: (*untyped _) -> untyped + + def self.remove_entry_secure: (*untyped _) -> untyped + + def self.remove_file: (*untyped _) -> untyped + + def self.rm: (*untyped args, **untyped options) -> untyped + + def self.rm_f: (*untyped args, **untyped options) -> untyped + + def self.rm_r: (*untyped args, **untyped options) -> untyped + + def self.rm_rf: (*untyped args, **untyped options) -> untyped + + def self.rmdir: (*untyped args, **untyped options) -> untyped + + def self.rmtree: (*untyped args, **untyped options) -> untyped + + def self.safe_unlink: (*untyped args, **untyped options) -> untyped + + def self.symlink: (*untyped args, **untyped options) -> untyped + + def self.touch: (*untyped args, **untyped options) -> untyped + + def self.uptodate?: (*untyped _) -> untyped +end + +module Bundler::FileUtils::StreamUtils_ +end + +# This module has all methods of +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html) +# module, but it outputs messages before acting. This equates to passing the +# `:verbose` flag to methods in +# [`Bundler::FileUtils`](https://docs.ruby-lang.org/en/2.7.0/Bundler/FileUtils.html). +module Bundler::FileUtils::Verbose + include ::Bundler::FileUtils + + include ::Bundler::FileUtils::StreamUtils_ + + extend ::Bundler::FileUtils::Verbose + + extend ::Bundler::FileUtils + + extend ::Bundler::FileUtils::StreamUtils_ + + def self.cd: (*untyped args, **untyped options) -> untyped + + def self.chdir: (*untyped args, **untyped options) -> untyped + + def self.chmod: (*untyped args, **untyped options) -> untyped + + def self.chmod_R: (*untyped args, **untyped options) -> untyped + + def self.chown: (*untyped args, **untyped options) -> untyped + + def self.chown_R: (*untyped args, **untyped options) -> untyped + + def self.cmp: (untyped a, untyped b) -> untyped + + def self.compare_file: (untyped a, untyped b) -> untyped + + def self.compare_stream: (untyped a, untyped b) -> untyped + + def self.copy: (*untyped args, **untyped options) -> untyped + + def self.copy_entry: (untyped src, untyped dest, ?untyped preserve, ?untyped dereference_root, ?untyped remove_destination) -> untyped + + def self.copy_file: (untyped src, untyped dest, ?untyped preserve, ?untyped dereference) -> untyped + + def self.copy_stream: (untyped src, untyped dest) -> untyped + + def self.cp: (*untyped args, **untyped options) -> untyped + + def self.cp_r: (*untyped args, **untyped options) -> untyped + + def self.getwd: () -> untyped + + def self.identical?: (untyped a, untyped b) -> untyped + + def self.install: (*untyped args, **untyped options) -> untyped + + def self.link: (*untyped args, **untyped options) -> untyped + + def self.ln: (*untyped args, **untyped options) -> untyped + + def self.ln_s: (*untyped args, **untyped options) -> untyped + + def self.ln_sf: (*untyped args, **untyped options) -> untyped + + def self.makedirs: (*untyped args, **untyped options) -> untyped + + def self.mkdir: (*untyped args, **untyped options) -> untyped + + def self.mkdir_p: (*untyped args, **untyped options) -> untyped + + def self.mkpath: (*untyped args, **untyped options) -> untyped + + def self.move: (*untyped args, **untyped options) -> untyped + + def self.mv: (*untyped args, **untyped options) -> untyped + + def self.pwd: () -> untyped + + def self.remove: (*untyped args, **untyped options) -> untyped + + def self.remove_dir: (untyped path, ?untyped force) -> untyped + + def self.remove_entry: (untyped path, ?untyped force) -> untyped + + def self.remove_entry_secure: (untyped path, ?untyped force) -> untyped + + def self.remove_file: (untyped path, ?untyped force) -> untyped + + def self.rm: (*untyped args, **untyped options) -> untyped + + def self.rm_f: (*untyped args, **untyped options) -> untyped + + def self.rm_r: (*untyped args, **untyped options) -> untyped + + def self.rm_rf: (*untyped args, **untyped options) -> untyped + + def self.rmdir: (*untyped args, **untyped options) -> untyped + + def self.rmtree: (*untyped args, **untyped options) -> untyped + + def self.safe_unlink: (*untyped args, **untyped options) -> untyped + + def self.symlink: (*untyped args, **untyped options) -> untyped + + def self.touch: (*untyped args, **untyped options) -> untyped + + def self.uptodate?: (untyped new, untyped old_list) -> untyped +end + +class Bundler::GemHelper + def allowed_push_host: () -> untyped + + def already_tagged?: () -> untyped + + def base: () -> untyped + + def build_checksum: (?untyped built_gem_path) -> untyped + + def build_gem: () -> untyped + + def built_gem_path: () -> untyped + + def clean?: () -> untyped + + def committed?: () -> untyped + + def current_branch: () -> untyped + + def default_remote: () -> untyped + + def gem_command: () -> untyped + + def gem_key: () -> untyped + + def gem_push?: () -> untyped + + def gem_push_host: () -> untyped + + def gemspec: () -> untyped + + def git_push: (?untyped remote) -> untyped + + def guard_clean: () -> untyped + + def initialize: (?untyped base, ?untyped name) -> void + + def install: () -> untyped + + def install_gem: (?untyped built_gem_path, ?untyped local) -> untyped + + def name: () -> untyped + + def rubygem_push: (untyped path) -> untyped + + def sh: (untyped cmd) { () -> untyped } -> untyped + + def sh_with_input: (untyped cmd) -> untyped + + def sh_with_status: (untyped cmd) { () -> untyped } -> untyped + + def spec_path: () -> untyped + + def tag_prefix=: (untyped tag_prefix) -> untyped + + def tag_version: () -> untyped + + def version: () -> untyped + + def version_tag: () -> untyped + + def self.instance: () -> untyped + + def self.instance=: (untyped instance) -> untyped + + def self.install_tasks: (?untyped opts) -> untyped + + def self.tag_prefix=: (untyped tag_prefix) -> untyped + + def self.gemspec: () { () -> untyped } -> untyped +end + +module Bundler::GemHelpers + def self.generic: (untyped p) -> untyped + + def self.generic_local_platform: () -> untyped + + def self.platform_specificity_match: (untyped spec_platform, untyped user_platform) -> untyped + + def self.select_best_platform_match: (untyped specs, untyped platform) -> untyped +end + +Bundler::GemHelpers::GENERICS: untyped + +Bundler::GemHelpers::GENERIC_CACHE: untyped + +class Bundler::GemHelpers::PlatformMatch < Struct + def <=>: (untyped other) -> untyped + + def cpu_match: () -> untyped + + def cpu_match=: (untyped _) -> untyped + + def os_match: () -> untyped + + def os_match=: (untyped _) -> untyped + + def platform_version_match: () -> untyped + + def platform_version_match=: (untyped _) -> untyped + + def self.[]: (*untyped _) -> untyped + + def self.cpu_match: (untyped spec_platform, untyped user_platform) -> untyped + + def self.members: () -> untyped + + def self.new: (*untyped _) -> untyped + + def self.os_match: (untyped spec_platform, untyped user_platform) -> untyped + + def self.platform_version_match: (untyped spec_platform, untyped user_platform) -> untyped +end + +Bundler::GemHelpers::PlatformMatch::Elem: untyped + +Bundler::GemHelpers::PlatformMatch::EXACT_MATCH: untyped + +Bundler::GemHelpers::PlatformMatch::WORST_MATCH: untyped + +class Bundler::GemNotFound < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::GemRequireError < Bundler::BundlerError + def initialize: (untyped orig_exception, untyped msg) -> void + + def orig_exception: () -> untyped + + def status_code: () -> untyped +end + +class Bundler::GemfileError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::GemfileEvalError < Bundler::GemfileError +end + +class Bundler::GemfileLockNotFound < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::GemfileNotFound < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::GemspecError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::GenericSystemCallError < Bundler::BundlerError + def initialize: (untyped underlying_error, untyped message) -> void + + def status_code: () -> untyped + + def underlying_error: () -> untyped +end + +class Bundler::GitError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::HTTPError < Bundler::BundlerError + def filter_uri: (untyped uri) -> untyped + + def status_code: () -> untyped +end + +# Handles all the fetching with the rubygems server +class Bundler::Fetcher +end + +# This error is raised if HTTP authentication is required, but not provided. +class Bundler::Fetcher::AuthenticationRequiredError < Bundler::HTTPError +end + +# This error is raised if HTTP authentication is provided, but incorrect. +class Bundler::Fetcher::BadAuthenticationError < Bundler::HTTPError +end + +# This is the error raised if +# [`OpenSSL`](https://docs.ruby-lang.org/en/2.7.0/OpenSSL.html) fails the cert +# verification +class Bundler::Fetcher::CertificateFailureError < Bundler::HTTPError +end + +# This error is raised if the API returns a 413 (only printed in verbose) +class Bundler::Fetcher::FallbackError < Bundler::HTTPError +end + +# This error is raised when it looks like the network is down +class Bundler::Fetcher::NetworkDownError < Bundler::HTTPError +end + +# This is the error raised when a source is HTTPS and +# [`OpenSSL`](https://docs.ruby-lang.org/en/2.7.0/OpenSSL.html) didn't load +class Bundler::Fetcher::SSLError < Bundler::HTTPError +end + +class Bundler::Index[out Elem] + include ::Enumerable + + def <<: (untyped spec) -> untyped + + def ==: (untyped other) -> untyped + + def []: (untyped query, ?untyped base) -> untyped + + def add_source: (untyped index) -> untyped + + def all_specs: () -> untyped + + def dependencies_eql?: (untyped spec, untyped other_spec) -> untyped + + def dependency_names: () -> untyped + + def each: () { () -> untyped } -> untyped + + def empty?: () -> untyped + + def initialize: () -> void + + def inspect: () -> untyped + + def local_search: (untyped query, ?untyped base) -> untyped + + def search: (untyped query, ?untyped base) -> untyped + + def search_all: (untyped name) -> untyped + + def size: () -> untyped + + def sort_specs: (untyped specs) -> untyped + + def sources: () -> untyped + + def spec_names: () -> untyped + + def specs: () -> untyped + + # returns a list of the dependencies + def unmet_dependency_names: () -> untyped + + def unsorted_search: (untyped query, untyped base) -> untyped + + def use: (untyped other, ?untyped override_dupes) -> untyped + + def self.build: () -> untyped + + def self.sort_specs: (untyped specs) -> untyped +end + +Bundler::Index::EMPTY_SEARCH: untyped + +Bundler::Index::NULL: untyped + +Bundler::Index::RUBY: untyped + +class Bundler::InstallError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::InstallHookError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::InvalidOption < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::LazySpecification + include ::Bundler::MatchPlatform + + include ::Bundler::GemHelpers + + def ==: (untyped other) -> untyped + + def __materialize__: () -> untyped + + def dependencies: () -> untyped + + def full_name: () -> untyped + + def git_version: () -> untyped + + def identifier: () -> untyped + + def initialize: (untyped name, untyped version, untyped platform, ?untyped source) -> void + + def name: () -> String + + def platform: () -> untyped + + def remote: () -> untyped + + def remote=: (untyped remote) -> untyped + + def respond_to?: (*untyped args) -> untyped + + def satisfies?: (untyped dependency) -> untyped + + def source: () -> untyped + + def source=: (untyped source) -> untyped + + def to_lock: () -> untyped + + def to_s: () -> untyped + + def version: () -> String +end + +class Bundler::LazySpecification::Identifier < Struct + include ::Comparable + + extend ::T::Generic + + def <=>: (untyped other) -> untyped + + def dependencies: () -> untyped + + def dependencies=: (untyped _) -> untyped + + def name: () -> untyped + + def name=: (untyped _) -> untyped + + def platform: () -> untyped + + def platform=: (untyped _) -> untyped + + def platform_string: () -> untyped + + def source: () -> untyped + + def source=: (untyped _) -> untyped + + def version: () -> untyped + + def version=: (untyped _) -> untyped + + def self.[]: (*untyped _) -> untyped + + def self.members: () -> untyped + + def self.new: (*untyped _) -> untyped +end + +Bundler::LazySpecification::Identifier::Elem: untyped + +class Bundler::LockfileError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::LockfileParser + def bundler_version: () -> untyped + + def dependencies: () -> Hash[String, Bundler::Dependency] + + def initialize: (untyped lockfile) -> void + + def platforms: () -> untyped + + def ruby_version: () -> untyped + + def sources: () -> untyped + + def specs: () -> ::Array[::Bundler::LazySpecification] + + def warn_for_outdated_bundler_version: () -> untyped + + def self.sections_in_lockfile: (untyped lockfile_contents) -> untyped + + def self.sections_to_ignore: (?untyped base_version) -> untyped + + def self.unknown_sections_in_lockfile: (untyped lockfile_contents) -> untyped +end + +Bundler::LockfileParser::BUNDLED: untyped + +Bundler::LockfileParser::DEPENDENCIES: untyped + +Bundler::LockfileParser::ENVIRONMENT_VERSION_SECTIONS: untyped + +Bundler::LockfileParser::GEM: untyped + +Bundler::LockfileParser::GIT: untyped + +Bundler::LockfileParser::KNOWN_SECTIONS: untyped + +Bundler::LockfileParser::NAME_VERSION: untyped + +Bundler::LockfileParser::OPTIONS: untyped + +Bundler::LockfileParser::PATH: untyped + +Bundler::LockfileParser::PLATFORMS: untyped + +Bundler::LockfileParser::PLUGIN: untyped + +Bundler::LockfileParser::RUBY: untyped + +Bundler::LockfileParser::SECTIONS_BY_VERSION_INTRODUCED: untyped + +Bundler::LockfileParser::SOURCE: untyped + +Bundler::LockfileParser::SPECS: untyped + +Bundler::LockfileParser::TYPES: untyped + +class Bundler::MarshalError < StandardError +end + +module Bundler::MatchPlatform + include ::Bundler::GemHelpers + + def match_platform: (untyped p) -> untyped + + def self.platforms_match?: (untyped gemspec_platform, untyped local_platform) -> untyped +end + +# [`Bundler::Molinillo`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo.html) +# is a generic dependency resolution algorithm. +module Bundler::Molinillo +end + +Bundler::Molinillo::VERSION: untyped + +# An error caused by attempting to fulfil a dependency that was circular +# +# @note This exception will be thrown iff a {Vertex} is added to a +# +# ``` +# {DependencyGraph} that has a {DependencyGraph::Vertex#path_to?} an +# existing {DependencyGraph::Vertex} +# ``` +class Bundler::Molinillo::CircularDependencyError < Bundler::Molinillo::ResolverError + # [`Set`](https://docs.ruby-lang.org/en/2.7.0/Set.html) + # : the dependencies responsible for causing the error + def dependencies: () -> untyped + + def initialize: (untyped vertices) -> void +end + +# Hacks needed for old Ruby versions. +module Bundler::Molinillo::Compatibility + def self.flat_map: (untyped enum) { () -> untyped } -> untyped +end + +# @!visibility private +module Bundler::Molinillo::Delegates +end + +# [`Delegates`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo/Delegates.html) +# all {Bundler::Molinillo::ResolutionState} methods to a `#state` property. +module Bundler::Molinillo::Delegates::ResolutionState + # (see Bundler::Molinillo::ResolutionState#activated) + def activated: () -> untyped + + # (see Bundler::Molinillo::ResolutionState#conflicts) + def conflicts: () -> untyped + + # (see Bundler::Molinillo::ResolutionState#depth) + def depth: () -> untyped + + # (see Bundler::Molinillo::ResolutionState#name) + def name: () -> untyped + + # (see Bundler::Molinillo::ResolutionState#possibilities) + def possibilities: () -> untyped + + # (see Bundler::Molinillo::ResolutionState#requirement) + def requirement: () -> untyped + + # (see Bundler::Molinillo::ResolutionState#requirements) + def requirements: () -> untyped + + # (see Bundler::Molinillo::ResolutionState#unused\_unwind\_options) + def unused_unwind_options: () -> untyped +end + +# [`Delegates`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo/Delegates.html) +# all {Bundler::Molinillo::SpecificationProvider} methods to a +# `#specification\_provider` property. +module Bundler::Molinillo::Delegates::SpecificationProvider + def allow_missing?: (untyped dependency) -> untyped + + def dependencies_for: (untyped specification) -> untyped + + def name_for: (untyped dependency) -> untyped + + # (see + # [`Bundler::Molinillo::SpecificationProvider#name_for_explicit_dependency_source`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo/SpecificationProvider.html#method-i-name_for_explicit_dependency_source)) + def name_for_explicit_dependency_source: () -> untyped + + # (see + # [`Bundler::Molinillo::SpecificationProvider#name_for_locking_dependency_source`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo/SpecificationProvider.html#method-i-name_for_locking_dependency_source)) + def name_for_locking_dependency_source: () -> untyped + + def requirement_satisfied_by?: (untyped requirement, untyped activated, untyped spec) -> untyped + + def search_for: (untyped dependency) -> untyped + + def sort_dependencies: (untyped dependencies, untyped activated, untyped conflicts) -> untyped +end + +# A directed acyclic graph that is tuned to hold named dependencies +class Bundler::Molinillo::DependencyGraph[out Elem] + include ::TSort + + include ::Enumerable + + def ==: (untyped other) -> untyped + + def add_child_vertex: (untyped name, untyped payload, untyped parent_names, untyped requirement) -> untyped + + def add_edge: (untyped origin, untyped destination, untyped requirement) -> untyped + + def add_vertex: (untyped name, untyped payload, ?untyped root) -> untyped + + def delete_edge: (untyped edge) -> untyped + + def detach_vertex_named: (untyped name) -> untyped + + def each: () { () -> untyped } -> untyped + + def initialize: () -> void + + # @return [String] a string suitable for debugging + def inspect: () -> untyped + + # @return [Log] the op log for this graph + def log: () -> untyped + + def rewind_to: (untyped tag) -> untyped + + def root_vertex_named: (untyped name) -> untyped + + def set_payload: (untyped name, untyped payload) -> untyped + + def tag: (untyped tag) -> untyped + + def to_dot: (?untyped options) -> untyped + + def tsort_each_child: (untyped vertex) { () -> untyped } -> untyped + + # @!visibility private + # + # Alias for: + # [`each`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo/DependencyGraph.html#method-i-each) + def tsort_each_node: () -> untyped + + def vertex_named: (untyped name) -> untyped + + # @return [{String => Vertex}] the vertices of the dependency graph, keyed + # + # ``` + # by {Vertex#name} + # ``` + def vertices: () -> untyped + + def self.tsort: (untyped vertices) -> untyped +end + +# An action that modifies a {DependencyGraph} that is reversible. @abstract +class Bundler::Molinillo::DependencyGraph::Action + def down: (untyped graph) -> untyped + + # @return [Action,Nil] The next action + def next: () -> untyped + + def next=: (untyped _) -> untyped + + # @return [Action,Nil] The previous action + def previous: () -> untyped + + def previous=: (untyped previous) -> untyped + + def up: (untyped graph) -> untyped + + # @return [Symbol] The name of the action. + def self.action_name: () -> untyped +end + +# @!visibility private (see +# [`DependencyGraph#add_edge_no_circular`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo/DependencyGraph.html#method-i-add_edge_no_circular)) +class Bundler::Molinillo::DependencyGraph::AddEdgeNoCircular < Bundler::Molinillo::DependencyGraph::Action + # @return [String] the name of the destination of the edge + def destination: () -> untyped + + def down: (untyped graph) -> untyped + + def initialize: (untyped origin, untyped destination, untyped requirement) -> void + + def make_edge: (untyped graph) -> untyped + + # @return [String] the name of the origin of the edge + def origin: () -> untyped + + # @return [Object] the requirement that the edge represents + def requirement: () -> untyped + + def up: (untyped graph) -> untyped + + # (see Action.action\_name) + def self.action_name: () -> untyped +end + +class Bundler::Molinillo::DependencyGraph::AddVertex < Bundler::Molinillo::DependencyGraph::Action + def down: (untyped graph) -> untyped + + def initialize: (untyped name, untyped payload, untyped root) -> void + + def name: () -> untyped + + def payload: () -> untyped + + def root: () -> untyped + + def up: (untyped graph) -> untyped + + def self.action_name: () -> untyped +end + +# @!visibility private (see +# [`DependencyGraph#delete_edge`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo/DependencyGraph.html#method-i-delete_edge)) +class Bundler::Molinillo::DependencyGraph::DeleteEdge < Bundler::Molinillo::DependencyGraph::Action + # @return [String] the name of the destination of the edge + def destination_name: () -> untyped + + def down: (untyped graph) -> untyped + + def initialize: (untyped origin_name, untyped destination_name, untyped requirement) -> void + + def make_edge: (untyped graph) -> untyped + + # @return [String] the name of the origin of the edge + def origin_name: () -> untyped + + # @return [Object] the requirement that the edge represents + def requirement: () -> untyped + + def up: (untyped graph) -> untyped + + # (see Action.action\_name) + def self.action_name: () -> untyped +end + +# @!visibility private @see +# [`DependencyGraph#detach_vertex_named`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo/DependencyGraph.html#method-i-detach_vertex_named) +class Bundler::Molinillo::DependencyGraph::DetachVertexNamed < Bundler::Molinillo::DependencyGraph::Action + def down: (untyped graph) -> untyped + + def initialize: (untyped name) -> void + + # @return [String] the name of the vertex to detach + def name: () -> untyped + + def up: (untyped graph) -> untyped + + # (see Action#name) + def self.action_name: () -> untyped +end + +# A directed edge of a {DependencyGraph} @attr [Vertex] origin The origin of the +# directed edge @attr [Vertex] destination The destination of the directed edge +# @attr [Object] requirement The requirement the directed edge represents +class Bundler::Molinillo::DependencyGraph::Edge < Struct + def destination: () -> untyped + + def destination=: (untyped _) -> untyped + + def origin: () -> untyped + + def origin=: (untyped _) -> untyped + + def requirement: () -> untyped + + def requirement=: (untyped _) -> untyped + + def self.[]: (*untyped _) -> untyped + + def self.members: () -> untyped + + def self.new: (*untyped _) -> untyped +end + +Bundler::Molinillo::DependencyGraph::Edge::Elem: untyped + +# A log for dependency graph actions +class Bundler::Molinillo::DependencyGraph::Log + extend ::Enumerable + + def add_edge_no_circular: (untyped graph, untyped origin, untyped destination, untyped requirement) -> untyped + + def add_vertex: (untyped graph, untyped name, untyped payload, untyped root) -> untyped + + def delete_edge: (untyped graph, untyped origin_name, untyped destination_name, untyped requirement) -> untyped + + def detach_vertex_named: (untyped graph, untyped name) -> untyped + + def each: () { () -> untyped } -> untyped + + def initialize: () -> void + + def pop!: (untyped graph) -> untyped + + # @!visibility private Enumerates each action in the log in reverse order + # @yield [Action] + def reverse_each: () -> untyped + + def rewind_to: (untyped graph, untyped tag) -> untyped + + def set_payload: (untyped graph, untyped name, untyped payload) -> untyped + + def tag: (untyped graph, untyped tag) -> untyped +end + +Bundler::Molinillo::DependencyGraph::Log::Elem: untyped + +class Bundler::Molinillo::DependencyGraph::SetPayload < Bundler::Molinillo::DependencyGraph::Action + def down: (untyped graph) -> untyped + + def initialize: (untyped name, untyped payload) -> void + + def name: () -> untyped + + def payload: () -> untyped + + def up: (untyped graph) -> untyped + + def self.action_name: () -> untyped +end + +# @!visibility private @see +# [`DependencyGraph#tag`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo/DependencyGraph.html#method-i-tag) +class Bundler::Molinillo::DependencyGraph::Tag < Bundler::Molinillo::DependencyGraph::Action + def down: (untyped _graph) -> untyped + + def initialize: (untyped tag) -> void + + # @return [Object] An opaque tag + def tag: () -> untyped + + def up: (untyped _graph) -> untyped + + # (see Action.action\_name) + def self.action_name: () -> untyped +end + +# A vertex in a {DependencyGraph} that encapsulates a {#name} and a {#payload} +class Bundler::Molinillo::DependencyGraph::Vertex + def ==: (untyped other) -> untyped + + def _path_to?: (untyped other, ?untyped visited) -> untyped + + def ancestor?: (untyped other) -> untyped + + def descendent?: (untyped other) -> untyped + + def eql?: (untyped other) -> untyped + + # @return [Array] the explicit requirements that required + # + # ```ruby + # this vertex + # ``` + def explicit_requirements: () -> untyped + + # @return [Fixnum] a hash for the vertex based upon its {#name} + def hash: () -> untyped + + # @return [Array] the edges of {#graph} that have `self` as their + # + # ``` + # {Edge#destination} + # ``` + def incoming_edges: () -> untyped + + def incoming_edges=: (untyped incoming_edges) -> untyped + + def initialize: (untyped name, untyped payload) -> void + + # @return [String] a string suitable for debugging + def inspect: () -> untyped + + def is_reachable_from?: (untyped other) -> untyped + + # @return [String] the name of the vertex + def name: () -> untyped + + def name=: (untyped name) -> untyped + + # @return [Array] the edges of {#graph} that have `self` as their + # + # ``` + # {Edge#origin} + # ``` + def outgoing_edges: () -> untyped + + def outgoing_edges=: (untyped outgoing_edges) -> untyped + + def path_to?: (untyped other) -> untyped + + # @return [Object] the payload the vertex holds + def payload: () -> untyped + + def payload=: (untyped payload) -> untyped + + # @return [Array] the vertices of {#graph} that have an edge with + # + # ``` + # `self` as their {Edge#destination} + # ``` + def predecessors: () -> untyped + + # @return [Set] the vertices of {#graph} where `self` is a + # + # ``` + # {#descendent?} + # ``` + def recursive_predecessors: () -> untyped + + # @return [Set] the vertices of {#graph} where `self` is an + # + # ``` + # {#ancestor?} + # ``` + def recursive_successors: () -> untyped + + # @return [Array] all of the requirements that required + # + # ```ruby + # this vertex + # ``` + def requirements: () -> untyped + + # @return [Boolean] whether the vertex is considered a root vertex + def root: () -> untyped + + def root=: (untyped root) -> untyped + + # @return [Boolean] whether the vertex is considered a root vertex + def root?: () -> untyped + + def shallow_eql?: (untyped other) -> untyped + + # @return [Array] the vertices of {#graph} that have an edge with + # + # ``` + # `self` as their {Edge#origin} + # ``` + def successors: () -> untyped +end + +# A state that encapsulates a set of {#requirements} with an {Array} of +# possibilities +class Bundler::Molinillo::DependencyState < Bundler::Molinillo::ResolutionState + # Removes a possibility from `self` @return [PossibilityState] a state with a + # single possibility, + # + # ```ruby + # the possibility that was removed from `self` + # ``` + def pop_possibility_state: () -> untyped +end + +Bundler::Molinillo::DependencyState::Elem: untyped + +# An error caused by searching for a dependency that is completely unknown, i.e. +# has no versions available whatsoever. +class Bundler::Molinillo::NoSuchDependencyError < Bundler::Molinillo::ResolverError + # @return [Object] the dependency that could not be found + def dependency: () -> untyped + + def dependency=: (untyped dependency) -> untyped + + def initialize: (untyped dependency, ?untyped required_by) -> void + + # The error message for the missing dependency, including the specifications + # that had this dependency. + def message: () -> untyped + + # @return [Array] the specifications that depended upon {#dependency} + def required_by: () -> untyped + + def required_by=: (untyped required_by) -> untyped +end + +# A state that encapsulates a single possibility to fulfill the given +# {#requirement} +class Bundler::Molinillo::PossibilityState < Bundler::Molinillo::ResolutionState +end + +Bundler::Molinillo::PossibilityState::Elem: untyped + +class Bundler::Molinillo::ResolutionState < Struct + def activated: () -> untyped + + def activated=: (untyped _) -> untyped + + def conflicts: () -> untyped + + def conflicts=: (untyped _) -> untyped + + def depth: () -> untyped + + def depth=: (untyped _) -> untyped + + def name: () -> untyped + + def name=: (untyped _) -> untyped + + def possibilities: () -> untyped + + def possibilities=: (untyped _) -> untyped + + def requirement: () -> untyped + + def requirement=: (untyped _) -> untyped + + def requirements: () -> untyped + + def requirements=: (untyped _) -> untyped + + def unused_unwind_options: () -> untyped + + def unused_unwind_options=: (untyped _) -> untyped + + def self.[]: (*untyped _) -> untyped + + # Returns an empty resolution state @return [ResolutionState] an empty state + def self.empty: () -> untyped + + def self.members: () -> untyped + + def self.new: (*untyped _) -> untyped +end + +Bundler::Molinillo::ResolutionState::Elem: untyped + +# This class encapsulates a dependency resolver. The resolver is responsible for +# determining which set of dependencies to activate, with feedback from the +# {#specification\_provider} +class Bundler::Molinillo::Resolver + def initialize: (untyped specification_provider, untyped resolver_ui) -> void + + def resolve: (untyped requested, ?untyped base) -> untyped + + # @return [UI] the UI module used to communicate back to the user + # + # ```ruby + # during the resolution process + # ``` + def resolver_ui: () -> untyped + + # @return [SpecificationProvider] the specification provider used + # + # ``` + # in the resolution process + # ``` + def specification_provider: () -> untyped +end + +# A specific resolution from a given {Resolver} +class Bundler::Molinillo::Resolver::Resolution + include ::Bundler::Molinillo::Delegates::SpecificationProvider + + include ::Bundler::Molinillo::Delegates::ResolutionState + + # @return [DependencyGraph] the base dependency graph to which + # + # ```ruby + # dependencies should be 'locked' + # ``` + def base: () -> untyped + + def initialize: (untyped specification_provider, untyped resolver_ui, untyped requested, untyped base) -> void + + def iteration_rate=: (untyped iteration_rate) -> untyped + + # @return [Array] the dependencies that were explicitly required + def original_requested: () -> untyped + + # Resolves the {#original\_requested} dependencies into a full dependency + # + # ```ruby + # graph + # ``` + # + # @raise [ResolverError] if successful resolution is impossible @return + # [DependencyGraph] the dependency graph of successfully resolved + # + # ```ruby + # dependencies + # ``` + def resolve: () -> untyped + + # @return [UI] the UI that knows how to communicate feedback about the + # + # ```ruby + # resolution process back to the user + # ``` + def resolver_ui: () -> untyped + + # @return [SpecificationProvider] the provider that knows about + # + # ``` + # dependencies, requirements, specifications, versions, etc. + # ``` + def specification_provider: () -> untyped + + def started_at=: (untyped started_at) -> untyped + + def states=: (untyped states) -> untyped +end + +class Bundler::Molinillo::Resolver::Resolution::Conflict < Struct + def activated_by_name: () -> untyped + + def activated_by_name=: (untyped _) -> untyped + + def existing: () -> untyped + + def existing=: (untyped _) -> untyped + + def locked_requirement: () -> untyped + + def locked_requirement=: (untyped _) -> untyped + + # @return [Object] a spec that was unable to be activated due to a conflict + def possibility: () -> untyped + + def possibility_set: () -> untyped + + def possibility_set=: (untyped _) -> untyped + + def requirement: () -> untyped + + def requirement=: (untyped _) -> untyped + + def requirement_trees: () -> untyped + + def requirement_trees=: (untyped _) -> untyped + + def requirements: () -> untyped + + def requirements=: (untyped _) -> untyped + + def underlying_error: () -> untyped + + def underlying_error=: (untyped _) -> untyped + + def self.[]: (*untyped _) -> untyped + + def self.members: () -> untyped + + def self.new: (*untyped _) -> untyped +end + +Bundler::Molinillo::Resolver::Resolution::Conflict::Elem: untyped + +class Bundler::Molinillo::Resolver::Resolution::PossibilitySet < Struct + def dependencies: () -> untyped + + def dependencies=: (untyped _) -> untyped + + # @return [Object] most up-to-date dependency in the possibility set + def latest_version: () -> untyped + + def possibilities: () -> untyped + + def possibilities=: (untyped _) -> untyped + + # [`String`](https://docs.ruby-lang.org/en/2.7.0/String.html) representation + # of the possibility set, for debugging + def to_s: () -> untyped + + def self.[]: (*untyped _) -> untyped + + def self.members: () -> untyped + + def self.new: (*untyped _) -> untyped +end + +Bundler::Molinillo::Resolver::Resolution::PossibilitySet::Elem: untyped + +class Bundler::Molinillo::Resolver::Resolution::UnwindDetails < Struct + include ::Comparable + + def <=>: (untyped other) -> untyped + + # @return [Array] array of all the requirements that led to the need for + # + # ```ruby + # this unwind + # ``` + def all_requirements: () -> untyped + + def conflicting_requirements: () -> untyped + + def conflicting_requirements=: (untyped _) -> untyped + + def requirement_tree: () -> untyped + + def requirement_tree=: (untyped _) -> untyped + + def requirement_trees: () -> untyped + + def requirement_trees=: (untyped _) -> untyped + + def requirements_unwound_to_instead: () -> untyped + + def requirements_unwound_to_instead=: (untyped _) -> untyped + + # @return [Integer] index of state requirement in reversed requirement tree + # + # ```ruby + # (the conflicting requirement itself will be at position 0) + # ``` + def reversed_requirement_tree_index: () -> untyped + + def state_index: () -> untyped + + def state_index=: (untyped _) -> untyped + + def state_requirement: () -> untyped + + def state_requirement=: (untyped _) -> untyped + + # @return [Array] array of sub-dependencies to avoid when choosing a + # + # ``` + # new possibility for the state we've unwound to. Only relevant for + # non-primary unwinds + # ``` + def sub_dependencies_to_avoid: () -> untyped + + # @return [Boolean] where the requirement of the state we're unwinding + # + # ``` + # to directly caused the conflict. Note: in this case, it is + # impossible for the state we're unwinding to to be a parent of + # any of the other conflicting requirements (or we would have + # circularity) + # ``` + def unwinding_to_primary_requirement?: () -> untyped + + def self.[]: (*untyped _) -> untyped + + def self.members: () -> untyped + + def self.new: (*untyped _) -> untyped +end + +Bundler::Molinillo::Resolver::Resolution::UnwindDetails::Elem: untyped + +# An error that occurred during the resolution process +class Bundler::Molinillo::ResolverError < StandardError +end + +# Provides information about specifications and dependencies to the resolver, +# allowing the {Resolver} class to remain generic while still providing power +# and flexibility. +# +# This module contains the methods that users of +# [`Bundler::Molinillo`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Molinillo.html) +# must to implement, using knowledge of their own model classes. +module Bundler::Molinillo::SpecificationProvider + def allow_missing?: (untyped dependency) -> untyped + + def dependencies_for: (untyped specification) -> untyped + + def name_for: (untyped dependency) -> untyped + + # @return [String] the name of the source of explicit dependencies, i.e. + # + # ``` + # those passed to {Resolver#resolve} directly. + # ``` + def name_for_explicit_dependency_source: () -> untyped + + # @return [String] the name of the source of 'locked' dependencies, i.e. + # + # ``` + # those passed to {Resolver#resolve} directly as the `base` + # ``` + def name_for_locking_dependency_source: () -> untyped + + def requirement_satisfied_by?: (untyped requirement, untyped activated, untyped spec) -> untyped + + def search_for: (untyped dependency) -> untyped + + def sort_dependencies: (untyped dependencies, untyped activated, untyped conflicts) -> untyped +end + +# Conveys information about the resolution process to a user. +module Bundler::Molinillo::UI + # Called after resolution ends (either successfully or with an error). By + # default, prints a newline. + # + # @return [void] + def after_resolution: () -> untyped + + # Called before resolution begins. + # + # @return [void] + def before_resolution: () -> untyped + + def debug: (?untyped depth) -> untyped + + # Whether or not debug messages should be printed. By default, whether or not + # the `MOLINILLO\_DEBUG` environment variable is set. + # + # @return [Boolean] + def debug?: () -> untyped + + # Called roughly every {#progress\_rate}, this method should convey progress + # to the user. + # + # @return [void] + def indicate_progress: () -> untyped + + # The {IO} object that should be used to print output. `STDOUT`, by default. + # + # @return [IO] + def output: () -> untyped + + # How often progress should be conveyed to the user via {#indicate\_progress}, + # in seconds. A third of a second, by default. + # + # @return [Float] + def progress_rate: () -> untyped +end + +# An error caused by conflicts in version +class Bundler::Molinillo::VersionConflict < Bundler::Molinillo::ResolverError + include ::Bundler::Molinillo::Delegates::SpecificationProvider + + # @return [{String => Resolution::Conflict}] the conflicts that caused + # + # ```ruby + # resolution to fail + # ``` + def conflicts: () -> untyped + + def initialize: (untyped conflicts, untyped specification_provider) -> void + + def message_with_trees: (?untyped opts) -> untyped + + # @return [SpecificationProvider] the specification provider used during + # + # ```ruby + # resolution + # ``` + def specification_provider: () -> untyped +end + +class Bundler::NoSpaceOnDeviceError < Bundler::PermissionError + def message: () -> untyped + + def status_code: () -> untyped +end + +class Bundler::OperationNotSupportedError < Bundler::PermissionError + def message: () -> untyped + + def status_code: () -> untyped +end + +class Bundler::PathError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::PermissionError < Bundler::BundlerError + def action: () -> untyped + + def initialize: (untyped path, ?untyped permission_type) -> void + + def message: () -> untyped + + def status_code: () -> untyped +end + +# This is the interfacing class represents the API that we intend to provide the +# plugins to use. +# +# For plugins to be independent of the +# [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) internals they +# shall limit their interactions to methods of this class only. This will save +# them from breaking when some internal change. +# +# Currently we are delegating the methods defined in +# [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) class to itself. +# So, this class acts as a buffer. +# +# If there is some change in the +# [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) class that is +# incompatible to its previous behavior or if otherwise desired, we can +# reimplement(or implement) the method to preserve compatibility. +# +# To use this, either the class can inherit this class or use it directly. For +# example of both types of use, refer the file `spec/plugins/command.rb` +# +# To use it without inheriting, you will have to create an object of this to use +# the functions (except for declaration functions like command, source, and +# hooks). +# Manages which plugins are installed and their sources. This also is supposed +# to map which plugin does what (currently the features are not implemented so +# this class is now a stub class). +# Handles the installation of plugin in appropriate directories. +# +# This class is supposed to be wrapper over the existing gem installation infra +# but currently it itself handles everything as the Source's subclasses (e.g. +# Source::RubyGems) are heavily dependent on the Gemfile. +# SourceList object to be used while parsing the Gemfile, setting the +# approptiate options to be used with Source classes for plugin installation +module Bundler::Plugin + def self.add_command: (untyped command, untyped cls) -> untyped + + def self.add_hook: (untyped event) { () -> untyped } -> untyped + + def self.add_source: (untyped source, untyped cls) -> untyped + + def self.cache: () -> untyped + + def self.command?: (untyped command) -> untyped + + def self.exec_command: (untyped command, untyped args) -> untyped + + def self.gemfile_install: (?untyped gemfile) { () -> untyped } -> untyped + + def self.global_root: () -> untyped + + def self.hook: (untyped event, *untyped args) { () -> untyped } -> untyped + + def self.index: () -> untyped + + def self.install: (untyped names, untyped options) -> untyped + + def self.installed?: (untyped plugin) -> untyped + + def self.local_root: () -> untyped + + def self.reset!: () -> untyped + + def self.root: () -> untyped + + def self.source: (untyped name) -> untyped + + def self.source?: (untyped name) -> untyped + + def self.source_from_lock: (untyped locked_opts) -> untyped +end + +Bundler::Plugin::PLUGIN_FILE_NAME: untyped + +class Bundler::Plugin::API + # The cache dir to be used by the plugins for storage + # + # @return [Pathname] path of the cache dir + def cache_dir: () -> untyped + + def method_missing: (untyped name, *untyped args) { () -> untyped } -> untyped + + def tmp: (*untyped names) -> untyped + + def self.command: (untyped command, ?untyped cls) -> untyped + + def self.hook: (untyped event) { () -> untyped } -> untyped + + def self.source: (untyped source, ?untyped cls) -> untyped +end + +# Dsl to parse the Gemfile looking for plugins to install +class Bundler::Plugin::DSL < Bundler::Dsl + def _gem: (untyped name, *untyped args) -> untyped + + # This lists the plugins that was added automatically and not specified by the + # user. + # + # When we encounter :type attribute with a source block, we add a plugin by + # name bundler-source- to list of plugins to be installed. + # + # These plugins are optional and are not installed when there is conflict with + # any other plugin. + def inferred_plugins: () -> untyped + + def plugin: (untyped name, *untyped args) -> untyped +end + +module Bundler::Plugin::Events +end + +class Bundler::Plugin::Index +end + +class Bundler::Plugin::MalformattedPlugin < Bundler::PluginError +end + +class Bundler::Plugin::UndefinedCommandError < Bundler::PluginError +end + +class Bundler::Plugin::UnknownSourceError < Bundler::PluginError +end + +class Bundler::Plugin::DSL::PluginGemfileError < Bundler::PluginError +end + +class Bundler::PluginError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::ProductionError < Bundler::BundlerError + def status_code: () -> untyped +end + +# Represents a lazily loaded gem specification, where the full specification is +# on the source server in rubygems' "quick" index. The proxy object is to be +# seeded with what we're given from the source's abbreviated index - the full +# specification will only be fetched when necessary. +class Bundler::RemoteSpecification + include ::Comparable + + include ::Bundler::MatchPlatform + + include ::Bundler::GemHelpers + + def <=>: (untyped other) -> untyped + + def __swap__: (untyped spec) -> untyped + + def dependencies: () -> untyped + + def dependencies=: (untyped dependencies) -> untyped + + # Needed before installs, since the arch matters then and quick specs don't + # bother to include the arch in the platform string + def fetch_platform: () -> untyped + + def full_name: () -> untyped + + def git_version: () -> untyped + + def initialize: (untyped name, untyped version, untyped platform, untyped spec_fetcher) -> void + + def name: () -> String + + def platform: () -> untyped + + def remote: () -> untyped + + def remote=: (untyped remote) -> untyped + + def respond_to?: (untyped method, ?untyped include_all) -> untyped + + # Create a delegate used for sorting. This strategy is copied from RubyGems + # 2.23 and ensures that Bundler's specifications can be compared and sorted + # with RubyGems' own specifications. + # + # @see #<=> @see + # [`Gem::Specification#sort_obj`](https://docs.ruby-lang.org/en/2.7.0/Gem/Specification.html#method-i-sort_obj) + # + # @return [Array] an object you can use to compare and sort this + # + # ```ruby + # specification against other specifications + # ``` + def sort_obj: () -> untyped + + def source: () -> untyped + + def source=: (untyped source) -> untyped + + def to_s: () -> untyped + + def version: () -> String +end + +class Bundler::Resolver + include ::Bundler::Molinillo::SpecificationProvider + + include ::Bundler::Molinillo::UI + + def after_resolution: () -> untyped + + def before_resolution: () -> untyped + + def debug: (?untyped depth) -> untyped + + def debug?: () -> untyped + + def dependencies_for: (untyped specification) -> untyped + + def index_for: (untyped dependency) -> untyped + + def indicate_progress: () -> untyped + + def initialize: (untyped index, untyped source_requirements, untyped base, untyped gem_version_promoter, untyped additional_base_requirements, untyped platforms) -> void + + def name_for: (untyped dependency) -> untyped + + def name_for_explicit_dependency_source: () -> untyped + + def name_for_locking_dependency_source: () -> untyped + + def relevant_sources_for_vertex: (untyped vertex) -> untyped + + def requirement_satisfied_by?: (untyped requirement, untyped activated, untyped spec) -> untyped + + def search_for: (untyped dependency) -> untyped + + def sort_dependencies: (untyped dependencies, untyped activated, untyped conflicts) -> untyped + + def start: (untyped requirements) -> untyped + + def self.platform_sort_key: (untyped platform) -> untyped + + def self.resolve: (untyped requirements, untyped index, ?untyped source_requirements, ?untyped base, ?untyped gem_version_promoter, ?untyped additional_base_requirements, ?untyped platforms) -> untyped + + def self.sort_platforms: (untyped platforms) -> untyped +end + +class Bundler::Resolver::SpecGroup + include ::Bundler::GemHelpers + + def ==: (untyped other) -> untyped + + def activate_platform!: (untyped platform) -> untyped + + def dependencies_for_activated_platforms: () -> untyped + + def eql?: (untyped other) -> untyped + + def for?: (untyped platform) -> untyped + + def hash: () -> untyped + + def ignores_bundler_dependencies: () -> untyped + + def ignores_bundler_dependencies=: (untyped ignores_bundler_dependencies) -> untyped + + def initialize: (untyped all_specs) -> void + + def name: () -> untyped + + def name=: (untyped name) -> untyped + + def source: () -> untyped + + def source=: (untyped source) -> untyped + + def to_s: () -> untyped + + def to_specs: () -> untyped + + def version: () -> untyped + + def version=: (untyped version) -> untyped +end + +module Bundler::RubyDsl + @ruby_version: untyped + + def ruby: (*::String ruby_version) -> void + + # Support the various file formats found in .ruby-version files. + # + # 3.2.2 + # ruby-3.2.2 + # + # Also supports .tool-versions files for asdf. Lines not starting with "ruby" are ignored. + # + # ruby 2.5.1 # comment is ignored + # ruby 2.5.1# close comment and extra spaces doesn't confuse + # + # Intentionally does not support `3.2.1@gemset` since rvm recommends using .ruby-gemset instead + # + # Loads the file relative to the dirname of the Gemfile itself. + def normalize_ruby_file: (::String filename) -> ::String +end + +class Bundler::RubyVersion + def ==: (untyped other) -> untyped + + def diff: (untyped other) -> untyped + + def engine: () -> untyped + + def engine_gem_version: () -> untyped + + def engine_versions: () -> untyped + + def exact?: () -> untyped + + def gem_version: () -> untyped + + def host: () -> untyped + + def initialize: (untyped versions, untyped patchlevel, untyped engine, untyped engine_version) -> void + + def patchlevel: () -> untyped + + def single_version_string: () -> untyped + + def to_gem_version_with_patchlevel: () -> untyped + + def to_s: (?untyped versions) -> untyped + + def versions: () -> untyped + + def versions_string: (untyped versions) -> untyped + + def self.from_string: (untyped string) -> untyped + + def self.system: () -> untyped +end + +Bundler::RubyVersion::PATTERN: untyped + +class Bundler::RubyVersionMismatch < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::RubygemsIntegration + # This backports base\_dir which replaces installation path RubyGems 1.8+ + def backport_base_dir: () -> untyped + + def backport_cache_file: () -> untyped + + # This backports the correct segment generation code from RubyGems 1.4+ by + # monkeypatching it into the method in RubyGems 1.3.6 and 1.3.7. + def backport_segment_generation: () -> untyped + + def backport_spec_file: () -> untyped + + # This backport fixes the marshaling of @segments. + def backport_yaml_initialize: () -> untyped + + def bin_path: (untyped gem, untyped bin, untyped ver) -> untyped + + def binstubs_call_gem?: () -> untyped + + def build: (untyped spec, ?untyped skip_validation) -> untyped + + def build_args: () -> untyped + + def build_args=: (untyped args) -> untyped + + def build_gem: (untyped gem_dir, untyped spec) -> untyped + + def clear_paths: () -> untyped + + def config_map: () -> untyped + + def configuration: () -> untyped + + def download_gem: (untyped spec, untyped uri, untyped path) -> untyped + + def ext_lock: () -> untyped + + def fetch_all_remote_specs: (untyped remote) -> untyped + + def fetch_prerelease_specs: () -> untyped + + def fetch_specs: (untyped all, untyped pre) { () -> untyped } -> untyped + + def gem_bindir: () -> untyped + + def gem_cache: () -> untyped + + def gem_dir: () -> untyped + + def gem_from_path: (untyped path, ?untyped policy) -> untyped + + def gem_path: () -> untyped + + def inflate: (untyped obj) -> untyped + + def initialize: () -> void + + def install_with_build_args: (untyped args) -> untyped + + def load_path_insert_index: () -> untyped + + def load_plugin_files: (untyped files) -> untyped + + def load_plugins: () -> untyped + + def loaded_gem_paths: () -> untyped + + def loaded_specs: (untyped name) -> untyped + + def mark_loaded: (untyped spec) -> untyped + + def marshal_spec_dir: () -> untyped + + def method_visibility: (untyped klass, untyped method) -> untyped + + def path: (untyped obj) -> untyped + + def path_separator: () -> untyped + + def platforms: () -> untyped + + def post_reset_hooks: () -> untyped + + def preserve_paths: () -> untyped + + def provides?: (untyped req_str) -> untyped + + def read_binary: (untyped path) -> untyped + + def redefine_method: (untyped klass, untyped method, ?untyped unbound_method) { () -> untyped } -> untyped + + def replace_bin_path: (untyped specs, untyped specs_by_name) -> untyped + + def replace_entrypoints: (untyped specs) -> untyped + + def replace_gem: (untyped specs, untyped specs_by_name) -> untyped + + # Because [`Bundler`](https://docs.ruby-lang.org/en/2.6.0/Bundler.html) has a + # static view of what specs are available, we don't refresh, so stub it out. + def replace_refresh: () -> untyped + + def repository_subdirectories: () -> untyped + + def reset: () -> untyped + + def reverse_rubygems_kernel_mixin: () -> untyped + + def ruby_engine: () -> untyped + + def security_policies: () -> untyped + + def security_policy_keys: () -> untyped + + def set_installed_by_version: (untyped spec, ?untyped installed_by_version) -> untyped + + def sources: () -> untyped + + def sources=: (untyped val) -> untyped + + def spec_cache_dirs: () -> untyped + + def spec_default_gem?: (untyped spec) -> untyped + + def spec_extension_dir: (untyped spec) -> untyped + + def spec_from_gem: (untyped path, ?untyped policy) -> untyped + + def spec_matches_for_glob: (untyped spec, untyped glob) -> untyped + + def spec_missing_extensions?: (untyped spec, ?untyped default) -> untyped + + def stub_set_spec: (untyped stub, untyped spec) -> untyped + + def stub_source_index: (untyped specs) -> untyped + + def stubs_provide_full_functionality?: () -> untyped + + def suffix_pattern: () -> untyped + + def ui=: (untyped obj) -> untyped + + def undo_replacements: () -> untyped + + def user_home: () -> untyped + + def validate: (untyped spec) -> untyped + + def version: () -> untyped + + def with_build_args: (untyped args) -> untyped + + def self.provides?: (untyped req_str) -> untyped + + def self.version: () -> untyped +end + +Bundler::RubygemsIntegration::EXT_LOCK: untyped + +# RubyGems 1.8.0 to 1.8.4 +class Bundler::RubygemsIntegration::AlmostModern < Bundler::RubygemsIntegration::Modern + # RubyGems [>= 1.8.0, < 1.8.5] has a bug that changes + # [`Gem.dir`](https://docs.ruby-lang.org/en/2.6.0/Gem.html#method-c-dir) + # whenever you call + # [`Gem::Installer#install`](https://docs.ruby-lang.org/en/2.6.0/Installer.html#method-i-install) + # with an :install\_dir set. We have to change it back for our sudo mode to + # work. + def preserve_paths: () -> untyped +end + +# RubyGems versions 1.3.6 and 1.3.7 +class Bundler::RubygemsIntegration::Ancient < Bundler::RubygemsIntegration::Legacy + def initialize: () -> void +end + +# RubyGems 2.0 +class Bundler::RubygemsIntegration::Future < Bundler::RubygemsIntegration + def all_specs: () -> untyped + + def build: (untyped spec, ?untyped skip_validation) -> untyped + + def download_gem: (untyped spec, untyped uri, untyped path) -> untyped + + def fetch_all_remote_specs: (untyped remote) -> untyped + + def fetch_specs: (untyped source, untyped remote, untyped name) -> untyped + + def find_name: (untyped name) -> untyped + + def gem_from_path: (untyped path, ?untyped policy) -> untyped + + def gem_remote_fetcher: () -> untyped + + def install_with_build_args: (untyped args) -> untyped + + def path_separator: () -> untyped + + def repository_subdirectories: () -> untyped + + def stub_rubygems: (untyped specs) -> untyped +end + +# RubyGems 1.4 through 1.6 +class Bundler::RubygemsIntegration::Legacy < Bundler::RubygemsIntegration + def all_specs: () -> untyped + + def find_name: (untyped name) -> untyped + + def initialize: () -> void + + def post_reset_hooks: () -> untyped + + def reset: () -> untyped + + def stub_rubygems: (untyped specs) -> untyped + + def validate: (untyped spec) -> untyped +end + +# RubyGems 1.8.5-1.8.19 +class Bundler::RubygemsIntegration::Modern < Bundler::RubygemsIntegration + def all_specs: () -> untyped + + def find_name: (untyped name) -> untyped + + def stub_rubygems: (untyped specs) -> untyped +end + +# RubyGems 2.1.0 +class Bundler::RubygemsIntegration::MoreFuture < Bundler::RubygemsIntegration::Future + def all_specs: () -> untyped + + # RubyGems-generated binstubs call + # [`Kernel#gem`](https://docs.ruby-lang.org/en/2.6.0/Kernel.html#method-i-gem) + def binstubs_call_gem?: () -> untyped + + def find_name: (untyped name) -> untyped + + def initialize: () -> void + + # only 2.5.2+ has all of the stub methods we want to use, and since this is a + # performance optimization *only*, we'll restrict ourselves to the most recent + # RG versions instead of all versions that have stubs + def stubs_provide_full_functionality?: () -> untyped + + def use_gemdeps: (untyped gemfile) -> untyped +end + +# RubyGems 1.8.20+ +class Bundler::RubygemsIntegration::MoreModern < Bundler::RubygemsIntegration::Modern + def build: (untyped spec, ?untyped skip_validation) -> untyped +end + +# RubyGems 1.7 +class Bundler::RubygemsIntegration::Transitional < Bundler::RubygemsIntegration::Legacy + def stub_rubygems: (untyped specs) -> untyped + + def validate: (untyped spec) -> untyped +end + +class Bundler::Runtime + include ::Bundler::SharedHelpers + + def cache: (?untyped custom_path) -> untyped + + def clean: (?untyped dry_run) -> untyped + + def current_dependencies: () -> untyped + + def dependencies: () -> untyped + + def gems: () -> untyped + + def initialize: (untyped root, untyped definition) -> void + + def lock: (?untyped opts) -> untyped + + def prune_cache: (untyped cache_path) -> untyped + + def requested_specs: () -> untyped + + def require: (*untyped groups) -> untyped + + def requires: () -> untyped + + def setup: (*untyped groups) -> untyped + + def specs: () -> untyped +end + +Bundler::Runtime::REQUIRE_ERRORS: untyped + +class Bundler::SecurityError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::Settings + def []: (untyped name) -> untyped + + def all: () -> untyped + + def allow_sudo?: () -> untyped + + def app_cache_path: () -> untyped + + def credentials_for: (untyped uri) -> untyped + + def gem_mirrors: () -> untyped + + def ignore_config?: () -> untyped + + def initialize: (?untyped root) -> void + + def key_for: (untyped key) -> untyped + + def local_overrides: () -> untyped + + def locations: (untyped key) -> untyped + + def mirror_for: (untyped uri) -> untyped + + # for legacy reasons, in + # [`Bundler`](https://docs.ruby-lang.org/en/2.7.0/Bundler.html) 2, we do not + # respect :disable\_shared\_gems + def path: () -> untyped + + def pretty_values_for: (untyped exposed_key) -> untyped + + def set_command_option: (untyped key, untyped value) -> untyped + + def set_command_option_if_given: (untyped key, untyped value) -> untyped + + def set_global: (untyped key, untyped value) -> untyped + + def set_local: (untyped key, untyped value) -> untyped + + def temporary: (untyped update) -> untyped + + def validate!: () -> untyped + + def self.normalize_uri: (untyped uri) -> untyped +end + +Bundler::Settings::ARRAY_KEYS: untyped + +Bundler::Settings::BOOL_KEYS: untyped + +Bundler::Settings::CONFIG_REGEX: untyped + +Bundler::Settings::DEFAULT_CONFIG: untyped + +Bundler::Settings::NORMALIZE_URI_OPTIONS_PATTERN: untyped + +Bundler::Settings::NUMBER_KEYS: untyped + +Bundler::Settings::PER_URI_OPTIONS: untyped + +class Bundler::Settings::Path < Struct + def append_ruby_scope: () -> untyped + + def append_ruby_scope=: (untyped _) -> untyped + + def base_path: () -> untyped + + def base_path_relative_to_pwd: () -> untyped + + def default_install_uses_path: () -> untyped + + def default_install_uses_path=: (untyped _) -> untyped + + def explicit_path: () -> untyped + + def explicit_path=: (untyped _) -> untyped + + def path: () -> untyped + + def system_path: () -> untyped + + def system_path=: (untyped _) -> untyped + + def use_system_gems?: () -> untyped + + def validate!: () -> untyped + + def self.[]: (*untyped _) -> untyped + + def self.members: () -> untyped + + def self.new: (*untyped _) -> untyped +end + +Bundler::Settings::Path::Elem: untyped + +module Bundler::SharedHelpers + extend ::Bundler::SharedHelpers + + def chdir: (untyped dir) { () -> untyped } -> untyped + + def const_get_safely: (untyped constant_name, untyped namespace) -> untyped + + def default_bundle_dir: () -> untyped + + def default_gemfile: () -> untyped + + def default_lockfile: () -> untyped + + def digest: (untyped name) -> untyped + + def ensure_same_dependencies: (untyped spec, untyped old_deps, untyped new_deps) -> untyped + + def filesystem_access: (untyped path, ?untyped action) { () -> untyped } -> untyped + + def in_bundle?: () -> untyped + + def major_deprecation: (untyped major_version, untyped message) -> untyped + + def md5_available?: () -> untyped + + def pretty_dependency: (untyped dep, ?untyped print_source) -> untyped + + def print_major_deprecations!: () -> untyped + + def pwd: () -> untyped + + def root: () -> untyped + + def set_bundle_environment: () -> untyped + + def set_env: (untyped key, untyped value) -> untyped + + def trap: (untyped signal, ?untyped override) { () -> untyped } -> untyped + + def with_clean_git_env: () { () -> untyped } -> untyped + + def write_to_gemfile: (untyped gemfile_path, untyped contents) -> untyped +end + +class Bundler::Source + def can_lock?: (untyped spec) -> untyped + + def dependency_names: () -> untyped + + def dependency_names=: (untyped dependency_names) -> untyped + + def dependency_names_to_double_check: () -> untyped + + def double_check_for: (*untyped _) -> untyped + + def extension_cache_path: (untyped spec) -> untyped + + def include?: (untyped other) -> untyped + + def inspect: () -> untyped + + def path?: () -> untyped + + def unmet_deps: () -> untyped + + def version_message: (untyped spec) -> untyped +end + +class Bundler::Source::Gemspec < Bundler::Source::Path + def as_path_source: () -> untyped + + def gemspec: () -> untyped + + def initialize: (untyped options) -> void +end + +class Bundler::Source::Git < Bundler::Source::Path + def ==: (untyped other) -> untyped + + def allow_git_ops?: () -> untyped + + def app_cache_dirname: () -> untyped + + def branch: () -> untyped + + def cache: (untyped spec, ?untyped custom_path) -> untyped + + # This is the path which is going to contain a cache of the git repository. + # When using the same git repository across different projects, this cache + # will be shared. When using local git repos, this is set to the local repo. + def cache_path: () -> untyped + + def eql?: (untyped other) -> untyped + + def extension_dir_name: () -> untyped + + def hash: () -> untyped + + def initialize: (untyped options) -> void + + def install: (untyped spec, ?untyped options) -> untyped + + # This is the path which is going to contain a specific checkout of the git + # repository. When using local git repos, this is set to the local repo. + # + # Also aliased as: + # [`path`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Source/Git.html#method-i-path) + def install_path: () -> untyped + + def load_spec_files: () -> untyped + + def local_override!: (untyped path) -> untyped + + def name: () -> untyped + + def options: () -> untyped + + # Alias for: + # [`install_path`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Source/Git.html#method-i-install_path) + def path: () -> untyped + + def ref: () -> untyped + + def revision: () -> untyped + + def specs: (*untyped _) -> untyped + + def submodules: () -> untyped + + def to_lock: () -> untyped + + def to_s: () -> untyped + + def unlock!: () -> untyped + + def uri: () -> untyped + + def self.from_lock: (untyped options) -> untyped +end + +class Bundler::Source::Git::GitCommandError < Bundler::GitError + def initialize: (untyped command, ?untyped path, ?untyped extra_info) -> void +end + +class Bundler::Source::Git::GitNotAllowedError < Bundler::GitError + def initialize: (untyped command) -> void +end + +class Bundler::Source::Git::GitNotInstalledError < Bundler::GitError + def initialize: () -> void +end + +# The +# [`GitProxy`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Source/Git/GitProxy.html) +# is responsible to interact with git repositories. All actions required by the +# [`Git`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Source/Git.html) source is +# encapsulated in this object. +class Bundler::Source::Git::GitProxy + def branch: () -> untyped + + def checkout: () -> untyped + + def contains?: (untyped commit) -> untyped + + def copy_to: (untyped destination, ?untyped submodules) -> untyped + + def full_version: () -> untyped + + def initialize: (untyped path, untyped uri, untyped ref, ?untyped revision, ?untyped git) -> void + + def path: () -> untyped + + def path=: (untyped path) -> untyped + + def ref: () -> untyped + + def ref=: (untyped ref) -> untyped + + def revision: () -> untyped + + def revision=: (untyped revision) -> untyped + + def uri: () -> untyped + + def uri=: (untyped uri) -> untyped + + def version: () -> untyped +end + +class Bundler::Source::Git::MissingGitRevisionError < Bundler::GitError + def initialize: (untyped ref, untyped repo) -> void +end + +class Bundler::Source::Metadata < Bundler::Source + def ==: (untyped other) -> untyped + + def cached!: () -> untyped + + def eql?: (untyped other) -> untyped + + def hash: () -> untyped + + def install: (untyped spec, ?untyped _opts) -> untyped + + def options: () -> untyped + + def remote!: () -> untyped + + def specs: () -> untyped + + def to_s: () -> untyped + + def version_message: (untyped spec) -> untyped +end + +class Bundler::Source::Path < Bundler::Source + def ==: (untyped other) -> untyped + + def app_cache_dirname: () -> untyped + + def cache: (untyped spec, ?untyped custom_path) -> untyped + + def cached!: () -> untyped + + def eql?: (untyped other) -> untyped + + def expanded_original_path: () -> untyped + + def hash: () -> untyped + + def initialize: (untyped options) -> void + + def install: (untyped spec, ?untyped options) -> untyped + + def local_specs: (*untyped _) -> untyped + + def name: () -> untyped + + def name=: (untyped name) -> untyped + + def options: () -> untyped + + def original_path: () -> untyped + + def path: () -> untyped + + def remote!: () -> untyped + + def root: () -> untyped + + def root_path: () -> untyped + + def specs: () -> untyped + + def to_lock: () -> untyped + + def to_s: () -> untyped + + def version: () -> untyped + + def version=: (untyped version) -> untyped + + def self.from_lock: (untyped options) -> untyped +end + +Bundler::Source::Path::DEFAULT_GLOB: untyped + +class Bundler::Source::Rubygems < Bundler::Source + def ==: (untyped other) -> untyped + + def add_remote: (untyped source) -> untyped + + def api_fetchers: () -> untyped + + def builtin_gem?: (untyped spec) -> untyped + + def cache: (untyped spec, ?untyped custom_path) -> untyped + + def cache_path: () -> untyped + + def cached!: () -> untyped + + def cached_built_in_gem: (untyped spec) -> untyped + + def cached_gem: (untyped spec) -> untyped + + def cached_path: (untyped spec) -> untyped + + def cached_specs: () -> untyped + + def caches: () -> untyped + + def can_lock?: (untyped spec) -> untyped + + def credless_remotes: () -> untyped + + def dependency_names_to_double_check: () -> untyped + + def double_check_for: (untyped unmet_dependency_names) -> untyped + + def eql?: (untyped other) -> untyped + + def equivalent_remotes?: (untyped other_remotes) -> untyped + + def fetch_gem: (untyped spec) -> untyped + + def fetch_names: (untyped fetchers, untyped dependency_names, untyped index, untyped override_dupes) -> untyped + + def fetchers: () -> untyped + + def hash: () -> untyped + + def include?: (untyped o) -> untyped + + def initialize: (?untyped options) -> void + + def install: (untyped spec, ?untyped opts) -> untyped + + def installed?: (untyped spec) -> untyped + + def installed_specs: () -> untyped + + def loaded_from: (untyped spec) -> untyped + + # Alias for: + # [`to_s`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Source/Rubygems.html#method-i-to_s) + def name: () -> untyped + + def normalize_uri: (untyped uri) -> untyped + + def options: () -> untyped + + def remote!: () -> untyped + + def remote_specs: () -> untyped + + def remotes: () -> untyped + + def remotes_for_spec: (untyped spec) -> untyped + + def remove_auth: (untyped remote) -> untyped + + def replace_remotes: (untyped other_remotes, ?untyped allow_equivalent) -> untyped + + def requires_sudo?: () -> untyped + + def rubygems_dir: () -> untyped + + def specs: () -> untyped + + def suppress_configured_credentials: (untyped remote) -> untyped + + def to_lock: () -> untyped + + # Also aliased as: + # [`name`](https://docs.ruby-lang.org/en/2.7.0/Bundler/Source/Rubygems.html#method-i-name) + def to_s: () -> untyped + + def unmet_deps: () -> untyped + + def self.from_lock: (untyped options) -> untyped +end + +Bundler::Source::Rubygems::API_REQUEST_LIMIT: untyped + +Bundler::Source::Rubygems::API_REQUEST_SIZE: untyped + +class Bundler::SourceList + def add_git_source: (?untyped options) -> untyped + + def add_path_source: (?untyped options) -> untyped + + def add_plugin_source: (untyped source, ?untyped options) -> untyped + + def add_rubygems_remote: (untyped uri) -> untyped + + def add_rubygems_source: (?untyped options) -> untyped + + def all_sources: () -> untyped + + def cached!: () -> untyped + + def default_source: () -> untyped + + def get: (untyped source) -> untyped + + def git_sources: () -> untyped + + def global_rubygems_source: () -> untyped + + def global_rubygems_source=: (untyped uri) -> untyped + + def initialize: () -> void + + def lock_sources: () -> untyped + + def metadata_source: () -> untyped + + def path_sources: () -> untyped + + def plugin_sources: () -> untyped + + def remote!: () -> untyped + + def replace_sources!: (untyped replacement_sources) -> untyped + + def rubygems_primary_remotes: () -> untyped + + def rubygems_remotes: () -> untyped + + def rubygems_sources: () -> untyped +end + +class Bundler::SpecSet[out Elem] + include ::TSort + + include ::Enumerable + + def <<: (*untyped args) { () -> untyped } -> untyped + + def []: (untyped key) -> untyped + + def []=: (untyped key, untyped value) -> untyped + + def add: (*untyped args) { () -> untyped } -> untyped + + def each: (*untyped args) { () -> untyped } -> untyped + + def empty?: (*untyped args) { () -> untyped } -> untyped + + def find_by_name_and_platform: (untyped name, untyped platform) -> untyped + + def for: (untyped dependencies, ?untyped skip, ?untyped check, ?untyped match_current_platform, ?untyped raise_on_missing) -> untyped + + def initialize: (untyped specs) -> void + + def length: (*untyped args) { () -> untyped } -> untyped + + def materialize: (untyped deps, ?untyped missing_specs) -> untyped + + # Materialize for all the specs in the spec set, regardless of what platform + # they're for This is in contrast to how for does platform filtering (and + # specifically different from how `materialize` calls `for` only for the + # current platform) @return [Array] + def materialized_for_all_platforms: () -> untyped + + def merge: (untyped set) -> untyped + + def remove: (*untyped args) { () -> untyped } -> untyped + + def size: (*untyped args) { () -> untyped } -> untyped + + def sort!: () -> untyped + + def to_a: () -> untyped + + def to_hash: () -> untyped + + def valid_for?: (untyped deps) -> untyped + + def what_required: (untyped spec) -> untyped +end + +class Bundler::StubSpecification < Bundler::RemoteSpecification + def activated: () -> untyped + + def activated=: (untyped activated) -> untyped + + def default_gem: () -> untyped + + def default_gem?: () -> bool + + def full_gem_path: () -> untyped + + def full_require_paths: () -> untyped + + def ignored: () -> untyped + + def ignored=: (untyped ignored) -> untyped + + # This is what we do in bundler/rubygems\_ext + # [`full_require_paths`](https://docs.ruby-lang.org/en/2.6.0/Bundler/StubSpecification.html#method-i-full_require_paths) + # is always implemented in >= 2.2.0 + def load_paths: () -> untyped + + def loaded_from: () -> untyped + + def matches_for_glob: (untyped glob) -> untyped + + # This is defined directly to avoid having to load every installed spec + def missing_extensions?: () -> untyped + + def raw_require_paths: () -> untyped + + def source=: (untyped source) -> untyped + + def stub: () -> untyped + + def stub=: (untyped stub) -> untyped + + def to_yaml: () -> untyped + + def self.from_stub: (untyped stub) -> untyped +end + +class Bundler::SudoNotPermittedError < Bundler::BundlerError + def status_code: () -> untyped +end + +class Bundler::TemporaryResourceError < Bundler::PermissionError + def message: () -> untyped + + def status_code: () -> untyped +end + +class Bundler::ThreadCreationError < Bundler::BundlerError + def status_code: () -> untyped +end + +module Bundler::UI +end + +class Bundler::UI::RGProxy < Gem::SilentUI + def initialize: (untyped ui) -> void + + def say: (untyped message) -> untyped +end + +class Bundler::UI::Silent + def add_color: (untyped string, untyped color) -> untyped + + def ask: (untyped message) -> untyped + + def confirm: (untyped message, ?untyped newline) -> untyped + + def debug: (untyped message, ?untyped newline) -> untyped + + def debug?: () -> untyped + + def error: (untyped message, ?untyped newline) -> untyped + + def info: (untyped message, ?untyped newline) -> untyped + + def initialize: () -> void + + def level: (?untyped name) -> untyped + + def level=: (untyped name) -> untyped + + def no?: () -> untyped + + def quiet?: () -> untyped + + def shell=: (untyped shell) -> untyped + + def silence: () -> untyped + + def trace: (untyped message, ?untyped newline, ?untyped force) -> untyped + + def unprinted_warnings: () -> untyped + + def warn: (untyped message, ?untyped newline) -> untyped + + def yes?: (untyped msg) -> untyped +end + +module Bundler::URICredentialsFilter + def self.credential_filtered_string: (untyped str_to_filter, untyped uri) -> untyped + + def self.credential_filtered_uri: (untyped uri_to_anonymize) -> untyped +end + +# Internal error, should be rescued +class Bundler::VersionConflict < Bundler::BundlerError + def conflicts: () -> untyped + + def initialize: (untyped conflicts, ?untyped msg) -> void + + def status_code: () -> untyped +end + +class Bundler::VirtualProtocolError < Bundler::BundlerError + def message: () -> untyped + + def status_code: () -> untyped +end + +# A stub yaml serializer that can handle only hashes and strings (as of now). +module Bundler::YAMLSerializer + def self.dump: (untyped hash) -> untyped + + def self.load: (untyped str) -> untyped +end + +Bundler::YAMLSerializer::ARRAY_REGEX: untyped + +Bundler::YAMLSerializer::HASH_REGEX: untyped + +class Bundler::YamlSyntaxError < Bundler::BundlerError + def initialize: (untyped orig_exception, untyped msg) -> void + + def orig_exception: () -> untyped + + def status_code: () -> untyped +end + +class Bundler::Installer + def self.ambiguous_gems=: (untyped ambiguous_gems) -> untyped + + def self.ambiguous_gems: () -> untyped + + def post_install_messages: () -> untyped + + def self.install: (untyped root, untyped definition, ?untyped options) -> untyped + + def initialize: (untyped root, untyped definition) -> void + + def run: (untyped options) -> void + + def generate_bundler_executable_stubs: (untyped spec, ?untyped options) -> void + + def generate_standalone_bundler_executable_stubs: (untyped spec, ?untyped options) -> void +end diff --git a/rbs/fills/open3/0/open3.rbs b/rbs/fills/open3/0/open3.rbs new file mode 100644 index 000000000..11d909572 --- /dev/null +++ b/rbs/fills/open3/0/open3.rbs @@ -0,0 +1,172 @@ +# +# Module Open3 supports creating child processes with access to their $stdin, +# $stdout, and $stderr streams. +# +# ## What's Here +# +# Each of these methods executes a given command in a new process or subshell, +# or multiple commands in new processes and/or subshells: +# +# * Each of these methods executes a single command in a process or subshell, +# accepts a string for input to $stdin, and returns string output from +# $stdout, $stderr, or both: +# +# * Open3.capture2: Executes the command; returns the string from $stdout. +# * Open3.capture2e: Executes the command; returns the string from merged +# $stdout and $stderr. +# * Open3.capture3: Executes the command; returns strings from $stdout and +# $stderr. +# +# * Each of these methods executes a single command in a process or subshell, +# and returns pipes for $stdin, $stdout, and/or $stderr: +# +# * Open3.popen2: Executes the command; returns pipes for $stdin and +# $stdout. +# * Open3.popen2e: Executes the command; returns pipes for $stdin and +# merged $stdout and $stderr. +# * Open3.popen3: Executes the command; returns pipes for $stdin, $stdout, +# and $stderr. +# +# * Each of these methods executes one or more commands in processes and/or +# subshells, returns pipes for the first $stdin, the last $stdout, or both: +# +# * Open3.pipeline_r: Returns a pipe for the last $stdout. +# * Open3.pipeline_rw: Returns pipes for the first $stdin and the last +# $stdout. +# * Open3.pipeline_w: Returns a pipe for the first $stdin. +# * Open3.pipeline_start: Does not wait for processes to complete. +# * Open3.pipeline: Waits for processes to complete. +# +# Each of the methods above accepts: +# +# * An optional hash of environment variable names and values; see [Execution +# Environment](rdoc-ref:Process@Execution+Environment). +# * A required string argument that is a `command_line` or `exe_path`; see +# [Argument command_line or +# exe_path](rdoc-ref:Process@Argument+command_line+or+exe_path). +# * An optional hash of execution options; see [Execution +# Options](rdoc-ref:Process@Execution+Options). +# +module Open3 + # + # Basically a wrapper for Open3.popen3 that: + # + # * Creates a child process, by calling Open3.popen3 with the given arguments + # (except for certain entries in hash `options`; see below). + # * Returns as string `stdout_and_stderr_s` the merged standard output and + # standard error of the child process. + # * Returns as `status` a `Process::Status` object that represents the exit + # status of the child process. + # + # Returns the array `[stdout_and_stderr_s, status]`: + # + # stdout_and_stderr_s, status = Open3.capture2e('echo "Foo"') + # # => ["Foo\n", #] + # + # Like Process.spawn, this method has potential security vulnerabilities if + # called with untrusted input; see [Command + # Injection](rdoc-ref:command_injection.rdoc@Command+Injection). + # + # Unlike Process.spawn, this method waits for the child process to exit before + # returning, so the caller need not do so. + # + # If the first argument is a hash, it becomes leading argument `env` in the call + # to Open3.popen3; see [Execution + # Environment](rdoc-ref:Process@Execution+Environment). + # + # If the last argument is a hash, it becomes trailing argument `options` in the + # call to Open3.popen3; see [Execution + # Options](rdoc-ref:Process@Execution+Options). + # + # The hash `options` is given; two options have local effect in method + # Open3.capture2e: + # + # * If entry `options[:stdin_data]` exists, the entry is removed and its + # string value is sent to the command's standard input: + # + # Open3.capture2e('tee', stdin_data: 'Foo') + # # => ["Foo", #] + # + # * If entry `options[:binmode]` exists, the entry is removed and the internal + # streams are set to binary mode. + # + # The single required argument is one of the following: + # + # * `command_line` if it is a string, and if it begins with a shell reserved + # word or special built-in, or if it contains one or more metacharacters. + # * `exe_path` otherwise. + # + # **Argument `command_line`** + # + # String argument `command_line` is a command line to be passed to a shell; it + # must begin with a shell reserved word, begin with a special built-in, or + # contain meta characters: + # + # Open3.capture2e('if true; then echo "Foo"; fi') # Shell reserved word. + # # => ["Foo\n", #] + # Open3.capture2e('echo') # Built-in. + # # => ["\n", #] + # Open3.capture2e('date > date.tmp') # Contains meta character. + # # => ["", #] + # + # The command line may also contain arguments and options for the command: + # + # Open3.capture2e('echo "Foo"') + # # => ["Foo\n", #] + # + # **Argument `exe_path`** + # + # Argument `exe_path` is one of the following: + # + # * The string path to an executable to be called. + # * A 2-element array containing the path to an executable and the string to + # be used as the name of the executing process. + # + # Example: + # + # Open3.capture2e('/usr/bin/date') + # # => ["Sat Sep 30 09:01:46 AM CDT 2023\n", #] + # + # Ruby invokes the executable directly, with no shell and no shell expansion: + # + # Open3.capture2e('doesnt_exist') # Raises Errno::ENOENT + # + # If one or more `args` is given, each is an argument or option to be passed to + # the executable: + # + # Open3.capture2e('echo', 'C #') + # # => ["C #\n", #] + # Open3.capture2e('echo', 'hello', 'world') + # # => ["hello world\n", #] + # + def self.capture2e: (*String, ?stdin_data: String, ?chdir: String, ?binmode: boolish) -> [String, Process::Status] + | (Hash[String, String] env, *String cmds, ?chdir: String, ?stdin_data: String, ?binmode: boolish) -> [String, Process::Status] + + def self.capture2: (?Hash[String, String] env, *String cmds, ?chdir: String) -> [String, Process::Status] + + def self.capture3: (?Hash[String, String] env, *String cmds, ?chdir: String) -> [String, String, Process::Status] + + def self.pipeline: (?Hash[String, String] env, *String cmds, ?chdir: String) -> Array[Process::Status] + + def self.pipeline_r: (?Hash[String, String] env, *String cmds, ?chdir: String) -> [IO, Process::Waiter] + + def self.pipeline_rw: (?Hash[String, String] env, *String cmds, ?chdir: String) -> [IO, IO, Process::Waiter] + + def self.pipeline_start: (?Hash[String, String] env, *String cmds, ?chdir: String) -> Array[Process::Waiter] + + def self.pipeline_w: (?Hash[String, String] env, *String cmds, ?chdir: String) -> [IO, Process::Waiter] + + def self.popen2: (?Hash[String, String] env, *String exe_path_or_cmd_with_args, ?chdir: String) -> [IO, IO, Process::Waiter] + | [U] (?Hash[String, String] env, *String exe_path_or_cmd_with_args, ?chdir: String) { (IO stdin, IO stdout, Process::Waiter wait_thread) -> U } -> U + + def self.popen2e: (?Hash[String, String] env, *String exe_path_or_cmd_with_args, ?chdir: String) -> [IO, IO, Process::Waiter] + | [U] (?Hash[String, String] env, *String exe_path_or_cmd_with_args, ?chdir: String) { (IO stdin, IO stdout_and_stderr, Process::Waiter wait_thread) -> U } -> U + + def self.popen3: (?Hash[String, String] env, *String exe_path_or_cmd_with_args, ?chdir: String) -> [IO, IO, IO, Process::Waiter] + | [U] (?Hash[String, String] env, *String exe_path_or_cmd_with_args, ?chdir: String) { (IO stdin, IO stdout, IO stderr, Process::Waiter wait_thread) -> U } -> U + +end diff --git a/rbs/fills/rubygems/0/basic_specification.rbs b/rbs/fills/rubygems/0/basic_specification.rbs new file mode 100644 index 000000000..361027e67 --- /dev/null +++ b/rbs/fills/rubygems/0/basic_specification.rbs @@ -0,0 +1,326 @@ +# +# BasicSpecification is an abstract class which implements some common code used +# by both Specification and StubSpecification. +# +class Gem::BasicSpecification + @ignored: untyped + + @extension_dir: untyped + + @full_gem_path: untyped + + @full_require_paths: untyped + + @paths_map: untyped + + @gem_dir: untyped + + attr_writer base_dir: untyped + + attr_writer extension_dir: untyped + + attr_writer ignored: untyped + + # + # The path this gemspec was loaded from. This attribute is not persisted. + # + attr_accessor loaded_from: untyped + + attr_writer full_gem_path: untyped + + # + # + def initialize: () -> void + + # + # + def self.default_specifications_dir: () -> untyped + + extend Gem::Deprecate + + def gem_build_complete_path: () -> untyped + + # + # True when the gem has been activated + # + def activated?: () -> untyped + + # + # Returns the full path to the base gem directory. + # + # eg: /usr/local/lib/ruby/gems/1.8 + # + def base_dir: () -> untyped + + # + # Return true if this spec can require `file`. + # + def contains_requirable_file?: (untyped file) -> (false | untyped) + + # + # Return true if this spec should be ignored because it's missing extensions. + # + def ignored?: () -> untyped + + # + # + def default_gem?: () -> untyped + + # + # Regular gems take precedence over default gems + # + def default_gem_priority: () -> (1 | -1) + + # + # Gems higher up in `gem_path` take precedence + # + def base_dir_priority: (untyped gem_path) -> untyped + + # + # Returns full path to the directory where gem's extensions are installed. + # + def extension_dir: () -> untyped + + # + # Returns path to the extensions directory. + # + def extensions_dir: () -> untyped + + private + + def find_full_gem_path: () -> untyped + + public + + # + # The full path to the gem (install path + full name). + # + # TODO: This is duplicated with #gem_dir. Eventually either of them should be + # deprecated. + # + def full_gem_path: () -> untyped + + # + # Returns the full name (name-version) of this Gem. Platform information is + # included (name-version-platform) if it is specified and not the default Ruby + # platform. + # + def full_name: () -> ::String + + # + # Returns the full name of this Gem (see `Gem::BasicSpecification#full_name`). + # Information about where the gem is installed is also included if not installed + # in the default GEM_HOME. + # + def full_name_with_location: () -> (::String | untyped) + + # + # Full paths in the gem to add to `$LOAD_PATH` when this gem is activated. + # + def full_require_paths: () -> untyped + + # + # The path to the data directory for this gem. + # + def datadir: () -> untyped + + # + # Full path of the target library file. If the file is not in this gem, return + # nil. + # + def to_fullpath: (untyped path) -> (untyped | nil) + + # + # Returns the full path to this spec's gem directory. eg: + # /usr/local/lib/ruby/1.8/gems/mygem-1.0 + # + # TODO: This is duplicated with #full_gem_path. Eventually either of them should + # be deprecated. + # + def gem_dir: () -> untyped + + # + # Returns the full path to the gems directory containing this spec's gem + # directory. eg: /usr/local/lib/ruby/1.8/gems + # + def gems_dir: () -> untyped + + def internal_init: () -> untyped + + # + # Name of the gem + # + def name: () -> untyped + + # + # Platform of the gem + # + def platform: () -> untyped + + def raw_require_paths: () -> untyped + + # + # Paths in the gem to add to `$LOAD_PATH` when this gem is activated. + # + # See also #require_paths= + # + # If you have an extension you do not need to add `"ext"` to the require path, + # the extension build process will copy the extension files into "lib" for you. + # + # The default value is `"lib"` + # + # Usage: + # + # # If all library files are in the root directory... + # spec.require_path = '.' + # + def require_paths: () -> untyped + + # + # Returns the paths to the source files for use with analysis and documentation + # tools. These paths are relative to full_gem_path. + # + def source_paths: () -> untyped + + # + # Return all files in this gem that match for `glob`. + # + def matches_for_glob: (untyped glob) -> untyped + + # + # Returns the list of plugins in this spec. + # + def plugins: () -> untyped + + # + # Returns a string usable in Dir.glob to match all requirable paths for this + # spec. + # + def lib_dirs_glob: () -> ::String + + # + # Return a Gem::Specification from this gem + # + def to_spec: () -> untyped + + # + # Version of the gem + # + def version: () -> untyped + + # + # Whether this specification is stubbed - i.e. we have information about the gem + # from a stub line, without having to evaluate the entire gemspec file. + # + def stubbed?: () -> untyped + + # + # + def this: () -> self + + private + + # + # + def have_extensions?: () -> untyped + + # + # + def have_file?: (untyped file, untyped suffixes) -> (true | untyped) +end diff --git a/rbs/fills/rubygems/0/errors.rbs b/rbs/fills/rubygems/0/errors.rbs new file mode 100644 index 000000000..34b97fcf1 --- /dev/null +++ b/rbs/fills/rubygems/0/errors.rbs @@ -0,0 +1,364 @@ +# +# RubyGems is the Ruby standard for publishing and managing third party +# libraries. +# +# For user documentation, see: +# +# * `gem help` and `gem help [command]` +# * [RubyGems User Guide](https://guides.rubygems.org/) +# * [Frequently Asked Questions](https://guides.rubygems.org/faqs) +# +# For gem developer documentation see: +# +# * [Creating Gems](https://guides.rubygems.org/make-your-own-gem) +# * Gem::Specification +# * Gem::Version for version dependency notes +# +# Further RubyGems documentation can be found at: +# +# * [RubyGems Guides](https://guides.rubygems.org) +# * [RubyGems API](https://www.rubydoc.info/github/rubygems/rubygems) (also +# available from `gem server`) +# +# ## RubyGems Plugins +# +# RubyGems will load plugins in the latest version of each installed gem or +# $LOAD_PATH. Plugins must be named 'rubygems_plugin' (.rb, .so, etc) and +# placed at the root of your gem's #require_path. Plugins are installed at a +# special location and loaded on boot. +# +# For an example plugin, see the [Graph gem](https://github.com/seattlerb/graph) +# which adds a `gem graph` command. +# +# ## RubyGems Defaults, Packaging +# +# RubyGems defaults are stored in lib/rubygems/defaults.rb. If you're packaging +# RubyGems or implementing Ruby you can change RubyGems' defaults. +# +# For RubyGems packagers, provide lib/rubygems/defaults/operating_system.rb and +# override any defaults from lib/rubygems/defaults.rb. +# +# For Ruby implementers, provide lib/rubygems/defaults/#{RUBY_ENGINE}.rb and +# override any defaults from lib/rubygems/defaults.rb. +# +# If you need RubyGems to perform extra work on install or uninstall, your +# defaults override file can set pre/post install and uninstall hooks. See +# Gem::pre_install, Gem::pre_uninstall, Gem::post_install, Gem::post_uninstall. +# +# ## Bugs +# +# You can submit bugs to the [RubyGems bug +# tracker](https://github.com/rubygems/rubygems/issues) on GitHub +# +# ## Credits +# +# RubyGems is currently maintained by Eric Hodel. +# +# RubyGems was originally developed at RubyConf 2003 by: +# +# * Rich Kilmer -- rich(at)infoether.com +# * Chad Fowler -- chad(at)chadfowler.com +# * David Black -- dblack(at)wobblini.net +# * Paul Brannan -- paul(at)atdesk.com +# * Jim Weirich -- jim(at)weirichhouse.org +# +# Contributors: +# +# * Gavin Sinclair -- gsinclair(at)soyabean.com.au +# * George Marrows -- george.marrows(at)ntlworld.com +# * Dick Davies -- rasputnik(at)hellooperator.net +# * Mauricio Fernandez -- batsman.geo(at)yahoo.com +# * Simon Strandgaard -- neoneye(at)adslhome.dk +# * Dave Glasser -- glasser(at)mit.edu +# * Paul Duncan -- pabs(at)pablotron.org +# * Ville Aine -- vaine(at)cs.helsinki.fi +# * Eric Hodel -- drbrain(at)segment7.net +# * Daniel Berger -- djberg96(at)gmail.com +# * Phil Hagelberg -- technomancy(at)gmail.com +# * Ryan Davis -- ryand-ruby(at)zenspider.com +# * Evan Phoenix -- evan(at)fallingsnow.net +# * Steve Klabnik -- steve(at)steveklabnik.com +# +# (If your name is missing, PLEASE let us know!) +# +# ## License +# +# See +# [LICENSE.txt](https://github.com/rubygems/rubygems/blob/master/LICENSE.txt) +# for permissions. +# +# Thanks! +# +# -The RubyGems Team +# +# +# Provides 3 methods for declaring when something is going away. +# +# +deprecate(name, repl, year, month)+: +# Indicate something may be removed on/after a certain date. +# +# +rubygems_deprecate(name, replacement=:none)+: +# Indicate something will be removed in the next major RubyGems version, +# and (optionally) a replacement for it. +# +# `rubygems_deprecate_command`: +# Indicate a RubyGems command (in +lib/rubygems/commands/*.rb+) will be +# removed in the next RubyGems version. +# +# Also provides `skip_during` for temporarily turning off deprecation warnings. +# This is intended to be used in the test suite, so deprecation warnings don't +# cause test failures if you need to make sure stderr is otherwise empty. +# +# Example usage of `deprecate` and `rubygems_deprecate`: +# +# class Legacy +# def self.some_class_method +# # ... +# end +# +# def some_instance_method +# # ... +# end +# +# def some_old_method +# # ... +# end +# +# extend Gem::Deprecate +# deprecate :some_instance_method, "X.z", 2011, 4 +# rubygems_deprecate :some_old_method, "Modern#some_new_method" +# +# class << self +# extend Gem::Deprecate +# deprecate :some_class_method, :none, 2011, 4 +# end +# end +# +# Example usage of `rubygems_deprecate_command`: +# +# class Gem::Commands::QueryCommand < Gem::Command +# extend Gem::Deprecate +# rubygems_deprecate_command +# +# # ... +# end +# +# Example usage of `skip_during`: +# +# class TestSomething < Gem::Testcase +# def test_some_thing_with_deprecations +# Gem::Deprecate.skip_during do +# actual_stdout, actual_stderr = capture_output do +# Gem.something_deprecated +# end +# assert_empty actual_stdout +# assert_equal(expected, actual_stderr) +# end +# end +# end +# +module Gem + # + # Raised when RubyGems is unable to load or activate a gem. Contains the name + # and version requirements of the gem that either conflicts with already + # activated gems or that RubyGems is otherwise unable to activate. + # + class LoadError < ::LoadError + # + # Name of gem + # + attr_accessor name: untyped + + # + # Version requirement of gem + # + attr_accessor requirement: untyped + end + + # + # Raised when trying to activate a gem, and that gem does not exist on the + # system. Instead of rescuing from this class, make sure to rescue from the + # superclass Gem::LoadError to catch all types of load errors. + # + class MissingSpecError < Gem::LoadError + @name: untyped + + @requirement: untyped + + @extra_message: untyped + + # + # + def initialize: (untyped name, untyped requirement, ?untyped? extra_message) -> void + + def message: () -> untyped + + private + + # + # + def build_message: () -> ::String + end + + # + # Raised when trying to activate a gem, and the gem exists on the system, but + # not the requested version. Instead of rescuing from this class, make sure to + # rescue from the superclass Gem::LoadError to catch all types of load errors. + # + class MissingSpecVersionError < MissingSpecError + @specs: untyped + + attr_reader specs: untyped + + # + # + def initialize: (untyped name, untyped requirement, untyped specs) -> void + + private + + # + # + def build_message: () -> ::String + end + + # + # Raised when there are conflicting gem specs loaded + # + class ConflictError < LoadError + @target: untyped + + @conflicts: untyped + + @name: untyped + + # + # A Hash mapping conflicting specifications to the dependencies that caused the + # conflict + # + attr_reader conflicts: untyped + + # + # The specification that had the conflict + # + attr_reader target: untyped + + # + # + def initialize: (untyped target, untyped conflicts) -> void + end + + class ErrorReason + end + + # + # Generated when trying to lookup a gem to indicate that the gem was found, but + # that it isn't usable on the current platform. + # + # fetch and install read these and report them to the user to aid in figuring + # out why a gem couldn't be installed. + # + class PlatformMismatch < ErrorReason + @name: untyped + + @version: untyped + + @platforms: untyped + + # + # the name of the gem + # + attr_reader name: untyped + + # + # the version + # + attr_reader version: untyped + + # + # The platforms that are mismatched + # + attr_reader platforms: untyped + + # + # + def initialize: (untyped name, untyped version) -> void + + # + # append a platform to the list of mismatched platforms. + # + # Platforms are added via this instead of injected via the constructor so that + # we can loop over a list of mismatches and just add them rather than perform + # some kind of calculation mismatch summary before creation. + # + def add_platform: (untyped platform) -> untyped + + # + # A wordy description of the error. + # + def wordy: () -> untyped + end + + # + # An error that indicates we weren't able to fetch some data from a source + # + class SourceFetchProblem < ErrorReason + @source: untyped + + @error: untyped + + # + # Creates a new SourceFetchProblem for the given `source` and `error`. + # + def initialize: (untyped source, untyped error) -> void + + # + # The source that had the fetch problem. + # + attr_reader source: untyped + + # + # The fetch error which is an Exception subclass. + # + attr_reader error: untyped + + # + # An English description of the error. + # + def wordy: () -> ::String + + # + # The fetch error which is an Exception subclass. + # + alias exception error + end +end diff --git a/rbs/fills/rubygems/0/spec_fetcher.rbs b/rbs/fills/rubygems/0/spec_fetcher.rbs new file mode 100644 index 000000000..9914dc85d --- /dev/null +++ b/rbs/fills/rubygems/0/spec_fetcher.rbs @@ -0,0 +1,107 @@ +# +# SpecFetcher handles metadata updates from remote gem repositories. +# +class Gem::SpecFetcher + self.@fetcher: untyped + + @sources: untyped + + @update_cache: untyped + + @specs: untyped + + @latest_specs: untyped + + @prerelease_specs: untyped + + @caches: untyped + + @fetcher: untyped + + include Gem::UserInteraction + + include Gem::Text + + attr_reader latest_specs: untyped + + attr_reader sources: untyped + + attr_reader specs: untyped + + attr_reader prerelease_specs: untyped + + # + # Default fetcher instance. Use this instead of ::new to reduce object + # allocation. + # + def self.fetcher: () -> untyped + + def self.fetcher=: (untyped fetcher) -> untyped + + # + # Creates a new SpecFetcher. Ordinarily you want to use the default fetcher + # from Gem::SpecFetcher::fetcher which uses the Gem.sources. + # + # If you need to retrieve specifications from a different `source`, you can send + # it as an argument. + # + def initialize: (?untyped? sources) -> void + + # + # Find and fetch gem name tuples that match `dependency`. + # + # If `matching_platform` is false, gems for all platforms are returned. + # + def search_for_dependency: (untyped dependency, ?bool matching_platform) -> ::Array[untyped] + + # + # Return all gem name tuples who's names match `obj` + # + def detect: (?::Symbol type) { (untyped) -> untyped } -> untyped + + # + # Find and fetch specs that match `dependency`. + # + # If `matching_platform` is false, gems for all platforms are returned. + # + def spec_for_dependency: (untyped dependency, ?bool matching_platform) -> ::Array[untyped] + + # + # Suggests gems based on the supplied `gem_name`. Returns an array of + # alternative gem names. + # + def suggest_gems_from_name: (untyped gem_name, ?::Symbol type, ?::Integer num_results) -> (::Array[untyped] | untyped) + + # + # Returns a list of gems available for each source in Gem::sources. + # + # `type` can be one of 3 values: :released => Return the list of all released + # specs :complete => Return the list of all specs :latest => Return the + # list of only the highest version of each gem :prerelease => Return the list of + # all prerelease only specs + # + def available_specs: (untyped type) -> ::Array[untyped] + + def tuples_for: (untyped source, untyped type, ?bool gracefully_ignore) -> untyped +end diff --git a/rbs/fills/rubygems/0/specification.rbs b/rbs/fills/rubygems/0/specification.rbs new file mode 100644 index 000000000..ff51ef0b1 --- /dev/null +++ b/rbs/fills/rubygems/0/specification.rbs @@ -0,0 +1,1753 @@ +# +# The Specification class contains the information for a gem. Typically defined +# in a .gemspec file or a Rakefile, and looks like this: +# +# Gem::Specification.new do |s| +# s.name = 'example' +# s.version = '0.1.0' +# s.licenses = ['MIT'] +# s.summary = "This is an example!" +# s.description = "Much longer explanation of the example!" +# s.authors = ["Ruby Coder"] +# s.email = 'rubycoder@example.com' +# s.files = ["lib/example.rb"] +# s.homepage = 'https://rubygems.org/gems/example' +# s.metadata = { "source_code_uri" => "https://github.com/example/example" } +# end +# +# Starting in RubyGems 2.0, a Specification can hold arbitrary metadata. See +# #metadata for restrictions on the format and size of metadata items you may +# add to a specification. +# +class Gem::Specification < Gem::BasicSpecification + @@required_attributes: untyped + + @@default_value: untyped + + @@attributes: untyped + + @@array_attributes: untyped + + @@nil_attributes: untyped + + @@non_nil_attributes: untyped + + @@dirs: untyped + + self.@load_cache: untyped + + self.@load_cache_mutex: untyped + + self.@specification_record: untyped + + self.@unresolved_deps: untyped + + @removed_method_calls: untyped + + # DO NOT CHANGE TO ||= ! This is not a normal accessor. (yes, it sucks) + # DOC: Why isn't it normal? Why does it suck? How can we fix this? + @files: untyped + + @authors: untyped + + @licenses: untyped + + @original_platform: untyped + + @new_platform: untyped + + @platform: untyped + + @require_paths: untyped + + @executables: untyped + + @extensions: untyped + + @extra_rdoc_files: untyped + + @installed_by_version: untyped + + @rdoc_options: untyped + + @required_ruby_version: untyped + + @required_rubygems_version: untyped + + @requirements: untyped + + @test_files: untyped + + @extensions_dir: untyped + + @activated: untyped + + @loaded: untyped + + @bin_dir: untyped + + @cache_dir: untyped + + @cache_file: untyped + + @date: untyped + + @dependencies: untyped + + @description: untyped + + @doc_dir: untyped + + @full_name: untyped + + @gems_dir: untyped + + @has_rdoc: untyped + + @base_dir: untyped + + @loaded_from: untyped + + @ri_dir: untyped + + @spec_dir: untyped + + @spec_file: untyped + + @summary: untyped + + @test_suite_file: untyped + + @version: untyped + + extend Gem::Deprecate + + # + # The version number of a specification that does not specify one (i.e. RubyGems + # 0.7 or earlier). + # + NONEXISTENT_SPECIFICATION_VERSION: -1 + + CURRENT_SPECIFICATION_VERSION: 4 + + SPECIFICATION_VERSION_HISTORY: { -1 => ::Array["(RubyGems versions up to and including 0.7 did not have versioned specifications)"], 1 => ::Array["Deprecated \"test_suite_file\" in favor of the new, but equivalent, \"test_files\"" | "\"test_file=x\" is a shortcut for \"test_files=[x]\""], 2 => ::Array["Added \"required_rubygems_version\"" | "Now forward-compatible with future versions"], 3 => ::Array["Added Fixnum validation to the specification_version"], 4 => ::Array["Added sandboxed freeform metadata to the specification version."] } + + MARSHAL_FIELDS: { -1 => 16, 1 => 16, 2 => 16, 3 => 17, 4 => 18 } + + TODAY: untyped + + VALID_NAME_PATTERN: ::Regexp + + # rubocop:disable Style/MutableConstant + INITIALIZE_CODE_FOR_DEFAULTS: ::Hash[untyped, untyped] + + # Sentinel object to represent "not found" stubs + NOT_FOUND: untyped + + # Tracking removed method calls to warn users during build time. + REMOVED_METHODS: ::Array[:rubyforge_project= | :mark_version] + + # + # + def removed_method_calls: () -> untyped + + # + # This gem's name. + # + # Usage: + # + # spec.name = 'rake' + # + attr_accessor name: String + + # + # This gem's version. + # + # The version string can contain numbers and periods, such as `1.0.0`. A gem is + # a 'prerelease' gem if the version has a letter in it, such as `1.0.0.pre`. + # + # Usage: + # + # spec.version = '0.4.1' + # + attr_reader version: String + + # + # A short summary of this gem's description. Displayed in `gem list -d`. + # + # The #description should be more detailed than the summary. + # + # Usage: + # + # spec.summary = "This is a small summary of my gem" + # + attr_reader summary: untyped + + # + # Files included in this gem. You cannot append to this accessor, you must + # assign to it. + # + # Only add files you can require to this list, not directories, etc. + # + # Directories are automatically stripped from this list when building a gem, + # other non-files cause an error. + # + # Usage: + # + # require 'rake' + # spec.files = FileList['lib/**/*.rb', + # 'bin/*', + # '[A-Z]*'].to_a + # + # # or without Rake... + # spec.files = Dir['lib/**/*.rb'] + Dir['bin/*'] + # spec.files += Dir['[A-Z]*'] + # spec.files.reject! { |fn| fn.include? "CVS" } + # + def files: () -> Enumerable[String] + + # + # A list of authors for this gem. + # + # Alternatively, a single author can be specified by assigning a string to + # `spec.author` + # + # Usage: + # + # spec.authors = ['John Jones', 'Mary Smith'] + # + def authors=: (untyped value) -> untyped + + # + # The version of Ruby required by this gem + # + # Usage: + # + # spec.required_ruby_version = '>= 2.7.0' + # + attr_reader required_ruby_version: untyped + + # + # A long description of this gem + # + # The description should be more detailed than the summary but not excessively + # long. A few paragraphs is a recommended length with no examples or + # formatting. + # + # Usage: + # + # spec.description = <<-EOF + # Rake is a Make-like program implemented in Ruby. Tasks and + # dependencies are specified in standard Ruby syntax. + # EOF + # + attr_reader description: untyped + + # + # A contact email address (or addresses) for this gem + # + # Usage: + # + # spec.email = 'john.jones@example.com' + # spec.email = ['jack@example.com', 'jill@example.com'] + # + attr_accessor email: untyped + + # + # The URL of this gem's home page + # + # Usage: + # + # spec.homepage = 'https://github.com/ruby/rake' + # + attr_accessor homepage: untyped + + # + # The license for this gem. + # + # The license must be no more than 64 characters. + # + # This should just be the name of your license. The full text of the license + # should be inside of the gem (at the top level) when you build it. + # + # The simplest way is to specify the standard SPDX ID https://spdx.org/licenses/ + # for the license. Ideally, you should pick one that is OSI (Open Source + # Initiative) http://opensource.org/licenses/alphabetical approved. + # + # The most commonly used OSI-approved licenses are MIT and Apache-2.0. GitHub + # also provides a license picker at http://choosealicense.com/. + # + # You can also use a custom license file along with your gemspec and specify a + # LicenseRef-, where idstring is the name of the file containing the + # license text. + # + # You should specify a license for your gem so that people know how they are + # permitted to use it and any restrictions you're placing on it. Not specifying + # a license means all rights are reserved; others have no right to use the code + # for any purpose. + # + # You can set multiple licenses with #licenses= + # + # Usage: + # spec.license = 'MIT' + # + def license=: (untyped o) -> untyped + + # + # The license(s) for the library. + # + # Each license must be a short name, no more than 64 characters. + # + # This should just be the name of your license. The full text of the license + # should be inside of the gem when you build it. + # + # See #license= for more discussion + # + # Usage: + # spec.licenses = ['MIT', 'GPL-2.0'] + # + def licenses=: (untyped licenses) -> untyped + + # + # The metadata holds extra data for this gem that may be useful to other + # consumers and is settable by gem authors. + # + # Metadata items have the following restrictions: + # + # * The metadata must be a Hash object + # * All keys and values must be Strings + # * Keys can be a maximum of 128 bytes and values can be a maximum of 1024 + # bytes + # * All strings must be UTF-8, no binary data is allowed + # + # You can use metadata to specify links to your gem's homepage, codebase, + # documentation, wiki, mailing list, issue tracker and changelog. + # + # s.metadata = { + # "bug_tracker_uri" => "https://example.com/user/bestgemever/issues", + # "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md", + # "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1", + # "homepage_uri" => "https://bestgemever.example.io", + # "mailing_list_uri" => "https://groups.example.com/bestgemever", + # "source_code_uri" => "https://example.com/user/bestgemever", + # "wiki_uri" => "https://example.com/user/bestgemever/wiki" + # "funding_uri" => "https://example.com/donate" + # } + # + # These links will be used on your gem's page on rubygems.org and must pass + # validation against following regex. + # + # %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z} + # + attr_accessor metadata: untyped + + # + # Singular (alternative) writer for #authors + # + # Usage: + # + # spec.author = 'John Jones' + # + def author=: (untyped o) -> untyped + + # + # The path in the gem for executable scripts. Usually 'bin' + # + # Usage: + # + # spec.bindir = 'bin' + # + attr_accessor bindir: untyped + + # + # The certificate chain used to sign this gem. See Gem::Security for details. + # + attr_accessor cert_chain: untyped + + # + # A message that gets displayed after the gem is installed. + # + # Usage: + # + # spec.post_install_message = "Thanks for installing!" + # + attr_accessor post_install_message: untyped + + # + # The platform this gem runs on. + # + # This is usually Gem::Platform::RUBY or Gem::Platform::CURRENT. + # + # Most gems contain pure Ruby code; they should simply leave the default value + # in place. Some gems contain C (or other) code to be compiled into a Ruby + # "extension". The gem should leave the default value in place unless the code + # will only compile on a certain type of system. Some gems consist of + # pre-compiled code ("binary gems"). It's especially important that they set + # the platform attribute appropriately. A shortcut is to set the platform to + # Gem::Platform::CURRENT, which will cause the gem builder to set the platform + # to the appropriate value for the system on which the build is being performed. + # + # If this attribute is set to a non-default value, it will be included in the + # filename of the gem when it is built such as: nokogiri-1.6.0-x86-mingw32.gem + # + # Usage: + # + # spec.platform = Gem::Platform.local + # + def platform=: (untyped platform) -> untyped + + # + # Paths in the gem to add to `$LOAD_PATH` when this gem is activated. If you + # have an extension you do not need to add `"ext"` to the require path, the + # extension build process will copy the extension files into "lib" for you. + # + # The default value is `"lib"` + # + # Usage: + # + # # If all library files are in the root directory... + # spec.require_paths = ['.'] + # + def require_paths=: (untyped val) -> untyped + + # + # The RubyGems version required by this gem + # + attr_reader required_rubygems_version: untyped + + # + # The key used to sign this gem. See Gem::Security for details. + # + attr_accessor signing_key: untyped + + # + # Adds a development dependency named `gem` with `requirements` to this gem. + # + # Usage: + # + # spec.add_development_dependency 'example', '~> 1.1', '>= 1.1.4' + # + # Development dependencies aren't installed by default and aren't activated when + # a gem is required. + # + def add_development_dependency: (untyped gem, *untyped requirements) -> untyped + + # + # + def add_dependency: (untyped gem, *untyped requirements) -> untyped + + # + # Executables included in the gem. + # + # For example, the rake gem has rake as an executable. You don’t specify the + # full path (as in bin/rake); all application-style files are expected to be + # found in bindir. These files must be executable Ruby files. Files that use + # bash or other interpreters will not work. + # + # Executables included may only be ruby scripts, not scripts for other languages + # or compiled binaries. + # + # Usage: + # + # spec.executables << 'rake' + # + def executables: () -> untyped + + # + # Extensions to build when installing the gem, specifically the paths to + # extconf.rb-style files used to compile extensions. + # + # These files will be run when the gem is installed, causing the C (or whatever) + # code to be compiled on the user’s machine. + # + # Usage: + # + # spec.extensions << 'ext/rmagic/extconf.rb' + # + # See Gem::Ext::Builder for information about writing extensions for gems. + # + def extensions: () -> untyped + + # + # Extra files to add to RDoc such as README or doc/examples.txt + # + # When the user elects to generate the RDoc documentation for a gem (typically + # at install time), all the library files are sent to RDoc for processing. This + # option allows you to have some non-code files included for a more complete set + # of documentation. + # + # Usage: + # + # spec.extra_rdoc_files = ['README', 'doc/user-guide.txt'] + # + def extra_rdoc_files: () -> untyped + + def installed_by_version: () -> untyped + + def installed_by_version=: (untyped version) -> untyped + + # + # Specifies the rdoc options to be used when generating API documentation. + # + # Usage: + # + # spec.rdoc_options << '--title' << 'Rake -- Ruby Make' << + # '--main' << 'README' << + # '--line-numbers' + # + def rdoc_options: () -> untyped + + LATEST_RUBY_WITHOUT_PATCH_VERSIONS: untyped + + # + # The version of Ruby required by this gem. The ruby version can be specified + # to the patch-level: + # + # $ ruby -v -e 'p Gem.ruby_version' + # ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-darwin12.4.0] + # # + # + # Prereleases can also be specified. + # + # Usage: + # + # # This gem will work with 1.8.6 or greater... + # spec.required_ruby_version = '>= 1.8.6' + # + # # Only with final releases of major version 2 where minor version is at least 3 + # spec.required_ruby_version = '~> 2.3' + # + # # Only prereleases or final releases after 2.6.0.preview2 + # spec.required_ruby_version = '> 2.6.0.preview2' + # + # # This gem will work with 2.3.0 or greater, including major version 3, but lesser than 4.0.0 + # spec.required_ruby_version = '>= 2.3', '< 4' + # + def required_ruby_version=: (untyped req) -> untyped + + # + # The RubyGems version required by this gem + # + def required_rubygems_version=: (untyped req) -> untyped + + # + # Lists the external (to RubyGems) requirements that must be met for this gem to + # work. It's simply information for the user. + # + # Usage: + # + # spec.requirements << 'libmagick, v6.0' + # spec.requirements << 'A good graphics card' + # + def requirements: () -> untyped + + def test_files=: (untyped files) -> untyped + + # + # The version of RubyGems used to create this gem. + # + # Do not set this, it is set automatically when the gem is packaged. + # + attr_accessor rubygems_version: untyped + + def extensions_dir: () -> untyped + + # + # True when this gemspec has been activated. This attribute is not persisted. + # + attr_accessor activated: untyped + + # + # True when this gemspec has been activated. This attribute is not persisted. + # + alias activated? activated + + attr_accessor autorequire: untyped + + attr_writer default_executable: untyped + + attr_writer original_platform: untyped + + # + # The Gem::Specification version of this gemspec. + # + # Do not set this, it is set automatically when the gem is packaged. + # + attr_accessor specification_version: untyped + + def self._all: () -> untyped + + def self.clear_load_cache: () -> untyped + + def self.gem_path: () -> untyped + + def self.each_gemspec: (untyped dirs) { (untyped) -> untyped } -> untyped + + # + # + def self.gemspec_stubs_in: (untyped dir, untyped pattern) { (untyped) -> untyped } -> untyped + + def self.each_spec: (untyped dirs) { (untyped) -> untyped } -> untyped + + # + # Returns a Gem::StubSpecification for every installed gem + # + def self.stubs: () -> untyped + + # + # Returns a Gem::StubSpecification for default gems + # + def self.default_stubs: (?::String pattern) -> untyped + + # + # Returns a Gem::StubSpecification for installed gem named `name` only returns + # stubs that match Gem.platforms + # + def self.stubs_for: (untyped name) -> untyped + + # + # Finds stub specifications matching a pattern from the standard locations, + # optionally filtering out specs not matching the current platform + # + def self.stubs_for_pattern: (untyped pattern, ?bool match_platform) -> untyped + + def self._resort!: (untyped specs) -> untyped + + # + # Loads the default specifications. It should be called only once. + # + def self.load_defaults: () -> untyped + + # + # Adds `spec` to the known specifications, keeping the collection properly + # sorted. + # + def self.add_spec: (untyped spec) -> untyped + + # + # Removes `spec` from the known specs. + # + def self.remove_spec: (untyped spec) -> untyped + + # + # Returns all specifications. This method is discouraged from use. You probably + # want to use one of the Enumerable methods instead. + # + def self.all: () -> untyped + + # + # Sets the known specs to `specs`. Not guaranteed to work for you in the future. + # Use at your own risk. Caveat emptor. Doomy doom doom. Etc etc. + # + def self.all=: (untyped specs) -> untyped + + # + # Return full names of all specs in sorted order. + # + def self.all_names: () -> untyped + + # + # Return the list of all array-oriented instance variables. + # + def self.array_attributes: () -> untyped + + # + # Return the list of all instance variables. + # + def self.attribute_names: () -> untyped + + # + # Return the directories that Specification uses to find specs. + # + def self.dirs: () -> untyped + + # + # Set the directories that Specification uses to find specs. Setting this resets + # the list of known specs. + # + def self.dirs=: (untyped dirs) -> untyped + + extend Enumerable[Gem::Specification] + + # + # Enumerate every known spec. See ::dirs= and ::add_spec to set the list of + # specs. + # + def self.each: () { (Gem::Specification) -> untyped } -> untyped + + # + # Returns every spec that matches `name` and optional `requirements`. + # + def self.find_all_by_name: (untyped name, *untyped requirements) -> untyped + + # + # Returns every spec that has the given `full_name` + # + def self.find_all_by_full_name: (untyped full_name) -> untyped + + # + # Find the best specification matching a `name` and `requirements`. Raises if + # the dependency doesn't resolve to a valid specification. + # + def self.find_by_name: (String name, *untyped requirements) -> instance + + # + # Find the best specification matching a +full_name+. + def self.find_by_full_name: (untyped full_name) -> instance + + # + # Return the best specification that contains the file matching `path`. + # + def self.find_by_path: (String path) -> instance + + # + # Return the best specification that contains the file matching `path` amongst + # the specs that are not activated. + # + def self.find_inactive_by_path: (untyped path) -> untyped + + # + # + def self.find_active_stub_by_path: (untyped path) -> untyped + + # + # Return currently unresolved specs that contain the file matching `path`. + # + def self.find_in_unresolved: (untyped path) -> untyped + + # + # Search through all unresolved deps and sub-dependencies and return specs that + # contain the file matching `path`. + # + def self.find_in_unresolved_tree: (untyped path) -> (untyped | ::Array[untyped]) + + # + # + def self.unresolved_specs: () -> untyped + + # + # Special loader for YAML files. When a Specification object is loaded from a + # YAML file, it bypasses the normal Ruby object initialization routine + # (#initialize). This method makes up for that and deals with gems of different + # ages. + # + # `input` can be anything that YAML.load() accepts: String or IO. + # + def self.from_yaml: (untyped input) -> untyped + + # + # Return the latest specs, optionally including prerelease specs if `prerelease` + # is true. + # + def self.latest_specs: (?bool prerelease) -> untyped + + # + # Return the latest installed spec for gem `name`. + # + def self.latest_spec_for: (untyped name) -> untyped + + def self._latest_specs: (untyped specs, ?bool prerelease) -> untyped + + # + # Loads Ruby format gemspec from `file`. + # + def self.load: (untyped file) -> (nil | untyped) + + # + # Specification attributes that must be non-nil + # + def self.non_nil_attributes: () -> untyped + + # + # Make sure the YAML specification is properly formatted with dashes + # + def self.normalize_yaml_input: (untyped input) -> untyped + + # + # Return a list of all outdated local gem names. This method is HEAVY as it + # must go fetch specifications from the server. + # + # Use outdated_and_latest_version if you wish to retrieve the latest remote + # version as well. + # + def self.outdated: () -> untyped + + # + # Enumerates the outdated local gems yielding the local specification and the + # latest remote version. + # + # This method may take some time to return as it must check each local gem + # against the server's index. + # + def self.outdated_and_latest_version: () ?{ (untyped) -> untyped } -> (untyped | nil) + + # + # Is `name` a required attribute? + # + def self.required_attribute?: (untyped name) -> untyped + + # + # Required specification attributes + # + def self.required_attributes: () -> untyped + + # + # Reset the list of known specs, running pre and post reset hooks registered in + # Gem. + # + def self.reset: () -> untyped + + def self.specification_record: () -> untyped + + # + # DOC: This method needs documented or nodoc'd + # + def self.unresolved_deps: () -> untyped + + # + # Load custom marshal format, re-initializing defaults as needed + # + def self._load: (untyped str) -> untyped + + def <=>: (untyped other) -> untyped + + def ==: (untyped other) -> untyped + + # + # Dump only crucial instance variables. + # + def _dump: (untyped limit) -> untyped + + # + # Activate this spec, registering it as a loaded spec and adding it's lib paths + # to $LOAD_PATH. Returns true if the spec was activated, false if it was + # previously activated. Freaks out if there are conflicts upon activation. + # + def activate: () -> (false | true) + + # + # Activate all unambiguously resolved runtime dependencies of this spec. Add any + # ambiguous dependencies to the unresolved list to be resolved later, as needed. + # + def activate_dependencies: () -> untyped + + # + # Abbreviate the spec for downloading. Abbreviated specs are only used for + # searching, downloading and related activities and do not need deployment + # specific information (e.g. list of files). So we abbreviate the spec, making + # it much smaller for quicker downloads. + # + def abbreviate: () -> untyped + + # + # Sanitize the descriptive fields in the spec. Sometimes non-ASCII characters + # will garble the site index. Non-ASCII characters will be replaced by their + # XML entity equivalent. + # + def sanitize: () -> untyped + + # + # Sanitize a single string. + # + def sanitize_string: (untyped string) -> untyped + + # + # Returns an array with bindir attached to each executable in the `executables` + # list + # + def add_bindir: (untyped executables) -> untyped + + private + + # + # Adds a dependency on gem `dependency` with type `type` that requires + # `requirements`. Valid types are currently `:runtime` and `:development`. + # + def add_dependency_with_type: (untyped dependency, untyped type, untyped requirements) -> untyped + + public + + # + # Adds a runtime dependency named `gem` with `requirements` to this gem. + # + # Usage: + # + # spec.add_runtime_dependency 'example', '~> 1.1', '>= 1.1.4' + # + alias add_runtime_dependency add_dependency + + # + # Adds this spec's require paths to LOAD_PATH, in the proper location. + # + def add_self_to_load_path: () -> (nil | untyped) + + # + # Singular reader for #authors. Returns the first author in the list + # + def author: () -> untyped + + # + # The list of author names who wrote this gem. + # + # spec.authors = ['Chad Fowler', 'Jim Weirich', 'Rich Kilmer'] + # + def authors: () -> untyped + + # + # Returns the full path to installed gem's bin directory. + # + # NOTE: do not confuse this with `bindir`, which is just 'bin', not a full path. + # + def bin_dir: () -> untyped + + # + # Returns the full path to an executable named `name` in this gem. + # + def bin_file: (untyped name) -> untyped + + # + # Returns the build_args used to install the gem + # + def build_args: () -> (untyped | ::Array[untyped]) + + def build_extensions: () -> (nil | untyped) + + # + # Returns the full path to the build info directory + # + def build_info_dir: () -> untyped + + # + # Returns the full path to the file containing the build information generated + # when the gem was installed + # + def build_info_file: () -> untyped + + # + # Returns the full path to the cache directory containing this spec's cached + # gem. + # + def cache_dir: () -> untyped + + # + # Returns the full path to the cached gem for this spec. + # + def cache_file: () -> untyped + + # + # Return any possible conflicts against the currently loaded specs. + # + def conflicts: () -> untyped + + def conficts_when_loaded_with?: (untyped list_of_specs) -> untyped + + # + # Return true if there are possible conflicts against the currently loaded + # specs. + # + def has_conflicts?: () -> untyped + + # + # The date this gem was created. + # + # If SOURCE_DATE_EPOCH is set as an environment variable, use that to support + # reproducible builds; otherwise, default to the current UTC date. + # + # Details on SOURCE_DATE_EPOCH: + # https://reproducible-builds.org/specs/source-date-epoch/ + # + def date: () -> untyped + + DateLike: untyped + + def self.===: (untyped obj) -> untyped + + DateTimeFormat: ::Regexp + + # + # The date this gem was created + # + # DO NOT set this, it is set automatically when the gem is packaged. + # + def date=: (untyped date) -> untyped + + def default_executable: () -> untyped + + # + # The default value for specification attribute `name` + # + def default_value: (untyped name) -> untyped + + # + # A list of Gem::Dependency objects this gem depends on. + # + # Use #add_dependency or #add_development_dependency to add dependencies to a + # gem. + # + def dependencies: () -> Array[Gem::Dependency] + + # + # Return a list of all gems that have a dependency on this gemspec. The list is + # structured with entries that conform to: + # + # [depending_gem, dependency, [list_of_gems_that_satisfy_dependency]] + # + def dependent_gems: (?bool check_dev) -> untyped + + # + # Returns all specs that matches this spec's runtime dependencies. + # + def dependent_specs: () -> untyped + + # + # A detailed description of this gem. See also #summary + # + def description=: (untyped str) -> untyped + + # + # List of dependencies that are used for development + # + def development_dependencies: () -> Array[Gem::Dependency] + + # + # Returns the full path to this spec's documentation directory. If `type` is + # given it will be appended to the end. For example: + # + # spec.doc_dir # => "/path/to/gem_repo/doc/a-1" + # + # spec.doc_dir 'ri' # => "/path/to/gem_repo/doc/a-1/ri" + # + def doc_dir: (?untyped? type) -> untyped + + def encode_with: (untyped coder) -> untyped + + def eql?: (untyped other) -> untyped + + # + # Singular accessor for #executables + # + def executable: () -> untyped + + # + # Singular accessor for #executables + # + def executable=: (untyped o) -> untyped + + # + # Sets executables to `value`, ensuring it is an array. + # + def executables=: (untyped value) -> untyped + + # + # Sets extensions to `extensions`, ensuring it is an array. + # + def extensions=: (untyped extensions) -> untyped + + # + # Sets extra_rdoc_files to `files`, ensuring it is an array. + # + def extra_rdoc_files=: (untyped files) -> untyped + + # + # The default (generated) file name of the gem. See also #spec_name. + # + # spec.file_name # => "example-1.0.gem" + # + def file_name: () -> ::String + + # + # Sets files to `files`, ensuring it is an array. + # + def files=: (untyped files) -> untyped + + private + + # + # Finds all gems that satisfy `dep` + # + def find_all_satisfiers: (untyped dep) { (untyped) -> untyped } -> untyped + + public + + # + # Creates a duplicate spec without large blobs that aren't used at runtime. + # + def for_cache: () -> untyped + + # + # + def full_name: () -> untyped + + def gem_dir: () -> untyped + + # + # + def gems_dir: () -> untyped + + def has_rdoc: () -> true + + def has_rdoc=: (untyped ignored) -> untyped + + alias has_rdoc? has_rdoc + + def has_unit_tests?: () -> untyped + + # :stopdoc: + alias has_test_suite? has_unit_tests? + + def hash: () -> untyped + + def init_with: (untyped coder) -> untyped + + # + # Specification constructor. Assigns the default values to the attributes and + # yields itself for further initialization. Optionally takes `name` and + # `version`. + # + def initialize: (?untyped? name, ?untyped? version) ?{ (untyped) -> untyped } -> void + + # + # Duplicates array_attributes from `other_spec` so state isn't shared. + # + def initialize_copy: (untyped other_spec) -> untyped + + # + # + def base_dir: () -> untyped + + private + + # + # Expire memoized instance variables that can incorrectly generate, replace or + # miss files due changes in certain attributes used to compute them. + # + def invalidate_memoized_attributes: () -> untyped + + public + + def inspect: () -> (untyped | ::String) + + # + # Files in the Gem under one of the require_paths + # + def lib_files: () -> untyped + + # + # Singular accessor for #licenses + # + def license: () -> untyped + + # + # Plural accessor for setting licenses + # + # See #license= for details + # + def licenses: () -> untyped + + def internal_init: () -> untyped + + def method_missing: (untyped sym, *untyped a) { (?) -> untyped } -> (nil | untyped) + + # + # Is this specification missing its extensions? When this returns true you + # probably want to build_extensions + # + def missing_extensions?: () -> (false | true) + + # + # Normalize the list of files so that: + # * All file lists have redundancies removed. + # * Files referenced in the extra_rdoc_files are included in the package file + # list. + # + def normalize: () -> untyped + + # + # Return a NameTuple that represents this Specification + # + def name_tuple: () -> untyped + + def original_name: () -> ::String + + def original_platform: () -> untyped + + # + # The platform this gem runs on. See Gem::Platform for details. + # + def platform: () -> untyped + + def pretty_print: (untyped q) -> untyped + + private + + def check_version_conflict: (untyped other) -> (nil | untyped) + + public + + def raise_if_conflicts: () -> (untyped | nil) + + # + # Sets rdoc_options to `value`, ensuring it is an array. + # + def rdoc_options=: (untyped options) -> untyped + + # + # Singular accessor for #require_paths + # + def require_path: () -> untyped + + # + # Singular accessor for #require_paths + # + def require_path=: (untyped path) -> untyped + + # + # Set requirements to `req`, ensuring it is an array. + # + def requirements=: (untyped req) -> untyped + + def respond_to_missing?: (untyped m, ?bool include_private) -> false + + # + # Returns the full path to this spec's ri directory. + # + def ri_dir: () -> untyped + + private + + # + # Return a string containing a Ruby code representation of the given object. + # + def ruby_code: (untyped obj) -> untyped + + public + + # + # List of dependencies that will automatically be activated at runtime. + # + def runtime_dependencies: () -> untyped + + private + + # + # True if this gem has the same attributes as `other`. + # + def same_attributes?: (untyped spec) -> untyped + + public + + # + # Checks if this specification meets the requirement of `dependency`. + # + def satisfies_requirement?: (untyped dependency) -> untyped + + # + # Returns an object you can use to sort specifications in #sort_by. + # + def sort_obj: () -> ::Array[untyped] + + def source: () -> untyped + + # + # Returns the full path to the directory containing this spec's gemspec file. + # eg: /usr/local/lib/ruby/gems/1.8/specifications + # + def spec_dir: () -> untyped + + # + # Returns the full path to this spec's gemspec file. eg: + # /usr/local/lib/ruby/gems/1.8/specifications/mygem-1.0.gemspec + # + def spec_file: () -> untyped + + # + # The default name of the gemspec. See also #file_name + # + # spec.spec_name # => "example-1.0.gemspec" + # + def spec_name: () -> ::String + + # + # A short summary of this gem's description. + # + def summary=: (untyped str) -> untyped + + def test_file: () -> untyped + + def test_file=: (untyped file) -> untyped + + def test_files: () -> untyped + + # + # Returns a Ruby code representation of this specification, such that it can be + # eval'ed and reconstruct the same specification later. Attributes that still + # have their default values are omitted. + # + def to_ruby: () -> untyped + + # + # Returns a Ruby lighter-weight code representation of this specification, used + # for indexing only. + # + # See #to_ruby. + # + def to_ruby_for_cache: () -> untyped + + def to_s: () -> ::String + + # + # Returns self + # + def to_spec: () -> self + + def to_yaml: (?::Hash[untyped, untyped] opts) -> untyped + + # + # Recursively walk dependencies of this spec, executing the `block` for each + # hop. + # + def traverse: (?untyped trail, ?::Hash[untyped, untyped] visited) { (?) -> untyped } -> untyped + + # + # Checks that the specification contains all required fields, and does a very + # basic sanity check. + # + # Raises InvalidSpecificationException if the spec does not pass the checks.. + # + def validate: (?bool packaging, ?bool strict) -> untyped + + # + # + def keep_only_files_and_directories: () -> untyped + + def validate_for_resolution: () -> untyped + + # + # + def validate_metadata: () -> untyped + + # + # + def validate_dependencies: () -> untyped + + # + # + def validate_permissions: () -> untyped + + # + # Set the version to `version`, potentially also setting + # required_rubygems_version if `version` indicates it is a prerelease. + # + def version=: (untyped version) -> (nil | untyped) + + # + # + def stubbed?: () -> false + + def yaml_initialize: (untyped tag, untyped vals) -> untyped + + # + # Reset nil attributes to their default values to make the spec valid + # + def reset_nil_attributes_to_default: () -> nil + + def flatten_require_paths: () -> (nil | untyped) + + def raw_require_paths: () -> untyped +end diff --git a/rbs/fills/tuple.rbs b/rbs/fills/tuple/tuple.rbs similarity index 100% rename from rbs/fills/tuple.rbs rename to rbs/fills/tuple/tuple.rbs diff --git a/rbs_collection.yaml b/rbs_collection.yaml index 66e30ecfe..898239cac 100644 --- a/rbs_collection.yaml +++ b/rbs_collection.yaml @@ -1,15 +1,15 @@ # Download sources sources: + - type: local + name: shims + path: sig/shims + - type: git name: ruby/gem_rbs_collection remote: https://github.com/ruby/gem_rbs_collection.git revision: main repo_dir: gems -# You can specify local directories as sources also. -# - type: local -# path: path/to/your/local/repository - # A directory to install the downloaded RBSs path: .gem_rbs_collection diff --git a/sig/shims/ast/0/node.rbs b/sig/shims/ast/0/node.rbs new file mode 100644 index 000000000..fab1a4de0 --- /dev/null +++ b/sig/shims/ast/0/node.rbs @@ -0,0 +1,5 @@ +module ::AST + class Node + def children: () -> [self, Integer, String, Symbol, nil] + end +end diff --git a/sig/shims/ast/2.4/.rbs_meta.yaml b/sig/shims/ast/2.4/.rbs_meta.yaml new file mode 100644 index 000000000..f361b3112 --- /dev/null +++ b/sig/shims/ast/2.4/.rbs_meta.yaml @@ -0,0 +1,9 @@ +--- +name: ast +version: '2.4' +source: + type: git + name: ruby/gem_rbs_collection + revision: c604d278dd6c14a1bb6cf0c0051af643b268a981 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems diff --git a/sig/shims/ast/2.4/ast.rbs b/sig/shims/ast/2.4/ast.rbs new file mode 100644 index 000000000..e7fb8975e --- /dev/null +++ b/sig/shims/ast/2.4/ast.rbs @@ -0,0 +1,73 @@ +module AST + interface _ToAst + def to_ast: () -> Node + end + + interface _ToSym + def to_sym: () -> Symbol + end + + class Node + public + + attr_reader children: Array[Node] + attr_reader hash: String + attr_reader type: Symbol + + alias + concat + + alias << append + + def ==: (untyped other) -> bool + + def append: (untyped element) -> self + + alias clone dup + + def concat: (_ToA[untyped] array) -> self + + def dup: () -> self + + def eql?: (untyped other) -> bool + + def inspect: (?Integer indent) -> String + + alias to_a children + + def to_ast: () -> self + + alias to_s to_sexp + + def to_sexp: (?Integer indent) -> String + + def to_sexp_array: () -> Array[untyped] + + def updated: (?_ToSym? `type`, ?_ToA[untyped]? children, ?Hash[Symbol, untyped]? properties) -> self + + private + + def initialize: (_ToSym `type`, ?_ToA[untyped]? children, ?Hash[Symbol, untyped] properties) -> void + + alias original_dup dup + end + + class Processor + include Mixin + + module Mixin + public + + def handler_missing: (Node node) -> Node? + + def process: (_ToAst? node) -> Node? + + def process_all: (Array[_ToAst] nodes) -> Array[Node] + end + end + + module Sexp + public + + def s: (_ToSym `type`, *untyped children) -> Node + end +end diff --git a/sig/shims/parser/3.2.0.1/builders/default.rbs b/sig/shims/parser/3.2.0.1/builders/default.rbs new file mode 100644 index 000000000..861a9e371 --- /dev/null +++ b/sig/shims/parser/3.2.0.1/builders/default.rbs @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +module Parser + ## + # Default AST builder. Uses {AST::Node}s. + # + module Builders + class Default + ## + # AST compatibility attribute; since `-> {}` is not semantically + # equivalent to `lambda {}`, all new code should set this attribute + # to true. + # + # If set to false (the default), `-> {}` is emitted as + # `s(:block, s(:send, nil, :lambda), s(:args), nil)`. + # + # If set to true, `-> {}` is emitted as + # `s(:block, s(:lambda), s(:args), nil)`. + # + # @return [Boolean] + attr_accessor self.emit_lambda: bool + + ## + # AST compatibility attribute; block arguments of `m { |a| }` are + # not semantically equivalent to block arguments of `m { |a,| }` or `m { |a, b| }`, + # all new code should set this attribute to true. + # + # If set to false (the default), arguments of `m { |a| }` are emitted as + # `s(:args, s(:arg, :a))`. + # + # If set to true, arguments of `m { |a| }` are emitted as + # `s(:args, s(:procarg0, :a)). + # + # @return [Boolean] + attr_accessor self.emit_procarg0: bool + + ## + # AST compatibility attribute; locations of `__ENCODING__` are not the same + # as locations of `Encoding::UTF_8` causing problems during rewriting, + # all new code should set this attribute to true. + # + # If set to false (the default), `__ENCODING__` is emitted as + # ` s(:const, s(:const, nil, :Encoding), :UTF_8)`. + # + # If set to true, `__ENCODING__` is emitted as + # `s(:__ENCODING__)`. + # + # @return [Boolean] + attr_accessor self.emit_encoding: bool + + ## + # AST compatibility attribute; indexed assignment, `x[] = 1`, is not + # semantically equivalent to calling the method directly, `x.[]=(1)`. + # Specifically, in the former case, the expression's value is always 1, + # and in the latter case, the expression's value is the return value + # of the `[]=` method. + # + # If set to false (the default), `self[1]` is emitted as + # `s(:send, s(:self), :[], s(:int, 1))`, and `self[1] = 2` is + # emitted as `s(:send, s(:self), :[]=, s(:int, 1), s(:int, 2))`. + # + # If set to true, `self[1]` is emitted as + # `s(:index, s(:self), s(:int, 1))`, and `self[1] = 2` is + # emitted as `s(:indexasgn, s(:self), s(:int, 1), s(:int, 2))`. + # + # @return [Boolean] + attr_accessor self.emit_index: bool + + ## + # AST compatibility attribute; causes a single non-mlhs + # block argument to be wrapped in s(:procarg0). + # + # If set to false (the default), block arguments `|a|` are emitted as + # `s(:args, s(:procarg0, :a))` + # + # If set to true, block arguments `|a|` are emitted as + # `s(:args, s(:procarg0, s(:arg, :a))` + # + # @return [Boolean] + attr_accessor self.emit_arg_inside_procarg0: bool + + ## + # AST compatibility attribute; arguments forwarding initially + # didn't have support for leading arguments + # (i.e. `def m(a, ...); end` was a syntax error). However, Ruby 3.0 + # added support for any number of arguments in front of the `...`. + # + # If set to false (the default): + # 1. `def m(...) end` is emitted as + # s(:def, :m, s(:forward_args), nil) + # 2. `def m(a, b, ...) end` is emitted as + # s(:def, :m, + # s(:args, s(:arg, :a), s(:arg, :b), s(:forward_arg))) + # + # If set to true it uses a single format: + # 1. `def m(...) end` is emitted as + # s(:def, :m, s(:args, s(:forward_arg))) + # 2. `def m(a, b, ...) end` is emitted as + # s(:def, :m, s(:args, s(:arg, :a), s(:arg, :b), s(:forward_arg))) + # + # It does't matter that much on 2.7 (because there can't be any leading arguments), + # but on 3.0 it should be better enabled to use a single AST format. + # + # @return [Boolean] + attr_accessor self.emit_forward_arg: bool + + ## + # AST compatibility attribute; Starting from Ruby 2.7 keyword arguments + # of method calls that are passed explicitly as a hash (i.e. with curly braces) + # are treated as positional arguments and Ruby 2.7 emits a warning on such method + # call. Ruby 3.0 given an ArgumentError. + # + # If set to false (the default) the last hash argument is emitted as `hash`: + # + # ``` + # (send nil :foo + # (hash + # (pair + # (sym :bar) + # (int 42)))) + # ``` + # + # If set to true it is emitted as `kwargs`: + # + # ``` + # (send nil :foo + # (kwargs + # (pair + # (sym :bar) + # (int 42)))) + # ``` + # + # Note that `kwargs` node is just a replacement for `hash` argument, + # so if there's are multiple arguments (or a `kwsplat`) all of them + # are wrapped into `kwargs` instead of `hash`: + # + # ``` + # (send nil :foo + # (kwargs + # (pair + # (sym :a) + # (int 42)) + # (kwsplat + # (send nil :b)) + # (pair + # (sym :c) + # (int 10)))) + # ``` + attr_accessor self.emit_kwargs: bool + + ## + # AST compatibility attribute; Starting from 3.0 Ruby returns + # true/false from single-line pattern matching with `in` keyword. + # + # Before 3.0 there was an exception if given value doesn't match pattern. + # + # NOTE: This attribute affects only Ruby 2.7 grammar. + # 3.0 grammar always emits `match_pattern`/`match_pattern_p` + # + # If compatibility attribute set to false `foo in bar` is emitted as `in_match`: + # + # ``` + # (in-match + # (send nil :foo) + # (match-var :bar)) + # ``` + # + # If set to true it's emitted as `match_pattern_p`: + # ``` + # (match-pattern-p + # (send nil :foo) + # (match-var :bar)) + # ``` + attr_accessor self.emit_match_pattern: bool + + ## + # If set to true (the default), `__FILE__` and `__LINE__` are transformed to + # literal nodes. For example, `s(:str, "lib/foo.rb")` and `s(:int, 10)`. + # + # If set to false, `__FILE__` and `__LINE__` are emitted as-is, + # i.e. as `s(:__FILE__)` and `s(:__LINE__)` nodes. + # + # Source maps are identical in both cases. + # + # @return [Boolean] + attr_accessor emit_file_line_as_literals: bool + + def value: (untyped token) -> untyped + + def string_value: (untyped token) -> String + + def loc: (untyped token) -> untyped + end + end +end diff --git a/sig/shims/parser/3.2.0.1/manifest.yaml b/sig/shims/parser/3.2.0.1/manifest.yaml new file mode 100644 index 000000000..f00038381 --- /dev/null +++ b/sig/shims/parser/3.2.0.1/manifest.yaml @@ -0,0 +1,7 @@ +# manifest.yaml describes dependencies which do not appear in the gemspec. +# If this gem includes such dependencies, comment-out the following lines and +# declare the dependencies. +# If all dependencies appear in the gemspec, you should remove this file. +# +dependencies: + - name: ast diff --git a/sig/shims/parser/3.2.0.1/parser.rbs b/sig/shims/parser/3.2.0.1/parser.rbs new file mode 100644 index 000000000..065fcf5ca --- /dev/null +++ b/sig/shims/parser/3.2.0.1/parser.rbs @@ -0,0 +1,199 @@ +module Parser + CurrentRuby: Parser::Base + + class SyntaxError < StandardError + end + class UnknownEncodingInMagicComment < StandardError + end + + class Base < Racc::Parser + def version: -> Integer + def self.parse: (String string, ?String file, ?Integer line) -> Parser::AST::Node? + def self.parse_with_comments: (String string, ?String file, ?Integer line) -> [Parser::AST::Node?, Array[Source::Comment]] + def parse: (Parser::Source::Buffer source_buffer) -> Parser::AST::Node? + end + + class Ruby18 < Base + end + class Ruby19 < Base + end + class Ruby20 < Base + end + class Ruby21 < Base + end + class Ruby22 < Base + end + class Ruby23 < Base + end + class Ruby24 < Base + end + class Ruby25 < Base + end + class Ruby26 < Base + end + class Ruby27 < Base + end + class Ruby30 < Base + end + class Ruby31 < Base + end + class Ruby32 < Base + end + class Ruby33 < Base + end + + module AST + class Node < ::AST::Node + attr_reader location: Source::Map + alias loc location + end + + class Processor + module Mixin + def process: (Node? node) -> Node? + end + + include Mixin + end + end + + module Source + class Range + attr_reader source_buffer: Buffer + attr_reader begin_pos: Integer + attr_reader end_pos: Integer + def begin: () -> Range + def end: () -> Range + def size: () -> Integer + alias length size + def line: () -> Integer + alias first_line line + def column: () -> Integer + def last_line: () -> Integer + def last_column: () -> Integer + def column_range: () -> ::Range[Integer] + def source_line: () -> String + def source: () -> String + def with: (?begin_pos: Integer, ?end_pos: Integer) -> Range + def adjust: (?begin_pos: Integer, ?end_pos: Integer) -> Range + def resize: (Integer new_size) -> Range + def join: (Range other) -> Range + def intersect: (Range other) -> Range? + def disjoint?: (Range other) -> bool + def overlaps?: (Range other) -> bool + def contains?: (Range other) -> bool + def contained?: (Range other) -> bool + def crossing?: (Range other) -> bool + def empty?: () -> bool + end + + ## + # A buffer with source code. {Buffer} contains the source code itself, + # associated location information (name and first line), and takes care + # of encoding. + # + # A source buffer is immutable once populated. + # + # @!attribute [r] name + # Buffer name. If the buffer was created from a file, the name corresponds + # to relative path to the file. + # @return [String] buffer name + # + # @!attribute [r] first_line + # First line of the buffer, 1 by default. + # @return [Integer] first line + # + # @api public + # + class Buffer + attr_reader name: String + attr_reader first_line: Integer + + def self.recognize_encoding: (String) -> Encoding + def self.reencode_string: (String) -> String + + def initialize: (untyped name, ?Integer first_line, ?source: untyped) -> void + def read: () -> self + def source: () -> String + def source=: (String) -> String + def raw_source: (String) -> String + def decompose_position: (Integer) -> [Integer, Integer] + def source_lines: () -> Array[String] + def source_line: (Integer) -> String + def line_range: (Integer) -> ::Range[Integer] + def source_range: () -> ::Range[Integer] + def last_line: () -> Integer + end + + class TreeRewriter + def replace: (Range range, String content) -> self + def remove: (Range range) -> self + def insert_before: (Range range, String content) -> self + def insert_after: (Range range, String content) -> self + end + + class Map + attr_reader node: AST::Node | nil + attr_reader expression: Range + def line: () -> Integer + def first_line: () -> Integer + def last_line: () -> Integer + def column: () -> Integer + def last_column: () -> Integer + end + + class Map::Collection < Map + attr_reader begin: Range? + attr_reader end: Range? + end + + class Map::Condition < Map + attr_reader keyword: Range + attr_reader begin: Range? + attr_reader else: Range? + attr_reader end: Range + end + + class Map::Heredoc < Map + attr_reader heredoc_body: Range + attr_reader heredoc_end: Range + end + + class Map::Keyword < Map + attr_reader keyword: Range + attr_reader begin: Range? + attr_reader end: Range? + end + + class Map::MethodDefinition < Map + attr_reader keyword: Range + attr_reader operator: Range? + attr_reader name: Range? + attr_reader end: Range? + attr_reader assignment: Range? + end + + class Map::Operator < Map + attr_reader operator: Range? + end + + class Map::Send < Map + attr_reader dot: Range? + attr_reader selector: Range + attr_reader operator: Range? + attr_reader begin: Range? + attr_reader end: Range? + end + + class Map::Ternary < Map + attr_reader question: Range? + attr_reader colon: Range + end + + class Comment + attr_reader text: String + attr_reader location: Map + alias loc location + end + end +end diff --git a/sig/shims/parser/3.2.0.1/polyfill.rbs b/sig/shims/parser/3.2.0.1/polyfill.rbs new file mode 100644 index 000000000..2e8c12487 --- /dev/null +++ b/sig/shims/parser/3.2.0.1/polyfill.rbs @@ -0,0 +1,4 @@ +module Racc + class Parser + end +end diff --git a/sig/shims/thor/1.2.0.1/.rbs_meta.yaml b/sig/shims/thor/1.2.0.1/.rbs_meta.yaml new file mode 100644 index 000000000..27c2fb2e4 --- /dev/null +++ b/sig/shims/thor/1.2.0.1/.rbs_meta.yaml @@ -0,0 +1,9 @@ +--- +name: thor +version: '1.2' +source: + type: git + name: ruby/gem_rbs_collection + revision: 98541aabafdf403b16ebae6fe4060d18bee75e93 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems diff --git a/sig/shims/thor/1.2.0.1/manifest.yaml b/sig/shims/thor/1.2.0.1/manifest.yaml new file mode 100644 index 000000000..a0cb4d149 --- /dev/null +++ b/sig/shims/thor/1.2.0.1/manifest.yaml @@ -0,0 +1,7 @@ +# manifest.yaml describes dependencies which do not appear in the gemspec. +# If this gem includes such dependencies, comment-out the following lines and +# declare the dependencies. +# If all dependencies appear in the gemspec, you should remove this file. +# +# dependencies: +# - name: pathname diff --git a/sig/shims/thor/1.2.0.1/thor.rbs b/sig/shims/thor/1.2.0.1/thor.rbs new file mode 100644 index 000000000..2247507e7 --- /dev/null +++ b/sig/shims/thor/1.2.0.1/thor.rbs @@ -0,0 +1,17 @@ +class Thor + class Group + end + + module Actions + class CreateFile + end + + def create_file: (String destination, String data, ?verbose: bool) -> String + | (String destination, ?verbose: bool) { () -> String } -> String + end + + class Error + end + + def self.start: (Array[String] given_args, ?Hash[Symbol, untyped] config) -> void +end diff --git a/solargraph.gemspec b/solargraph.gemspec index e6bb9394a..ca39ebd30 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -11,7 +11,10 @@ Gem::Specification.new do |s| s.authors = ["Fred Snyder"] s.email = 'admin@castwide.com' s.files = Dir.chdir(File.expand_path('..', __FILE__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + # @sg-ignore Need backtick support + # @type [String] + all_files = `git ls-files -z` + all_files.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end s.homepage = 'https://solargraph.org' s.license = 'MIT' @@ -23,6 +26,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 3.0' + s.add_runtime_dependency 'ast', '~> 2.4.3' s.add_runtime_dependency 'backport', '~> 1.2' s.add_runtime_dependency 'benchmark', '~> 0.4' s.add_runtime_dependency 'bundler', '~> 2.0' @@ -33,11 +37,12 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'logger', '~> 1.6' s.add_runtime_dependency 'observer', '~> 0.1' s.add_runtime_dependency 'ostruct', '~> 0.6' + s.add_runtime_dependency 'open3', '~> 0.2.1' s.add_runtime_dependency 'parser', '~> 3.0' s.add_runtime_dependency 'prism', '~> 1.4' s.add_runtime_dependency 'rbs', ['>= 3.6.1', '<= 4.0.0.dev.4'] s.add_runtime_dependency 'reverse_markdown', '~> 3.0' - s.add_runtime_dependency 'rubocop', '~> 1.76' + s.add_runtime_dependency 'sord', '~> 7.0' s.add_runtime_dependency 'thor', '~> 1.0' s.add_runtime_dependency 'tilt', '~> 2.0' s.add_runtime_dependency 'yard', '~> 0.9', '>= 0.9.24' @@ -48,6 +53,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'public_suffix', '~> 3.1' s.add_development_dependency 'rake', '~> 13.2' s.add_development_dependency 'rspec', '~> 3.5' + s.add_development_dependency 'rspec-time-guard', '~> 0.2.0' s.add_development_dependency 'rubocop-rake', '~> 0.7' s.add_development_dependency 'rubocop-rspec', '~> 3.6' s.add_development_dependency 'rubocop-yard', '~> 1.0' diff --git a/spec/api_map/api_map_method_spec.rb b/spec/api_map/api_map_method_spec.rb new file mode 100644 index 000000000..389646124 --- /dev/null +++ b/spec/api_map/api_map_method_spec.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +require 'tmpdir' + +describe Solargraph::ApiMap do + let(:api_map) { described_class.new } + let(:bench) do + Solargraph::Bench.new(external_requires: external_requires, workspace: Solargraph::Workspace.new('.')) + end + let(:external_requires) { [] } + + before do + api_map.catalog bench + end + + describe '.load_with_cache' do + it 'loads the API map with cache', time_limit_seconds: 120 do + Solargraph::PinCache.uncache_core + + output = Dir.mktmpdir do |dir| + capture_both do + described_class.load_with_cache(dir) + end + end + + expect(output).to include('aching RBS pins for Ruby core') + end + end + + describe '#qualify' do + let(:external_requires) { ['yaml'] } + + it 'resolves YAML to Psych' do + expect(api_map.qualify('YAML', '')).to eq('Psych') + end + + it 'resolves constants used to alias namespaces' do + map = Solargraph::SourceMap.load_string(%( + class Foo + def bing; end + end + + module Bar + Baz = ::Foo + end + )) + api_map.index map.pins + fqns = api_map.qualify('Bar::Baz') + expect(fqns).to eq('Foo') + end + + it 'understands alias namespaces resolving types' do + source = Solargraph::Source.load_string(%( + class Foo + # @return [Symbol] + def bing; end + end + + module Bar + Baz = ::Foo + end + + a = Bar::Baz.new.bing + a + Bar::Baz + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [11, 8]) + expect(clip.infer.to_s).to eq('Symbol') + end + + it 'understands nested alias namespaces to nested classes resolving types' do + source = Solargraph::Source.load_string(%( + module A + class Foo + # @return [Symbol] + def bing; end + end + end + + module Bar + Baz = A::Foo + end + + a = Bar::Baz.new.bing + a + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [13, 8]) + expect(clip.infer.to_s).to eq('Symbol') + end + + it 'understands nested alias namespaces resolving types' do + source = Solargraph::Source.load_string(%( + module Bar + module A + class Foo + # @return [Symbol] + def bing; :bingo; end + end + end + end + + module Bar + Foo = A::Foo + end + + a = Bar::Foo.new.bing + a + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [15, 8]) + expect(clip.infer.to_s).to eq('Symbol') + end + + it 'understands includes using nested alias namespaces resolving types' do + source = Solargraph::Source.load_string(%( + module Foo + # @return [Symbol] + def bing; :yay; end + end + + module Bar + Baz = Foo + end + + class B + include Foo + end + + a = B.new.bing + a + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [15, 8]) + expect(clip.infer.to_s).to eq('Symbol') + end + end + + describe '#get_method_stack', time_limit_seconds: 240 do + let(:out) { StringIO.new } + let(:api_map) { described_class.load_with_cache(Dir.pwd, out) } + + context 'with stdlib that has vital dependencies' do + let(:external_requires) { ['yaml'] } + + let(:method_stack) { api_map.get_method_stack('YAML', 'safe_load', scope: :class) } + + it 'handles the YAML gem aliased to Psych' do + expect(method_stack).not_to be_empty + end + end + + context 'with thor' do + let(:external_requires) { ['thor'] } + let(:method_stack) { api_map.get_method_stack('Thor', 'desc', scope: :class) } + + it 'handles finding Thor.desc' do + expect(method_stack).not_to be_empty + end + end + end +end diff --git a/spec/api_map/index_spec.rb b/spec/api_map/index_spec.rb new file mode 100644 index 000000000..8afb74759 --- /dev/null +++ b/spec/api_map/index_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +describe Solargraph::ApiMap::Index do + subject(:output_pins) { described_class.new(input_pins).pins } + + describe '#map_overrides' do + let(:foo_class) do + Solargraph::Pin::Namespace.new(name: 'Foo') + end + + let(:foo_initialize) do + init = Solargraph::Pin::Method.new(name: 'initialize', + scope: :instance, + parameters: [], + closure: foo_class) + # no return type specified + param = Solargraph::Pin::Parameter.new(name: 'bar', + closure: init) + init.parameters << param + init + end + + let(:foo_new) do + init = Solargraph::Pin::Method.new(name: 'new', + scope: :class, + parameters: [], + closure: foo_class) + # no return type specified + param = Solargraph::Pin::Parameter.new(name: 'bar', + closure: init) + init.parameters << param + init + end + + let(:foo_override) do + Solargraph::Pin::Reference::Override.from_comment('Foo#initialize', + '@param [String] bar') + end + + let(:input_pins) do + [ + foo_initialize, + foo_new, + foo_override + ] + end + + it 'has a docstring to process on override' do + expect(foo_override.docstring.tags).to be_empty + end + + it 'overrides .new method' do + method_pin = output_pins.find { |pin| pin.path == 'Foo.new' } + first_parameter = method_pin.parameters.first + expect(first_parameter.return_type.tag).to eq('String') + end + + it 'overrides #initialize method in signature' do + method_pin = output_pins.find { |pin| pin.path == 'Foo#initialize' } + first_parameter = method_pin.parameters.first + expect(first_parameter.return_type.tag).to eq('String') + end + end +end diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index c95d4d8ec..cdeed30d3 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -1,12 +1,16 @@ +# frozen_string_literal: true + require 'tmpdir' describe Solargraph::ApiMap do + # avoid performance hit of doing this many times + # rubocop:disable RSpec/InstanceVariable before :all do - @api_map = Solargraph::ApiMap.new + @api_map = described_class.load_with_cache(Dir.pwd, nil) end it 'returns core methods' do - pins = @api_map.get_methods('String') + pins = @api_map.get_methods('String') # rubocop:disable RSpec/InstanceVariable expect(pins.map(&:path)).to include('String#upcase') end @@ -193,7 +197,7 @@ def prot end it 'adds Object instance methods to duck types' do - api_map = Solargraph::ApiMap.new + api_map = described_class.new type = Solargraph::ComplexType.parse('#foo') pins = api_map.get_complex_type_methods(type) expect(pins.any? { |p| p.namespace == 'BasicObject' }).to be(true) @@ -434,14 +438,14 @@ class Sup xit 'understands tuples inherit from regular arrays' do method_pins = @api_map.get_method_stack("Array(1, 2, 'a')", 'include?') method_pin = method_pins.first - expect(method_pin).to_not be_nil + expect(method_pin).not_to be_nil expect(method_pin.path).to eq('Array#include?') parameter_type = method_pin.signatures.first.parameters.first.return_type expect(parameter_type.rooted_tags).to eq("1, 2, 'a'") end it 'loads workspaces from directories' do - api_map = Solargraph::ApiMap.load('spec/fixtures/workspace') + api_map = described_class.load('spec/fixtures/workspace') expect(api_map.source_map(File.absolute_path('spec/fixtures/workspace/app.rb'))).to be_a(Solargraph::SourceMap) end @@ -531,7 +535,7 @@ module Includer expect(fqns).to eq('Foo::Bar') end - it 'handles multiple type parameters without losing cache coherence' do + it 'understands type parameters' do tag = @api_map.qualify('Array') expect(tag).to eq('Array') tag = @api_map.qualify('Array') @@ -755,18 +759,18 @@ def bar; end end it 'can qualify "Boolean"' do - api_map = Solargraph::ApiMap.new + api_map = described_class.new expect(api_map.qualify('Boolean')).to eq('Boolean') end it 'knows that true is a "subtype" of Boolean' do - api_map = Solargraph::ApiMap.new + api_map = described_class.new expect(api_map.super_and_sub?('Boolean', 'true')).to be(true) end it 'knows that false is a "subtype" of Boolean' do - api_map = Solargraph::ApiMap.new - expect(api_map.super_and_sub?('Boolean', 'true')).to be(true) + api_map = described_class.new + expect(api_map.super_and_sub?('Boolean', 'false')).to be(true) end it 'resolves aliases for YARD methods' do @@ -781,7 +785,7 @@ class Foo alias baz foo end )).pins - # api_map = Solargraph::ApiMap.new(pins: yard_pins + source_pins) + # api_map = described_class.new(pins: yard_pins + source_pins) @api_map.index yard_pins + source_pins baz = @api_map.get_method_stack('Foo', 'baz').first expect(baz).to be_a(Solargraph::Pin::Method) @@ -790,8 +794,10 @@ class Foo it 'ignores malformed mixins' do closure = Solargraph::Pin::Namespace.new(name: 'Foo', closure: Solargraph::Pin::ROOT_PIN, type: :class) - mixin = Solargraph::Pin::Reference::Include.new(name: 'defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable)', closure: closure) - api_map = Solargraph::ApiMap.new(pins: [closure, mixin]) + mixin = Solargraph::Pin::Reference::Include.new( + name: 'defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable)', closure: closure + ) + api_map = described_class.new(pins: [closure, mixin]) expect(api_map.get_method_stack('Foo', 'foo')).to be_empty end @@ -812,9 +818,67 @@ def baz end ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) + api_map = described_class.new.map(source) clip = api_map.clip_at('test.rb', [11, 10]) expect(clip.infer.to_s).to eq('Symbol') end + + it 'resolves aliases in identically named deeply nested classes' do + source = Solargraph::Source.load_string(%( + module A + module Bar + # @return [Integer] + def quux; 123; end + end + + Baz = Bar + + class Foo + include Baz + end + end + + def c + b = A::Foo.new.quux + b + end + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [16, 4]) + expect(clip.infer.to_s).to eq('Integer') + end + + it 'resolves aliases in nested classes' do + source = Solargraph::Source.load_string(%( + module A + module Bar + class Baz + # @return [Integer] + def quux; 123; end + end + end + + Baz = Bar::Baz + + class Foo + include Baz + end + end + + def c + b = A::Foo.new.quux + b + end + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [18, 4]) + expect(clip.infer.to_s).to eq('Integer') + end + + # rubocop:enable RSpec/InstanceVariable end diff --git a/spec/complex_type/conforms_to_spec.rb b/spec/complex_type/conforms_to_spec.rb new file mode 100644 index 000000000..f8a623bf0 --- /dev/null +++ b/spec/complex_type/conforms_to_spec.rb @@ -0,0 +1,257 @@ +# frozen_string_literal: true + +describe Solargraph::ComplexType do + let(:api_map) do + Solargraph::ApiMap.new + end + + it 'validates simple core types' do + exp = described_class.parse('String') + inf = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'invalidates simple core types' do + exp = described_class.parse('String') + inf = described_class.parse('Integer') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(false) + end + + it 'allows subtype skew if told' do + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call, [:allow_subtype_skew]) + expect(match).to be(true) + end + + it 'allows covariant behavior in parameters of Array' do + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'does not allow contravariant behavior in parameters of Array' do + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(false) + end + + it 'allows covariant behavior in key types of Hash' do + exp = described_class.parse('Hash{Object => String}') + inf = described_class.parse('Hash{Integer => String}') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'accepts valid tuple conformance' do + exp = described_class.parse('Array(Integer, Integer)') + inf = described_class.parse('Array(Integer, Integer)') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'rejects invalid tuple conformance' do + exp = described_class.parse('Array(Integer, Integer)') + inf = described_class.parse('Array(Integer, String)') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(false) + end + + it 'allows empty params when specified' do + exp = described_class.parse('Array(Integer, Integer)') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) + expect(match).to be(true) + end + + it 'validates expected superclasses' do + source = Solargraph::Source.load_string(%( + class Sup; end + class Sub < Sup; end + )) + api_map.map source + sup = described_class.parse('Sup') + sub = described_class.parse('Sub') + match = sub.conforms_to?(api_map, sup, :method_call) + expect(match).to be(true) + end + + it 'handles singleton types compared against their literals' do + exp = Solargraph::ComplexType::UniqueType.new('nil', rooted: true) + inf = Solargraph::ComplexType::UniqueType.new('NilClass', rooted: true) + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + # it 'invalidates inferred superclasses (expected must be super)' do + # # @todo This test might be invalid. There are use cases where inheritance + # # between inferred and expected classes should be acceptable in either + # # direction. + # # source = Solargraph::Source.load_string(%( + # # class Sup; end + # # class Sub < Sup; end + # # )) + # # api_map.map source + # # sup = described_class.parse('Sup') + # # sub = described_class.parse('Sub') + # # match = Solargraph::TypeChecker::Checks.types_match?(api_map, sub, sup) + # # expect(match).to be(false) + # end + + it 'fuzzy matches arrays with parameters' do + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'fuzzy matches sets with parameters' do + source = Solargraph::Source.load_string("require 'set'") + source_map = Solargraph::SourceMap.map(source) + api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['set']) + exp = described_class.parse('Set') + inf = described_class.parse('Set') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'fuzzy matches hashes with parameters' do + exp = described_class.parse('Hash{ Symbol => String}') + inf = described_class.parse('Hash') + match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) + expect(match).to be(true) + end + + it 'matches multiple types' do + exp = described_class.parse('String, Integer') + inf = described_class.parse('String, Integer') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'matches multiple types out of order' do + exp = described_class.parse('String, Integer') + inf = described_class.parse('Integer, String') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'invalidates inferred types missing from expected' do + exp = described_class.parse('String') + inf = described_class.parse('String, Integer') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(false) + end + + it 'matches nil' do + exp = described_class.parse('nil') + inf = described_class.parse('nil') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'validates classes with expected superclasses' do + exp = described_class.parse('Class') + inf = described_class.parse('Class') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'validates generic classes with expected Class' do + inf = described_class.parse('Class') + exp = described_class.parse('Class') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + context 'with invariant matching' do + it 'rejects String matching an Object' do + inf = described_class.parse('String') + exp = described_class.parse('Object') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :invariant) + expect(match).to be(false) + end + + it 'rejects Object matching an String' do + inf = described_class.parse('Object') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :invariant) + expect(match).to be(false) + end + + it 'accepts String matching a String' do + inf = described_class.parse('String') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :invariant) + expect(match).to be(true) + end + end + + context 'with contravariant matching' do + it 'rejects String matching an Objet' do + inf = described_class.parse('String') + exp = described_class.parse('Object') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :contravariant) + expect(match).to be(false) + end + + it 'accepts Object matching an String' do + inf = described_class.parse('Object') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :contravariant) + expect(match).to be(true) + end + + it 'accepts String matching a String' do + inf = described_class.parse('String') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :contravariant) + expect(match).to be(true) + end + end + + context 'with an inheritence relationship' do + let(:source) do + Solargraph::Source.load_string(%( + class Sup; end + class Sub < Sup; end + )) + end + let(:sup) { described_class.parse('Sup') } + let(:sub) { described_class.parse('Sub') } + + before do + api_map.map source + end + + it 'validates inheritance in one way' do + match = sub.conforms_to?(api_map, sup, :method_call, [:allow_reverse_match]) + expect(match).to be(true) + end + + it 'validates inheritance the other way' do + match = sup.conforms_to?(api_map, sub, :method_call, [:allow_reverse_match]) + expect(match).to be(true) + end + end + + context 'with inheritance relationship in allow_reverse_match mode' do + let(:api_map) { Solargraph::ApiMap.new } + let(:sup) { described_class.parse('String') } + let(:sub) { described_class.parse('Array') } + + it 'conforms one way' do + match = sub.conforms_to?(api_map, sup, :method_call, [:allow_reverse_match]) + expect(match).to be(false) + end + + it 'conforms the other way' do + match = sup.conforms_to?(api_map, sub, :method_call, [:allow_reverse_match]) + expect(match).to be(false) + end + end +end diff --git a/spec/complex_type/unique_type_spec.rb b/spec/complex_type/unique_type_spec.rb new file mode 100644 index 000000000..2d9812600 --- /dev/null +++ b/spec/complex_type/unique_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +describe Solargraph::ComplexType::UniqueType do + describe '#any?' do + let(:type) { described_class.parse('String') } + + it 'yields one and only one type, itself' do + types_encountered = [] + type.any? { |t| types_encountered << t } + expect(types_encountered).to eq([type]) + end + end +end diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index f876d642f..dd20099eb 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -733,5 +733,33 @@ def make_bar expect(type.to_rbs).to eq('[Symbol, String, [Integer, Integer]]') expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') end + + it 'recognizes String conforms with itself' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('String') + atype = Solargraph::ComplexType.parse('String') + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) + end + + it 'recognizes an erased container type conforms with itself' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('Hash') + atype = Solargraph::ComplexType.parse('Hash') + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) + end + + it 'recognizes an unerased container type conforms with itself' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('Array') + atype = Solargraph::ComplexType.parse('Array') + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) + end + + it 'recognizes a literal conforms with its type' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('Symbol') + atype = Solargraph::ComplexType.parse(':foo') + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) + end end end diff --git a/spec/convention/activesupport_concern_spec.rb b/spec/convention/activesupport_concern_spec.rb index ffa12ee6c..e75e8749c 100644 --- a/spec/convention/activesupport_concern_spec.rb +++ b/spec/convention/activesupport_concern_spec.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true describe Solargraph::Convention::ActiveSupportConcern do - let :source do - Solargraph::Source.load_string(%( + let(:api_map) { Solargraph::ApiMap.new.map(source) } + + context 'with a simple activesupport concern' do + let :source do + Solargraph::Source.load_string(%( # Example from here: https://api.rubyonrails.org/v7.0/classes/ActiveSupport/Concern.html require "active_support/concern" @@ -30,12 +33,67 @@ class Host # this should print 'test' ), 'test.rb') + end + + it 'handles block method super scenarios' do + api_map = Solargraph::ApiMap.new.map(source) + + pin = api_map.get_method_stack('Host', 'method_injected_by_foo', scope: :class) + expect(pin.map(&:name)).to eq(['method_injected_by_foo']) + end end - it 'handles block method super scenarios' do - api_map = Solargraph::ApiMap.new.map(source) + context 'with static method defined in both included module and class' do + let :source do + Solargraph::Source.load_string(%( + # Example from here: https://api.rubyonrails.org/v7.0/classes/ActiveSupport/Concern.html + require "active_support/concern" + + module Foo + extend ActiveSupport::Concern + included do + def self.my_method + puts 'test' + end + end + end + + module Bar + extend ActiveSupport::Concern + include Foo + + included do + self.my_method + end + end + + class B + # @return [Numeric] + def self.my_method; end + end + + class A < B + include Bar # It works, now Bar takes care of its dependencies + + def self.my_method; end + end + + # this should print 'test' + ), 'test.rb') + end + + let(:pins) { api_map.get_method_stack('A', 'my_method', scope: :class) } + + it 'sees all three methods' do + expect(pins.map(&:name)).to eq(%w[my_method my_method my_method]) + end + + it 'prefers directly defined method' do + expect(pins.map(&:path).first).to eq('A.my_method') + end - pin = api_map.get_method_stack('Host', 'method_injected_by_foo', scope: :class) - expect(pin.map(&:name)).to eq(['method_injected_by_foo']) + it 'is able to typify from superclass' do + expect(pins.first.typify(api_map).map(&:tag)).to include('Numeric') + end end end diff --git a/spec/convention/gemfile_spec.rb b/spec/convention/gemfile_spec.rb new file mode 100644 index 000000000..827da7993 --- /dev/null +++ b/spec/convention/gemfile_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +describe Solargraph::Convention::Gemfile do + describe 'parsing Gemfiles' do + def type_checker code + Solargraph::TypeChecker.load_string(code, 'Gemfile', :strong) + end + + it 'typechecks valid files without error' do + checker = type_checker(%( + source 'https://rubygems.org' + + ruby "~> 3.3.5" + + gemspec name: 'solargraph' + + # Local gemfile for development tools, etc. + local_gemfile = File.expand_path(".Gemfile", __dir__) + instance_eval File.read local_gemfile if File.exist? local_gemfile + )) + + expect(checker.problems).to be_empty + end + + it 'finds bad arguments to DSL methods' do + checker = type_checker(%( + source File + + gemspec bad_name: 'solargraph' + + # Local gemfile for development tools, etc. + local_gemfile = File.expand_path(".Gemfile", __dir__) + instance_eval File.read local_gemfile if File.exist? local_gemfile + )) + + expect(checker.problems.map(&:message).sort) + .to eq(['Unrecognized keyword argument bad_name to Bundler::Dsl#gemspec', + 'Wrong argument type for Bundler::Dsl#source: source expected String, received Class'].sort) + end + + it 'finds bad arguments to DSL ruby method' do + pending 'missing support for restargs in the typechecker' + + checker = type_checker(%( + ruby 123 + )) + + expect(checker.problems.map(&:message)) + .to eq(['Wrong argument type for Bundler::Dsl#ruby: ruby_version expected String, received Integer']) + end + end +end diff --git a/spec/convention/struct_definition_spec.rb b/spec/convention/struct_definition_spec.rb index fe317a42b..0436dbe80 100644 --- a/spec/convention/struct_definition_spec.rb +++ b/spec/convention/struct_definition_spec.rb @@ -21,7 +21,7 @@ expect(param_baz.return_type.tag).to eql('Integer') end - it 'should set closure to method on assignment operator parameters' do + it 'sets closure to method on assignment operator parameters' do source = Solargraph::SourceMap.load_string(%( # @param bar [String] # @param baz [Integer] @@ -140,7 +140,7 @@ def type_checker code Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end - it 'should not crash' do + it 'does not crash' do checker = type_checker(%( Foo = Struct.new(:bar, :baz) )) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index b03e573f0..cdf12d75a 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -1,80 +1,160 @@ # frozen_string_literal: true +require 'bundler' +require 'benchmark' + describe Solargraph::DocMap do - before :all do - # We use ast here because it's a known dependency. - gemspec = Gem::Specification.find_by_name('ast') - yard_pins = Solargraph::GemPins.build_yard_pins([], gemspec) - Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) + subject(:doc_map) do + described_class.new(requires, workspace, out: out) end - it 'generates pins from gems' do - doc_map = Solargraph::DocMap.new(['ast'], []) - doc_map.cache_all!($stderr) - node_pin = doc_map.pins.find { |pin| pin.path == 'AST::Node' } - expect(node_pin).to be_a(Solargraph::Pin::Namespace) + let(:out) { StringIO.new } + let(:pre_cache) { true } + let(:requires) { [] } + + let(:workspace) do + Solargraph::Workspace.new(Dir.pwd) end - it 'tracks unresolved requires' do - doc_map = Solargraph::DocMap.new(['not_a_gem'], []) - expect(doc_map.unresolved_requires).to include('not_a_gem') + let(:plain_doc_map) { described_class.new([], workspace, out: nil) } + + before do + doc_map.cache_doc_map_gems!(nil) if pre_cache end - it 'tracks uncached_gemspecs' do - gemspec = Gem::Specification.new do |spec| - spec.name = 'not_a_gem' - spec.version = '1.0.0' + context 'with a require in solargraph test bundle' do + let(:requires) do + ['ast'] + end + + it 'generates pins from gems' do + node_pin = doc_map.pins.find { |pin| pin.path == 'AST::Node' } + expect(node_pin).to be_a(Solargraph::Pin::Namespace) end - allow(Gem::Specification).to receive(:find_by_path).and_return(gemspec) - doc_map = Solargraph::DocMap.new(['not_a_gem'], [gemspec]) - expect(doc_map.uncached_yard_gemspecs).to eq([gemspec]) - expect(doc_map.uncached_rbs_collection_gemspecs).to eq([gemspec]) end - it 'imports all gems when bundler/require used' do - workspace = Solargraph::Workspace.new(Dir.pwd) - plain_doc_map = Solargraph::DocMap.new([], [], workspace) - doc_map_with_bundler_require = Solargraph::DocMap.new(['bundler/require'], [], workspace) + context 'with an invalid require' do + let(:requires) do + ['not_a_gem'] + end - expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive + it 'tracks unresolved requires' do + # These are auto-required by solargraph-rspec in case the bundle + # includes these gems. In our case, it doesn't! + unprovided_solargraph_rspec_requires = [ + 'rspec-rails', + 'actionmailer', + 'activerecord', + 'shoulda-matchers', + 'rspec-sidekiq', + 'airborne', + 'activesupport' + ] + expect(doc_map.unresolved_requires - unprovided_solargraph_rspec_requires) + .to eq(['not_a_gem']) + end end - it 'does not warn for redundant requires' do - # Requiring 'set' is unnecessary because it's already included in core. It - # might make sense to log redundant requires, but a warning is overkill. - expect(Solargraph.logger).not_to receive(:warn).with(/path set/) - Solargraph::DocMap.new(['set'], []) + context 'when deserialization takes a while' do + let(:pre_cache) { false } + let(:requires) { ['backport'] } + + before do + # proxy this method to simulate a long-running deserialization + allow(Benchmark).to receive(:measure) do |&block| + block.call + 5.0 + end + end + + it 'logs timing' do + # force lazy evaluation + _pins = doc_map.pins + expect(out.string).to include('Deserialized ').and include(' gem pins ').and include(' ms') + end end - it 'ignores nil requires' do - expect { Solargraph::DocMap.new([nil], []) }.not_to raise_error + context 'with an uncached but valid gemspec' do + let(:requires) { ['uncached_gem'] } + let(:pre_cache) { false } + let(:workspace) { instance_double(Solargraph::Workspace) } + + it 'tracks uncached_gemspecs' do + pincache = instance_double(Solargraph::PinCache) + uncached_gemspec = Gem::Specification.new('uncached_gem', '1.0.0') + allow(workspace).to receive_messages(resolve_require: [], fresh_pincache: pincache) + allow(workspace).to receive(:global_environ).and_return(Solargraph::Environ.new) + allow(workspace).to receive(:resolve_require).with('uncached_gem').and_return([uncached_gemspec]) + allow(workspace).to receive(:fetch_dependencies).with(uncached_gemspec, out: out).and_return([]) + allow(pincache).to receive(:deserialize_combined_pin_cache).with(uncached_gemspec).and_return(nil) + expect(doc_map.uncached_gemspecs).to eq([uncached_gemspec]) + end end - it 'ignores empty requires' do - expect { Solargraph::DocMap.new([''], []) }.not_to raise_error + context 'with require as bundle/require' do + it 'imports all gems when bundler/require used' do + doc_map_with_bundler_require = described_class.new(['bundler/require'], workspace, out: nil) + doc_map_with_bundler_require.cache_doc_map_gems!(nil) + expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive + end end - it 'collects dependencies' do - doc_map = Solargraph::DocMap.new(['rspec'], []) - expect(doc_map.dependencies.map(&:name)).to include('rspec-core') + context 'with a require not needed by Ruby core' do + let(:requires) { ['set'] } + + it 'does not warn' do + # Requiring 'set' is unnecessary because it's already included in core. It + # might make sense to log redundant requires, but a warning is overkill. + allow(Solargraph.logger).to receive(:warn) + doc_map + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) + end end - it 'includes convention requires from environ' do - dummy_convention = Class.new(Solargraph::Convention::Base) do - def global(doc_map) - Solargraph::Environ.new( - requires: ['convention_gem1', 'convention_gem2'] - ) - end + context 'with a nil require' do + let(:requires) { [nil] } + + it 'does not raise error' do + expect { doc_map }.not_to raise_error end + end - Solargraph::Convention.register dummy_convention + context 'with an empty require' do + let(:requires) { [''] } - doc_map = Solargraph::DocMap.new(['original_gem'], []) + it 'does not raise error' do + expect { doc_map }.not_to raise_error + end + end - expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') + context 'with a require that has dependencies' do + let(:requires) { ['rspec'] } - # Clean up the registered convention - Solargraph::Convention.deregister dummy_convention + it 'collects dependencies' do + expect(doc_map.dependencies.map(&:name)).to include('rspec-core') + end + end + + context 'with convention' do + let(:pre_cache) { false } + + it 'includes convention requires from environ' do + dummy_convention = Class.new(Solargraph::Convention::Base) do + def global(doc_map) + Solargraph::Environ.new( + requires: ['convention_gem1', 'convention_gem2'] + ) + end + end + + Solargraph::Convention.register dummy_convention + + doc_map = Solargraph::DocMap.new(['original_gem'], workspace) + + expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') + ensure + # Clean up the registered convention + Solargraph::Convention.deregister dummy_convention + end end end diff --git a/spec/gem_pins_spec.rb b/spec/gem_pins_spec.rb index d630784cf..944afd331 100644 --- a/spec/gem_pins_spec.rb +++ b/spec/gem_pins_spec.rb @@ -1,14 +1,49 @@ # frozen_string_literal: true describe Solargraph::GemPins do - it 'can merge YARD and RBS' do - gemspec = Gem::Specification.find_by_name('rbs') - yard_pins = Solargraph::GemPins.build_yard_pins([], gemspec) - rbs_map = Solargraph::RbsMap.from_gemspec(gemspec, nil, nil) - pins = Solargraph::GemPins.combine yard_pins, rbs_map.pins - - core_root = pins.find { |pin| pin.path == 'RBS::EnvironmentLoader#core_root' } - expect(core_root.return_type.to_s).to eq('Pathname, nil') - expect(core_root.location.filename).to end_with('environment_loader.rb') + let(:workspace) { Solargraph::Workspace.new(Dir.pwd) } + let(:doc_map) { Solargraph::DocMap.new(requires, workspace, out: nil) } + let(:pin) { doc_map.pins.find { |pin| pin.path == path } } + + before do + doc_map.cache_doc_map_gems!(STDERR) # rubocop:disable Style/GlobalStdStream + end + + context 'with a combined method pin' do + let(:path) { 'RBS::EnvironmentLoader#core_root' } + let(:requires) { ['rbs'] } + + it 'can merge YARD and RBS' do + expect(pin.source).to eq(:combined) + end + + it 'finds types from RBS' do + expect(pin.return_type.to_s).to eq('Pathname, nil') + end + + it 'finds locations from YARD' do + expect(pin.location.filename).to end_with('environment_loader.rb') + end + end + + context 'with a YARD-only pin' do + let(:requires) { ['rake'] } + let(:path) { 'Rake::Task#prerequisites' } + + it 'found a pin' do + expect(pin.source).not_to be_nil + end + + it 'can merge YARD and RBS' do + expect(pin.source).to eq(:yardoc) + end + + it 'does not find types from YARD in this case' do + expect(pin.return_type.to_s).to eq('undefined') + end + + it 'finds locations from YARD' do + expect(pin.location.filename).to end_with('task.rb') + end end end diff --git a/spec/language_server/host/diagnoser_spec.rb b/spec/language_server/host/diagnoser_spec.rb index d59a843f1..69ee0b866 100644 --- a/spec/language_server/host/diagnoser_spec.rb +++ b/spec/language_server/host/diagnoser_spec.rb @@ -3,7 +3,8 @@ host = double(Solargraph::LanguageServer::Host, options: { 'diagnostics' => true }, synchronizing?: false) diagnoser = Solargraph::LanguageServer::Host::Diagnoser.new(host) diagnoser.schedule 'file.rb' - expect(host).to receive(:diagnose).with('file.rb') + allow(host).to receive(:diagnose) diagnoser.tick + expect(host).to have_received(:diagnose).with('file.rb') end end diff --git a/spec/language_server/host/message_worker_spec.rb b/spec/language_server/host/message_worker_spec.rb index b9ce2a41f..9e5ce5721 100644 --- a/spec/language_server/host/message_worker_spec.rb +++ b/spec/language_server/host/message_worker_spec.rb @@ -2,11 +2,12 @@ it "handle requests on queue" do host = double(Solargraph::LanguageServer::Host) message = {'method' => '$/example'} - expect(host).to receive(:receive).with(message).and_return(nil) + allow(host).to receive(:receive).with(message).and_return(nil) worker = Solargraph::LanguageServer::Host::MessageWorker.new(host) worker.queue(message) expect(worker.messages).to eq [message] worker.tick + expect(host).to have_received(:receive).with(message) end end diff --git a/spec/language_server/host_spec.rb b/spec/language_server/host_spec.rb index 40c8b7292..bccf00a87 100644 --- a/spec/language_server/host_spec.rb +++ b/spec/language_server/host_spec.rb @@ -247,7 +247,7 @@ def initialize(foo); end expect(symbols).not_to be_empty end - it "opens a file outside of prepared libraries" do + it "opens a file outside of prepared libraries", time_limit_seconds: 120 do @host.prepare(File.absolute_path(File.join('spec', 'fixtures', 'workspace'))) @host.open('file:///file.rb', 'class Foo; end', 1) symbols = @host.document_symbols('file:///file.rb') diff --git a/spec/language_server/message/text_document/definition_spec.rb b/spec/language_server/message/text_document/definition_spec.rb index 72ff77f1e..b6c98b99b 100644 --- a/spec/language_server/message/text_document/definition_spec.rb +++ b/spec/language_server/message/text_document/definition_spec.rb @@ -1,4 +1,33 @@ describe Solargraph::LanguageServer::Message::TextDocument::Definition do + it 'prepares empty directory' do + Dir.mktmpdir do |dir| + host = Solargraph::LanguageServer::Host.new + test_rb_path = File.join(dir, 'test.rb') + thing_rb_path = File.join(dir, 'thing.rb') + FileUtils.cp('spec/fixtures/workspace/lib/other.rb', test_rb_path) + FileUtils.cp('spec/fixtures/workspace/lib/thing.rb', thing_rb_path) + host.prepare(dir) + sleep 0.1 until host.libraries.all?(&:mapped?) + host.catalog + file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(test_rb_path) + other_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(thing_rb_path) + message = Solargraph::LanguageServer::Message::TextDocument::Definition + .new(host, { + 'params' => { + 'textDocument' => { + 'uri' => file_uri + }, + 'position' => { + 'line' => 4, + 'character' => 10 + } + } + }) + message.process + expect(message.result.first[:uri]).to eq(other_uri) + end + end + it 'finds definitions of methods' do host = Solargraph::LanguageServer::Host.new host.prepare('spec/fixtures/workspace') @@ -21,7 +50,7 @@ expect(message.result.first[:uri]).to eq(other_uri) end - it 'finds definitions of require paths' do + it 'finds definitions of require paths', time_limit_seconds: 120 do path = File.absolute_path('spec/fixtures/workspace') host = Solargraph::LanguageServer::Host.new host.prepare(path) diff --git a/spec/language_server/message/text_document/rename_spec.rb b/spec/language_server/message/text_document/rename_spec.rb index 32fb6d011..d43793716 100644 --- a/spec/language_server/message/text_document/rename_spec.rb +++ b/spec/language_server/message/text_document/rename_spec.rb @@ -55,6 +55,9 @@ def foo(bar) } }) rename.process + expect(rename.result).not_to be_nil + expect(rename.result[:changes]).not_to be_nil + expect(rename.result[:changes]['file:///file.rb']).not_to be_nil expect(rename.result[:changes]['file:///file.rb'].length).to eq(3) end diff --git a/spec/language_server/protocol_spec.rb b/spec/language_server/protocol_spec.rb index e88fb9c05..5021d3937 100644 --- a/spec/language_server/protocol_spec.rb +++ b/spec/language_server/protocol_spec.rb @@ -34,7 +34,7 @@ def stop end end -describe Protocol do +describe Protocol, order: :defined do before :all do @protocol = Protocol.new(Solargraph::LanguageServer::Host.new) end @@ -82,7 +82,7 @@ def stop it "handles initialized" do @protocol.request 'initialized', nil response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } end it "configured default dynamic registration capabilities from initialized" do @@ -163,7 +163,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']['items'].length > 0).to be(true) end @@ -173,6 +173,9 @@ def bar baz item = response['result']['items'].select{|h| h['label'] == 'bar'}.first @protocol.request 'completionItem/resolve', item response = @protocol.response + expect(response).not_to be_nil + expect(response['error']).to be_nil + expect(response['result']).to be_a(Hash) expect(response['result']['documentation']['value']).to include('bar method') end @@ -186,7 +189,7 @@ def bar baz 'character' => 1 } } - expect(@protocol.response['error']).to be_nil + expect(@protocol.response['error']).to be_nil, ->{ "Received response #{@protocol.response.inspect} wtih unexpected error" } end it "documents YARD pins" do @@ -212,7 +215,7 @@ def bar baz 'query' => 'test' } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } end it "handles textDocument/definition" do @@ -227,7 +230,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']).not_to be_empty end @@ -242,7 +245,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']).to be_empty end @@ -253,7 +256,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } end it "handles textDocument/hover" do @@ -267,7 +270,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } # Given this request hovers over `Foo`, the result should not be empty expect(response['result']['contents']).not_to be_empty end @@ -282,7 +285,7 @@ def bar baz 'character' => 17 } } - expect(@protocol.response['error']).to be_nil + expect(@protocol.response['error']).to be_nil, ->{ "Received response #{@protocol.response.inspect} wtih unexpected error" } end it "handles textDocument/signatureHelp" do @@ -296,7 +299,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']['signatures']).not_to be_empty end @@ -305,7 +308,7 @@ def bar baz 'query' => 'Foo' } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']).not_to be_empty end @@ -320,7 +323,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']).not_to be_empty end @@ -335,7 +338,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']).not_to be_empty end @@ -351,7 +354,7 @@ def bar baz 'newName' => 'new_name' } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']['changes']['file:///file.rb']).to be_a(Array) end @@ -367,7 +370,7 @@ def bar baz 'newName' => 'new_name' } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']).to be_a(Hash) end @@ -378,7 +381,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result'].length).not_to be_zero end @@ -397,7 +400,7 @@ def bar baz 'query' => 'Foo#bar' } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']['content']).not_to be_empty end @@ -406,7 +409,7 @@ def bar baz 'query' => 'String' } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']['content']).not_to be_empty end @@ -426,7 +429,7 @@ def bar baz it "handles $/solargraph/checkGemVersion" do @protocol.request '$/solargraph/checkGemVersion', { verbose: false } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result']['installed']).to be_a(String) expect(response['result']['available']).to be_a(String) end @@ -434,7 +437,7 @@ def bar baz it "handles $/solargraph/documentGems" do @protocol.request '$/solargraph/documentGems', {} response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } end it "handles textDocument/formatting" do @@ -451,7 +454,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(response['result'].first['newText']).to be_a(String) end @@ -469,7 +472,7 @@ def bar baz } } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } # @todo Rules for parenthesized parameters have apparently changed in RuboCop 0.89 # expect(response['result'].first['newText']).to include('def barbaz(parameter); end') end @@ -490,7 +493,7 @@ def bar baz ] } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } end it "handles didChangeWatchedFiles for changed files" do @@ -503,7 +506,7 @@ def bar baz ] } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } end it "handles didChangeWatchedFiles for deleted files" do @@ -516,7 +519,7 @@ def bar baz ] } response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } end it "handles didChangeWatchedFiles for invalid change types" do @@ -529,7 +532,7 @@ def bar baz ] } response = @protocol.response - expect(response['error']).not_to be_nil + expect(response['error']).not_to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } end it "adds folders to the workspace" do @@ -583,13 +586,13 @@ def bar baz it "handles shutdown" do @protocol.request 'shutdown', {} response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } end it "handles exit" do @protocol.request 'exit', {} response = @protocol.response - expect(response['error']).to be_nil + expect(response['error']).to be_nil, ->{ "Received response #{response.inspect} wtih unexpected error" } expect(@protocol.host.stopped?).to be(true) end end diff --git a/spec/library_spec.rb b/spec/library_spec.rb index bea0f2983..f7daafdf4 100644 --- a/spec/library_spec.rb +++ b/spec/library_spec.rb @@ -26,6 +26,32 @@ expect(completion.pins.map(&:name)).to include('x') end + context 'with a require from a not-yet-cached external gem' do + before do + Solargraph::Shell.new.uncache('backport') + end + + it "returns a Completion", time_limit_seconds: 50 do + library = Solargraph::Library.new(Solargraph::Workspace.new(Dir.pwd, + Solargraph::Workspace::Config.new)) + library.attach Solargraph::Source.load_string(%( + require 'backport' + + # @param adapter [Backport::Adapter] + def foo(adapter) + adapter.remo + end + ), 'file.rb', 0) + completion = nil + # give Solargraph time to cache the gem + while (completion = library.completions_at('file.rb', 5, 19)).pins.empty? + sleep 0.25 + end + expect(completion).to be_a(Solargraph::SourceMap::Completion) + expect(completion.pins.map(&:name)).to include('remote') + end + end + context 'with a require from an already-cached external gem' do before do Solargraph::Shell.new.gems('backport') @@ -132,6 +158,20 @@ def bar baz, key: '' # @todo More tests end + it 'diagnoses using all reporters' do + directory = '' + config = instance_double(Solargraph::Workspace::Config) + allow(config).to receive_messages(plugins: [], required: [], reporters: ['all!']) + workspace = Solargraph::Workspace.new directory, config + library = Solargraph::Library.new workspace + src = Solargraph::Source.load_string(%( + puts 'hello' + ), 'file.rb', 0) + library.attach src + result = library.diagnose 'file.rb' + expect(result.to_s).to include('rubocop') + end + it "documents symbols" do library = Solargraph::Library.new src = Solargraph::Source.load_string(%( @@ -147,10 +187,47 @@ def bar expect(pins.map(&:path)).to include('Foo#bar') end - it "collects references to an instance method symbol" do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - src1 = Solargraph::Source.load_string(%( + describe '#references_from' do + it "collects references to a new method on a constant from assignment of Class.new" do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + src1 = Solargraph::Source.load_string(%( + Foo.new + ), 'file1.rb', 0) + library.merge src1 + src2 = Solargraph::Source.load_string(%( + Foo = Class.new + ), 'file2.rb', 0) + library.merge src2 + library.catalog + locs = library.references_from('file1.rb', 1, 12) + expect(locs.map { |l| [l.filename, l.range.start.line] }) + .to eq([["file1.rb", 1]]) + end + + it "collects references to a new method to a constant from assignment" do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + src1 = Solargraph::Source.load_string(%( + Foo.new + ), 'file1.rb', 0) + library.merge src1 + src2 = Solargraph::Source.load_string(%( + class Foo + end + blah = Foo.new + ), 'file2.rb', 0) + library.merge src2 + library.catalog + locs = library.references_from('file2.rb', 3, 21) + expect(locs.map { |l| [l.filename, l.range.start.line] }) + .to eq([["file1.rb", 1], ["file2.rb", 3]]) + end + + it "collects references to an instance method symbol" do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + src1 = Solargraph::Source.load_string(%( class Foo def bar end @@ -158,8 +235,8 @@ def bar Foo.new.bar ), 'file1.rb', 0) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( foo = Foo.new foo.bar class Other @@ -167,17 +244,17 @@ def bar; end end Other.new.bar ), 'file2.rb', 0) - library.merge src2 - library.catalog - locs = library.references_from('file2.rb', 2, 11) - expect(locs.length).to eq(3) - expect(locs.select{|l| l.filename == 'file2.rb' && l.range.start.line == 6}).to be_empty - end + library.merge src2 + library.catalog + locs = library.references_from('file2.rb', 2, 11) + expect(locs.length).to eq(3) + expect(locs.select{|l| l.filename == 'file2.rb' && l.range.start.line == 6}).to be_empty + end - it "collects references to a class method symbol" do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - src1 = Solargraph::Source.load_string(%( + it "collects references to a class method symbol" do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + src1 = Solargraph::Source.load_string(%( class Foo def self.bar end @@ -189,8 +266,8 @@ def bar Foo.bar Foo.new.bar ), 'file1.rb', 0) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( Foo.bar Foo.new.bar class Other @@ -200,48 +277,48 @@ def bar; end Other.bar Other.new.bar ), 'file2.rb', 0) - library.merge src2 - library.catalog - locs = library.references_from('file2.rb', 1, 11) - expect(locs.length).to eq(3) - expect(locs.select{|l| l.filename == 'file1.rb' && l.range.start.line == 2}).not_to be_empty - expect(locs.select{|l| l.filename == 'file1.rb' && l.range.start.line == 9}).not_to be_empty - expect(locs.select{|l| l.filename == 'file2.rb' && l.range.start.line == 1}).not_to be_empty - end + library.merge src2 + library.catalog + locs = library.references_from('file2.rb', 1, 11) + expect(locs.length).to eq(3) + expect(locs.select{|l| l.filename == 'file1.rb' && l.range.start.line == 2}).not_to be_empty + expect(locs.select{|l| l.filename == 'file1.rb' && l.range.start.line == 9}).not_to be_empty + expect(locs.select{|l| l.filename == 'file2.rb' && l.range.start.line == 1}).not_to be_empty + end - it "collects stripped references to constant symbols" do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - src1 = Solargraph::Source.load_string(%( + it "collects stripped references to constant symbols" do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + src1 = Solargraph::Source.load_string(%( class Foo def bar end end Foo.new.bar ), 'file1.rb', 0) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( class Other foo = Foo.new foo.bar end ), 'file2.rb', 0) - library.merge src2 - library.catalog - locs = library.references_from('file1.rb', 1, 12, strip: true) - expect(locs.length).to eq(3) - locs.each do |l| - code = library.read_text(l.filename) - o1 = Solargraph::Position.to_offset(code, l.range.start) - o2 = Solargraph::Position.to_offset(code, l.range.ending) - expect(code[o1..o2-1]).to eq('Foo') + library.merge src2 + library.catalog + locs = library.references_from('file1.rb', 1, 12, strip: true) + expect(locs.length).to eq(3) + locs.each do |l| + code = library.read_text(l.filename) + o1 = Solargraph::Position.to_offset(code, l.range.start) + o2 = Solargraph::Position.to_offset(code, l.range.ending) + expect(code[o1..o2-1]).to eq('Foo') + end end - end - it 'rejects new references from different classes' do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - source = Solargraph::Source.load_string(%( + it 'rejects new references from different classes' do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + source = Solargraph::Source.load_string(%( class Foo def bar end @@ -249,106 +326,131 @@ def bar Foo.new Array.new ), 'test.rb') - library.merge source - library.catalog - foo_new_locs = library.references_from('test.rb', 5, 10) - expect(foo_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 10, 5, 13))]) - obj_new_locs = library.references_from('test.rb', 6, 12) - expect(obj_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(6, 12, 6, 15))]) - end + library.merge source + library.catalog + foo_new_locs = library.references_from('test.rb', 5, 10) + expect(foo_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 10, 5, 13))]) + obj_new_locs = library.references_from('test.rb', 6, 12) + expect(obj_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(6, 12, 6, 15))]) + end - it "searches the core for queries" do - library = Solargraph::Library.new - result = library.search('String') - expect(result).not_to be_empty - end + it "searches the core for queries" do + library = Solargraph::Library.new + result = library.search('String') + expect(result).not_to be_empty + end - it "returns YARD documentation from the core" do - library = Solargraph::Library.new - api_map, result = library.document('String') - expect(result).not_to be_empty - expect(result.first).to be_a(Solargraph::Pin::Base) - end + it "returns YARD documentation from the core" do + library = Solargraph::Library.new + api_map, result = library.document('String') + expect(result).not_to be_empty + expect(result.first).to be_a(Solargraph::Pin::Base) + end - it "returns YARD documentation from sources" do - library = Solargraph::Library.new - src = Solargraph::Source.load_string(%( + it "returns YARD documentation from sources" do + library = Solargraph::Library.new + src = Solargraph::Source.load_string(%( class Foo # My bar method def bar; end end ), 'test.rb', 0) - library.attach src - api_map, result = library.document('Foo#bar') - expect(result).not_to be_empty - expect(result.first).to be_a(Solargraph::Pin::Base) - end + library.attach src + api_map, result = library.document('Foo#bar') + expect(result).not_to be_empty + expect(result.first).to be_a(Solargraph::Pin::Base) + end - it "synchronizes sources from updaters" do - library = Solargraph::Library.new - src = Solargraph::Source.load_string(%( + it "synchronizes sources from updaters" do + library = Solargraph::Library.new + src = Solargraph::Source.load_string(%( class Foo end ), 'test.rb', 1) - library.attach src - repl = %( + library.attach src + repl = %( class Foo def bar; end end ) - updater = Solargraph::Source::Updater.new( - 'test.rb', - 2, - [Solargraph::Source::Change.new(nil, repl)] - ) - library.attach src.synchronize(updater) - expect(library.current.code).to eq(repl) - end + updater = Solargraph::Source::Updater.new( + 'test.rb', + 2, + [Solargraph::Source::Change.new(nil, repl)] + ) + library.attach src.synchronize(updater) + expect(library.current.code).to eq(repl) + end - it "finds unique references" do - library = Solargraph::Library.new(Solargraph::Workspace.new('*')) - src1 = Solargraph::Source.load_string(%( + it "finds unique references" do + library = Solargraph::Library.new(Solargraph::Workspace.new('*')) + src1 = Solargraph::Source.load_string(%( class Foo end ), 'src1.rb', 1) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( foo = Foo.new ), 'src2.rb', 1) - library.merge src2 - library.catalog - refs = library.references_from('src2.rb', 1, 12) - expect(refs.length).to eq(2) - end + library.merge src2 + library.catalog + refs = library.references_from('src2.rb', 1, 12) + expect(refs.length).to eq(2) + end - it "includes method parameters in references" do - library = Solargraph::Library.new(Solargraph::Workspace.new('*')) - source = Solargraph::Source.load_string(%( + it "includes method parameters in references" do + library = Solargraph::Library.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( class Foo def bar(baz) baz.upcase end end ), 'test.rb', 1) - library.attach source - from_def = library.references_from('test.rb', 2, 16) - expect(from_def.length).to eq(2) - from_ref = library.references_from('test.rb', 3, 10) - expect(from_ref.length).to eq(2) - end + library.attach source + from_def = library.references_from('test.rb', 2, 16) + expect(from_def.length).to eq(2) + from_ref = library.references_from('test.rb', 3, 10) + expect(from_ref.length).to eq(2) + end - it "includes block parameters in references" do - library = Solargraph::Library.new(Solargraph::Workspace.new('*')) - source = Solargraph::Source.load_string(%( + it "lies about names when client can't handle the truth" do + library = Solargraph::Library.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( + class Foo + def 🤦🏻foo♀️; 123; end + end + ), 'test.rb', 1) + library.attach source + from_def = library.references_from('test.rb', 2, 16, strip: true) + expect(from_def.first.range.start.column).to eq(14) + end + + it "tells the truth about names when client can handle the truth" do + library = Solargraph::Library.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( + class Foo + def 🤦🏻foo♀️; 123; end + end + ), 'test.rb', 1) + library.attach source + from_def = library.references_from('test.rb', 2, 16, strip: false) + expect(from_def.first.range.start.column).to eq(12) + end + + it "includes block parameters in references" do + library = Solargraph::Library.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( 100.times do |foo| puts foo end ), 'test.rb', 1) - library.attach source - from_def = library.references_from('test.rb', 1, 20) - expect(from_def.length).to eq(2) - from_ref = library.references_from('test.rb', 2, 13) - expect(from_ref.length).to eq(2) + library.attach source + from_def = library.references_from('test.rb', 1, 20) + expect(from_def.length).to eq(2) + from_ref = library.references_from('test.rb', 2, 13) + expect(from_ref.length).to eq(2) + end end it 'defines YARD tags' do diff --git a/spec/parser/node_methods_spec.rb b/spec/parser/node_methods_spec.rb index eb026725b..f9504b584 100644 --- a/spec/parser/node_methods_spec.rb +++ b/spec/parser/node_methods_spec.rb @@ -440,5 +440,45 @@ def super_with_block calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) expect(calls).to be_one end + + it 'handles chained calls' do + source = Solargraph::Source.load_string(%( + Foo.new.bar('string') + )) + calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + expect(calls.length).to eq(2) + end + + it 'handles calls from inside array literals' do + source = Solargraph::Source.load_string(%( + [ Foo.new.bar('string') ] + )) + calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + expect(calls.length).to eq(2) + end + + it 'handles calls from inside array literals that are chained' do + source = Solargraph::Source.load_string(%( + [ Foo.new.bar('string') ].compact + )) + calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + expect(calls.length).to eq(3) + end + + it 'does not over-report calls' do + source = Solargraph::Source.load_string(%( + class Foo + def something + end + end + class Bar < Foo + def something + super(1) + 2 + end + end + )) + calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + expect(calls.length).to eq(2) + end end end diff --git a/spec/pin/combine_with_spec.rb b/spec/pin/combine_with_spec.rb new file mode 100644 index 000000000..cc80d76d5 --- /dev/null +++ b/spec/pin/combine_with_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +describe Solargraph::Pin::Base, '#combine_with' do + it 'combines return types with another method pin with same arity' do + pin1 = Solargraph::Pin::Method.new(name: 'foo', parameters: [], comments: '@return [String]') + pin2 = Solargraph::Pin::Method.new(name: 'foo', parameters: [], comments: '@return [Integer]') + combined = pin1.combine_with(pin2) + expect(combined.return_type.to_s).to eq('String, Integer') + end + + it 'combines return types with another method without type parameters' do + pin1 = Solargraph::Pin::Method.new(name: 'foo', parameters: [], comments: '@return [Array]') + pin2 = Solargraph::Pin::Method.new(name: 'foo', parameters: [], comments: '@return [Array]') + combined = pin1.combine_with(pin2) + expect(combined.return_type.to_s).to eq('Array') + end + + context 'with dodgy return types' do + let(:dodgy_location_pin) do + range = Solargraph::Range.new(Solargraph::Position.new(1, 0), Solargraph::Position.new(1, 10)) + location = Solargraph::Location.new('/home/user/.rbenv/versions/3.1.7/lib/ruby/gems/3.1.0/gems' \ + '/activesupport-7.0.8.7/lib/active_support/core_ext/object' \ + '/conversions.rb', + range) + Solargraph::Pin::Method.new(name: 'foo', parameters: [], comments: '@return [Object]', + location: location) + end + + let(:normal_pin) { Solargraph::Pin::Method.new(name: 'foo', parameters: [], comments: '@return [self]') } + + it 'combines a dodgy return type with a valid one' do + combined = dodgy_location_pin.combine_with(normal_pin) + expect(combined.return_type.to_s).to eq('self') + end + + it 'combines a valid return type with a dodgy one' do + combined = normal_pin.combine_with(dodgy_location_pin) + expect(combined.return_type.to_s).to eq('self') + end + end + + context 'with return types that should probably be self' do + let(:closure) do + Solargraph::Pin::Namespace.new( + name: 'Foo', + closure: Solargraph::Pin::ROOT_PIN, + type: :class + ) + end + + let(:likely_selfy_pin) do + Solargraph::Pin::Method.new(name: 'foo', closure: closure, parameters: [], comments: '@return [::Foo]') + end + + let(:selfy_pin) { Solargraph::Pin::Method.new(name: 'foo', closure: closure, parameters: [], comments: '@return [self]') } + + it 'combines a selfy return type with a likely-selfy one' do + combined = likely_selfy_pin.combine_with(selfy_pin) + expect(combined.return_type.to_s).to eq('self') + end + + it 'combines a likely-selfy return type with a selfy one' do + combined = selfy_pin.combine_with(likely_selfy_pin) + expect(combined.return_type.to_s).to eq('self') + end + end +end diff --git a/spec/pin/local_variable_spec.rb b/spec/pin/local_variable_spec.rb index 88075efb9..369a58bc4 100644 --- a/spec/pin/local_variable_spec.rb +++ b/spec/pin/local_variable_spec.rb @@ -46,7 +46,7 @@ class Bar # set env variable 'FOO' to 'true' in block with_env_var('SOLARGRAPH_ASSERTS', 'on') do - expect(Solargraph.asserts_on?(:combine_with_closure_name)).to be true + expect(Solargraph.asserts_on?).to be true expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :closure name/) end end diff --git a/spec/pin/symbol_spec.rb b/spec/pin/symbol_spec.rb index 98d88137e..16961cadc 100644 --- a/spec/pin/symbol_spec.rb +++ b/spec/pin/symbol_spec.rb @@ -1,10 +1,15 @@ describe Solargraph::Pin::Symbol do context "as an unquoted literal" do - it "is a kind of keyword" do + it "is a kind of keyword to the LSP" do pin = Solargraph::Pin::Symbol.new(nil, ':symbol') expect(pin.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::KEYWORD) end + it "has global closure" do + pin = Solargraph::Pin::Symbol.new(nil, ':symbol') + expect(pin.closure).to eq(Solargraph::Pin::ROOT_PIN) + end + it "has a Symbol return type" do pin = Solargraph::Pin::Symbol.new(nil, ':symbol') expect(pin.return_type.tag).to eq('Symbol') diff --git a/spec/pin_cache_spec.rb b/spec/pin_cache_spec.rb new file mode 100644 index 000000000..0a11686f5 --- /dev/null +++ b/spec/pin_cache_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'bundler' +require 'benchmark' + +describe Solargraph::PinCache do + subject(:pin_cache) do + described_class.new(rbs_collection_path: '.gem_rbs_collection', + rbs_collection_config_path: 'rbs_collection.yaml', + directory: Dir.pwd, + yard_plugins: ['activesupport-concern']) + end + + describe '#cached?' do + it 'returns true for a gem that is cached' do + allow(File).to receive(:file?).with(%r{.*stdlib/backport.ser$}).and_return(false) + allow(File).to receive(:file?).with(%r{.*combined/.*/backport-.*.ser$}).and_return(true) + + gemspec = Gem::Specification.find_by_name('backport') + expect(pin_cache.cached?(gemspec)).to be true + end + + it 'returns false for a gem that is not cached' do + gemspec = Gem::Specification.new.tap do |spec| + spec.name = 'nonexistent' + spec.version = '0.0.1' + end + expect(pin_cache.cached?(gemspec)).to be false + end + end + + describe '.core?' do + it 'returns true when core pins exist' do + allow(File).to receive(:file?).with(%r{.*/core.ser$}).and_return(true) + + expect(described_class.core?).to be true + end + + it "returns true when core pins don't" do + allow(File).to receive(:file?).with(%r{.*/core.ser$}).and_return(false) + + expect(described_class.core?).to be false + end + end + + describe '#possible_stdlibs' do + it 'is tolerant of less usual Ruby installations' do + stub_const('Gem::RUBYGEMS_DIR', nil) + + expect(pin_cache.possible_stdlibs).to eq([]) + end + end + + describe '#cache_all_stdlibs' do + it 'creates stdlibmaps' do + allow(Solargraph::RbsMap::StdlibMap).to receive(:new).and_return(instance_double(Solargraph::RbsMap::StdlibMap)) + + pin_cache.cache_all_stdlibs + + expect(Solargraph::RbsMap::StdlibMap).to have_received(:new).at_least(:once) + end + end + + describe '#cache_gem' do + context 'with an already in-memory gem' do + let(:backport_gemspec) { Gem::Specification.find_by_name('backport') } + + before do + pin_cache.cache_gem(gemspec: backport_gemspec, out: nil) + end + + it 'does not load the gem again' do + allow(Marshal).to receive(:load).and_call_original + + pin_cache.cache_gem(gemspec: backport_gemspec, out: nil) + + expect(Marshal).not_to have_received(:load).with(anything) + end + end + + context 'with the parser gem' do + before do + Solargraph::Shell.new.uncache('parser') + allow(Solargraph::Yardoc).to receive(:build_docs) + end + + it 'chooses not to use YARD' do + parser_gemspec = Gem::Specification.find_by_name('parser') + pin_cache.cache_gem(gemspec: parser_gemspec, out: nil) + # if this fails, you may not have run `bundle exec rbs collection update` + expect(Solargraph::Yardoc).not_to have_received(:build_docs).with(any_args) + end + end + + context 'with an installed gem' do + before do + Solargraph::Shell.new.gems('kramdown') + end + + it 'uncaches when asked' do + gemspec = Gem::Specification.find_by_name('kramdown') + expect do + pin_cache.uncache_gem(gemspec, out: nil) + end.not_to raise_error + end + end + + context 'with the rebuild flag' do + before do + allow(Solargraph::Yardoc).to receive(:build_docs) + end + + it 'chooses not to use YARD' do + parser_gemspec = Gem::Specification.find_by_name('parser') + pin_cache.cache_gem(gemspec: parser_gemspec, rebuild: true, out: nil) + # if this fails, you may not have run `bundle exec rbs collection update` + expect(Solargraph::Yardoc).not_to have_received(:build_docs).with(any_args) + end + end + + context 'with a stdlib gem' do + let(:gem_name) { 'logger' } + + before do + Solargraph::Shell.new.uncache(gem_name) + end + + it 'caches' do + yaml_gemspec = Gem::Specification.find_by_name(gem_name) + allow(File).to receive(:write).and_call_original + + pin_cache.cache_gem(gemspec: yaml_gemspec, out: nil) + + # match arguments with regexp using rspec-matchers syntax + expect(File).to have_received(:write).with(%r{combined/.*/logger-.*-stdlib.ser$}, any_args).once + end + end + + context 'with gem packaged with its own RBS gem' do + let(:gem_name) { 'base64' } + + before do + Solargraph::Shell.new.uncache(gem_name) + end + + it 'caches' do + yaml_gemspec = Gem::Specification.find_by_name(gem_name) + allow(File).to receive(:write).and_call_original + + pin_cache.cache_gem(gemspec: yaml_gemspec, out: nil) + + # match arguments with regexp using rspec-matchers syntax + expect(File).to have_received(:write).with(%r{combined/.*/base64-.*-export.ser$}, any_args, mode: 'wb').once + end + end + end + + describe '#uncache_gem' do + subject(:call) { pin_cache.uncache_gem(gemspec, out: out) } + + let(:out) { StringIO.new } + + before do + allow(FileUtils).to receive(:rm_rf) + end + + context 'with an already cached gem' do + let(:gemspec) { Gem::Specification.find_by_name('backport') } + + it 'deletes files' do + call + + expect(FileUtils).to have_received(:rm_rf).at_least(:once) + end + end + + context 'with a non-existent gem' do + let(:gemspec) { instance_double(Gem::Specification, name: 'nonexistent', version: '0.0.1') } + + it 'does not raise an error' do + expect { call }.not_to raise_error + end + + it 'logs a message' do + call + + expect(out.string).to include('does not exist') + end + + it 'does not delete files' do + call + + expect(FileUtils).not_to have_received(:rm_rf) + end + end + end +end diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index 09c203687..00e75732b 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -1,54 +1,99 @@ describe Solargraph::RbsMap::Conversions do - # create a temporary directory with the scope of the spec - around do |example| - require 'tmpdir' - Dir.mktmpdir("rspec-solargraph-") do |dir| - @temp_dir = dir - example.run + context 'with custom RBS files' do + # create a temporary directory with the scope of the spec + around do |example| + require 'tmpdir' + Dir.mktmpdir("rspec-solargraph-") do |dir| + @temp_dir = dir + example.run + end end - end - let(:rbs_repo) do - RBS::Repository.new(no_stdlib: false) - end + let(:rbs_repo) do + RBS::Repository.new(no_stdlib: false) + end - let(:loader) do - RBS::EnvironmentLoader.new(core_root: nil, repository: rbs_repo) - end + let(:loader) do + RBS::EnvironmentLoader.new(core_root: nil, repository: rbs_repo) + end - let(:conversions) do - Solargraph::RbsMap::Conversions.new(loader: loader) - end + let(:conversions) do + Solargraph::RbsMap::Conversions.new(loader: loader) + end - let(:pins) do - conversions.pins - end + let(:pins) do + conversions.pins + end - before do - rbs_file = File.join(temp_dir, 'foo.rbs') - File.write(rbs_file, rbs) - loader.add(path: Pathname(temp_dir)) - end + before do + rbs_file = File.join(temp_dir, 'foo.rbs') + File.write(rbs_file, rbs) + loader.add(path: Pathname(temp_dir)) + end - attr_reader :temp_dir + attr_reader :temp_dir - context 'with untyped response' do - let(:rbs) do - <<~RBS + context 'with untyped response' do + let(:rbs) do + <<~RBS class Foo def bar: () -> untyped end RBS + end + + subject(:method_pin) { pins.find { |pin| pin.path == 'Foo#bar' } } + + it { should_not be_nil } + + it { should be_a(Solargraph::Pin::Method) } + + it 'maps untyped in RBS to undefined in Solargraph 'do + expect(method_pin.return_type.tag).to eq('undefined') + end + end + end + + context 'with standard loads for solargraph project' do + before :all do # rubocop:disable RSpec/BeforeAfterAll + @api_map = Solargraph::ApiMap.load_with_cache('.') end - subject(:method_pin) { pins.find { |pin| pin.path == 'Foo#bar' } } + let(:api_map) { @api_map } # rubocop:disable RSpec/InstanceVariable + + context 'with superclass pin for Parser::AST::Node' do + let(:superclass_pin) do + api_map.pins.find do |pin| + pin.is_a?(Solargraph::Pin::Reference::Superclass) && pin.context.namespace == 'Parser::AST::Node' + end + end + + it 'generates a rooted pin' do + # rooted! + expect(superclass_pin&.name).to eq('::AST::Node') + end + end + end + + if Gem::Version.new(RBS::VERSION) >= Gem::Version.new('3.9.1') + context 'with method pin for Open3.capture2e' do + let(:api_map) { Solargraph::ApiMap.load_with_cache('.') } - it { should_not be_nil } + let(:method_pin) do + api_map.pins.find do |pin| + pin.is_a?(Solargraph::Pin::Method) && pin.path == 'Open3.capture2e' + end + end - it { should be_a(Solargraph::Pin::Method) } + let(:chdir_param) do + method_pin&.signatures&.flat_map(&:parameters)&.find do |param| # rubocop:disable Style/SafeNavigationChainLength + param.name == 'chdir' + end + end - it 'maps untyped in RBS to undefined in Solargraph 'do - expect(method_pin.return_type.tag).to eq('undefined') + it 'accepts chdir kwarg' do + expect(chdir_param).not_to be_nil, -> { "Found pin #{method_pin.to_rbs} from #{method_pin.type_location}" } + end end end end diff --git a/spec/rbs_map/core_map_spec.rb b/spec/rbs_map/core_map_spec.rb index 352d29937..88590925b 100644 --- a/spec/rbs_map/core_map_spec.rb +++ b/spec/rbs_map/core_map_spec.rb @@ -6,7 +6,7 @@ pin = store.get_path_pins("Errno::#{const}").first expect(pin).to be_a(Solargraph::Pin::Namespace) superclass = store.get_superclass(pin.path) - expect(superclass).to eq('SystemCallError') + expect(superclass).to eq('::SystemCallError') end end diff --git a/spec/rbs_map_spec.rb b/spec/rbs_map_spec.rb index b06c975d1..f3ca90a36 100644 --- a/spec/rbs_map_spec.rb +++ b/spec/rbs_map_spec.rb @@ -3,7 +3,18 @@ spec = Gem::Specification.find_by_name('rbs') rbs_map = Solargraph::RbsMap.from_gemspec(spec, nil, nil) pin = rbs_map.path_pin('RBS::EnvironmentLoader#add_collection') - expect(pin).to be + expect(pin).not_to be_nil + end + + it 'fails if it does not find data from gemspec' do + spec = Gem::Specification.find_by_name('backport') + rbs_map = Solargraph::RbsMap.from_gemspec(spec, nil, nil) + expect(rbs_map).not_to be_resolved + end + + it 'fails if it does not find data from name' do + rbs_map = Solargraph::RbsMap.new('lskdflaksdfjl') + expect(rbs_map.pins).to be_empty end it 'converts constants and aliases to correct types' do diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index 91f84b4c7..0d48bfb97 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + require 'tmpdir' require 'open3' describe Solargraph::Shell do + let(:shell) { described_class.new } let(:temp_dir) { Dir.mktmpdir } before do @@ -25,20 +28,294 @@ def bundle_exec(*cmd) FileUtils.rm_rf(temp_dir) end - describe "--version" do - it "returns a version when run" do - output = bundle_exec("solargraph", "--version") + describe '--version' do + let(:output) { bundle_exec('solargraph', '--version') } + it 'returns output' do expect(output).not_to be_empty + end + + it 'returns a version when run' do expect(output).to eq("#{Solargraph::VERSION}\n") end end - describe "uncache" do - it "uncaches without erroring out" do - output = bundle_exec("solargraph", "uncache", "solargraph") + describe 'uncache' do + it 'uncaches without erroring out' do + output = capture_stdout do + shell.uncache('backport') + end expect(output).to include('Clearing pin cache in') end + + it 'uncaches stdlib without erroring out' do + expect { shell.uncache('stdlib') }.not_to raise_error + end + + it 'uncaches core without erroring out' do + expect { shell.uncache('core') }.not_to raise_error + end + end + + describe 'scan' do + context 'with mocked dependencies' do + let(:api_map) { instance_double(Solargraph::ApiMap) } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + end + + it 'scans without erroring out' do + allow(api_map).to receive(:pins).and_return([]) + output = capture_stdout do + shell.options = { directory: 'spec/fixtures/workspace' } + shell.scan + end + + expect(output).to include('Scanned ').and include(' seconds.') + end + end + end + + describe 'typecheck' do + context 'with mocked dependencies' do + let(:type_checker) { instance_double(Solargraph::TypeChecker) } + let(:api_map) { instance_double(Solargraph::ApiMap) } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + allow(Solargraph::TypeChecker).to receive(:new).and_return(type_checker) + allow(type_checker).to receive(:problems).and_return([]) + end + + it 'typechecks without erroring out' do + output = capture_stdout do + shell.options = { level: 'normal', directory: '.' } + shell.typecheck('Gemfile') + end + + expect(output).to include('Typecheck finished in') + end + end + end + + describe 'gems' do + context 'without mocked ApiMap' do + it 'complains when gem does not exist' do + output = capture_both do + shell.gems('nonexistentgem') + end + + expect(output).to include("Gem 'nonexistentgem' not found") + end + + it 'caches core without erroring out' do + capture_both do + shell.uncache('core') + end + + expect { shell.cache('core') }.not_to raise_error + end + + it 'gives sensible error for gem that does not exist' do + output = capture_both do + shell.gems('solargraph123') + end + + expect(output).to include("Gem 'solargraph123' not found") + end + end + + context 'with mocked Workspace' do + let(:workspace) { instance_double(Solargraph::Workspace) } + let(:gemspec) { instance_double(Gem::Specification, name: 'backport') } + + before do + allow(Solargraph::Workspace).to receive(:new).and_return(workspace) + end + + it 'caches all without erroring out' do + allow(workspace).to receive(:cache_all_for_workspace!) + + _output = capture_both { shell.gems } + + expect(workspace).to have_received(:cache_all_for_workspace!) + end + + it 'caches single gem without erroring out' do + allow(workspace).to receive(:find_gem).with('backport').and_return(gemspec) + allow(workspace).to receive(:cache_gem) + + capture_both do + shell.options = { rebuild: false } + shell.gems('backport') + end + + expect(workspace).to have_received(:cache_gem).with(gemspec, out: an_instance_of(StringIO), rebuild: false) + end + end + end + + describe 'cache' do + it 'caches a stdlib gem without erroring out' do + expect { shell.cache('stringio') }.not_to raise_error + end + + context 'when gem does not exist' do + subject(:call) { shell.cache('nonexistentgem8675309') } + + it 'gives a good error message' do + # capture stderr output + expect { call }.to output(/not found/).to_stderr + end + end + end + + describe 'method_pin' do + let(:api_map) { instance_double(Solargraph::ApiMap) } + let(:to_s_pin) { instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + allow(api_map).to receive(:get_path_pins).with('String#to_s').and_return([to_s_pin]) + end + + context 'with no options' do + it 'prints a pin' do + allow(to_s_pin).to receive(:inspect).and_return('pin inspect result') + + out = capture_both { shell.method_pin('String#to_s') } + + expect(out).to eq("pin inspect result\n") + end + end + + context 'with --rbs option' do + it 'prints a pin with RBS type' do + allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') + + out = capture_both do + shell.options = { rbs: true } + shell.method_pin('String#to_s') + end + expect(out).to eq("pin RBS result\n") + end + end + + context 'with --stack option' do + it 'prints a pin using stack results' do + allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') + + allow(api_map).to receive(:get_method_stack).and_return([to_s_pin]) + capture_both do + shell.options = { stack: true } + shell.method_pin('String#to_s') + end + expect(api_map).to have_received(:get_method_stack).with('String', 'to_s', scope: :instance) + end + + it 'prints a static pin using stack results' do + # allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') + string_new_pin = instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) + + allow(api_map).to receive(:get_method_stack).with('String', 'new', scope: :class).and_return([string_new_pin]) + capture_both do + shell.options = { stack: true } + shell.method_pin('String.new') + end + expect(api_map).to have_received(:get_method_stack).with('String', 'new', scope: :class) + end + end + + context 'with --typify option' do + it 'prints a pin with typify type' do + allow(to_s_pin).to receive(:typify).and_return(Solargraph::ComplexType.parse('::String')) + + out = capture_both do + shell.options = { typify: true } + shell.method_pin('String#to_s') + end + expect(out).to eq("::String\n") + end + end + + context 'with --typify --rbs options' do + it 'prints a pin with typify type' do + allow(to_s_pin).to receive(:typify).and_return(Solargraph::ComplexType.parse('::String')) + + out = capture_both do + shell.options = { typify: true, rbs: true } + shell.method_pin('String#to_s') + end + expect(out).to eq("::String\n") + end + end + + context 'with no pin' do + it 'prints error' do + allow(api_map).to receive(:get_path_pins).with('Not#found').and_return([]) + + out = capture_both do + shell.options = {} + shell.method_pin('Not#found') + rescue SystemExit + # Ignore the SystemExit raised by the shell when no pin is found + end + expect(out).to include("Pin not found for path 'Not#found'") + end + end + end + + describe 'rbs' do + let(:api_map) { instance_double(Solargraph::ApiMap) } + + before do + allow(shell).to receive(:`) + allow(Solargraph::ApiMap).to receive(:load).and_return(api_map) + allow(api_map).to receive(:source_maps).and_return(source_maps) + end + + context 'without inference' do + let(:source_maps) { [] } + + it 'invokes sord' do + capture_both do + shell.options = { filename: 'foo.rbs' } + shell.rbs + end + expect(shell) + .to have_received(:`) + .with("sord #{Dir.pwd}/sig/foo.rbs --rbs --no-regenerate") + end + end + + context 'with inference' do + let(:source_maps) { [source_map] } + let(:source_map) { instance_double(Solargraph::SourceMap) } + let(:pin) do + instance_double(Solargraph::Pin::Method, + namespace: 'My::Namespace', path: 'My::Namespace#foo', + visibility: :public, + parameters: [], + scope: :instance, + location: nil, + name: 'foo', + class: Solargraph::Pin::Method, + return_type: Solargraph::ComplexType::UNDEFINED) + end + + it 'infers unknown types on pins' do + allow(source_map).to receive(:pins).and_return([pin]) + allow(pin).to receive_messages(typify: Solargraph::ComplexType.parse('String'), + docstring: YARD::Docstring.new('')) + allow(pin).to receive(:code_object).and_return(nil) + capture_both do + shell.options = { filename: 'foo.rbs', inference: true } + shell.rbs + end + expect(pin).to have_received(:typify) + end + end end end diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 0f83331ec..788380478 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -302,6 +302,23 @@ def foo expect(type.tag).to eq('String') end + it 'infers method types from return nodes' do + source = Solargraph::Source.load_string(%( + class Foo + # @return [self] + def foo + bar + end + end + Foo.new.foo + ), 'test.rb') + map = Solargraph::ApiMap.new + map.map source + clip = map.clip_at('test.rb', Solargraph::Position.new(7, 10)) + type = clip.infer + expect(type.tag).to eq('Foo') + end + it 'infers multiple method types from return nodes' do source = Solargraph::Source.load_string(%( def foo @@ -1644,10 +1661,12 @@ def foo; end api_map = Solargraph::ApiMap.new.map(source) array_names = api_map.clip_at('test.rb', [5, 22]).complete.pins.map(&:name) - expect(array_names).to eq(["byteindex", "byterindex", "bytes", "bytesize", "byteslice", "bytesplice"]) + # other methods may come in via plugin default requires + expect(array_names).to include("byteindex", "byterindex", "bytes", "bytesize", "byteslice", "bytesplice") string_names = api_map.clip_at('test.rb', [6, 22]).complete.pins.map(&:name) - expect(string_names).to eq(['upcase', 'upcase!', 'upto']) + # other methods may come in via plugin default requires + expect(string_names).to include('upcase', 'upcase!', 'upto') end it 'completes global methods defined in top level scope inside class when referenced inside a namespace' do diff --git a/spec/source_map/mapper_spec.rb b/spec/source_map/mapper_spec.rb index af3678cfd..96d2bdab5 100644 --- a/spec/source_map/mapper_spec.rb +++ b/spec/source_map/mapper_spec.rb @@ -1525,9 +1525,9 @@ def bar; end def quz; end end )) - expect(map.first_pin('Foo#bar').visibility).to be(:public) - expect(map.first_pin('Foo#baz').visibility).to be(:private) - expect(map.first_pin('Foo#quz').visibility).to be(:public) + expect(map.first_pin('Foo#bar').visibility).to eq(:public) + expect(map.first_pin('Foo#baz').visibility).to eq(:private) + expect(map.first_pin('Foo#quz').visibility).to eq(:public) end it 'encloses class_eval calls in receivers' do diff --git a/spec/source_map_spec.rb b/spec/source_map_spec.rb index 398355075..b634491e4 100644 --- a/spec/source_map_spec.rb +++ b/spec/source_map_spec.rb @@ -72,7 +72,7 @@ class Foo end end )) - pin = map.locate_block_pin(3, 0) + pin = map.locate_closure_pin(3, 0) expect(pin).to be_a(Solargraph::Pin::Block) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 00cc6c8c3..5e3879a67 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,11 +1,14 @@ require 'bundler/setup' require 'webmock/rspec' +require 'rspec_time_guard' WebMock.disable_net_connect!(allow_localhost: true) unless ENV['SIMPLECOV_DISABLED'] # set up lcov reporting for undercover require 'simplecov' + require 'simplecov-lcov' require 'undercover/simplecov_formatter' - + SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true + SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter SimpleCov.start do cname = ENV.fetch('TEST_COVERAGE_COMMAND_NAME', 'ad-hoc') command_name cname @@ -25,8 +28,16 @@ # Allow use of --only-failures with rspec, handy for local development c.example_status_persistence_file_path = 'rspec-examples.txt' end +RspecTimeGuard.setup +RspecTimeGuard.configure do |config| + config.global_time_limit_seconds = 60 + config.continue_on_timeout = false +end + require 'solargraph' -# Suppress logger output in specs (if possible) +# execute any logging blocks to make sure they don't blow up +Solargraph::Logging.logger.sev_threshold = Logger::DEBUG +# ...but still suppress logger output in specs (if possible) if Solargraph::Logging.logger.respond_to?(:reopen) && !ENV.key?('SOLARGRAPH_LOG') Solargraph::Logging.logger.reopen(File::NULL) end @@ -43,3 +54,29 @@ def with_env_var(name, value) ENV[name] = old_value # Restore the old value end end + +def capture_stdout &block + original_stdout = $stdout + $stdout = StringIO.new + begin + block.call + $stdout.string + ensure + $stdout = original_stdout + end +end + +def capture_both &block + original_stdout = $stdout + original_stderr = $stderr + stringio = StringIO.new + $stdout = stringio + $stderr = stringio + begin + block.call + ensure + $stdout = original_stdout + $stderr = original_stderr + end + stringio.string +end diff --git a/spec/type_checker/checks_spec.rb b/spec/type_checker/checks_spec.rb deleted file mode 100644 index 41119cefd..000000000 --- a/spec/type_checker/checks_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -describe Solargraph::TypeChecker::Checks do - it 'validates simple core types' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String') - inf = Solargraph::ComplexType.parse('String') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'invalidates simple core types' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String') - inf = Solargraph::ComplexType.parse('Integer') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(false) - end - - it 'validates expected superclasses' do - source = Solargraph::Source.load_string(%( - class Sup; end - class Sub < Sup; end - )) - api_map = Solargraph::ApiMap.new - api_map.map source - sup = Solargraph::ComplexType.parse('Sup') - sub = Solargraph::ComplexType.parse('Sub') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, sup, sub) - expect(match).to be(true) - end - - it 'invalidates inferred superclasses (expected must be super)' do - # @todo This test might be invalid. There are use cases where inheritance - # between inferred and expected classes should be acceptable in either - # direction. - # source = Solargraph::Source.load_string(%( - # class Sup; end - # class Sub < Sup; end - # )) - # api_map = Solargraph::ApiMap.new - # api_map.map source - # sup = Solargraph::ComplexType.parse('Sup') - # sub = Solargraph::ComplexType.parse('Sub') - # match = Solargraph::TypeChecker::Checks.types_match?(api_map, sub, sup) - # expect(match).to be(false) - end - - it 'fuzzy matches arrays with parameters' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Array') - inf = Solargraph::ComplexType.parse('Array') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'fuzzy matches sets with parameters' do - source = Solargraph::Source.load_string("require 'set'") - source_map = Solargraph::SourceMap.map(source) - api_map = Solargraph::ApiMap.new - api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['set']) - exp = Solargraph::ComplexType.parse('Set') - inf = Solargraph::ComplexType.parse('Set') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'fuzzy matches hashes with parameters' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Hash{ Symbol => String}') - inf = Solargraph::ComplexType.parse('Hash') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'matches multiple types' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String, Integer') - inf = Solargraph::ComplexType.parse('String, Integer') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'matches multiple types out of order' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String, Integer') - inf = Solargraph::ComplexType.parse('Integer, String') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'invalidates inferred types missing from expected' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String') - inf = Solargraph::ComplexType.parse('String, Integer') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(false) - end - - it 'matches nil' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('nil') - inf = Solargraph::ComplexType.parse('nil') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'validates classes with expected superclasses' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Class') - inf = Solargraph::ComplexType.parse('Class') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'validates generic classes with expected Class' do - api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Class') - inf = Solargraph::ComplexType.parse('Class') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) - expect(match).to be(true) - end - - it 'validates inheritance in both directions' do - source = Solargraph::Source.load_string(%( - class Sup; end - class Sub < Sup; end - )) - api_map = Solargraph::ApiMap.new - api_map.map source - sup = Solargraph::ComplexType.parse('Sup') - sub = Solargraph::ComplexType.parse('Sub') - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sup, sub) - expect(match).to be(true) - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sub, sup) - expect(match).to be(true) - end - - it 'invalidates inheritance in both directions' do - api_map = Solargraph::ApiMap.new - sup = Solargraph::ComplexType.parse('String') - sub = Solargraph::ComplexType.parse('Array') - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sup, sub) - expect(match).to be(false) - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sub, sup) - expect(match).to be(false) - end -end diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb new file mode 100644 index 000000000..3ff5aa6c3 --- /dev/null +++ b/spec/type_checker/levels/alpha_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +describe Solargraph::TypeChecker do + context 'when at alpha level' do + def type_checker code + Solargraph::TypeChecker.load_string(code, 'test.rb', :alpha) + end + + it 'reports nilable type issues' do + checker = type_checker(%( + # @param a [String] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)) + .to eq(['Wrong argument type for #foo: a expected String, received String, nil']) + end + end +end diff --git a/spec/type_checker/levels/normal_spec.rb b/spec/type_checker/levels/normal_spec.rb index 3b38f55d8..0b3024f62 100644 --- a/spec/type_checker/levels/normal_spec.rb +++ b/spec/type_checker/levels/normal_spec.rb @@ -1,5 +1,5 @@ describe Solargraph::TypeChecker do - context 'normal level' do + context 'when checking at normal level' do def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :normal) end @@ -221,9 +221,9 @@ def bar; end # @todo This test uses kramdown-parser-gfm because it's a gem dependency known to # lack typed methods. A better test wouldn't depend on the state of # vendored code. + workspace = Solargraph::Workspace.new(Dir.pwd) gemspec = Gem::Specification.find_by_name('kramdown-parser-gfm') - yard_pins = Solargraph::GemPins.build_yard_pins([], gemspec) - Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) + workspace.cache_gem(gemspec) checker = type_checker(%( require 'kramdown-parser-gfm' diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index b198cec89..aac5c1ece 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -1,5 +1,5 @@ describe Solargraph::TypeChecker do - context 'strict level' do + context 'when checking at strict level' do # @return [Solargraph::TypeChecker] def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strict) @@ -59,7 +59,7 @@ def bar(a); end require 'kramdown-parser-gfm' Kramdown::Parser::GFM.undefined_call ), 'test.rb') - api_map = Solargraph::ApiMap.load_with_cache('.', $stdout) + api_map = Solargraph::ApiMap.load '.' api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['kramdown-parser-gfm']) checker = Solargraph::TypeChecker.new('test.rb', api_map: api_map, level: :strict) expect(checker.problems).to be_empty @@ -115,6 +115,39 @@ def bar(baz); end expect(checker.problems.first.message).to include('Wrong argument type') end + it 'reports mismatched argument types in chained calls' do + checker = type_checker(%( + # @param baz [Integer] + # @return [String] + def bar(baz); "foo"; end + bar('string').upcase + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('Wrong argument type') + end + + it 'reports mismatched argument types in calls inside array literals' do + checker = type_checker(%( + # @param baz [Integer] + # @return [String] + def bar(baz); "foo"; end + [ bar('string') ] + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('Wrong argument type') + end + + it 'reports mismatched argument types in calls inside array literals used in a chain' do + checker = type_checker(%( + # @param baz [Integer] + # @return [String] + def bar(baz); "foo"; end + [ bar('string') ].compact + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('Wrong argument type') + end + xit 'complains about calling a private method from an illegal place' xit 'complains about calling a non-existent method' @@ -126,7 +159,7 @@ def foo(a) a[0] = :something end )) - expect(checker.problems.map(&:problems)).to eq(['Wrong argument type']) + expect(checker.problems.map(&:message)).to eq(['Wrong argument type']) end it 'complains about dereferencing a non-existent tuple slot' @@ -666,6 +699,19 @@ def test(foo: nil) expect(checker.problems).to be_empty end + + it 'validates parameters in function calls' do + checker = type_checker(%( + # @param bar [String] + def foo(bar); end + + def baz + foo(123) + end + )) + expect(checker.problems.map(&:message)).to eq(['Wrong argument type for #foo: bar expected String, received 123']) + end + it 'validates inferred return types with complex tags' do checker = type_checker(%( # @param foo [Numeric, nil] a foo @@ -814,6 +860,23 @@ def foo *path, baz; end expect(checker.problems.map(&:message)).to eq([]) end + it "understands enough of define_method not to think the block is in class scope" do + checker = type_checker(%( + class Foo + def initialize + @resolved_method = nil + end + + def bar + end + + define_method('a') do + bar + end + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end it 'understands tuple superclass' do checker = type_checker(%( @@ -941,5 +1004,22 @@ def bar(a) )) expect(checker.problems.map(&:message)).to eq([]) end + + it 'does not complain on defaulted reader with detailed expression' do + checker = type_checker(%( + class Foo + # @return [Integer, nil] + def bar + @bar ||= + if rand + 123 + elsif rand + 456 + end + end + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end end end diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 6fdf84e30..36988adbe 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -1,5 +1,5 @@ describe Solargraph::TypeChecker do - context 'strong level' do + context 'when tracking at strong level' do def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end @@ -15,6 +15,48 @@ def bar; end expect(checker.problems.map(&:message)).to eq(['Unneeded @sg-ignore comment']) end + it 'does not complain on array dereference' do + checker = type_checker(%( + # @param idx [Integer, nil] an index + # @param arr [Array] an array of integers + # + # @return [void] + def foo(idx, arr) + arr[idx] + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'complains on bad @type assignment' do + checker = type_checker(%( + # @type [Integer] + c = Class.new + )) + expect(checker.problems.map(&:message)) + .to eq ['Declared type Integer does not match inferred type Class for variable c'] + end + + it 'does not complain on another variant of Class.new' do + checker = type_checker(%( + class Class + # @return [self] + def self.blah + new + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on indirect Class.new', skip: 'hangs in a loop currently' do + checker = type_checker(%( + class Foo < Class; end + Foo.new + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'reports missing return tags' do checker = type_checker(%( class Foo @@ -25,6 +67,69 @@ def bar; end expect(checker.problems.first.message).to include('Missing @return tag') end + it 'ignores nilable type issues' do + checker = type_checker(%( + # @param a [String] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'calls out keyword issues even when required arg count matches' do + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b:); end + + # @return [void] + def bar + foo('baz') + end + )) + expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') + end + + it 'calls out type issues even when keyword issues are there' do + pending('fixes to arg vs param checking algorithm') + + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b:); end + + # @return [void] + def bar + foo(123) + end + )) + expect(checker.problems.map(&:message)) + .to include('Wrong argument type for #foo: a expected String, received 123') + end + + it 'calls out keyword issues even when arg type issues are there' do + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b:); end + + # @return [void] + def bar + foo(123) + end + )) + expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') + end + it 'reports missing param tags' do checker = type_checker(%( class Foo @@ -136,6 +241,149 @@ def bar &block expect(checker.problems).to be_empty end + it 'does not need fully specified container types' do + checker = type_checker(%( + class Foo + # @param foo [Array] + # @return [void] + def bar foo: []; end + + # @param bing [Array] + # @return [void] + def baz(bing) + bar(foo: bing) + generic_values = [1,2,3].map(&:to_s) + bar(foo: generic_values) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'treats a parameter type of undefined as not provided' do + checker = type_checker(%( + class Foo + # @param foo [Array] + # @return [void] + def bar foo: []; end + + # @param bing [Array] + # @return [void] + def baz(bing) + bar(foo: bing) + generic_values = [1,2,3].map(&:to_s) + bar(foo: generic_values) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failure with no generic tag' do + checker = type_checker(%( + class Foo + # @param foo [Class] + # @return [void] + def bar foo:; end + + # @param bing [Class>] + # @return [void] + def baz(bing) + bar(foo: bing) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores undefined resolution failures' do + checker = type_checker(%( + class Foo + # @generic T + # @param klass [Class>] + # @return [Set>] + def pins_by_class klass; [].to_set; end + end + class Bar + # @return [Enumerable] + def block_pins + foo = Foo.new + foo.pins_by_class(Integer) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failures from current Solargraph limitation' do + checker = type_checker(%( + class Foo + # @generic T + # @param klass [Class>] + # @return [Set>] + def pins_by_class klass; [].to_set; end + end + class Bar + # @return [Enumerable] + def block_pins + foo = Foo.new + foo.pins_by_class(Integer) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failures with only one arg' do + checker = type_checker(%( + # @generic T + # @param path [String] + # @param klass [Class>] + # @return [void] + def code_object_at path, klass = Integer + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on select { is_a? } pattern' do + checker = type_checker(%( + # @param arr [Enumerable} + # @return [Enumerable] + def downcast_arr(arr) + arr.select { |pin| pin.is_a?(Integer) } + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on adding nil to types via return value' do + checker = type_checker(%( + # @param bar [Integer] + # @return [Integer, nil] + def foo(bar) + bar + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on adding nil to types via select' do + checker = type_checker(%( + # @return [Float, nil]} + def bar; rand; end + + # @param arr [Enumerable} + # @return [Integer, nil] + def downcast_arr(arr) + # @type [Object, nil] + foo = arr.select { |pin| pin.is_a?(Integer) && bar }.last + foo + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'inherits param tags from superclass methods' do checker = type_checker(%( class Foo @@ -152,5 +400,19 @@ def meth arg )) expect(checker.problems).to be_empty end + + it 'understands Open3 methods' do + checker = type_checker(%( + require 'open3' + + # @return [void] + def run_command + # @type [Hash{String => String}] + foo = {'foo' => 'bar'} + Open3.capture2e(foo, 'ls', chdir: '/tmp') + end + )) + expect(checker.problems.map(&:message)).to be_empty + end end end diff --git a/spec/type_checker/levels/typed_spec.rb b/spec/type_checker/levels/typed_spec.rb index b10bbd42c..e07aba2d3 100644 --- a/spec/type_checker/levels/typed_spec.rb +++ b/spec/type_checker/levels/typed_spec.rb @@ -1,6 +1,6 @@ describe Solargraph::TypeChecker do - context 'typed level' do - def type_checker(code) + context 'when checking at typed level' do + def type_checker code Solargraph::TypeChecker.load_string(code, 'test.rb', :typed) end @@ -38,6 +38,19 @@ def bar expect(checker.problems.first.message).to include('does not match') end + it 'reports mismatched key and subtypes' do + checker = type_checker(%( + # @return [Hash{String => String}] + def foo + # @type h [Hash{Integer => String}] + h = {} + h + end + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('does not match') + end + it 'reports mismatched inherited return tags' do checker = type_checker(%( class Sup @@ -189,6 +202,31 @@ def foo expect(checker.problems).to be_empty end + it 'validates default values of parameters' do + checker = type_checker(%( + # @param bar [String] + def foo(bar = 123); end + )) + expect(checker.problems.map(&:message)) + .to eq(['Declared type String does not match inferred type 123 for variable bar']) + end + + it 'validates string default values of parameters' do + checker = type_checker(%( + # @param bar [String] + def foo(bar = 'foo'); end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'validates symbol default values of parameters' do + checker = type_checker(%( + # @param bar [Symbol] + def foo(bar = :baz); end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + it 'validates subclass arguments of param types' do checker = type_checker(%( class Sup diff --git a/spec/workspace/gemspecs_fetch_dependencies_spec.rb b/spec/workspace/gemspecs_fetch_dependencies_spec.rb new file mode 100644 index 000000000..9a21f967c --- /dev/null +++ b/spec/workspace/gemspecs_fetch_dependencies_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +describe Solargraph::Workspace::Gemspecs, '#fetch_dependencies' do + subject(:deps) { gemspecs.fetch_dependencies(gemspec) } + + let(:gemspecs) { described_class.new(dir_path) } + let(:dir_path) { Dir.pwd } + + context 'when in our bundle' do + context 'with a Bundler::LazySpecification' do + let(:gemspec) do + Bundler::LazySpecification.new('solargraph', nil, nil) + end + + it 'finds a known dependency' do + expect(deps.map(&:name)).to include('backport') + end + end + + context 'with gem whose dependency does not exist in our bundle' do + let(:gemspec) do + instance_double(Gem::Specification, + dependencies: [Gem::Dependency.new('activerecord')], + development_dependencies: [], + name: 'my_fake_gem', + version: '123') + end + let(:gem_name) { 'my_fake_gem' } + + it 'gives a useful message' do + output = capture_both { deps.map(&:name) } + expect(output).to include('Please install the gem activerecord') + end + end + end + + context 'with external bundle' do + let(:dir_path) { File.realpath(Dir.mktmpdir).to_s } + + let(:gemspec) do + Bundler::LazySpecification.new(gem_name, nil, nil) + end + + before do + # write out Gemfile + File.write(File.join(dir_path, 'Gemfile'), <<~GEMFILE) + source 'https://rubygems.org' + gem '#{gem_name}' + GEMFILE + + # run bundle install + output, status = Solargraph.with_clean_env do + Open3.capture2e('bundle install --verbose', chdir: dir_path) + end + raise "Failure installing bundle: #{output}" unless status.success? + + # ensure Gemfile.lock exists + unless File.exist?(File.join(dir_path, 'Gemfile.lock')) + raise "Gemfile.lock not found after bundle install in #{dir_path}" + end + end + + context 'with gem that exists in our bundle' do + let(:gem_name) { 'undercover' } + + it 'finds dependencies' do + expect(deps.map(&:name)).to include('ast') + end + end + + context 'with gem does not exist in our bundle' do + let(:gem_name) { 'activerecord' } + + it 'gives a useful message' do + dep_names = nil + output = capture_both { dep_names = deps.map(&:name) } + expect(output).to include('Please install the gem activerecord') + end + end + end +end diff --git a/spec/workspace/gemspecs_find_gem_spec.rb b/spec/workspace/gemspecs_find_gem_spec.rb new file mode 100644 index 000000000..35f5e7a15 --- /dev/null +++ b/spec/workspace/gemspecs_find_gem_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +describe Solargraph::Workspace::Gemspecs, '#find_gem' do + subject(:gemspec) { gemspecs.find_gem(name, version, out: out) } + + let(:gemspecs) { described_class.new(dir_path) } + let(:out) { StringIO.new } + + context 'with local bundle' do + let(:dir_path) { File.realpath(Dir.pwd) } + + context 'with solargraph from bundle' do + let(:name) { 'solargraph' } + let(:version) { nil } + + it 'returns the gem' do + expect(gemspec.name).to eq(name) + end + end + + context 'with random from core' do + let(:name) { 'random' } + let(:version) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'does not complain' do + expect(out.string).to be_empty + end + end + + context 'with ripper from core' do + let(:name) { 'ripper' } + let(:version) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + end + + context 'with base64 from stdlib' do + let(:name) { 'base64' } + let(:version) { nil } + + it 'returns a gemspec' do + expect(gemspec).not_to be_nil + end + end + + context 'with gem not in bundle' do + let(:name) { 'checkoff' } + let(:version) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'complains' do + gemspec + + expect(out.string).to include('install the gem checkoff ') + end + end + + context 'with gem not in bundle but no logger' do + let(:name) { 'checkoff' } + let(:version) { nil } + let(:out) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'does not fail' do + expect { gemspec }.not_to raise_error + end + end + + context 'with gem not in bundle with version' do + let(:name) { 'checkoff' } + let(:version) { '1.0.0' } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'complains' do + gemspec + + expect(out.string).to include('install the gem checkoff:1.0.0') + end + end + end +end diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb new file mode 100644 index 000000000..f62ece83b --- /dev/null +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -0,0 +1,291 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +describe Solargraph::Workspace::Gemspecs, '#resolve_require' do + subject(:specs) { gemspecs.resolve_require(require) } + + let(:gemspecs) { described_class.new(dir_path) } + + def find_or_install gem_name, version + Gem::Specification.find_by_name(gem_name, version) + rescue Gem::LoadError + install_gem(gem_name, version) + end + + def add_bundle + # write out Gemfile + File.write(File.join(dir_path, 'Gemfile'), <<~GEMFILE) + source 'https://rubygems.org' + gem 'backport' + GEMFILE + # run bundle install + output, status = Solargraph.with_clean_env do + Open3.capture2e('bundle install --verbose', chdir: dir_path) + end + raise "Failure installing bundle: #{output}" unless status.success? + # ensure Gemfile.lock exists + return if File.exist?(File.join(dir_path, 'Gemfile.lock')) + raise "Gemfile.lock not found after bundle install in #{dir_path}" + end + + def install_gem gem_name, version + Bundler.with_unbundled_env do + cmd = Gem::Commands::InstallCommand.new + cmd.handle_options [gem_name, '-v', version] + cmd.execute + rescue Gem::SystemExitException => e + raise unless e.exit_code == 0 + end + end + + context 'with local bundle' do + let(:dir_path) { File.realpath(Dir.pwd) } + + context 'with a known gem' do + let(:require) { 'solargraph' } + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with an unknown type from Bundler / RubyGems' do + let(:require) { 'solargraph' } + let(:specish_objects) { [double] } + + before do + lockfile = instance_double(Pathname) + locked_gems = instance_double(Bundler::LockfileParser, specs: specish_objects) + + definition = instance_double(Bundler::Definition, + locked_gems: locked_gems, + lockfile: lockfile) + allow(Bundler).to receive(:definition).and_return(definition) + allow(lockfile).to receive(:to_s).and_return(dir_path) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + def configure_bundler_spec stub_value + platform = Gem::Platform::RUBY + bundler_stub_spec = Bundler::StubSpecification.new('solargraph', '123', platform, spec_fetcher) + specish_objects = [bundler_stub_spec] + lockfile = instance_double(Pathname) + locked_gems = instance_double(Bundler::LockfileParser, specs: specish_objects) + definition = instance_double(Bundler::Definition, + locked_gems: locked_gems, + lockfile: lockfile) + # specish_objects = Bundler.definition.locked_gems.specs + allow(Bundler).to receive(:definition).and_return(definition) + allow(lockfile).to receive(:to_s).and_return(dir_path) + allow(bundler_stub_spec).to receive(:respond_to?).with(:name).and_return(true) + allow(bundler_stub_spec).to receive(:respond_to?).with(:version).and_return(true) + allow(bundler_stub_spec).to receive(:respond_to?).with(:gem_dir).and_return(false) + allow(bundler_stub_spec).to receive(:respond_to?).with(:materialize_for_installation).and_return(false) + allow(bundler_stub_spec).to receive_messages(name: 'solargraph', stub: stub_value) + end + + context 'with a Bundler::StubSpecification from Bundler / RubyGems' do + # this can happen from local gems, which is hard to test + # organically + + let(:require) { 'solargraph' } + let(:spec_fetcher) { instance_double(Gem::SpecFetcher) } + + before do + platform = Gem::Platform::RUBY + real_spec = instance_double(Gem::Specification) + allow(real_spec).to receive(:name).and_return('solargraph') + gem_stub_spec = Gem::StubSpecification.new('solargraph', '123', platform, spec_fetcher) + configure_bundler_spec(gem_stub_spec) + allow(gem_stub_spec).to receive_messages(name: 'solargraph', version: '123', spec: real_spec) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with a Bundler::StubSpecification that resolves straight to Gem::Specification' do + # have seen different behavior with different versions of rubygems/bundler + + let(:require) { 'solargraph' } + let(:spec_fetcher) { instance_double(Gem::SpecFetcher) } + let(:real_spec) { Gem::Specification.new('solargraph', '123') } + + before do + configure_bundler_spec(real_spec) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with a less usual require mapping' do + let(:require) { 'diff/lcs' } + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['diff-lcs']) + end + end + + context 'with Bundler.require' do + let(:require) { 'bundler/require' } + + it 'returns the gemspec gem' do + expect(specs.map(&:name)).to include('solargraph') + end + end + end + + context 'with nil as directory' do + let(:dir_path) { nil } + + context 'with simple require' do + let(:require) { 'solargraph' } + + it 'finds solargraph' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with Bundler.require' do + let(:require) { 'bundler/require' } + + it 'finds nothing' do + expect(specs).to be_empty + end + end + end + + context 'with external bundle' do + let(:dir_path) { File.realpath(Dir.mktmpdir).to_s } + + context 'with no actual bundle' do + let(:require) { 'bundler/require' } + + it 'raises' do + expect { specs }.to raise_error(Solargraph::BundleNotFoundError) + end + end + + context 'with Gemfile and Bundler.require' do + before { add_bundle } + + let(:require) { 'bundler/require' } + + it 'does not raise' do + expect { specs }.not_to raise_error + end + + it 'returns gems' do + expect(specs.map(&:name)).to include('backport') + end + end + + context 'with Gemfile but an unknown gem' do + before { add_bundle } + + let(:require) { 'unknown_gemlaksdflkdf' } + + it 'returns nil' do + expect(specs).to be_nil + end + end + + context 'with a Gemfile and a gem preference' do + # find_or_install helper doesn't seem to work on older versions + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.1.0') + before do + add_bundle + find_or_install('backport', '1.0.0') + Gem::Specification.find_by_name('backport', '= 1.0.0') + end + + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = '1.0.0' + end + ] + end + + it 'returns the preferred gemspec' do + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq('1.0.0') + end + + context 'with a gem preference that does not exist' do + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = '99.0.0' + end + ] + end + + it 'returns the gemspec we do have' do + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq('1.2.0') + end + end + + context 'with a gem preference already set to the version we use' do + let(:version) { Gem::Specification.find_by_name('backport').version.to_s } + + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = version + end + ] + end + + it 'returns the gemspec we do have' do + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq(version) + end + end + end + end + end +end diff --git a/spec/workspace/require_paths_spec.rb b/spec/workspace/require_paths_spec.rb new file mode 100644 index 000000000..eb95d0c5b --- /dev/null +++ b/spec/workspace/require_paths_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' + +describe Solargraph::Workspace::RequirePaths do + subject(:paths) { described_class.new(dir_path, config).generate } + + let(:config) { Solargraph::Workspace::Config.new(dir_path) } + + context 'with no config' do + let(:dir_path) { Dir.pwd } + let(:config) { nil } + + it 'includes the lib directory' do + expect(paths).to include(File.join(dir_path, 'lib')) + end + end + + context 'with config and no gemspec' do + let(:dir_path) { File.realpath(Dir.pwd) } + + let(:config) { instance_double(Solargraph::Workspace::Config, require_paths: [], allow?: true) } + + it 'includes the lib directory' do + expect(paths).to include(File.join(dir_path, 'lib')) + end + end + + context 'with current bundle' do + let(:dir_path) { Dir.pwd } + + it 'includes the lib directory' do + expect(paths).to include(File.join(dir_path, 'lib')) + end + + it 'queried via Open3.capture3' do + allow(Open3).to receive(:capture3).and_call_original + + paths + + expect(Open3).to have_received(:capture3) + end + end + + context 'with an invalid gemspec file' do + let(:dir_path) { File.realpath(Dir.mktmpdir) } + let(:gemspec_file) { File.join(dir_path, 'invalid.gemspec') } + + before do + File.write(gemspec_file, 'bogus') + end + + it 'includes the lib directory' do + expect(paths).to include(File.join(dir_path, 'lib')) + end + + it 'does not raise an error' do + expect { paths }.not_to raise_error + end + end + + context 'with a valid gemspec file that outputs to stdout' do + let(:dir_path) { File.realpath(Dir.mktmpdir) } + let(:gemspec_file) { File.join(dir_path, 'invalid.gemspec') } + + before do + File.write(gemspec_file, "print '{'; Gem::Specification.new") + end + + it 'includes the lib directory' do + expect(paths).to include(File.join(dir_path, 'lib')) + end + + it 'does not raise an error' do + expect { paths }.not_to raise_error + end + end + + context 'with no gemspec file' do + let(:dir_path) { File.realpath(Dir.mktmpdir) } + + it 'includes the lib directory' do + expect(paths).to include(File.join(dir_path, 'lib')) + end + end +end diff --git a/spec/workspace_spec.rb b/spec/workspace_spec.rb index 572c3e131..74d232a31 100644 --- a/spec/workspace_spec.rb +++ b/spec/workspace_spec.rb @@ -68,13 +68,6 @@ }.not_to raise_error end - it "detects gemspecs in workspaces" do - gemspec_file = File.join(dir_path, 'test.gemspec') - File.write(gemspec_file, '') - expect(workspace.gemspec?).to be(true) - expect(workspace.gemspecs).to eq([gemspec_file]) - end - it "generates default require path" do expect(workspace.require_paths).to eq([File.join(dir_path, 'lib')]) end @@ -124,19 +117,52 @@ it "uses configured require paths" do workspace = Solargraph::Workspace.new('spec/fixtures/workspace') - expect(workspace.require_paths).to eq(['spec/fixtures/workspace/lib', 'spec/fixtures/workspace/ext']) - end - - it 'ignores gemspecs in excluded directories' do - # vendor/**/* is excluded by default - workspace = Solargraph::Workspace.new('spec/fixtures/vendored') - expect(workspace.gemspecs).to be_empty + expect(workspace.require_paths).to eq([File.absolute_path('spec/fixtures/workspace/lib'), + File.absolute_path('spec/fixtures/workspace/ext')]) end it 'rescues errors loading files into sources' do - config = double(:Config, directory: './path', calculated: ['./path/does_not_exist.rb'], max_files: 5000, require_paths: [], plugins: []) + config = double(:Config, directory: './path', calculated: ['./path/does_not_exist.rb'], max_files: 5000, + require_paths: [], plugins: []) expect { Solargraph::Workspace.new('./path', config) }.not_to raise_error end + + describe '#cache_all_for_workspace!' do + let(:pin_cache) { instance_double(Solargraph::PinCache) } + let(:gemspecs) { instance_double(Solargraph::Workspace::Gemspecs) } + + before do + allow(Solargraph::Workspace::Gemspecs).to receive(:new).and_return(gemspecs) + allow(Solargraph::PinCache).to receive(:cache_core) + allow(Solargraph::PinCache).to receive(:possible_stdlibs) + allow(Solargraph::PinCache).to receive(:new).and_return(pin_cache) + allow(pin_cache).to receive(:cache_gem) + allow(pin_cache).to receive(:cache_all_stdlibs) + end + + it 'caches core pins' do + allow(Solargraph::PinCache).to receive(:core?).and_return(false) + allow(gemspecs).to receive(:all_gemspecs_from_bundle).and_return([]) + allow(pin_cache).to receive(:possible_stdlibs).and_return([]) + + workspace.cache_all_for_workspace!(nil, rebuild: false) + + expect(Solargraph::PinCache).to have_received(:cache_core).with(out: nil) + end + + it 'caches gems' do + gemspec = instance_double(Gem::Specification, name: 'test_gem', version: '1.0.0') + allow(Solargraph::PinCache).to receive(:core?).and_return(true) + allow(gemspecs).to receive(:all_gemspecs_from_bundle).and_return([gemspec]) + allow(pin_cache).to receive(:cached?).with(gemspec).and_return(false) + allow(pin_cache).to receive(:possible_stdlibs).and_return([]) + + workspace.cache_all_for_workspace!(nil, rebuild: false) + + expect(pin_cache).to have_received(:cache_gem) + .with(gemspec: gemspec, rebuild: false, out: nil) + end + end end diff --git a/spec/yard_map/mapper_spec.rb b/spec/yard_map/mapper_spec.rb index 6b00e5c33..450b459e4 100644 --- a/spec/yard_map/mapper_spec.rb +++ b/spec/yard_map/mapper_spec.rb @@ -1,4 +1,14 @@ describe Solargraph::YardMap::Mapper do + before :all do # rubocop:disable RSpec/BeforeAfterAll + @api_map = Solargraph::ApiMap.load('.') + end + + def pins_with require + doc_map = Solargraph::DocMap.new([require], @api_map.workspace, out: nil) # rubocop:disable RSpec/InstanceVariable + doc_map.cache_doc_map_gems!(nil) + doc_map.pins + end + it 'converts nil docstrings to empty strings' do dir = File.absolute_path(File.join('spec', 'fixtures', 'yard_map')) Dir.chdir dir do @@ -14,50 +24,33 @@ it 'marks explicit methods' do # Using rspec-expectations because it's a known dependency - rspec = Gem::Specification.find_by_name('rspec-expectations') - Solargraph::Yardoc.cache([], rspec) - Solargraph::Yardoc.load!(rspec) - pins = Solargraph::YardMap::Mapper.new(YARD::Registry.all).map - pin = pins.find { |pin| pin.path == 'RSpec::Matchers#be_truthy' } + pin = pins_with('rspec-expectations').find { |pin| pin.path == 'RSpec::Matchers#be_truthy' } + expect(pin).not_to be_nil expect(pin.explicit?).to be(true) end it 'marks correct return type from Logger.new' do # Using logger because it's a known dependency - logger = Gem::Specification.find_by_name('logger') - Solargraph::Yardoc.cache([], logger) - registry = Solargraph::Yardoc.load!(logger) - pins = Solargraph::YardMap::Mapper.new(registry).map - pins = pins.select { |pin| pin.path == 'Logger.new' } + pins = pins_with('logger').select { |pin| pin.path == 'Logger.new' } expect(pins.map(&:return_type).uniq.map(&:to_s)).to eq(['self']) end it 'marks correct return type from RuboCop::Options.new' do # Using rubocop because it's a known dependency - rubocop = Gem::Specification.find_by_name('rubocop') - Solargraph::Yardoc.cache([], rubocop) - Solargraph::Yardoc.load!(rubocop) - pins = Solargraph::YardMap::Mapper.new(YARD::Registry.all).map - pins = pins.select { |pin| pin.path == 'RuboCop::Options.new' } + pins = pins_with('rubocop').select { |pin| pin.path == 'RuboCop::Options.new' } expect(pins.map(&:return_type).uniq.map(&:to_s)).to eq(['self']) expect(pins.flat_map(&:signatures).map(&:return_type).uniq.map(&:to_s)).to eq(['self']) end it 'marks non-explicit methods' do # Using rspec-expectations because it's a known dependency - rspec = Gem::Specification.find_by_name('rspec-expectations') - Solargraph::Yardoc.load!(rspec) - pins = Solargraph::YardMap::Mapper.new(YARD::Registry.all).map - pin = pins.find { |pin| pin.path == 'RSpec::Matchers#expect' } + pin = pins_with('rspec-expectations').find { |pin| pin.path == 'RSpec::Matchers#expect' } expect(pin.explicit?).to be(false) end it 'adds superclass references' do # Asssuming the yard gem exists because it's a known dependency - gemspec = Gem::Specification.find_by_name('yard') - Solargraph::Yardoc.cache([], gemspec) - pins = Solargraph::YardMap::Mapper.new(Solargraph::Yardoc.load!(gemspec)).map - pin = pins.find do |pin| + pin = pins_with('yard').find do |pin| pin.is_a?(Solargraph::Pin::Reference::Superclass) && pin.name == 'YARD::CodeObjects::NamespaceObject' end expect(pin.closure.path).to eq('YARD::CodeObjects::ClassObject') @@ -65,10 +58,7 @@ it 'adds include references' do # Asssuming the ast gem exists because it's a known dependency - gemspec = Gem::Specification.find_by_name('ast') - Solargraph::Yardoc.cache([], gemspec) - pins = Solargraph::YardMap::Mapper.new(Solargraph::Yardoc.load!(gemspec)).map - inc= pins.find do |pin| + inc = pins_with('ast').find do |pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.name == 'AST::Processor::Mixin' && pin.closure.path == 'AST::Processor' end expect(inc).to be_a(Solargraph::Pin::Reference::Include) @@ -76,10 +66,7 @@ it 'adds extend references' do # Asssuming the yard gem exists because it's a known dependency - gemspec = Gem::Specification.find_by_name('yard') - Solargraph::Yardoc.cache([], gemspec) - pins = Solargraph::YardMap::Mapper.new(Solargraph::Yardoc.load!(gemspec)).map - ext = pins.find do |pin| + ext = pins_with('yard').find do |pin| pin.is_a?(Solargraph::Pin::Reference::Extend) && pin.name == 'Enumerable' && pin.closure.path == 'YARD::Registry' end expect(ext).to be_a(Solargraph::Pin::Reference::Extend) diff --git a/spec/yardoc_spec.rb b/spec/yardoc_spec.rb new file mode 100644 index 000000000..e2fa5b89b --- /dev/null +++ b/spec/yardoc_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'tmpdir' +require 'open3' + +describe Solargraph::Yardoc do + around do |testobj| + @tmpdir = Dir.mktmpdir + + testobj.run + ensure + FileUtils.remove_entry(@tmpdir) # rubocop:disable RSpec/InstanceVariable + end + + let(:gem_yardoc_path) do + File.join(@tmpdir, 'solargraph', 'yardoc', 'test_gem') # rubocop:disable RSpec/InstanceVariable + end + + before do + FileUtils.mkdir_p(gem_yardoc_path) + end + + describe '#processing?' do + it 'returns true if the yardoc is being processed' do + FileUtils.touch(File.join(gem_yardoc_path, 'processing')) + expect(described_class.processing?(gem_yardoc_path)).to be(true) + end + + it 'returns false if the yardoc is not being processed' do + expect(described_class.processing?(gem_yardoc_path)).to be(false) + end + end + + describe '#load!' do + it 'does not blow up when called on empty directory' do + expect { described_class.load!(gem_yardoc_path) }.not_to raise_error + end + end + + describe '#build_docs' do + let(:workspace) { Solargraph::Workspace.new(Dir.pwd) } + let(:gemspec) { workspace.find_gem('rubocop') } + let(:output) { '' } + + before do + allow(Solargraph.logger).to receive(:warn) + allow(Solargraph.logger).to receive(:info) + end + + it 'builds docs for a gem' do + described_class.build_docs(gem_yardoc_path, [], gemspec) + expect(File.exist?(File.join(gem_yardoc_path, 'complete'))).to be true + end + + it 'bails quietly if directory given does not exist' do + allow(File).to receive(:exist?).and_return(false) + + expect do + described_class.build_docs(gem_yardoc_path, [], gemspec) + end.not_to raise_error + end + + it 'is idempotent' do + described_class.build_docs(gem_yardoc_path, [], gemspec) + described_class.build_docs(gem_yardoc_path, [], gemspec) # second time + expect(File.exist?(File.join(gem_yardoc_path, 'complete'))).to be true + end + + context 'with an error from yard' do + before do + allow(Open3).to receive(:capture2e).and_return([output, result]) + end + + let(:result) { instance_double(Process::Status) } + + it 'does not raise on error from yard' do + allow(result).to receive(:success?).and_return(false) + + expect do + described_class.build_docs(gem_yardoc_path, [], gemspec) + end.not_to raise_error + end + end + end +end