diff --git a/.github/workflows/indexing.yml b/.github/workflows/indexing.yml deleted file mode 100644 index d4723f72ed..0000000000 --- a/.github/workflows/indexing.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: CI (indexing) - -on: - push: - paths: - - 'Gemfile.lock' - - 'lib/ruby_indexer/**' - pull_request: - paths: - - 'Gemfile.lock' - - 'lib/ruby_indexer/**' - -jobs: - indexing_sanity_check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Ruby - uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0 - with: - bundler-cache: true - - - name: Index Top 100 Ruby gems - run: bundle exec rake index:topgems diff --git a/.rubocop.yml b/.rubocop.yml index 8509ba3bc5..3683494c79 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -49,8 +49,6 @@ Sorbet/TrueSigil: Enabled: true Include: - "test/**/*.rb" - - "lib/ruby_indexer/test/**/*.rb" - - "lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb" - "lib/ruby_lsp/scripts/compose_bundle.rb" - "lib/ruby_lsp/test_reporters/test_unit_reporter.rb" Exclude: @@ -64,9 +62,7 @@ Sorbet/StrictSigil: Exclude: - "**/*.rake" - "test/**/*.rb" - - "lib/ruby_indexer/test/**/*.rb" - "lib/ruby-lsp.rb" - - "lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb" - "lib/ruby_lsp/scripts/compose_bundle.rb" - "lib/ruby_lsp/test_helper.rb" - "lib/ruby_lsp/test_reporters/test_unit_reporter.rb" diff --git a/AGENTS.md b/AGENTS.md index 9a4ca1f7ee..d9704649e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,6 @@ Its parts are: - `ruby-lsp` gem: language server implementation and extra custom functionality to support the VS Code extension. This is the top level of the repository -- Ruby code indexer: static analysis engine to support features like go to definition, completion and workspace - symbols. This is entirely implemented inside `lib/ruby_indexer` - Companion VS Code extension that includes several integrations. The extension is entirely implemented in the `vscode` directory - Jekyll static documentation site. Fully implemented in `jekyll` diff --git a/Rakefile b/Rakefile index 76b5329adb..9cd37d67d0 100644 --- a/Rakefile +++ b/Rakefile @@ -9,16 +9,8 @@ Rake::TestTask.new(:test) do |t| t.test_files = FileList["test/**/*_test.rb"].exclude("test/fixtures/prism/**/*") end -namespace :test do - Rake::TestTask.new(:indexer) do |t| - t.libs << "test" - t.libs << "lib" - t.test_files = FileList["lib/ruby_indexer/test/**/*_test.rb"] - end -end - require "rubocop/rake_task" RuboCop::RakeTask.new -task default: ["test:indexer", :test] +task default: :test diff --git a/exe/ruby-lsp b/exe/ruby-lsp index c48ab2d800..e54c61b367 100755 --- a/exe/ruby-lsp +++ b/exe/ruby-lsp @@ -17,14 +17,6 @@ parser = OptionParser.new do |opts| options[:debug] = true end - opts.on("--time-index", "Measure the time it takes to index the project") do - options[:time_index] = true - end - - opts.on("--doctor", "Run troubleshooting steps") do - options[:doctor] = true - end - opts.on("--use-launcher", "[EXPERIMENTAL] Use launcher mechanism to handle missing dependencies gracefully") do options[:launcher] = true end @@ -110,44 +102,6 @@ if options[:debug] end end -if options[:time_index] - index = RubyIndexer::Index.new - - time_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) - index.index_all - elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - time_start - - entries = index.instance_variable_get(:@entries) - entries_by_entry_type = entries.values.flatten.group_by(&:class) - - puts <<~MSG - Ruby LSP v#{RubyLsp::VERSION}: Indexing took #{elapsed_time.round(5)} seconds and generated: - - #{entries_by_entry_type.sort_by { |k, _| k.to_s }.map { |k, v| "#{k.name.split("::").last}: #{v.size}" }.join("\n- ")} - MSG - return -end - -if options[:doctor] - index = RubyIndexer::Index.new - - if File.exist?(".index.yml") - begin - config = YAML.parse_file(".index.yml").to_ruby - rescue => e - abort("Error parsing config: #{e.message}") - end - index.configuration.apply_config(config) - end - - puts "Globbing for indexable files" - - index.configuration.indexable_uris.each do |uri| - puts "indexing: #{uri}" - index.index_file(uri) - end - return -end - server = RubyLsp::Server.new # Ensure all output goes out stderr by default to allow puts/p/pp to work diff --git a/exe/ruby-lsp-check b/exe/ruby-lsp-check index 3b1125ef9b..723b89141d 100755 --- a/exe/ruby-lsp-check +++ b/exe/ruby-lsp-check @@ -36,21 +36,6 @@ ensure end puts "\n" -# Indexing -puts "Verifying that indexing executes successfully. This may take a while..." - -index = RubyIndexer::Index.new -uris = index.configuration.indexable_uris - -uris.each_with_index do |uri, i| - index.index_file(uri) -rescue => e - errors[uri.full_path] = e -ensure - print("\033[M\033[0KIndexed #{i + 1}/#{uris.length}") unless ENV["CI"] -end -puts "\n" - if errors.empty? puts "All operations completed successfully!" exit diff --git a/lib/ruby_indexer/lib/ruby_indexer/configuration.rb b/lib/ruby_indexer/lib/ruby_indexer/configuration.rb deleted file mode 100644 index 46afacca99..0000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +++ /dev/null @@ -1,276 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module RubyIndexer - class Configuration - CONFIGURATION_SCHEMA = { - "excluded_gems" => Array, - "included_gems" => Array, - "excluded_patterns" => Array, - "included_patterns" => Array, - "excluded_magic_comments" => Array, - }.freeze #: Hash[String, untyped] - - #: String - attr_writer :workspace_path - - #: Encoding - attr_accessor :encoding - - #: -> void - def initialize - @workspace_path = Dir.pwd #: String - @encoding = Encoding::UTF_8 #: Encoding - @excluded_gems = initial_excluded_gems #: Array[String] - @included_gems = [] #: Array[String] - - @excluded_patterns = [ - "**/{test,spec}/**/{*_test.rb,test_*.rb,*_spec.rb}", - "**/fixtures/**/*", - ] #: Array[String] - - path = Bundler.settings["path"] - if path - # Substitute Windows backslashes into forward slashes, which are used in glob patterns - glob = path.gsub(/[\\]+/, "/") - glob.delete_suffix!("/") - @excluded_patterns << "#{glob}/**/*.rb" - end - - # We start the included patterns with only the non excluded directories so that we can avoid paying the price of - # traversing large directories that don't include Ruby files like `node_modules` - @included_patterns = ["{#{top_level_directories.join(",")}}/**/*.rb", "*.rb"] #: Array[String] - @excluded_magic_comments = [ - "frozen_string_literal:", - "typed:", - "compiled:", - "encoding:", - "shareable_constant_value:", - "warn_indent:", - "rubocop:", - "nodoc:", - "doc:", - "coding:", - "warn_past_scope:", - ] #: Array[String] - end - - #: -> Array[URI::Generic] - def indexable_uris - excluded_gems = @excluded_gems - @included_gems - locked_gems = Bundler.locked_gems&.specs - - # NOTE: indexing the patterns (both included and excluded) needs to happen before indexing gems, otherwise we risk - # having duplicates if BUNDLE_PATH is set to a folder inside the project structure - - flags = File::FNM_PATHNAME | File::FNM_EXTGLOB - - uris = @included_patterns.flat_map do |pattern| - load_path_entry = nil #: String? - - Dir.glob(File.join(@workspace_path, pattern), flags).map! do |path| - # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every - # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This happens - # on repositories that define multiple gems, like Rails. All frameworks are defined inside the current - # workspace directory, but each one of them belongs to a different $LOAD_PATH entry - if load_path_entry.nil? || !path.start_with?(load_path_entry) - load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) } - end - - URI::Generic.from_path(path: path, load_path_entry: load_path_entry) - end - end - - # If the patterns are relative, we make it relative to the workspace path. If they are absolute, then we shouldn't - # concatenate anything - excluded_patterns = @excluded_patterns.map do |pattern| - if File.absolute_path?(pattern) - pattern - else - File.join(@workspace_path, pattern) - end - end - - # Remove user specified patterns - bundle_path = Bundler.settings["path"]&.gsub(/[\\]+/, "/") - uris.reject! do |indexable| - path = indexable.full_path #: as !nil - next false if test_files_ignored_from_exclusion?(path, bundle_path) - - excluded_patterns.any? { |pattern| File.fnmatch?(pattern, path, flags) } - end - - # Add default gems to the list of files to be indexed - Dir.glob(File.join(RbConfig::CONFIG["rubylibdir"], "*")).each do |default_path| - # The default_path might be a Ruby file or a folder with the gem's name. For example: - # bundler/ - # bundler.rb - # psych/ - # psych.rb - pathname = Pathname.new(default_path) - short_name = pathname.basename.to_s.delete_suffix(".rb") - - # If the gem name is excluded, then we skip it - next if excluded_gems.include?(short_name) - - # If the default gem is also a part of the bundle, we skip indexing the default one and index only the one in - # the bundle, which won't be in `default_path`, but will be in `Bundler.bundle_path` instead - next if locked_gems&.any? do |locked_spec| - locked_spec.name == short_name && - !Gem::Specification.find_by_name(short_name).full_gem_path.start_with?(RbConfig::CONFIG["rubylibprefix"]) - rescue Gem::MissingSpecError - # If a default gem is scoped to a specific platform, then `find_by_name` will raise. We want to skip those - # cases - true - end - - if pathname.directory? - # If the default_path is a directory, we index all the Ruby files in it - uris.concat( - Dir.glob(File.join(default_path, "**", "*.rb"), File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path| - URI::Generic.from_path(path: path, load_path_entry: RbConfig::CONFIG["rubylibdir"]) - end, - ) - elsif pathname.extname == ".rb" - # If the default_path is a Ruby file, we index it - uris << URI::Generic.from_path(path: default_path, load_path_entry: RbConfig::CONFIG["rubylibdir"]) - end - end - - # Add the locked gems to the list of files to be indexed - locked_gems&.each do |lazy_spec| - next if excluded_gems.include?(lazy_spec.name) - - spec = Gem::Specification.find_by_name(lazy_spec.name) - - # When working on a gem, it will be included in the locked_gems list. Since these are the project's own files, - # we have already included and handled exclude patterns for it and should not re-include or it'll lead to - # duplicates or accidentally ignoring exclude patterns - next if spec.full_gem_path == @workspace_path - - uris.concat( - spec.require_paths.flat_map do |require_path| - load_path_entry = File.join(spec.full_gem_path, require_path) - Dir.glob(File.join(load_path_entry, "**", "*.rb")).map! do |path| - URI::Generic.from_path(path: path, load_path_entry: load_path_entry) - end - end, - ) - rescue Gem::MissingSpecError - # If a gem is scoped only to some specific platform, then its dependencies may not be installed either, but they - # are still listed in locked_gems. We can't index them because they are not installed for the platform, so we - # just ignore if they're missing - end - - uris.uniq!(&:to_s) - uris - end - - #: -> Regexp - def magic_comment_regex - @magic_comment_regex ||= /^#\s*#{@excluded_magic_comments.join("|")}/ #: Regexp? - end - - #: (Hash[String, untyped] config) -> void - def apply_config(config) - validate_config!(config) - - @excluded_gems.concat(config["excluded_gems"]) if config["excluded_gems"] - @included_gems.concat(config["included_gems"]) if config["included_gems"] - @excluded_patterns.concat(config["excluded_patterns"]) if config["excluded_patterns"] - @included_patterns.concat(config["included_patterns"]) if config["included_patterns"] - @excluded_magic_comments.concat(config["excluded_magic_comments"]) if config["excluded_magic_comments"] - end - - private - - #: (Hash[String, untyped] config) -> void - def validate_config!(config) - errors = config.filter_map do |key, value| - type = CONFIGURATION_SCHEMA[key] - - if type.nil? - "Unknown configuration option: #{key}" - elsif !value.is_a?(type) - "Expected #{key} to be a #{type}, but got #{value.class}" - end - end - - raise ArgumentError, errors.join("\n") if errors.any? - end - - #: -> Array[String] - def initial_excluded_gems - excluded, others = Bundler.definition.dependencies.partition do |dependency| - dependency.groups == [:development] - end - - # When working on a gem, we need to make sure that its gemspec dependencies can't be excluded. This is necessary - # because Bundler doesn't assign groups to gemspec dependencies - # - # If the dependency is prerelease, `to_spec` may return `nil` due to a bug in older version of Bundler/RubyGems: - # https://github.com/Shopify/ruby-lsp/issues/1246 - this_gem = Bundler.definition.dependencies.find do |d| - d.to_spec&.full_gem_path == @workspace_path - rescue Gem::MissingSpecError - false - end - - others.concat(this_gem.to_spec.dependencies) if this_gem - others.concat( - others.filter_map do |d| - d.to_spec&.dependencies - rescue Gem::MissingSpecError - nil - end.flatten, - ) - others.uniq! - others.map!(&:name) - - transitive_excluded = excluded.each_with_object([]) do |dependency, acc| - next unless dependency.runtime? - - spec = dependency.to_spec - next unless spec - - spec.dependencies.each do |transitive_dependency| - next if others.include?(transitive_dependency.name) - - acc << transitive_dependency - end - rescue Gem::MissingSpecError - # If a gem is scoped only to some specific platform, then its dependencies may not be installed either, but they - # are still listed in dependencies. We can't index them because they are not installed for the platform, so we - # just ignore if they're missing - end - - excluded.concat(transitive_excluded) - excluded.uniq! - excluded.map(&:name) - rescue Bundler::GemfileNotFound - [] - end - - # Checks if the test file is never supposed to be ignored from indexing despite matching exclusion patterns, like - # `test_helper.rb` or `test_case.rb`. Also takes into consideration the possibility of finding these files under - # fixtures or inside gem source code if the bundle path points to a directory inside the workspace - #: (String path, String? bundle_path) -> bool - def test_files_ignored_from_exclusion?(path, bundle_path) - ["test_case.rb", "test_helper.rb"].include?(File.basename(path)) && - !File.fnmatch?("**/fixtures/**/*", path, File::FNM_PATHNAME | File::FNM_EXTGLOB) && - (!bundle_path || !path.start_with?(bundle_path)) - end - - #: -> Array[String] - def top_level_directories - excluded_directories = ["tmp", "node_modules", "sorbet"] - - Dir.glob("#{Dir.pwd}/*").filter_map do |path| - dir_name = File.basename(path) - next unless File.directory?(path) && !excluded_directories.include?(dir_name) - - dir_name - end - end - end -end diff --git a/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb b/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb deleted file mode 100644 index eb573cabfe..0000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +++ /dev/null @@ -1,1101 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module RubyIndexer - class DeclarationListener - OBJECT_NESTING = ["Object"].freeze #: Array[String] - BASIC_OBJECT_NESTING = ["BasicObject"].freeze #: Array[String] - - #: Array[String] - attr_reader :indexing_errors - - #: (Index index, Prism::Dispatcher dispatcher, Prism::ParseLexResult | Prism::ParseResult parse_result, URI::Generic uri, ?collect_comments: bool) -> void - def initialize(index, dispatcher, parse_result, uri, collect_comments: false) - @index = index - @uri = uri - @enhancements = Enhancement.all(self) #: Array[Enhancement] - @visibility_stack = [VisibilityScope.public_scope] #: Array[VisibilityScope] - @comments_by_line = parse_result.comments.to_h do |c| - [c.location.start_line, c] - end #: Hash[Integer, Prism::Comment] - @inside_def = false #: bool - @code_units_cache = parse_result - .code_units_cache(@index.configuration.encoding) #: (^(Integer arg0) -> Integer | Prism::CodeUnitsCache) - - @source_lines = parse_result.source.lines #: Array[String] - - # The nesting stack we're currently inside. Used to determine the fully qualified name of constants, but only - # stored by unresolved aliases which need the original nesting to be lazily resolved - @stack = [] #: Array[String] - - # A stack of namespace entries that represent where we currently are. Used to properly assign methods to an owner - @owner_stack = [] #: Array[Entry::Namespace] - @indexing_errors = [] #: Array[String] - @collect_comments = collect_comments - - dispatcher.register( - self, - :on_class_node_enter, - :on_class_node_leave, - :on_module_node_enter, - :on_module_node_leave, - :on_singleton_class_node_enter, - :on_singleton_class_node_leave, - :on_def_node_enter, - :on_def_node_leave, - :on_call_node_enter, - :on_call_node_leave, - :on_multi_write_node_enter, - :on_constant_path_write_node_enter, - :on_constant_path_or_write_node_enter, - :on_constant_path_operator_write_node_enter, - :on_constant_path_and_write_node_enter, - :on_constant_write_node_enter, - :on_constant_or_write_node_enter, - :on_constant_and_write_node_enter, - :on_constant_operator_write_node_enter, - :on_global_variable_and_write_node_enter, - :on_global_variable_operator_write_node_enter, - :on_global_variable_or_write_node_enter, - :on_global_variable_target_node_enter, - :on_global_variable_write_node_enter, - :on_instance_variable_write_node_enter, - :on_instance_variable_and_write_node_enter, - :on_instance_variable_operator_write_node_enter, - :on_instance_variable_or_write_node_enter, - :on_instance_variable_target_node_enter, - :on_alias_method_node_enter, - :on_class_variable_and_write_node_enter, - :on_class_variable_operator_write_node_enter, - :on_class_variable_or_write_node_enter, - :on_class_variable_target_node_enter, - :on_class_variable_write_node_enter, - ) - end - - #: (Prism::ClassNode node) -> void - def on_class_node_enter(node) - constant_path = node.constant_path - superclass = node.superclass - nesting = Index.actual_nesting(@stack, constant_path.slice) - - parent_class = case superclass - when Prism::ConstantReadNode, Prism::ConstantPathNode - superclass.slice - else - case nesting - when OBJECT_NESTING - # When Object is reopened, its parent class should still be the top-level BasicObject - "::BasicObject" - when BASIC_OBJECT_NESTING - # When BasicObject is reopened, its parent class should still be nil - nil - else - # Otherwise, the parent class should be the top-level Object - "::Object" - end - end - - add_class( - nesting, - node.location, - constant_path.location, - parent_class_name: parent_class, - comments: collect_comments(node), - ) - end - - #: (Prism::ClassNode node) -> void - def on_class_node_leave(node) - pop_namespace_stack - end - - #: (Prism::ModuleNode node) -> void - def on_module_node_enter(node) - constant_path = node.constant_path - add_module(constant_path.slice, node.location, constant_path.location, comments: collect_comments(node)) - end - - #: (Prism::ModuleNode node) -> void - def on_module_node_leave(node) - pop_namespace_stack - end - - #: (Prism::SingletonClassNode node) -> void - def on_singleton_class_node_enter(node) - @visibility_stack.push(VisibilityScope.public_scope) - - current_owner = @owner_stack.last - - if current_owner - expression = node.expression - name = (expression.is_a?(Prism::SelfNode) ? "<#{last_name_in_stack}>" : "<#{expression.slice}>") - real_nesting = Index.actual_nesting(@stack, name) - - existing_entries = @index[real_nesting.join("::")] #: as Array[Entry::SingletonClass]? - - if existing_entries - entry = existing_entries.first #: as !nil - entry.update_singleton_information( - Location.from_prism_location(node.location, @code_units_cache), - Location.from_prism_location(expression.location, @code_units_cache), - collect_comments(node), - ) - else - entry = Entry::SingletonClass.new( - @index.configuration, - real_nesting, - @uri, - Location.from_prism_location(node.location, @code_units_cache), - Location.from_prism_location(expression.location, @code_units_cache), - collect_comments(node), - nil, - ) - @index.add(entry, skip_prefix_tree: true) - end - - @owner_stack << entry - @stack << name - end - end - - #: (Prism::SingletonClassNode node) -> void - def on_singleton_class_node_leave(node) - pop_namespace_stack - end - - #: (Prism::MultiWriteNode node) -> void - def on_multi_write_node_enter(node) - value = node.value - values = value.is_a?(Prism::ArrayNode) && value.opening_loc ? value.elements : [] - - [*node.lefts, *node.rest, *node.rights].each_with_index do |target, i| - current_value = values[i] - # The moment we find a splat on the right hand side of the assignment, we can no longer figure out which value - # gets assigned to what - values.clear if current_value.is_a?(Prism::SplatNode) - - case target - when Prism::ConstantTargetNode - add_constant(target, fully_qualify_name(target.name.to_s), current_value) - when Prism::ConstantPathTargetNode - add_constant(target, fully_qualify_name(target.slice), current_value) - end - end - end - - #: (Prism::ConstantPathWriteNode node) -> void - def on_constant_path_write_node_enter(node) - # ignore variable constants like `var::FOO` or `self.class::FOO` - target = node.target - return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode) - - name = fully_qualify_name(target.location.slice) - add_constant(node, name) - end - - #: (Prism::ConstantPathOrWriteNode node) -> void - def on_constant_path_or_write_node_enter(node) - # ignore variable constants like `var::FOO` or `self.class::FOO` - target = node.target - return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode) - - name = fully_qualify_name(target.location.slice) - add_constant(node, name) - end - - #: (Prism::ConstantPathOperatorWriteNode node) -> void - def on_constant_path_operator_write_node_enter(node) - # ignore variable constants like `var::FOO` or `self.class::FOO` - target = node.target - return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode) - - name = fully_qualify_name(target.location.slice) - add_constant(node, name) - end - - #: (Prism::ConstantPathAndWriteNode node) -> void - def on_constant_path_and_write_node_enter(node) - # ignore variable constants like `var::FOO` or `self.class::FOO` - target = node.target - return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode) - - name = fully_qualify_name(target.location.slice) - add_constant(node, name) - end - - #: (Prism::ConstantWriteNode node) -> void - def on_constant_write_node_enter(node) - name = fully_qualify_name(node.name.to_s) - add_constant(node, name) - end - - #: (Prism::ConstantOrWriteNode node) -> void - def on_constant_or_write_node_enter(node) - name = fully_qualify_name(node.name.to_s) - add_constant(node, name) - end - - #: (Prism::ConstantAndWriteNode node) -> void - def on_constant_and_write_node_enter(node) - name = fully_qualify_name(node.name.to_s) - add_constant(node, name) - end - - #: (Prism::ConstantOperatorWriteNode node) -> void - def on_constant_operator_write_node_enter(node) - name = fully_qualify_name(node.name.to_s) - add_constant(node, name) - end - - #: (Prism::CallNode node) -> void - def on_call_node_enter(node) - message = node.name - - case message - when :private_constant - handle_private_constant(node) - when :attr_reader - handle_attribute(node, reader: true, writer: false) - when :attr_writer - handle_attribute(node, reader: false, writer: true) - when :attr_accessor - handle_attribute(node, reader: true, writer: true) - when :attr - has_writer = node.arguments&.arguments&.last&.is_a?(Prism::TrueNode) || false - handle_attribute(node, reader: true, writer: has_writer) - when :alias_method - handle_alias_method(node) - when :include, :prepend, :extend - handle_module_operation(node, message) - when :public - handle_visibility_change(node, :public) - when :protected - handle_visibility_change(node, :protected) - when :private - handle_visibility_change(node, :private) - when :module_function - handle_module_function(node) - when :private_class_method - handle_private_class_method(node) - end - - @enhancements.each do |enhancement| - enhancement.on_call_node_enter(node) - rescue StandardError => e - @indexing_errors << <<~MSG - Indexing error in #{@uri} with '#{enhancement.class.name}' on call node enter enhancement: #{e.message} - MSG - end - end - - #: (Prism::CallNode node) -> void - def on_call_node_leave(node) - message = node.name - case message - when :public, :protected, :private, :private_class_method - # We want to restore the visibility stack when we leave a method definition with a visibility modifier - # e.g. `private def foo; end` - if node.arguments&.arguments&.first&.is_a?(Prism::DefNode) - @visibility_stack.pop - end - end - - @enhancements.each do |enhancement| - enhancement.on_call_node_leave(node) - rescue StandardError => e - @indexing_errors << <<~MSG - Indexing error in #{@uri} with '#{enhancement.class.name}' on call node leave enhancement: #{e.message} - MSG - end - end - - #: (Prism::DefNode node) -> void - def on_def_node_enter(node) - owner = @owner_stack.last - return unless owner - - @inside_def = true - method_name = node.name.to_s - comments = collect_comments(node) - scope = current_visibility_scope - - case node.receiver - when nil - location = Location.from_prism_location(node.location, @code_units_cache) - name_location = Location.from_prism_location(node.name_loc, @code_units_cache) - signatures = [Entry::Signature.new(list_params(node.parameters))] - - @index.add(Entry::Method.new( - @index.configuration, - method_name, - @uri, - location, - name_location, - comments, - signatures, - scope.visibility, - owner, - )) - - if scope.module_func - singleton = @index.existing_or_new_singleton_class(owner.name) - - @index.add(Entry::Method.new( - @index.configuration, - method_name, - @uri, - location, - name_location, - comments, - signatures, - :public, - singleton, - )) - end - when Prism::SelfNode - singleton = @index.existing_or_new_singleton_class(owner.name) - - @index.add(Entry::Method.new( - @index.configuration, - method_name, - @uri, - Location.from_prism_location(node.location, @code_units_cache), - Location.from_prism_location(node.name_loc, @code_units_cache), - comments, - [Entry::Signature.new(list_params(node.parameters))], - scope.visibility, - singleton, - )) - - @owner_stack << singleton - end - end - - #: (Prism::DefNode node) -> void - def on_def_node_leave(node) - @inside_def = false - - if node.receiver.is_a?(Prism::SelfNode) - @owner_stack.pop - end - end - - #: (Prism::GlobalVariableAndWriteNode node) -> void - def on_global_variable_and_write_node_enter(node) - handle_global_variable(node, node.name_loc) - end - - #: (Prism::GlobalVariableOperatorWriteNode node) -> void - def on_global_variable_operator_write_node_enter(node) - handle_global_variable(node, node.name_loc) - end - - #: (Prism::GlobalVariableOrWriteNode node) -> void - def on_global_variable_or_write_node_enter(node) - handle_global_variable(node, node.name_loc) - end - - #: (Prism::GlobalVariableTargetNode node) -> void - def on_global_variable_target_node_enter(node) - handle_global_variable(node, node.location) - end - - #: (Prism::GlobalVariableWriteNode node) -> void - def on_global_variable_write_node_enter(node) - handle_global_variable(node, node.name_loc) - end - - #: (Prism::InstanceVariableWriteNode node) -> void - def on_instance_variable_write_node_enter(node) - handle_instance_variable(node, node.name_loc) - end - - #: (Prism::InstanceVariableAndWriteNode node) -> void - def on_instance_variable_and_write_node_enter(node) - handle_instance_variable(node, node.name_loc) - end - - #: (Prism::InstanceVariableOperatorWriteNode node) -> void - def on_instance_variable_operator_write_node_enter(node) - handle_instance_variable(node, node.name_loc) - end - - #: (Prism::InstanceVariableOrWriteNode node) -> void - def on_instance_variable_or_write_node_enter(node) - handle_instance_variable(node, node.name_loc) - end - - #: (Prism::InstanceVariableTargetNode node) -> void - def on_instance_variable_target_node_enter(node) - handle_instance_variable(node, node.location) - end - - #: (Prism::AliasMethodNode node) -> void - def on_alias_method_node_enter(node) - method_name = node.new_name.slice - comments = collect_comments(node) - @index.add( - Entry::UnresolvedMethodAlias.new( - @index.configuration, - method_name, - node.old_name.slice, - @owner_stack.last, - @uri, - Location.from_prism_location(node.new_name.location, @code_units_cache), - comments, - ), - ) - end - - #: (Prism::ClassVariableAndWriteNode node) -> void - def on_class_variable_and_write_node_enter(node) - handle_class_variable(node, node.name_loc) - end - - #: (Prism::ClassVariableOperatorWriteNode node) -> void - def on_class_variable_operator_write_node_enter(node) - handle_class_variable(node, node.name_loc) - end - - #: (Prism::ClassVariableOrWriteNode node) -> void - def on_class_variable_or_write_node_enter(node) - handle_class_variable(node, node.name_loc) - end - - #: (Prism::ClassVariableTargetNode node) -> void - def on_class_variable_target_node_enter(node) - handle_class_variable(node, node.location) - end - - #: (Prism::ClassVariableWriteNode node) -> void - def on_class_variable_write_node_enter(node) - handle_class_variable(node, node.name_loc) - end - - #: (String name, Prism::Location node_location, Array[Entry::Signature] signatures, ?visibility: Symbol, ?comments: String?) -> void - def add_method(name, node_location, signatures, visibility: :public, comments: nil) - location = Location.from_prism_location(node_location, @code_units_cache) - - @index.add(Entry::Method.new( - @index.configuration, - name, - @uri, - location, - location, - comments, - signatures, - visibility, - @owner_stack.last, - )) - end - - #: (String name, Prism::Location full_location, Prism::Location name_location, ?comments: String?) -> void - def add_module(name, full_location, name_location, comments: nil) - location = Location.from_prism_location(full_location, @code_units_cache) - name_loc = Location.from_prism_location(name_location, @code_units_cache) - - entry = Entry::Module.new( - @index.configuration, - Index.actual_nesting(@stack, name), - @uri, - location, - name_loc, - comments, - ) - - advance_namespace_stack(name, entry) - end - - #: ((String | Array[String]) name_or_nesting, Prism::Location full_location, Prism::Location name_location, ?parent_class_name: String?, ?comments: String?) -> void - def add_class(name_or_nesting, full_location, name_location, parent_class_name: nil, comments: nil) - nesting = name_or_nesting.is_a?(Array) ? name_or_nesting : Index.actual_nesting(@stack, name_or_nesting) - entry = Entry::Class.new( - @index.configuration, - nesting, - @uri, - Location.from_prism_location(full_location, @code_units_cache), - Location.from_prism_location(name_location, @code_units_cache), - comments, - parent_class_name, - ) - - advance_namespace_stack( - nesting.last, #: as !nil - entry, - ) - end - - #: { (Index index, Entry::Namespace base) -> void } -> void - def register_included_hook(&block) - owner = @owner_stack.last - return unless owner - - @index.register_included_hook(owner.name) do |index, base| - block.call(index, base) - end - end - - #: -> void - def pop_namespace_stack - @stack.pop - @owner_stack.pop - @visibility_stack.pop - end - - #: -> Entry::Namespace? - def current_owner - @owner_stack.last - end - - private - - #: ((Prism::GlobalVariableAndWriteNode | Prism::GlobalVariableOperatorWriteNode | Prism::GlobalVariableOrWriteNode | Prism::GlobalVariableTargetNode | Prism::GlobalVariableWriteNode) node, Prism::Location loc) -> void - def handle_global_variable(node, loc) - name = node.name.to_s - comments = collect_comments(node) - - @index.add(Entry::GlobalVariable.new( - @index.configuration, - name, - @uri, - Location.from_prism_location(loc, @code_units_cache), - comments, - )) - end - - #: ((Prism::ClassVariableAndWriteNode | Prism::ClassVariableOperatorWriteNode | Prism::ClassVariableOrWriteNode | Prism::ClassVariableTargetNode | Prism::ClassVariableWriteNode) node, Prism::Location loc) -> void - def handle_class_variable(node, loc) - name = node.name.to_s - # Ignore incomplete class variable names, which aren't valid Ruby syntax. - # This could occur if the code is in an incomplete or temporary state. - return if name == "@@" - - comments = collect_comments(node) - - owner = @owner_stack.last - - # set the class variable's owner to the attached context when defined within a singleton scope. - if owner.is_a?(Entry::SingletonClass) - owner = @owner_stack.reverse.find { |entry| !entry.name.include?("<") } - end - - @index.add(Entry::ClassVariable.new( - @index.configuration, - name, - @uri, - Location.from_prism_location(loc, @code_units_cache), - comments, - owner, - )) - end - - #: ((Prism::InstanceVariableAndWriteNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableWriteNode) node, Prism::Location loc) -> void - def handle_instance_variable(node, loc) - name = node.name.to_s - return if name == "@" - - # When instance variables are declared inside the class body, they turn into class instance variables rather than - # regular instance variables - owner = @owner_stack.last - - if owner && !@inside_def - owner = @index.existing_or_new_singleton_class(owner.name) - end - - @index.add(Entry::InstanceVariable.new( - @index.configuration, - name, - @uri, - Location.from_prism_location(loc, @code_units_cache), - collect_comments(node), - owner, - )) - end - - #: (Prism::CallNode node) -> void - def handle_private_constant(node) - arguments = node.arguments&.arguments - return unless arguments - - first_argument = arguments.first - - name = case first_argument - when Prism::StringNode - first_argument.content - when Prism::SymbolNode - first_argument.value - end - - return unless name - - receiver = node.receiver - name = "#{receiver.slice}::#{name}" if receiver - - # The private_constant method does not resolve the constant name. It always points to a constant that needs to - # exist in the current namespace - entries = @index[fully_qualify_name(name)] - entries&.each { |entry| entry.visibility = :private } - end - - #: (Prism::CallNode node) -> void - def handle_alias_method(node) - arguments = node.arguments&.arguments - return unless arguments - - new_name, old_name = arguments - return unless new_name && old_name - - new_name_value = case new_name - when Prism::StringNode - new_name.content - when Prism::SymbolNode - new_name.value - end - - return unless new_name_value - - old_name_value = case old_name - when Prism::StringNode - old_name.content - when Prism::SymbolNode - old_name.value - end - - return unless old_name_value - - comments = collect_comments(node) - @index.add( - Entry::UnresolvedMethodAlias.new( - @index.configuration, - new_name_value, - old_name_value, - @owner_stack.last, - @uri, - Location.from_prism_location(new_name.location, @code_units_cache), - comments, - ), - ) - end - - #: ((Prism::ConstantWriteNode | Prism::ConstantOrWriteNode | Prism::ConstantAndWriteNode | Prism::ConstantOperatorWriteNode | Prism::ConstantPathWriteNode | Prism::ConstantPathOrWriteNode | Prism::ConstantPathOperatorWriteNode | Prism::ConstantPathAndWriteNode | Prism::ConstantTargetNode | Prism::ConstantPathTargetNode) node, String name, ?Prism::Node? value) -> void - def add_constant(node, name, value = nil) - value = node.value unless node.is_a?(Prism::ConstantTargetNode) || node.is_a?(Prism::ConstantPathTargetNode) - comments = collect_comments(node) - - @index.add( - case value - when Prism::ConstantReadNode, Prism::ConstantPathNode - Entry::UnresolvedConstantAlias.new( - @index.configuration, - value.slice, - @stack.dup, - name, - @uri, - Location.from_prism_location(node.location, @code_units_cache), - comments, - ) - when Prism::ConstantWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantOrWriteNode, - Prism::ConstantOperatorWriteNode - - # If the right hand side is another constant assignment, we need to visit it because that constant has to be - # indexed too - Entry::UnresolvedConstantAlias.new( - @index.configuration, - value.name.to_s, - @stack.dup, - name, - @uri, - Location.from_prism_location(node.location, @code_units_cache), - comments, - ) - when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode, - Prism::ConstantPathAndWriteNode - - Entry::UnresolvedConstantAlias.new( - @index.configuration, - value.target.slice, - @stack.dup, - name, - @uri, - Location.from_prism_location(node.location, @code_units_cache), - comments, - ) - else - Entry::Constant.new( - @index.configuration, - name, - @uri, - Location.from_prism_location(node.location, @code_units_cache), - comments, - ) - end, - ) - end - - #: (Prism::Node node) -> String? - def collect_comments(node) - return unless @collect_comments - - comments = +"" - - start_line = node.location.start_line - 1 - start_line -= 1 unless comment_exists_at?(start_line) - start_line.downto(1) do |line| - comment = @comments_by_line[line] - break unless comment - - # a trailing comment from a previous line is not a comment for this node - break if comment.trailing? - - comment_content = comment.location.slice - - # invalid encodings would raise an "invalid byte sequence" exception - if !comment_content.valid_encoding? || comment_content.match?(@index.configuration.magic_comment_regex) - next - end - - comment_content.delete_prefix!("#") - comment_content.delete_prefix!(" ") - comments.prepend("#{comment_content}\n") - end - - comments.chomp! - comments - end - - #: (Integer line) -> bool - def comment_exists_at?(line) - @comments_by_line.key?(line) || !@source_lines[line - 1].to_s.strip.empty? - end - - #: (String name) -> String - def fully_qualify_name(name) - if @stack.empty? || name.start_with?("::") - name - else - "#{@stack.join("::")}::#{name}" - end.delete_prefix("::") - end - - #: (Prism::CallNode node, reader: bool, writer: bool) -> void - def handle_attribute(node, reader:, writer:) - arguments = node.arguments&.arguments - return unless arguments - - receiver = node.receiver - return unless receiver.nil? || receiver.is_a?(Prism::SelfNode) - - comments = collect_comments(node) - scope = current_visibility_scope - - arguments.each do |argument| - name, loc = case argument - when Prism::SymbolNode - [argument.value, argument.value_loc] - when Prism::StringNode - [argument.content, argument.content_loc] - end - - next unless name && loc - - if reader - @index.add(Entry::Accessor.new( - @index.configuration, - name, - @uri, - Location.from_prism_location(loc, @code_units_cache), - comments, - scope.visibility, - @owner_stack.last, - )) - end - - next unless writer - - @index.add(Entry::Accessor.new( - @index.configuration, - "#{name}=", - @uri, - Location.from_prism_location(loc, @code_units_cache), - comments, - scope.visibility, - @owner_stack.last, - )) - end - end - - #: (Prism::CallNode node, Symbol operation) -> void - def handle_module_operation(node, operation) - return if @inside_def - - owner = @owner_stack.last - return unless owner - - arguments = node.arguments&.arguments - return unless arguments - - arguments.each do |node| - next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode) || - (node.is_a?(Prism::SelfNode) && operation == :extend) - - if node.is_a?(Prism::SelfNode) - singleton = @index.existing_or_new_singleton_class(owner.name) - singleton.mixin_operations << Entry::Include.new(owner.name) - else - case operation - when :include - owner.mixin_operations << Entry::Include.new(node.full_name) - when :prepend - owner.mixin_operations << Entry::Prepend.new(node.full_name) - when :extend - singleton = @index.existing_or_new_singleton_class(owner.name) - singleton.mixin_operations << Entry::Include.new(node.full_name) - end - end - rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError, - Prism::ConstantPathNode::MissingNodesInConstantPathError - # Do nothing - end - end - - #: (Prism::CallNode node) -> void - def handle_module_function(node) - # Invoking `module_function` in a class raises - owner = @owner_stack.last - return unless owner.is_a?(Entry::Module) - - arguments_node = node.arguments - - # If `module_function` is invoked without arguments, all methods defined after it become singleton methods and the - # visibility for instance methods changes to private - unless arguments_node - @visibility_stack.push(VisibilityScope.module_function_scope) - return - end - - owner_name = owner.name - - arguments_node.arguments.each do |argument| - method_name = case argument - when Prism::StringNode - argument.content - when Prism::SymbolNode - argument.value - end - next unless method_name - - entries = @index.resolve_method(method_name, owner_name) - next unless entries - - entries.each do |entry| - entry_owner_name = entry.owner&.name - next unless entry_owner_name - - entry.visibility = :private - - singleton = @index.existing_or_new_singleton_class(entry_owner_name) - location = Location.from_prism_location(argument.location, @code_units_cache) - @index.add(Entry::Method.new( - @index.configuration, - method_name, - @uri, - location, - location, - collect_comments(node)&.concat(entry.comments), - entry.signatures, - :public, - singleton, - )) - end - end - end - - #: (Prism::CallNode node) -> void - def handle_private_class_method(node) - arguments = node.arguments&.arguments - return unless arguments - - # If we're passing a method definition directly to `private_class_method`, push a new private scope. That will be - # applied when the indexer finds the method definition and then popped on `call_node_leave` - if arguments.first.is_a?(Prism::DefNode) - @visibility_stack.push(VisibilityScope.new(visibility: :private)) - return - end - - owner_name = @owner_stack.last&.name - return unless owner_name - - # private_class_method accepts strings, symbols or arrays of strings and symbols as arguments. Here we build a - # single list of all of the method names that have to be made private - arrays, others = arguments.partition do |argument| - argument.is_a?(Prism::ArrayNode) - end #: as [Array[Prism::ArrayNode], Array[Prism::Node]] - arrays.each { |array| others.concat(array.elements) } - - names = others.filter_map do |argument| - case argument - when Prism::StringNode - argument.unescaped - when Prism::SymbolNode - argument.value - end - end - - names.each do |name| - entries = @index.resolve_method(name, @index.existing_or_new_singleton_class(owner_name).name) - next unless entries - - entries.each { |entry| entry.visibility = :private } - end - end - - #: -> VisibilityScope - def current_visibility_scope - @visibility_stack.last #: as !nil - end - - #: (Prism::ParametersNode? parameters_node) -> Array[Entry::Parameter] - def list_params(parameters_node) - return [] unless parameters_node - - parameters = [] - - parameters_node.requireds.each do |required| - name = parameter_name(required) - next unless name - - parameters << Entry::RequiredParameter.new(name: name) - end - - parameters_node.optionals.each do |optional| - name = parameter_name(optional) - next unless name - - parameters << Entry::OptionalParameter.new(name: name) - end - - rest = parameters_node.rest - - if rest.is_a?(Prism::RestParameterNode) - rest_name = rest.name || Entry::RestParameter::DEFAULT_NAME - parameters << Entry::RestParameter.new(name: rest_name) - end - - parameters_node.keywords.each do |keyword| - name = parameter_name(keyword) - next unless name - - case keyword - when Prism::RequiredKeywordParameterNode - parameters << Entry::KeywordParameter.new(name: name) - when Prism::OptionalKeywordParameterNode - parameters << Entry::OptionalKeywordParameter.new(name: name) - end - end - - keyword_rest = parameters_node.keyword_rest - - case keyword_rest - when Prism::KeywordRestParameterNode - keyword_rest_name = parameter_name(keyword_rest) || Entry::KeywordRestParameter::DEFAULT_NAME - parameters << Entry::KeywordRestParameter.new(name: keyword_rest_name) - when Prism::ForwardingParameterNode - parameters << Entry::ForwardingParameter.new - end - - parameters_node.posts.each do |post| - name = parameter_name(post) - next unless name - - parameters << Entry::RequiredParameter.new(name: name) - end - - block = parameters_node.block - parameters << Entry::BlockParameter.new(name: block.name || Entry::BlockParameter::DEFAULT_NAME) if block - - parameters - end - - #: (Prism::Node? node) -> Symbol? - def parameter_name(node) - case node - when Prism::RequiredParameterNode, Prism::OptionalParameterNode, - Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode, - Prism::RestParameterNode, Prism::KeywordRestParameterNode - node.name - when Prism::MultiTargetNode - names = node.lefts.map { |parameter_node| parameter_name(parameter_node) } - - rest = node.rest - if rest.is_a?(Prism::SplatNode) - name = rest.expression&.slice - names << (rest.operator == "*" ? "*#{name}".to_sym : name&.to_sym) - end - - names << nil if rest.is_a?(Prism::ImplicitRestNode) - - names.concat(node.rights.map { |parameter_node| parameter_name(parameter_node) }) - - names_with_commas = names.join(", ") - :"(#{names_with_commas})" - end - end - - #: (String short_name, Entry::Namespace entry) -> void - def advance_namespace_stack(short_name, entry) - @visibility_stack.push(VisibilityScope.public_scope) - @owner_stack << entry - @index.add(entry) - @stack << short_name - end - - # Returns the last name in the stack not as we found it, but in terms of declared constants. For example, if the - # last entry in the stack is a compact namespace like `Foo::Bar`, then the last name is `Bar` - #: -> String? - def last_name_in_stack - name = @stack.last - return unless name - - name.split("::").last - end - - #: (Prism::CallNode, Symbol) -> void - def handle_visibility_change(node, visibility) - owner = @owner_stack.last - return unless owner - - owner_name = owner.name - method_names = string_or_symbol_argument_values(node) - - if method_names.empty? - @visibility_stack.push(VisibilityScope.new(visibility: visibility)) - return - end - - method_names.each do |method_name| - entries = @index.resolve_method(method_name, owner_name) - next unless entries - - entries.each do |entry| - entry.visibility = visibility - end - end - end - - #: (Prism::CallNode) -> Array[String] - def string_or_symbol_argument_values(node) - arguments = node.arguments&.arguments - return [] unless arguments - - arguments.filter_map do |argument| - case argument - when Prism::StringNode - argument.content - when Prism::SymbolNode - argument.value - end - end - end - end -end diff --git a/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb b/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb deleted file mode 100644 index 2d6adc34d6..0000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +++ /dev/null @@ -1,44 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module RubyIndexer - # @abstract - class Enhancement - @enhancements = [] #: Array[Class[Enhancement]] - - class << self - #: (Class[Enhancement] child) -> void - def inherited(child) - @enhancements << child - super - end - - #: (DeclarationListener listener) -> Array[Enhancement] - def all(listener) - @enhancements.map { |enhancement| enhancement.new(listener) } - end - - # Only available for testing purposes - #: -> void - def clear - @enhancements.clear - end - end - - #: (DeclarationListener listener) -> void - def initialize(listener) - @listener = listener - end - - # The `on_extend` indexing enhancement is invoked whenever an extend is encountered in the code. It can be used to - # register for an included callback, similar to what `ActiveSupport::Concern` does in order to auto-extend the - # `ClassMethods` modules - # @overridable - #: (Prism::CallNode node) -> void - def on_call_node_enter(node); end # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod - - # @overridable - #: (Prism::CallNode node) -> void - def on_call_node_leave(node); end # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod - end -end diff --git a/lib/ruby_indexer/lib/ruby_indexer/entry.rb b/lib/ruby_indexer/lib/ruby_indexer/entry.rb deleted file mode 100644 index e5946b9c29..0000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/entry.rb +++ /dev/null @@ -1,605 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module RubyIndexer - class Entry - #: Configuration - attr_reader :configuration - - #: String - attr_reader :name - - #: URI::Generic - attr_reader :uri - - #: RubyIndexer::Location - attr_reader :location - - alias_method :name_location, :location - - #: Symbol - attr_accessor :visibility - - #: (Configuration configuration, String name, URI::Generic uri, Location location, String? comments) -> void - def initialize(configuration, name, uri, location, comments) - @configuration = configuration - @name = name - @uri = uri - @comments = comments - @visibility = :public #: Symbol - @location = location - end - - #: -> bool - def public? - @visibility == :public - end - - #: -> bool - def protected? - @visibility == :protected - end - - #: -> bool - def private? - @visibility == :private - end - - #: -> String - def file_name - if @uri.scheme == "untitled" - @uri.opaque #: as !nil - else - File.basename( - file_path, #: as !nil - ) - end - end - - #: -> String? - def file_path - @uri.full_path - end - - #: -> String - def comments - @comments ||= begin - # Parse only the comments based on the file path, which is much faster than parsing the entire file - path = file_path - parsed_comments = path ? Prism.parse_file_comments(path) : [] - - # Group comments based on whether they belong to a single block of comments - grouped = parsed_comments.slice_when do |left, right| - left.location.start_line + 1 != right.location.start_line - end - - # Find the group that is either immediately or two lines above the current entry - correct_group = grouped.find do |group| - comment_end_line = group.last.location.start_line - (comment_end_line..comment_end_line + 1).cover?(@location.start_line - 1) - end - - # If we found something, we join the comments together. Otherwise, the entry has no documentation and we don't - # want to accidentally re-parse it, so we set it to an empty string. If an entry is updated, the entire entry - # object is dropped, so this will not prevent updates - if correct_group - correct_group.filter_map do |comment| - content = comment.slice.chomp - - if content.valid_encoding? && !content.match?(@configuration.magic_comment_regex) - content.delete_prefix!("#") - content.delete_prefix!(" ") - content - end - end.join("\n") - else - "" - end - rescue Errno::ENOENT - # If the file was deleted, but the entry hasn't been removed yet (could happen due to concurrency), then we do - # not want to fail. Just set the comments to an empty string - "" - end - end - - # @abstract - class ModuleOperation - #: String - attr_reader :module_name - - #: (String module_name) -> void - def initialize(module_name) - @module_name = module_name - end - end - - class Include < ModuleOperation; end - class Prepend < ModuleOperation; end - - # @abstract - class Namespace < Entry - #: Array[String] - attr_reader :nesting - - # Returns the location of the constant name, excluding the parent class or the body - #: Location - attr_reader :name_location - - #: (Configuration configuration, Array[String] nesting, URI::Generic uri, Location location, Location name_location, String? comments) -> void - def initialize(configuration, nesting, uri, location, name_location, comments) # rubocop:disable Metrics/ParameterLists - @name = nesting.join("::") #: String - # The original nesting where this namespace was discovered - @nesting = nesting - - super(configuration, @name, uri, location, comments) - - @name_location = name_location - end - - #: -> Array[String] - def mixin_operation_module_names - mixin_operations.map(&:module_name) - end - - # Stores all explicit prepend, include and extend operations in the exact order they were discovered in the source - # code. Maintaining the order is essential to linearize ancestors the right way when a module is both included - # and prepended - #: -> Array[ModuleOperation] - def mixin_operations - @mixin_operations ||= [] #: Array[ModuleOperation]? - end - - #: -> Integer - def ancestor_hash - mixin_operation_module_names.hash - end - end - - class Module < Namespace - end - - class Class < Namespace - # The unresolved name of the parent class. This may return `nil`, which indicates the lack of an explicit parent - # and therefore ::Object is the correct parent class - #: String? - attr_reader :parent_class - - #: (Configuration configuration, Array[String] nesting, URI::Generic uri, Location location, Location name_location, String? comments, String? parent_class) -> void - def initialize(configuration, nesting, uri, location, name_location, comments, parent_class) # rubocop:disable Metrics/ParameterLists - super(configuration, nesting, uri, location, name_location, comments) - @parent_class = parent_class - end - - # @override - #: -> Integer - def ancestor_hash - [mixin_operation_module_names, @parent_class].hash - end - end - - class SingletonClass < Class - #: (Location location, Location name_location, String? comments) -> void - def update_singleton_information(location, name_location, comments) - @location = location - @name_location = name_location - (@comments ||= +"") << comments if comments - end - end - - class Constant < Entry - end - - # @abstract - class Parameter - # Name includes just the name of the parameter, excluding symbols like splats - #: Symbol - attr_reader :name - - # Decorated name is the parameter name including the splat or block prefix, e.g.: `*foo`, `**foo` or `&block` - alias_method :decorated_name, :name - - #: (name: Symbol) -> void - def initialize(name:) - @name = name - end - end - - # A required method parameter, e.g. `def foo(a)` - class RequiredParameter < Parameter - end - - # An optional method parameter, e.g. `def foo(a = 123)` - class OptionalParameter < Parameter - # @override - #: -> Symbol - def decorated_name - :"#{@name} = " - end - end - - # An required keyword method parameter, e.g. `def foo(a:)` - class KeywordParameter < Parameter - # @override - #: -> Symbol - def decorated_name - :"#{@name}:" - end - end - - # An optional keyword method parameter, e.g. `def foo(a: 123)` - class OptionalKeywordParameter < Parameter - # @override - #: -> Symbol - def decorated_name - :"#{@name}: " - end - end - - # A rest method parameter, e.g. `def foo(*a)` - class RestParameter < Parameter - DEFAULT_NAME = :"" #: Symbol - - # @override - #: -> Symbol - def decorated_name - :"*#{@name}" - end - end - - # A keyword rest method parameter, e.g. `def foo(**a)` - class KeywordRestParameter < Parameter - DEFAULT_NAME = :"" #: Symbol - - # @override - #: -> Symbol - def decorated_name - :"**#{@name}" - end - end - - # A block method parameter, e.g. `def foo(&block)` - class BlockParameter < Parameter - DEFAULT_NAME = :"" #: Symbol - - class << self - #: -> BlockParameter - def anonymous - new(name: DEFAULT_NAME) - end - end - - # @override - #: -> Symbol - def decorated_name - :"&#{@name}" - end - end - - # A forwarding method parameter, e.g. `def foo(...)` - class ForwardingParameter < Parameter - #: -> void - def initialize - # You can't name a forwarding parameter, it's always called `...` - super(name: :"...") - end - end - - # @abstract - class Member < Entry - #: Entry::Namespace? - attr_reader :owner - - #: (Configuration configuration, String name, URI::Generic uri, Location location, String? comments, Symbol visibility, Entry::Namespace? owner) -> void - def initialize(configuration, name, uri, location, comments, visibility, owner) # rubocop:disable Metrics/ParameterLists - super(configuration, name, uri, location, comments) - @visibility = visibility - @owner = owner - end - - # @abstract - #: -> Array[Signature] - def signatures - raise AbstractMethodInvokedError - end - - #: -> String - def decorated_parameters - first_signature = signatures.first - return "()" unless first_signature - - "(#{first_signature.format})" - end - - #: -> String - def formatted_signatures - overloads_count = signatures.size - case overloads_count - when 1 - "" - when 2 - "\n(+1 overload)" - else - "\n(+#{overloads_count - 1} overloads)" - end - end - end - - class Accessor < Member - # @override - #: -> Array[Signature] - def signatures - @signatures ||= begin - params = [] - params << RequiredParameter.new(name: name.delete_suffix("=").to_sym) if name.end_with?("=") - [Entry::Signature.new(params)] - end #: Array[Signature]? - end - end - - class Method < Member - # @override - #: Array[Signature] - attr_reader :signatures - - # Returns the location of the method name, excluding parameters or the body - #: Location - attr_reader :name_location - - #: (Configuration configuration, String name, URI::Generic uri, Location location, Location name_location, String? comments, Array[Signature] signatures, Symbol visibility, Entry::Namespace? owner) -> void - def initialize(configuration, name, uri, location, name_location, comments, signatures, visibility, owner) # rubocop:disable Metrics/ParameterLists - super(configuration, name, uri, location, comments, visibility, owner) - @signatures = signatures - @name_location = name_location - end - end - - # An UnresolvedAlias points to a constant alias with a right hand side that has not yet been resolved. For - # example, if we find - # - # ```ruby - # CONST = Foo - # ``` - # Before we have discovered `Foo`, there's no way to eagerly resolve this alias to the correct target constant. - # All aliases are inserted as UnresolvedAlias in the index first and then we lazily resolve them to the correct - # target in [rdoc-ref:Index#resolve]. If the right hand side contains a constant that doesn't exist, then it's not - # possible to resolve the alias and it will remain an UnresolvedAlias until the right hand side constant exists - class UnresolvedConstantAlias < Entry - #: String - attr_reader :target - - #: Array[String] - attr_reader :nesting - - #: (Configuration configuration, String target, Array[String] nesting, String name, URI::Generic uri, Location location, String? comments) -> void - def initialize(configuration, target, nesting, name, uri, location, comments) # rubocop:disable Metrics/ParameterLists - super(configuration, name, uri, location, comments) - - @target = target - @nesting = nesting - end - end - - # Alias represents a resolved alias, which points to an existing constant target - class ConstantAlias < Entry - #: String - attr_reader :target - - #: (String target, UnresolvedConstantAlias unresolved_alias) -> void - def initialize(target, unresolved_alias) - super( - unresolved_alias.configuration, - unresolved_alias.name, - unresolved_alias.uri, - unresolved_alias.location, - unresolved_alias.comments, - ) - - @visibility = unresolved_alias.visibility - @target = target - end - end - - # Represents a global variable e.g.: $DEBUG - class GlobalVariable < Entry; end - - # Represents a class variable e.g.: @@a = 1 - class ClassVariable < Entry - #: Entry::Namespace? - attr_reader :owner - - #: (Configuration configuration, String name, URI::Generic uri, Location location, String? comments, Entry::Namespace? owner) -> void - def initialize(configuration, name, uri, location, comments, owner) # rubocop:disable Metrics/ParameterLists - super(configuration, name, uri, location, comments) - @owner = owner - end - end - - # Represents an instance variable e.g.: @a = 1 - class InstanceVariable < Entry - #: Entry::Namespace? - attr_reader :owner - - #: (Configuration configuration, String name, URI::Generic uri, Location location, String? comments, Entry::Namespace? owner) -> void - def initialize(configuration, name, uri, location, comments, owner) # rubocop:disable Metrics/ParameterLists - super(configuration, name, uri, location, comments) - @owner = owner - end - end - - # An unresolved method alias is an alias entry for which we aren't sure what the right hand side points to yet. For - # example, if we have `alias a b`, we create an unresolved alias for `a` because we aren't sure immediate what `b` - # is referring to - class UnresolvedMethodAlias < Entry - #: String - attr_reader :new_name, :old_name - - #: Entry::Namespace? - attr_reader :owner - - #: (Configuration configuration, String new_name, String old_name, Entry::Namespace? owner, URI::Generic uri, Location location, String? comments) -> void - def initialize(configuration, new_name, old_name, owner, uri, location, comments) # rubocop:disable Metrics/ParameterLists - super(configuration, new_name, uri, location, comments) - - @new_name = new_name - @old_name = old_name - @owner = owner - end - end - - # A method alias is a resolved alias entry that points to the exact method target it refers to - class MethodAlias < Entry - #: (Member | MethodAlias) - attr_reader :target - - #: Entry::Namespace? - attr_reader :owner - - #: ((Member | MethodAlias) target, UnresolvedMethodAlias unresolved_alias) -> void - def initialize(target, unresolved_alias) - full_comments = +"Alias for #{target.name}\n" - full_comments << "#{unresolved_alias.comments}\n" - full_comments << target.comments - - super( - unresolved_alias.configuration, - unresolved_alias.new_name, - unresolved_alias.uri, - unresolved_alias.location, - full_comments, - ) - - @target = target - @owner = unresolved_alias.owner #: Entry::Namespace? - end - - #: -> String - def decorated_parameters - @target.decorated_parameters - end - - #: -> String - def formatted_signatures - @target.formatted_signatures - end - - #: -> Array[Signature] - def signatures - @target.signatures - end - end - - # Ruby doesn't support method overloading, so a method will have only one signature. - # However RBS can represent the concept of method overloading, with different return types based on the arguments - # passed, so we need to store all the signatures. - class Signature - #: Array[Parameter] - attr_reader :parameters - - #: (Array[Parameter] parameters) -> void - def initialize(parameters) - @parameters = parameters - end - - # Returns a string with the decorated names of the parameters of this member. E.g.: `(a, b = 1, c: 2)` - #: -> String - def format - @parameters.map(&:decorated_name).join(", ") - end - - # Returns `true` if the given call node arguments array matches this method signature. This method will prefer - # returning `true` for situations that cannot be analyzed statically, like the presence of splats, keyword splats - # or forwarding arguments. - # - # Since this method is used to detect which overload should be displayed in signature help, it will also return - # `true` if there are missing arguments since the user may not be done typing yet. For example: - # - # ```ruby - # def foo(a, b); end - # # All of the following are considered matches because the user might be in the middle of typing and we have to - # # show them the signature - # foo - # foo(1) - # foo(1, 2) - # ``` - #: (Array[Prism::Node] arguments) -> bool - def matches?(arguments) - min_pos = 0 - max_pos = 0 #: (Integer | Float) - names = [] - has_forward = false #: bool - has_keyword_rest = false #: bool - - @parameters.each do |param| - case param - when RequiredParameter - min_pos += 1 - max_pos += 1 - when OptionalParameter - max_pos += 1 - when RestParameter - max_pos = Float::INFINITY - when ForwardingParameter - max_pos = Float::INFINITY - has_forward = true - when KeywordParameter, OptionalKeywordParameter - names << param.name - when KeywordRestParameter - has_keyword_rest = true - end - end - - keyword_hash_nodes, positional_args = arguments.partition { |arg| arg.is_a?(Prism::KeywordHashNode) } - keyword_args = keyword_hash_nodes.first #: as Prism::KeywordHashNode? - &.elements - forwarding_arguments, positionals = positional_args.partition do |arg| - arg.is_a?(Prism::ForwardingArgumentsNode) - end - - return true if has_forward && min_pos == 0 - - # If the only argument passed is a forwarding argument, then anything will match - (positionals.empty? && forwarding_arguments.any?) || - ( - # Check if positional arguments match. This includes required, optional, rest arguments. We also need to - # verify if there's a trailing forwarding argument, like `def foo(a, ...); end` - positional_arguments_match?(positionals, forwarding_arguments, keyword_args, min_pos, max_pos) && - # If the positional arguments match, we move on to checking keyword, optional keyword and keyword rest - # arguments. If there's a forward argument, then it will always match. If the method accepts a keyword rest - # (**kwargs), then we can't analyze statically because the user could be passing a hash and we don't know - # what the runtime values inside the hash are. - # - # If none of those match, then we verify if the user is passing the expect names for the keyword arguments - (has_forward || has_keyword_rest || keyword_arguments_match?(keyword_args, names)) - ) - end - - #: (Array[Prism::Node] positional_args, Array[Prism::Node] forwarding_arguments, Array[Prism::Node]? keyword_args, Integer min_pos, (Integer | Float) max_pos) -> bool - def positional_arguments_match?(positional_args, forwarding_arguments, keyword_args, min_pos, max_pos) - # If the method accepts at least one positional argument and a splat has been passed - (min_pos > 0 && positional_args.any? { |arg| arg.is_a?(Prism::SplatNode) }) || - # If there's at least one positional argument unaccounted for and a keyword splat has been passed - (min_pos - positional_args.length > 0 && keyword_args&.any? { |arg| arg.is_a?(Prism::AssocSplatNode) }) || - # If there's at least one positional argument unaccounted for and a forwarding argument has been passed - (min_pos - positional_args.length > 0 && forwarding_arguments.any?) || - # If the number of positional arguments is within the expected range - (min_pos > 0 && positional_args.length <= max_pos) || - (min_pos == 0 && positional_args.empty?) - end - - #: (Array[Prism::Node]? args, Array[Symbol] names) -> bool - def keyword_arguments_match?(args, names) - return true unless args - return true if args.any? { |arg| arg.is_a?(Prism::AssocSplatNode) } - - arg_names = args.filter_map do |arg| - next unless arg.is_a?(Prism::AssocNode) - - key = arg.key - key.value&.to_sym if key.is_a?(Prism::SymbolNode) - end - - (arg_names - names).empty? - end - end - end -end diff --git a/lib/ruby_indexer/lib/ruby_indexer/index.rb b/lib/ruby_indexer/lib/ruby_indexer/index.rb deleted file mode 100644 index 1ec84f17c0..0000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/index.rb +++ /dev/null @@ -1,1077 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module RubyIndexer - class Index - class UnresolvableAliasError < StandardError; end - class NonExistingNamespaceError < StandardError; end - class IndexNotEmptyError < StandardError; end - - # The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query - ENTRY_SIMILARITY_THRESHOLD = 0.7 - - #: Configuration - attr_reader :configuration - - #: bool - attr_reader :initial_indexing_completed - - class << self - # Returns the real nesting of a constant name taking into account top level - # references that may be included anywhere in the name or nesting where that - # constant was found - #: (Array[String] stack, String? name) -> Array[String] - def actual_nesting(stack, name) - nesting = name ? stack + [name] : stack - corrected_nesting = [] - - nesting.reverse_each do |name| - corrected_nesting.prepend(name.delete_prefix("::")) - - break if name.start_with?("::") - end - - corrected_nesting - end - - # Returns the unresolved name for a constant reference including all parts of a constant path, or `nil` if the - # constant contains dynamic or incomplete parts - #: (Prism::Node) -> String? - def constant_name(node) - case node - when Prism::ConstantPathNode, Prism::ConstantReadNode, Prism::ConstantPathTargetNode - node.full_name - end - rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError, - Prism::ConstantPathNode::MissingNodesInConstantPathError - nil - end - end - - #: -> void - def initialize - # Holds all entries in the index using the following format: - # { - # "Foo" => [#, #], - # "Foo::Bar" => [#], - # } - @entries = {} #: Hash[String, Array[Entry]] - - # Holds all entries in the index using a prefix tree for searching based on prefixes to provide autocompletion - @entries_tree = PrefixTree.new #: PrefixTree[Array[Entry]] - - # Holds references to where entries where discovered so that we can easily delete them - # { - # "file:///my/project/foo.rb" => [#, #], - # "file:///my/project/bar.rb" => [#], - # "untitled:Untitled-1" => [#], - # } - @uris_to_entries = {} #: Hash[String, Array[Entry]] - - # Holds all require paths for every indexed item so that we can provide autocomplete for requires - @require_paths_tree = PrefixTree.new #: PrefixTree[URI::Generic] - - # Holds the linearized ancestors list for every namespace - @ancestors = {} #: Hash[String, Array[String]] - - # Map of module name to included hooks that have to be executed when we include the given module - @included_hooks = {} #: Hash[String, Array[^(Index index, Entry::Namespace base) -> void]] - - @configuration = RubyIndexer::Configuration.new #: Configuration - - @initial_indexing_completed = false #: bool - end - - # Register an included `hook` that will be executed when `module_name` is included into any namespace - #: (String module_name) { (Index index, Entry::Namespace base) -> void } -> void - def register_included_hook(module_name, &hook) - (@included_hooks[module_name] ||= []) << hook - end - - #: (URI::Generic uri, ?skip_require_paths_tree: bool) -> void - def delete(uri, skip_require_paths_tree: false) - key = uri.to_s - # For each constant discovered in `path`, delete the associated entry from the index. If there are no entries - # left, delete the constant from the index. - @uris_to_entries[key]&.each do |entry| - name = entry.name - entries = @entries[name] - next unless entries - - # Delete the specific entry from the list for this name - entries.delete(entry) - - # If all entries were deleted, then remove the name from the hash and from the prefix tree. Otherwise, update - # the prefix tree with the current entries - if entries.empty? - @entries.delete(name) - @entries_tree.delete(name) - else - @entries_tree.insert(name, entries) - end - end - - @uris_to_entries.delete(key) - return if skip_require_paths_tree - - require_path = uri.require_path - @require_paths_tree.delete(require_path) if require_path - end - - #: (Entry entry, ?skip_prefix_tree: bool) -> void - def add(entry, skip_prefix_tree: false) - name = entry.name - - (@entries[name] ||= []) << entry - (@uris_to_entries[entry.uri.to_s] ||= []) << entry - - unless skip_prefix_tree - @entries_tree.insert( - name, - @entries[name], #: as !nil - ) - end - end - - #: (String fully_qualified_name) -> Array[Entry]? - def [](fully_qualified_name) - @entries[fully_qualified_name.delete_prefix("::")] - end - - #: (String query) -> Array[URI::Generic] - def search_require_paths(query) - @require_paths_tree.search(query) - end - - # Searches for a constant based on an unqualified name and returns the first possible match regardless of whether - # there are more possible matching entries - #: (String name) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]? - def first_unqualified_const(name) - # Look for an exact match first - _name, entries = @entries.find do |const_name, _entries| - const_name == name || const_name.end_with?("::#{name}") - end - - # If an exact match is not found, then try to find a constant that ends with the name - unless entries - _name, entries = @entries.find do |const_name, _entries| - const_name.end_with?(name) - end - end - - entries #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]? - end - - # Searches entries in the index based on an exact prefix, intended for providing autocomplete. All possible matches - # to the prefix are returned. The return is an array of arrays, where each entry is the array of entries for a given - # name match. For example: - # ## Example - # ```ruby - # # If the index has two entries for `Foo::Bar` and one for `Foo::Baz`, then: - # index.prefix_search("Foo::B") - # # Will return: - # [ - # [#, #], - # [#], - # ] - # ``` - #: (String query, ?Array[String]? nesting) -> Array[Array[Entry]] - def prefix_search(query, nesting = nil) - unless nesting - results = @entries_tree.search(query) - results.uniq! - return results - end - - results = nesting.length.downto(0).flat_map do |i| - prefix = nesting[0...i] #: as !nil - .join("::") - namespaced_query = prefix.empty? ? query : "#{prefix}::#{query}" - @entries_tree.search(namespaced_query) - end - - results.uniq! - results - end - - # Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned - #: (String? query) ?{ (Entry) -> bool? } -> Array[Entry] - def fuzzy_search(query, &condition) - unless query - entries = @entries.filter_map do |_name, entries| - next if entries.first.is_a?(Entry::SingletonClass) - - entries = entries.select(&condition) if condition - entries - end - - return entries.flatten - end - - normalized_query = query.gsub("::", "").downcase - - results = @entries.filter_map do |name, entries| - next if entries.first.is_a?(Entry::SingletonClass) - - entries = entries.select(&condition) if condition - next if entries.empty? - - similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query) - [entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD - end - results.sort_by!(&:last) - results.flat_map(&:first) - end - - #: (String? name, String receiver_name) -> Array[(Entry::Member | Entry::MethodAlias)] - def method_completion_candidates(name, receiver_name) - ancestors = linearized_ancestors_of(receiver_name) - - candidates = name ? prefix_search(name).flatten : @entries.values.flatten - completion_items = candidates.each_with_object({}) do |entry, hash| - unless entry.is_a?(Entry::Member) || entry.is_a?(Entry::MethodAlias) || - entry.is_a?(Entry::UnresolvedMethodAlias) - next - end - - entry_name = entry.name - ancestor_index = ancestors.index(entry.owner&.name) - existing_entry, existing_entry_index = hash[entry_name] - - # Conditions for matching a method completion candidate: - # 1. If an ancestor_index was found, it means that this method is owned by the receiver. The exact index is - # where in the ancestor chain the method was found. For example, if the ancestors are ["A", "B", "C"] and we - # found the method declared in `B`, then the ancestors index is 1 - # - # 2. We already established that this method is owned by the receiver. Now, check if we already added a - # completion candidate for this method name. If not, then we just go and add it (the left hand side of the or) - # - # 3. If we had already found a method entry for the same name, then we need to check if the current entry that - # we are comparing appears first in the hierarchy or not. For example, imagine we have the method `open` defined - # in both `File` and its parent `IO`. If we first find the method `open` in `IO`, it will be inserted into the - # hash. Then, when we find the entry for `open` owned by `File`, we need to replace `IO.open` by `File.open`, - # since `File.open` appears first in the hierarchy chain and is therefore the correct method being invoked. The - # last part of the conditional checks if the current entry was found earlier in the hierarchy chain, in which - # case we must update the existing entry to avoid showing the wrong method declaration for overridden methods - next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index) - - if entry.is_a?(Entry::UnresolvedMethodAlias) - resolved_alias = resolve_method_alias(entry, receiver_name, []) - hash[entry_name] = [resolved_alias, ancestor_index] if resolved_alias.is_a?(Entry::MethodAlias) - else - hash[entry_name] = [entry, ancestor_index] - end - end - - completion_items.values.map!(&:first) - end - - #: (String name, Array[String] nesting) -> Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]] - def constant_completion_candidates(name, nesting) - # If we have a top level reference, then we don't need to include completions inside the current nesting - if name.start_with?("::") - return @entries_tree.search(name.delete_prefix("::")) #: as Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]] - end - - # Otherwise, we have to include every possible constant the user might be referring to. This is essentially the - # same algorithm as resolve, but instead of returning early we concatenate all unique results - - # Direct constants inside this namespace - entries = @entries_tree.search(nesting.any? ? "#{nesting.join("::")}::#{name}" : name) - - # Constants defined in enclosing scopes - nesting.length.downto(1) do |i| - namespace = nesting[0...i] #: as !nil - .join("::") - entries.concat(@entries_tree.search("#{namespace}::#{name}")) - end - - # Inherited constants - if name.end_with?("::") - entries.concat(inherited_constant_completion_candidates(nil, nesting + [name])) - else - entries.concat(inherited_constant_completion_candidates(name, nesting)) - end - - # Top level constants - entries.concat(@entries_tree.search(name)) - - # Filter only constants since methods may have names that look like constants - entries.select! do |definitions| - definitions.select! do |entry| - entry.is_a?(Entry::Constant) || entry.is_a?(Entry::ConstantAlias) || - entry.is_a?(Entry::Namespace) || entry.is_a?(Entry::UnresolvedConstantAlias) - end - - definitions.any? - end - - entries.uniq! - entries #: as Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]] - end - - # Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter - # documentation: - # - # name: the name of the reference how it was found in the source code (qualified or not) - # nesting: the nesting structure where the reference was found (e.g.: ["Foo", "Bar"]) - # seen_names: this parameter should not be used by consumers of the api. It is used to avoid infinite recursion when - # resolving circular references - #: (String name, Array[String] nesting, ?Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]? - def resolve(name, nesting, seen_names = []) - # If we have a top level reference, then we just search for it straight away ignoring the nesting - if name.start_with?("::") - entries = direct_or_aliased_constant(name.delete_prefix("::"), seen_names) - return entries if entries - end - - # Non qualified reference path - full_name = nesting.any? ? "#{nesting.join("::")}::#{name}" : name - - # When the name is not qualified with any namespaces, Ruby will take several steps to try to the resolve the - # constant. First, it will try to find the constant in the exact namespace where the reference was found - entries = direct_or_aliased_constant(full_name, seen_names) - return entries if entries - - # If the constant is not found yet, then Ruby will try to find the constant in the enclosing lexical scopes, - # unwrapping each level one by one. Important note: the top level is not included because that's the fallback of - # the algorithm after every other possibility has been exhausted - entries = lookup_enclosing_scopes(name, nesting, seen_names) - return entries if entries - - # If the constant does not exist in any enclosing scopes, then Ruby will search for it in the ancestors of the - # specific namespace where the reference was found - entries = lookup_ancestor_chain(name, nesting, seen_names) - return entries if entries - - # Finally, as a fallback, Ruby will search for the constant in the top level namespace - direct_or_aliased_constant(name, seen_names) - rescue UnresolvableAliasError - nil - end - - # Index all files for the given URIs, which defaults to what is configured. A block can be used to track and control - # indexing progress. That block is invoked with the current progress percentage and should return `true` to continue - # indexing or `false` to stop indexing. - #: (?uris: Array[URI::Generic]) ?{ (Integer progress) -> bool } -> void - def index_all(uris: @configuration.indexable_uris, &block) - # When troubleshooting an indexing issue, e.g. through irb, it's not obvious that `index_all` will augment the - # existing index values, meaning it may contain 'stale' entries. This check ensures that the user is aware of this - # behavior and can take appropriate action. - if @initial_indexing_completed - raise IndexNotEmptyError, - "The index is not empty. To prevent invalid entries, `index_all` can only be called once." - end - - RBSIndexer.new(self).index_ruby_core - # Calculate how many paths are worth 1% of progress - progress_step = (uris.length / 100.0).ceil - - uris.each_with_index do |uri, index| - if block && index % progress_step == 0 - progress = (index / progress_step) + 1 - break unless block.call(progress) - end - - index_file(uri, collect_comments: false) - end - - @initial_indexing_completed = true - end - - #: (URI::Generic uri, String source, ?collect_comments: bool) -> void - def index_single(uri, source, collect_comments: true) - dispatcher = Prism::Dispatcher.new - - result = Prism.parse(source) - listener = DeclarationListener.new(self, dispatcher, result, uri, collect_comments: collect_comments) - dispatcher.dispatch(result.value) - - require_path = uri.require_path - @require_paths_tree.insert(require_path, uri) if require_path - - indexing_errors = listener.indexing_errors.uniq - indexing_errors.each { |error| $stderr.puts(error) } if indexing_errors.any? - rescue SystemStackError => e - if e.backtrace&.first&.include?("prism") - $stderr.puts "Prism error indexing #{uri}: #{e.message}" - else - raise - end - end - - # Indexes a File URI by reading the contents from disk - #: (URI::Generic uri, ?collect_comments: bool) -> void - def index_file(uri, collect_comments: true) - path = uri.full_path #: as !nil - index_single(uri, File.read(path), collect_comments: collect_comments) - rescue Errno::EISDIR, Errno::ENOENT - # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore - # it - end - - # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows - # it. The idea is that we test the name in parts starting from the complete name to the first namespace. For - # `Foo::Bar::Baz`, we would test: - # 1. Is `Foo::Bar::Baz` an alias? Get the target and recursively follow its target - # 2. Is `Foo::Bar` an alias? Get the target and recursively follow its target - # 3. Is `Foo` an alias? Get the target and recursively follow its target - # - # If we find an alias, then we want to follow its target. In the same example, if `Foo::Bar` is an alias to - # `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other - # aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name - #: (String name, ?Array[String] seen_names) -> String - def follow_aliased_namespace(name, seen_names = []) - parts = name.split("::") - real_parts = [] - - (parts.length - 1).downto(0) do |i| - current_name = parts[0..i] #: as !nil - .join("::") - - entry = unless seen_names.include?(current_name) - @entries[current_name]&.first - end - - case entry - when Entry::ConstantAlias - target = entry.target - return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names) - when Entry::UnresolvedConstantAlias - resolved = resolve_alias(entry, seen_names) - - if resolved.is_a?(Entry::UnresolvedConstantAlias) - raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant" - end - - target = resolved.target - return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names) - else - real_parts.unshift( - parts[i], #: as !nil - ) - end - end - - real_parts.join("::") - end - - # Attempts to find methods for a resolved fully qualified receiver name. Do not provide the `seen_names` parameter - # as it is used only internally to prevent infinite loops when resolving circular aliases - # Returns `nil` if the method does not exist on that receiver - #: (String method_name, String receiver_name, ?Array[String] seen_names, ?inherited_only: bool) -> Array[(Entry::Member | Entry::MethodAlias)]? - def resolve_method(method_name, receiver_name, seen_names = [], inherited_only: false) - method_entries = self[method_name] - return unless method_entries - - ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::")) - ancestors.each do |ancestor| - next if inherited_only && ancestor == receiver_name - - found = method_entries.filter_map do |entry| - case entry - when Entry::Member, Entry::MethodAlias - entry if entry.owner&.name == ancestor - when Entry::UnresolvedMethodAlias - # Resolve aliases lazily as we find them - if entry.owner&.name == ancestor - resolved_alias = resolve_method_alias(entry, receiver_name, seen_names) - resolved_alias if resolved_alias.is_a?(Entry::MethodAlias) - end - end - end - - return found if found.any? - end - - nil - rescue NonExistingNamespaceError - nil - end - - # Linearizes the ancestors for a given name, returning the order of namespaces in which Ruby will search for method - # or constant declarations. - # - # When we add an ancestor in Ruby, that namespace might have ancestors of its own. Therefore, we need to linearize - # everything recursively to ensure that we are placing ancestors in the right order. For example, if you include a - # module that prepends another module, then the prepend module appears before the included module. - # - # The order of ancestors is [linearized_prepends, self, linearized_includes, linearized_superclass] - #: (String fully_qualified_name) -> Array[String] - def linearized_ancestors_of(fully_qualified_name) - # If we already computed the ancestors for this namespace, return it straight away - cached_ancestors = @ancestors[fully_qualified_name] - return cached_ancestors if cached_ancestors - - parts = fully_qualified_name.split("::") - singleton_levels = 0 - - parts.reverse_each do |part| - break unless part.start_with?("<") - - singleton_levels += 1 - parts.pop - end - - attached_class_name = parts.join("::") - - # If we don't have an entry for `name`, raise - entries = self[fully_qualified_name] - - if singleton_levels > 0 && !entries && indexed?(attached_class_name) - entries = [existing_or_new_singleton_class(attached_class_name)] - end - - raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries - - ancestors = [fully_qualified_name] - - # Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and - # this will prevent us from falling into an infinite recursion loop. Because we mutate the ancestors array later, - # the cache will reflect the final result - @ancestors[fully_qualified_name] = ancestors - - # If none of the entries for `name` are namespaces, raise - namespaces = entries.filter_map do |entry| - case entry - when Entry::Namespace - entry - when Entry::ConstantAlias - self[entry.target]&.grep(Entry::Namespace) - end - end.flatten - - raise NonExistingNamespaceError, - "None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty? - - # The original nesting where we discovered this namespace, so that we resolve the correct names of the - # included/prepended/extended modules and parent classes - nesting = namespaces.first #: as !nil - .nesting.flat_map { |n| n.split("::") } - - if nesting.any? - singleton_levels.times do - nesting << "<#{nesting.last}>" - end - end - - # We only need to run included hooks when linearizing singleton classes. Included hooks are typically used to add - # new singleton methods or to extend a module through an include. There's no need to support instance methods, the - # inclusion of another module or the prepending of another module, because those features are already a part of - # Ruby and can be used directly without any metaprogramming - run_included_hooks(attached_class_name, nesting) if singleton_levels > 0 - - linearize_mixins(ancestors, namespaces, nesting) - linearize_superclass( - ancestors, - attached_class_name, - fully_qualified_name, - namespaces, - nesting, - singleton_levels, - ) - - ancestors - end - - # Resolves an instance variable name for a given owner name. This method will linearize the ancestors of the owner - # and find inherited instance variables as well - #: (String variable_name, String owner_name) -> Array[Entry::InstanceVariable]? - def resolve_instance_variable(variable_name, owner_name) - entries = self[variable_name] #: as Array[Entry::InstanceVariable]? - return unless entries - - ancestors = linearized_ancestors_of(owner_name) - return if ancestors.empty? - - entries.select { |e| ancestors.include?(e.owner&.name) } - end - - #: (String variable_name, String owner_name) -> Array[Entry::ClassVariable]? - def resolve_class_variable(variable_name, owner_name) - entries = self[variable_name]&.grep(Entry::ClassVariable) - return unless entries&.any? - - ancestors = linearized_attached_ancestors(owner_name) - return if ancestors.empty? - - entries.select { |e| ancestors.include?(e.owner&.name) } - end - - # Returns a list of possible candidates for completion of instance variables for a given owner name. The name must - # include the `@` prefix - #: (String name, String owner_name) -> Array[(Entry::InstanceVariable | Entry::ClassVariable)] - def instance_variable_completion_candidates(name, owner_name) - entries = prefix_search(name).flatten #: as Array[Entry::InstanceVariable | Entry::ClassVariable] - # Avoid wasting time linearizing ancestors if we didn't find anything - return entries if entries.empty? - - ancestors = linearized_ancestors_of(owner_name) - - instance_variables, class_variables = entries.partition { |e| e.is_a?(Entry::InstanceVariable) } - variables = instance_variables.select { |e| ancestors.any?(e.owner&.name) } - - # Class variables are only owned by the attached class in our representation. If the owner is in a singleton - # context, we have to search for ancestors of the attached class - if class_variables.any? - name_parts = owner_name.split("::") - - if name_parts.last&.start_with?("<") - attached_name = name_parts[0..-2] #: as !nil - .join("::") - attached_ancestors = linearized_ancestors_of(attached_name) - variables.concat(class_variables.select { |e| attached_ancestors.any?(e.owner&.name) }) - else - variables.concat(class_variables.select { |e| ancestors.any?(e.owner&.name) }) - end - end - - variables.uniq!(&:name) - variables - end - - #: (String name, String owner_name) -> Array[Entry::ClassVariable] - def class_variable_completion_candidates(name, owner_name) - entries = prefix_search(name).flatten #: as Array[Entry::ClassVariable] - # Avoid wasting time linearizing ancestors if we didn't find anything - return entries if entries.empty? - - ancestors = linearized_attached_ancestors(owner_name) - variables = entries.select { |e| ancestors.any?(e.owner&.name) } - variables.uniq!(&:name) - variables - end - - # Synchronizes a change made to the given URI. This method will ensure that new declarations are indexed, removed - # declarations removed and that the ancestor linearization cache is cleared if necessary. If a block is passed, the - # consumer of this API has to handle deleting and inserting/updating entries in the index instead of passing the - # document's source (used to handle unsaved changes to files) - #: (URI::Generic uri, ?String? source) ?{ (Index index) -> void } -> void - def handle_change(uri, source = nil, &block) - key = uri.to_s - original_entries = @uris_to_entries[key] - - if block - block.call(self) - else - delete(uri) - index_single( - uri, - source, #: as !nil - ) - end - - updated_entries = @uris_to_entries[key] - return unless original_entries && updated_entries - - # A change in one ancestor may impact several different others, which could be including that ancestor through - # indirect means like including a module that than includes the ancestor. Trying to figure out exactly which - # ancestors need to be deleted is too expensive. Therefore, if any of the namespace entries has a change to their - # ancestor hash, we clear all ancestors and start linearizing lazily again from scratch - original_map = original_entries - .select { |e| e.is_a?(Entry::Namespace) } #: as Array[Entry::Namespace] - .to_h { |e| [e.name, e.ancestor_hash] } - - updated_map = updated_entries - .select { |e| e.is_a?(Entry::Namespace) } #: as Array[Entry::Namespace] - .to_h { |e| [e.name, e.ancestor_hash] } - - @ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash } - end - - #: -> void - def clear_ancestors - @ancestors.clear - end - - #: -> bool - def empty? - @entries.empty? - end - - #: -> Array[String] - def names - @entries.keys - end - - #: (String name) -> bool - def indexed?(name) - @entries.key?(name) - end - - #: -> Integer - def length - @entries.count - end - - #: (String name) -> Entry::SingletonClass - def existing_or_new_singleton_class(name) - *_namespace, unqualified_name = name.split("::") - full_singleton_name = "#{name}::<#{unqualified_name}>" - singleton = self[full_singleton_name]&.first #: as Entry::SingletonClass? - - unless singleton - attached_ancestor = self[name]&.first #: as !nil - - singleton = Entry::SingletonClass.new( - @configuration, - [full_singleton_name], - attached_ancestor.uri, - attached_ancestor.location, - attached_ancestor.name_location, - nil, - nil, - ) - add(singleton, skip_prefix_tree: true) - end - - singleton - end - - #: [T] (String uri, ?Class[(T & Entry)]? type) -> (Array[Entry] | Array[T])? - def entries_for(uri, type = nil) - entries = @uris_to_entries[uri.to_s] - return entries unless type - - entries&.grep(type) - end - - private - - # Always returns the linearized ancestors for the attached class, regardless of whether `name` refers to a singleton - # or attached namespace - #: (String name) -> Array[String] - def linearized_attached_ancestors(name) - name_parts = name.split("::") - - if name_parts.last&.start_with?("<") - attached_name = name_parts[0..-2] #: as !nil - .join("::") - linearized_ancestors_of(attached_name) - else - linearized_ancestors_of(name) - end - end - - # Runs the registered included hooks - #: (String fully_qualified_name, Array[String] nesting) -> void - def run_included_hooks(fully_qualified_name, nesting) - return if @included_hooks.empty? - - namespaces = self[fully_qualified_name]&.grep(Entry::Namespace) - return unless namespaces - - namespaces.each do |namespace| - namespace.mixin_operations.each do |operation| - next unless operation.is_a?(Entry::Include) - - # First we resolve the include name, so that we know the actual module being referred to in the include - resolved_modules = resolve(operation.module_name, nesting) - next unless resolved_modules - - module_name = resolved_modules.first #: as !nil - .name - - # Then we grab any hooks registered for that module - hooks = @included_hooks[module_name] - next unless hooks - - # We invoke the hooks with the index and the namespace that included the module - hooks.each { |hook| hook.call(self, namespace) } - end - end - end - - # Linearize mixins for an array of namespace entries. This method will mutate the `ancestors` array with the - # linearized ancestors of the mixins - #: (Array[String] ancestors, Array[Entry::Namespace] namespace_entries, Array[String] nesting) -> void - def linearize_mixins(ancestors, namespace_entries, nesting) - mixin_operations = namespace_entries.flat_map(&:mixin_operations) - main_namespace_index = 0 - - mixin_operations.each do |operation| - resolved_module = resolve(operation.module_name, nesting) - next unless resolved_module - - module_fully_qualified_name = resolved_module.first #: as !nil - .name - - case operation - when Entry::Prepend - # When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of - # the actual namespace twice. However, it does not check if it has been included because you are allowed to - # prepend the same module after it has already been included - linearized_prepends = linearized_ancestors_of(module_fully_qualified_name) - - # When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For - # example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to - # be inserted after `"A` - prepended_ancestors = ancestors[0...main_namespace_index] #: as !nil - uniq_prepends = linearized_prepends - prepended_ancestors - insert_position = linearized_prepends.length - uniq_prepends.length - - ancestors #: as untyped - .insert(insert_position, *uniq_prepends) - - main_namespace_index += linearized_prepends.length - when Entry::Include - # When including a module, Ruby will always prevent duplicate entries in case the module has already been - # prepended or included - linearized_includes = linearized_ancestors_of(module_fully_qualified_name) - ancestors #: as untyped - .insert(main_namespace_index + 1, *(linearized_includes - ancestors)) - end - end - end - - # Linearize the superclass of a given namespace (including modules with the implicit `Module` superclass). This - # method will mutate the `ancestors` array with the linearized ancestors of the superclass - #: (Array[String] ancestors, String attached_class_name, String fully_qualified_name, Array[Entry::Namespace] namespace_entries, Array[String] nesting, Integer singleton_levels) -> void - def linearize_superclass( # rubocop:disable Metrics/ParameterLists - ancestors, - attached_class_name, - fully_qualified_name, - namespace_entries, - nesting, - singleton_levels - ) - # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits - # from two different classes in different files, we simply ignore it - possible_parents = singleton_levels > 0 ? self[attached_class_name] : namespace_entries - superclass = nil #: Entry::Class? - - possible_parents&.each do |n| - # Ignore non class entries - next unless n.is_a?(Entry::Class) - - parent_class = n.parent_class - next unless parent_class - - # Always set the superclass, but break early if we found one that isn't `::Object` (meaning we found an explicit - # parent class and not the implicit default). Note that when setting different parents to the same class, which - # is invalid, we pick whatever is the first one we find - superclass = n - break if parent_class != "::Object" - end - - if superclass - # If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack - # error. We need to ensure that this isn't the case - parent_class = superclass.parent_class #: as !nil - - resolved_parent_class = resolve(parent_class, nesting) - parent_class_name = resolved_parent_class&.first&.name - - if parent_class_name && fully_qualified_name != parent_class_name - - parent_name_parts = parent_class_name.split("::") - singleton_levels.times do - parent_name_parts << "<#{parent_name_parts.last}>" - end - - ancestors.concat(linearized_ancestors_of(parent_name_parts.join("::"))) - end - - # When computing the linearization for a class's singleton class, it inherits from the linearized ancestors of - # the `Class` class - if parent_class_name&.start_with?("BasicObject") && singleton_levels > 0 - class_class_name_parts = ["Class"] - - (singleton_levels - 1).times do - class_class_name_parts << "<#{class_class_name_parts.last}>" - end - - ancestors.concat(linearized_ancestors_of(class_class_name_parts.join("::"))) - end - elsif singleton_levels > 0 - # When computing the linearization for a module's singleton class, it inherits from the linearized ancestors of - # the `Module` class - mod = self[attached_class_name]&.find { |n| n.is_a?(Entry::Module) } #: as Entry::Module? - - if mod - module_class_name_parts = ["Module"] - - (singleton_levels - 1).times do - module_class_name_parts << "<#{module_class_name_parts.last}>" - end - - ancestors.concat(linearized_ancestors_of(module_class_name_parts.join("::"))) - end - end - end - - # Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant - # that doesn't exist, then we return the same UnresolvedAlias - #: (Entry::UnresolvedConstantAlias entry, Array[String] seen_names) -> (Entry::ConstantAlias | Entry::UnresolvedConstantAlias) - def resolve_alias(entry, seen_names) - alias_name = entry.name - return entry if seen_names.include?(alias_name) - - seen_names << alias_name - - target = resolve(entry.target, entry.nesting, seen_names) - return entry unless target - - # Self referential alias can be unresolved we should bail out from resolving - return entry if target.first == entry - - target_name = target.first #: as !nil - .name - resolved_alias = Entry::ConstantAlias.new(target_name, entry) - - # Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later - original_entries = @entries[alias_name] #: as !nil - original_entries.delete(entry) - original_entries << resolved_alias - - @entries_tree.insert(alias_name, original_entries) - - resolved_alias - end - - #: (String name, Array[String] nesting, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]? - def lookup_enclosing_scopes(name, nesting, seen_names) - nesting.length.downto(1) do |i| - namespace = nesting[0...i] #: as !nil - .join("::") - - # If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases - - # because the user might be trying to jump to the alias definition. - # - # However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in - # the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing - # `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the - # `RubyLsp::Interface` part is an alias, that has to be resolved - entries = direct_or_aliased_constant("#{namespace}::#{name}", seen_names) - return entries if entries - end - - nil - end - - #: (String name, Array[String] nesting, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]? - def lookup_ancestor_chain(name, nesting, seen_names) - *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::") - return if nesting_parts.empty? - - namespace_entries = resolve(nesting_parts.join("::"), [], seen_names) - return unless namespace_entries - - namespace_name = namespace_entries.first #: as !nil - .name - ancestors = nesting_parts.empty? ? [] : linearized_ancestors_of(namespace_name) - - ancestors.each do |ancestor_name| - entries = direct_or_aliased_constant("#{ancestor_name}::#{constant_name}", seen_names) - return entries if entries - end - - nil - rescue NonExistingNamespaceError - nil - end - - #: (String? name, Array[String] nesting) -> Array[Array[(Entry::Namespace | Entry::ConstantAlias | Entry::UnresolvedConstantAlias | Entry::Constant)]] - def inherited_constant_completion_candidates(name, nesting) - namespace_entries = if name - *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::") - return [] if nesting_parts.empty? - - resolve(nesting_parts.join("::"), []) - else - resolve(nesting.join("::"), []) - end - return [] unless namespace_entries - - namespace_name = namespace_entries.first #: as !nil - .name - ancestors = linearized_ancestors_of(namespace_name) - candidates = ancestors.flat_map do |ancestor_name| - @entries_tree.search("#{ancestor_name}::#{constant_name}") - end - - # For candidates with the same name, we must only show the first entry in the inheritance chain, since that's the - # one the user will be referring to in completion - completion_items = candidates.each_with_object({}) do |entries, hash| - *parts, short_name = entries.first #: as !nil - .name.split("::") - namespace_name = parts.join("::") - ancestor_index = ancestors.index(namespace_name) - existing_entry, existing_entry_index = hash[short_name] - - next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index) - - hash[short_name] = [entries, ancestor_index] - end - - completion_items.values.map!(&:first) - rescue NonExistingNamespaceError - [] - end - - # Removes redundancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo` - # inside of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up - # with `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and - # the nesting - #: (String name, Array[String] nesting) -> String - def build_non_redundant_full_name(name, nesting) - # If there's no nesting, then we can just return the name as is - return name if nesting.empty? - - # If the name is not qualified, we can just concatenate the nesting and the name - return "#{nesting.join("::")}::#{name}" unless name.include?("::") - - name_parts = name.split("::") - first_redundant_part = nesting.index(name_parts[0]) - - # If there are no redundant parts between the name and the nesting, then the full name is both combined - return "#{nesting.join("::")}::#{name}" unless first_redundant_part - - # Otherwise, push all of the leading parts of the nesting that aren't redundant into the name. For example, if we - # have a reference to `Foo::Bar` inside the `[Namespace, Foo]` nesting, then only the `Foo` part is redundant, but - # we still need to include the `Namespace` part - name_parts.unshift(*nesting[0...first_redundant_part]) - name_parts.join("::") - end - - # Tries to return direct entry from index then non seen canonicalized alias or nil - #: (String full_name, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]? - def direct_or_aliased_constant(full_name, seen_names) - if (entries = @entries[full_name]) - return entries.map do |e| - e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e - end #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias])? - end - - aliased = follow_aliased_namespace(full_name, seen_names) - return if full_name == aliased || seen_names.include?(aliased) - - @entries[aliased]&.map do |e| - e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e - end #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias])? - end - - # Attempt to resolve a given unresolved method alias. This method returns the resolved alias if we managed to - # identify the target or the same unresolved alias entry if we couldn't - #: (Entry::UnresolvedMethodAlias entry, String receiver_name, Array[String] seen_names) -> (Entry::MethodAlias | Entry::UnresolvedMethodAlias) - def resolve_method_alias(entry, receiver_name, seen_names) - new_name = entry.new_name - return entry if new_name == entry.old_name - return entry if seen_names.include?(new_name) - - seen_names << new_name - - target_method_entries = resolve_method(entry.old_name, receiver_name, seen_names) - return entry unless target_method_entries - - resolved_alias = Entry::MethodAlias.new( - target_method_entries.first, #: as !nil - entry, - ) - original_entries = @entries[new_name] #: as !nil - original_entries.delete(entry) - original_entries << resolved_alias - resolved_alias - end - end -end diff --git a/lib/ruby_indexer/lib/ruby_indexer/location.rb b/lib/ruby_indexer/lib/ruby_indexer/location.rb deleted file mode 100644 index a8164ccc97..0000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/location.rb +++ /dev/null @@ -1,37 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module RubyIndexer - class Location - class << self - #: (Prism::Location prism_location, (^(Integer arg0) -> Integer | Prism::CodeUnitsCache) code_units_cache) -> instance - def from_prism_location(prism_location, code_units_cache) - new( - prism_location.start_line, - prism_location.end_line, - prism_location.cached_start_code_units_column(code_units_cache), - prism_location.cached_end_code_units_column(code_units_cache), - ) - end - end - - #: Integer - attr_reader :start_line, :end_line, :start_column, :end_column - - #: (Integer start_line, Integer end_line, Integer start_column, Integer end_column) -> void - def initialize(start_line, end_line, start_column, end_column) - @start_line = start_line - @end_line = end_line - @start_column = start_column - @end_column = end_column - end - - #: ((Location | Prism::Location) other) -> bool - def ==(other) - start_line == other.start_line && - end_line == other.end_line && - start_column == other.start_column && - end_column == other.end_column - end - end -end diff --git a/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb b/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb deleted file mode 100644 index 44a690850d..0000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +++ /dev/null @@ -1,149 +0,0 @@ -# typed: true -# frozen_string_literal: true - -module RubyIndexer - # A PrefixTree is a data structure that allows searching for partial strings fast. The tree is similar to a nested - # hash structure, where the keys are the characters of the inserted strings. - # - # ## Example - # ```ruby - # tree = PrefixTree[String].new - # # Insert entries using the same key and value - # tree.insert("bar", "bar") - # tree.insert("baz", "baz") - # # Internally, the structure is analogous to this, but using nodes: - # # { - # # "b" => { - # # "a" => { - # # "r" => "bar", - # # "z" => "baz" - # # } - # # } - # # } - # # When we search it, it finds all possible values based on partial (or complete matches): - # tree.search("") # => ["bar", "baz"] - # tree.search("b") # => ["bar", "baz"] - # tree.search("ba") # => ["bar", "baz"] - # tree.search("bar") # => ["bar"] - # ``` - # - # A PrefixTree is useful for autocomplete, since we always want to find all alternatives while the developer hasn't - # finished typing yet. This PrefixTree implementation allows for string keys and any arbitrary value using the generic - # `Value` type. - # - # See https://en.wikipedia.org/wiki/Trie for more information - #: [Value] - class PrefixTree - #: -> void - def initialize - @root = Node.new( - "", - "", #: as untyped - ) #: Node[Value] - end - - # Search the PrefixTree based on a given `prefix`. If `foo` is an entry in the tree, then searching for `fo` will - # return it as a result. The result is always an array of the type of value attribute to the generic `Value` type. - # Notice that if the `Value` is an array, this method will return an array of arrays, where each entry is the array - # of values for a given match - #: (String prefix) -> Array[Value] - def search(prefix) - node = find_node(prefix) - return [] unless node - - node.collect - end - - # Inserts a `value` using the given `key` - #: (String key, Value value) -> void - def insert(key, value) - node = @root - - key.each_char do |char| - node = node.children[char] ||= Node.new(char, value, node) - end - - # This line is to allow a value to be overridden. When we are indexing files, we want to be able to update entries - # for a given fully qualified name if we find more occurrences of it. Without being able to override, that would - # not be possible - node.value = value - node.leaf = true - end - - # Deletes the entry identified by `key` from the tree. Notice that a partial match will still delete all entries - # that match it. For example, if the tree contains `foo` and we ask to delete `fo`, then `foo` will be deleted - #: (String key) -> void - def delete(key) - node = find_node(key) - return unless node - - # Remove the node from the tree and then go up the parents to remove any of them with empty children - parent = node.parent #: Node[Value]? - - while parent - parent.children.delete(node.key) - return if parent.children.any? || parent.leaf - - node = parent - parent = parent.parent - end - end - - private - - # Find a node that matches the given `key` - #: (String key) -> Node[Value]? - def find_node(key) - node = @root - - key.each_char do |char| - snode = node.children[char] - return nil unless snode - - node = snode - end - - node - end - - #: [Value] - class Node - #: Hash[String, Node[Value]] - attr_reader :children - - #: String - attr_reader :key - - #: Value - attr_accessor :value - - #: bool - attr_accessor :leaf - - #: Node[Value]? - attr_reader :parent - - #: (String key, Value value, ?Node[Value]? parent) -> void - def initialize(key, value, parent = nil) - @key = key - @value = value - @parent = parent - @children = {} - @leaf = false - end - - #: -> Array[Value] - def collect - result = [] - stack = [self] - - while (node = stack.pop) - result << node.value if node.leaf - stack.concat(node.children.values) - end - - result - end - end - end -end diff --git a/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb b/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb deleted file mode 100644 index 2f3922bb88..0000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +++ /dev/null @@ -1,294 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module RubyIndexer - class RBSIndexer - HAS_UNTYPED_FUNCTION = !!defined?(RBS::Types::UntypedFunction) #: bool - - #: (Index index) -> void - def initialize(index) - @index = index - end - - #: -> void - def index_ruby_core - loader = RBS::EnvironmentLoader.new - RBS::Environment.from_loader(loader).resolve_type_names - - loader.each_signature do |_source, pathname, _buffer, declarations, _directives| - process_signature(pathname, declarations) - end - end - - #: (Pathname pathname, Array[RBS::AST::Declarations::Base] declarations) -> void - def process_signature(pathname, declarations) - declarations.each do |declaration| - process_declaration(declaration, pathname) - end - end - - private - - #: (RBS::AST::Declarations::Base declaration, Pathname pathname) -> void - def process_declaration(declaration, pathname) - case declaration - when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module - handle_class_or_module_declaration(declaration, pathname) - when RBS::AST::Declarations::Constant - namespace_nesting = declaration.name.namespace.path.map(&:to_s) - handle_constant(declaration, namespace_nesting, URI::Generic.from_path(path: pathname.to_s)) - when RBS::AST::Declarations::Global - handle_global_variable(declaration, pathname) - else # rubocop:disable Style/EmptyElse - # Other kinds not yet handled - end - end - - #: ((RBS::AST::Declarations::Class | RBS::AST::Declarations::Module) declaration, Pathname pathname) -> void - def handle_class_or_module_declaration(declaration, pathname) - nesting = [declaration.name.name.to_s] - uri = URI::Generic.from_path(path: pathname.to_s) - location = to_ruby_indexer_location(declaration.location) - comments = comments_to_string(declaration) - entry = if declaration.is_a?(RBS::AST::Declarations::Class) - parent_class = declaration.super_class&.name&.name&.to_s - Entry::Class.new(@index.configuration, nesting, uri, location, location, comments, parent_class) - else - Entry::Module.new(@index.configuration, nesting, uri, location, location, comments) - end - - add_declaration_mixins_to_entry(declaration, entry) - @index.add(entry) - - declaration.members.each do |member| - case member - when RBS::AST::Members::MethodDefinition - handle_method(member, entry) - when RBS::AST::Declarations::Constant - handle_constant(member, nesting, uri) - when RBS::AST::Members::Alias - # In RBS, an alias means that two methods have the same signature. - # It does not mean the same thing as a Ruby alias. - handle_signature_alias(member, entry) - end - end - end - - #: (RBS::Location rbs_location) -> RubyIndexer::Location - def to_ruby_indexer_location(rbs_location) - RubyIndexer::Location.new( - rbs_location.start_line, - rbs_location.end_line, - rbs_location.start_column, - rbs_location.end_column, - ) - end - - #: ((RBS::AST::Declarations::Class | RBS::AST::Declarations::Module) declaration, Entry::Namespace entry) -> void - def add_declaration_mixins_to_entry(declaration, entry) - declaration.each_mixin do |mixin| - name = mixin.name.name.to_s - case mixin - when RBS::AST::Members::Include - entry.mixin_operations << Entry::Include.new(name) - when RBS::AST::Members::Prepend - entry.mixin_operations << Entry::Prepend.new(name) - when RBS::AST::Members::Extend - singleton = @index.existing_or_new_singleton_class(entry.name) - singleton.mixin_operations << Entry::Include.new(name) - end - end - end - - #: (RBS::AST::Members::MethodDefinition member, Entry::Namespace owner) -> void - def handle_method(member, owner) - name = member.name.name - uri = URI::Generic.from_path(path: member.location.buffer.name.to_s) - location = to_ruby_indexer_location(member.location) - comments = comments_to_string(member) - - real_owner = member.singleton? ? @index.existing_or_new_singleton_class(owner.name) : owner - signatures = signatures(member) - @index.add(Entry::Method.new( - @index.configuration, - name, - uri, - location, - location, - comments, - signatures, - member.visibility || :public, - real_owner, - )) - end - - #: (RBS::AST::Members::MethodDefinition member) -> Array[Entry::Signature] - def signatures(member) - member.overloads.map do |overload| - parameters = process_overload(overload) - Entry::Signature.new(parameters) - end - end - - #: (RBS::AST::Members::MethodDefinition::Overload overload) -> Array[Entry::Parameter] - def process_overload(overload) - function = overload.method_type.type - - if function.is_a?(RBS::Types::Function) - parameters = parse_arguments(function) - - block = overload.method_type.block - parameters << Entry::BlockParameter.anonymous if block&.required - return parameters - end - - # Untyped functions are a new RBS feature (since v3.6.0) to declare methods that accept any parameters. For our - # purposes, accepting any argument is equivalent to `...` - if HAS_UNTYPED_FUNCTION && function.is_a?(RBS::Types::UntypedFunction) - [Entry::ForwardingParameter.new] - else - [] - end - end - - #: (RBS::Types::Function function) -> Array[Entry::Parameter] - def parse_arguments(function) - parameters = [] - parameters.concat(process_required_and_optional_positionals(function)) - parameters.concat(process_trailing_positionals(function)) if function.trailing_positionals - parameters << process_rest_positionals(function) if function.rest_positionals - parameters.concat(process_required_keywords(function)) if function.required_keywords - parameters.concat(process_optional_keywords(function)) if function.optional_keywords - parameters << process_rest_keywords(function) if function.rest_keywords - parameters - end - - #: (RBS::Types::Function function) -> Array[Entry::RequiredParameter] - def process_required_and_optional_positionals(function) - argument_offset = 0 - - required = function.required_positionals.map.with_index(argument_offset) do |param, i| - # Some parameters don't have names, e.g. - # def self.try_convert: [U] (untyped) -> ::Array[U]? - name = param.name || :"arg#{i}" - argument_offset += 1 - - Entry::RequiredParameter.new(name: name) - end - - optional = function.optional_positionals.map.with_index(argument_offset) do |param, i| - # Optional positionals may be unnamed, e.g. - # def self.polar: (Numeric, ?Numeric) -> Complex - name = param.name || :"arg#{i}" - - Entry::OptionalParameter.new(name: name) - end - - required + optional - end - - #: (RBS::Types::Function function) -> Array[Entry::OptionalParameter] - def process_trailing_positionals(function) - function.trailing_positionals.map do |param| - Entry::OptionalParameter.new(name: param.name) - end - end - - #: (RBS::Types::Function function) -> Entry::RestParameter - def process_rest_positionals(function) - rest = function.rest_positionals - - rest_name = rest.name || Entry::RestParameter::DEFAULT_NAME - - Entry::RestParameter.new(name: rest_name) - end - - #: (RBS::Types::Function function) -> Array[Entry::KeywordParameter] - def process_required_keywords(function) - function.required_keywords.map do |name, _param| - Entry::KeywordParameter.new(name: name) - end - end - - #: (RBS::Types::Function function) -> Array[Entry::OptionalKeywordParameter] - def process_optional_keywords(function) - function.optional_keywords.map do |name, _param| - Entry::OptionalKeywordParameter.new(name: name) - end - end - - #: (RBS::Types::Function function) -> Entry::KeywordRestParameter - def process_rest_keywords(function) - param = function.rest_keywords - - name = param.name || Entry::KeywordRestParameter::DEFAULT_NAME - - Entry::KeywordRestParameter.new(name: name) - end - - # RBS treats constant definitions differently depend on where they are defined. - # When constants' rbs are defined inside a class/module block, they are treated as - # members of the class/module. - # - # module Encoding - # US_ASCII = ... # US_ASCII is a member of Encoding - # end - # - # When constants' rbs are defined outside a class/module block, they are treated as - # top-level constants. - # - # Complex::I = ... # Complex::I is a top-level constant - # - # And we need to handle their nesting differently. - #: (RBS::AST::Declarations::Constant declaration, Array[String] nesting, URI::Generic uri) -> void - def handle_constant(declaration, nesting, uri) - fully_qualified_name = [*nesting, declaration.name.name.to_s].join("::") - @index.add(Entry::Constant.new( - @index.configuration, - fully_qualified_name, - uri, - to_ruby_indexer_location(declaration.location), - comments_to_string(declaration), - )) - end - - #: (RBS::AST::Declarations::Global declaration, Pathname pathname) -> void - def handle_global_variable(declaration, pathname) - name = declaration.name.to_s - uri = URI::Generic.from_path(path: pathname.to_s) - location = to_ruby_indexer_location(declaration.location) - comments = comments_to_string(declaration) - - @index.add(Entry::GlobalVariable.new( - @index.configuration, - name, - uri, - location, - comments, - )) - end - - #: (RBS::AST::Members::Alias member, Entry::Namespace owner_entry) -> void - def handle_signature_alias(member, owner_entry) - uri = URI::Generic.from_path(path: member.location.buffer.name.to_s) - comments = comments_to_string(member) - - entry = Entry::UnresolvedMethodAlias.new( - @index.configuration, - member.new_name.to_s, - member.old_name.to_s, - owner_entry, - uri, - to_ruby_indexer_location(member.location), - comments, - ) - - @index.add(entry) - end - - #: ((RBS::AST::Declarations::Class | RBS::AST::Declarations::Module | RBS::AST::Declarations::Constant | RBS::AST::Declarations::Global | RBS::AST::Members::MethodDefinition | RBS::AST::Members::Alias) declaration) -> String? - def comments_to_string(declaration) - declaration.comment&.string - end - end -end diff --git a/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb b/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb deleted file mode 100644 index ce12f1292c..0000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +++ /dev/null @@ -1,32 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module RubyIndexer - # Represents the visibility scope in a Ruby namespace. This keeps track of whether methods are in a public, private or - # protected section, and whether they are module functions. - class VisibilityScope - class << self - #: -> instance - def module_function_scope - new(module_func: true, visibility: :private) - end - - #: -> instance - def public_scope - new - end - end - - #: Symbol - attr_reader :visibility - - #: bool - attr_reader :module_func - - #: (?visibility: Symbol, ?module_func: bool) -> void - def initialize(visibility: :public, module_func: false) - @visibility = visibility - @module_func = module_func - end - end -end diff --git a/lib/ruby_indexer/ruby_indexer.rb b/lib/ruby_indexer/ruby_indexer.rb deleted file mode 100644 index f20a451f6b..0000000000 --- a/lib/ruby_indexer/ruby_indexer.rb +++ /dev/null @@ -1,19 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "yaml" -require "did_you_mean" - -require "ruby_indexer/lib/ruby_indexer/uri" -require "ruby_indexer/lib/ruby_indexer/visibility_scope" -require "ruby_indexer/lib/ruby_indexer/declaration_listener" -require "ruby_indexer/lib/ruby_indexer/enhancement" -require "ruby_indexer/lib/ruby_indexer/index" -require "ruby_indexer/lib/ruby_indexer/entry" -require "ruby_indexer/lib/ruby_indexer/configuration" -require "ruby_indexer/lib/ruby_indexer/prefix_tree" -require "ruby_indexer/lib/ruby_indexer/location" -require "ruby_indexer/lib/ruby_indexer/rbs_indexer" - -module RubyIndexer -end diff --git a/lib/ruby_indexer/test/class_variables_test.rb b/lib/ruby_indexer/test/class_variables_test.rb deleted file mode 100644 index a035361c21..0000000000 --- a/lib/ruby_indexer/test/class_variables_test.rb +++ /dev/null @@ -1,140 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require_relative "test_case" - -module RubyIndexer - class ClassVariableTest < TestCase - def test_class_variable_and_write - index(<<~RUBY) - class Foo - @@bar &&= 1 - end - RUBY - - assert_entry("@@bar", Entry::ClassVariable, "/fake/path/foo.rb:1-2:1-7") - - entry = @index["@@bar"]&.first #: as Entry::ClassVariable - owner = entry.owner #: as !nil - assert_instance_of(Entry::Class, owner) - assert_equal("Foo", owner.name) - end - - def test_class_variable_operator_write - index(<<~RUBY) - class Foo - @@bar += 1 - end - RUBY - - assert_entry("@@bar", Entry::ClassVariable, "/fake/path/foo.rb:1-2:1-7") - end - - def test_class_variable_or_write - index(<<~RUBY) - class Foo - @@bar ||= 1 - end - RUBY - - assert_entry("@@bar", Entry::ClassVariable, "/fake/path/foo.rb:1-2:1-7") - end - - def test_class_variable_target_node - index(<<~RUBY) - class Foo - @@foo, @@bar = 1 - end - RUBY - - assert_entry("@@foo", Entry::ClassVariable, "/fake/path/foo.rb:1-2:1-7") - assert_entry("@@bar", Entry::ClassVariable, "/fake/path/foo.rb:1-9:1-14") - - entry = @index["@@foo"]&.first #: as Entry::ClassVariable - owner = entry.owner #: as !nil - assert_instance_of(Entry::Class, owner) - assert_equal("Foo", owner.name) - - entry = @index["@@bar"]&.first #: as Entry::ClassVariable - owner = entry.owner #: as !nil - assert_instance_of(Entry::Class, owner) - assert_equal("Foo", owner.name) - end - - def test_class_variable_write - index(<<~RUBY) - class Foo - @@bar = 1 - end - RUBY - - assert_entry("@@bar", Entry::ClassVariable, "/fake/path/foo.rb:1-2:1-7") - end - - def test_empty_name_class_variable - index(<<~RUBY) - module Foo - @@ = 1 - end - RUBY - - refute_entry("@@") - end - - def test_top_level_class_variable - index(<<~RUBY) - @@foo = 123 - RUBY - - entry = @index["@@foo"]&.first #: as Entry::ClassVariable - assert_nil(entry.owner) - end - - def test_class_variable_inside_self_method - index(<<~RUBY) - class Foo - def self.bar - @@bar = 123 - end - end - RUBY - - entry = @index["@@bar"]&.first #: as Entry::ClassVariable - owner = entry.owner #: as !nil - assert_instance_of(Entry::Class, owner) - assert_equal("Foo", owner.name) - end - - def test_class_variable_inside_singleton_class - index(<<~RUBY) - class Foo - class << self - @@bar = 123 - end - end - RUBY - - entry = @index["@@bar"]&.first #: as Entry::ClassVariable - owner = entry.owner #: as !nil - assert_instance_of(Entry::Class, owner) - assert_equal("Foo", owner.name) - end - - def test_class_variable_in_singleton_class_method - index(<<~RUBY) - class Foo - class << self - def self.bar - @@bar = 123 - end - end - end - RUBY - - entry = @index["@@bar"]&.first #: as Entry::ClassVariable - owner = entry.owner #: as !nil - assert_instance_of(Entry::Class, owner) - assert_equal("Foo", owner.name) - end - end -end diff --git a/lib/ruby_indexer/test/classes_and_modules_test.rb b/lib/ruby_indexer/test/classes_and_modules_test.rb deleted file mode 100644 index 6439aa5784..0000000000 --- a/lib/ruby_indexer/test/classes_and_modules_test.rb +++ /dev/null @@ -1,790 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require_relative "test_case" - -module RubyIndexer - class ClassesAndModulesTest < TestCase - def test_empty_statements_class - index(<<~RUBY) - class Foo - end - RUBY - - assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3") - end - - def test_conditional_class - index(<<~RUBY) - class Foo - end if condition - RUBY - - assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3") - end - - def test_class_with_statements - index(<<~RUBY) - class Foo - def something; end - end - RUBY - - assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:2-3") - end - - def test_colon_colon_class - index(<<~RUBY) - class ::Foo - end - RUBY - - assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3") - end - - def test_colon_colon_class_inside_class - index(<<~RUBY) - class Bar - class ::Foo - end - end - RUBY - - assert_entry("Bar", Entry::Class, "/fake/path/foo.rb:0-0:3-3") - assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:1-2:2-5") - end - - def test_namespaced_class - index(<<~RUBY) - class Foo::Bar - end - RUBY - - assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:0-0:1-3") - end - - def test_dynamically_namespaced_class - index(<<~RUBY) - class self::Bar - end - RUBY - - assert_entry("self::Bar", Entry::Class, "/fake/path/foo.rb:0-0:1-3") - end - - def test_dynamically_namespaced_class_does_not_affect_other_classes - index(<<~RUBY) - class Foo - class self::Bar - end - - class Bar - end - end - RUBY - - refute_entry("self::Bar") - assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:6-3") - assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:4-2:5-5") - end - - def test_empty_statements_module - index(<<~RUBY) - module Foo - end - RUBY - - assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:1-3") - end - - def test_conditional_module - index(<<~RUBY) - module Foo - end if condition - RUBY - - assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:1-3") - end - - def test_module_with_statements - index(<<~RUBY) - module Foo - def something; end - end - RUBY - - assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:2-3") - end - - def test_colon_colon_module - index(<<~RUBY) - module ::Foo - end - RUBY - - assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:1-3") - end - - def test_namespaced_module - index(<<~RUBY) - module Foo::Bar - end - RUBY - - assert_entry("Foo::Bar", Entry::Module, "/fake/path/foo.rb:0-0:1-3") - end - - def test_dynamically_namespaced_module - index(<<~RUBY) - module self::Bar - end - RUBY - - assert_entry("self::Bar", Entry::Module, "/fake/path/foo.rb:0-0:1-3") - end - - def test_dynamically_namespaced_module_does_not_affect_other_modules - index(<<~RUBY) - module Foo - class self::Bar - end - - module Bar - end - end - RUBY - - assert_entry("Foo::self::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5") - assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:6-3") - assert_entry("Foo::Bar", Entry::Module, "/fake/path/foo.rb:4-2:5-5") - end - - def test_nested_modules_and_classes_with_multibyte_characters - index(<<~RUBY) - module A動物 - class Bねこ; end - end - RUBY - - assert_entry("A動物", Entry::Module, "/fake/path/foo.rb:0-0:2-3") - assert_entry("A動物::Bねこ", Entry::Class, "/fake/path/foo.rb:1-2:1-16") - end - - def test_nested_modules_and_classes - index(<<~RUBY) - module Foo - class Bar - end - - module Baz - class Qux - class Something - end - end - end - end - RUBY - - assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:10-3") - assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5") - assert_entry("Foo::Baz", Entry::Module, "/fake/path/foo.rb:4-2:9-5") - assert_entry("Foo::Baz::Qux", Entry::Class, "/fake/path/foo.rb:5-4:8-7") - assert_entry("Foo::Baz::Qux::Something", Entry::Class, "/fake/path/foo.rb:6-6:7-9") - end - - def test_deleting_from_index_based_on_file_path - index(<<~RUBY) - class Foo - end - RUBY - - assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3") - - @index.delete(URI::Generic.from_path(path: "/fake/path/foo.rb")) - refute_entry("Foo") - - assert_no_indexed_entries - end - - def test_comments_can_be_attached_to_a_class - index(<<~RUBY) - # This is method comment - def foo; end - # This is a Foo comment - # This is another Foo comment - class Foo - # This should not be attached - end - - # Ignore me - - # This Bar comment has 1 line padding - - class Bar; end - RUBY - - foo_entry = @index["Foo"] #: as !nil - .first #: as !nil - assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments) - - bar_entry = @index["Bar"] #: as !nil - .first #: as !nil - assert_equal("This Bar comment has 1 line padding", bar_entry.comments) - end - - def test_skips_comments_containing_invalid_encodings - index(<<~RUBY) - # comment \xBA - class Foo - end - RUBY - assert(@index["Foo"]&.first) - end - - def test_comments_can_be_attached_to_a_namespaced_class - index(<<~RUBY) - # This is a Foo comment - # This is another Foo comment - class Foo - # This is a Bar comment - class Bar; end - end - RUBY - - foo_entry = @index["Foo"] #: as !nil - .first #: as !nil - assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments) - - bar_entry = @index["Foo::Bar"] #: as !nil - .first #: as !nil - assert_equal("This is a Bar comment", bar_entry.comments) - end - - def test_comments_can_be_attached_to_a_reopened_class - index(<<~RUBY) - # This is a Foo comment - class Foo; end - - # This is another Foo comment - class Foo; end - RUBY - - first_foo_entry, second_foo_entry = @index["Foo"] #: as !nil - assert_equal("This is a Foo comment", first_foo_entry&.comments) - assert_equal("This is another Foo comment", second_foo_entry&.comments) - end - - def test_comments_removes_the_leading_pound_and_space - index(<<~RUBY) - # This is a Foo comment - class Foo; end - - #This is a Bar comment - class Bar; end - RUBY - - first_foo_entry = @index["Foo"] #: as !nil - .first #: as !nil - assert_equal("This is a Foo comment", first_foo_entry.comments) - - second_foo_entry = @index["Bar"] #: as !nil - .first #: as !nil - assert_equal("This is a Bar comment", second_foo_entry.comments) - end - - def test_private_class_and_module_indexing - index(<<~RUBY) - class A - class B; end - private_constant(:B) - - module C; end - private_constant("C") - - class D; end - end - RUBY - - b_const = @index["A::B"] #: as !nil - .first - assert_predicate(b_const, :private?) - - c_const = @index["A::C"] #: as !nil - .first - assert_predicate(c_const, :private?) - - d_const = @index["A::D"] #: as !nil - .first - assert_predicate(d_const, :public?) - end - - def test_keeping_track_of_super_classes - index(<<~RUBY) - class Foo < Bar - end - - class Baz - end - - module Something - class Baz - end - - class Qux < ::Baz - end - end - - class FinalThing < Something::Baz - end - RUBY - - foo = @index["Foo"] #: as !nil - .first #: as Entry::Class - assert_equal("Bar", foo.parent_class) - - baz = @index["Baz"] #: as !nil - .first #: as Entry::Class - assert_equal("::Object", baz.parent_class) - - qux = @index["Something::Qux"] #: as !nil - .first #: as Entry::Class - assert_equal("::Baz", qux.parent_class) - - final_thing = @index["FinalThing"] #: as !nil - .first #: as Entry::Class - assert_equal("Something::Baz", final_thing.parent_class) - end - - def test_keeping_track_of_included_modules - index(<<~RUBY) - class Foo - # valid syntaxes that we can index - include A1 - self.include A2 - include A3, A4 - self.include A5, A6 - - # valid syntaxes that we cannot index because of their dynamic nature - include some_variable_or_method_call - self.include some_variable_or_method_call - - def something - include A7 # We should not index this because of this dynamic nature - end - - # Valid inner class syntax definition with its own modules included - class Qux - include Corge - self.include Corge - include Baz - - include some_variable_or_method_call - end - end - - class ConstantPathReferences - include Foo::Bar - self.include Foo::Bar2 - - include dynamic::Bar - include Foo:: - end - RUBY - - foo = @index["Foo"] #: as !nil - .first #: as Entry::Class - assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names) - - qux = @index["Foo::Qux"] #: as !nil - .first #: as Entry::Class - assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names) - - constant_path_references = @index["ConstantPathReferences"] #: as !nil - .first #: as Entry::Class - assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names) - end - - def test_keeping_track_of_prepended_modules - index(<<~RUBY) - class Foo - # valid syntaxes that we can index - prepend A1 - self.prepend A2 - prepend A3, A4 - self.prepend A5, A6 - - # valid syntaxes that we cannot index because of their dynamic nature - prepend some_variable_or_method_call - self.prepend some_variable_or_method_call - - def something - prepend A7 # We should not index this because of this dynamic nature - end - - # Valid inner class syntax definition with its own modules prepended - class Qux - prepend Corge - self.prepend Corge - prepend Baz - - prepend some_variable_or_method_call - end - end - - class ConstantPathReferences - prepend Foo::Bar - self.prepend Foo::Bar2 - - prepend dynamic::Bar - prepend Foo:: - end - RUBY - - foo = @index["Foo"] #: as !nil - .first #: as Entry::Class - assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names) - - qux = @index["Foo::Qux"] #: as !nil - .first #: as Entry::Class - assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names) - - constant_path_references = @index["ConstantPathReferences"] #: as !nil - .first #: as Entry::Class - assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names) - end - - def test_keeping_track_of_extended_modules - index(<<~RUBY) - class Foo - # valid syntaxes that we can index - extend A1 - self.extend A2 - extend A3, A4 - self.extend A5, A6 - - # valid syntaxes that we cannot index because of their dynamic nature - extend some_variable_or_method_call - self.extend some_variable_or_method_call - - def something - extend A7 # We should not index this because of this dynamic nature - end - - # Valid inner class syntax definition with its own modules prepended - class Qux - extend Corge - self.extend Corge - extend Baz - - extend some_variable_or_method_call - end - end - - class ConstantPathReferences - extend Foo::Bar - self.extend Foo::Bar2 - - extend dynamic::Bar - extend Foo:: - end - RUBY - - foo = @index["Foo::"] #: as !nil - .first #: as Entry::Class - assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names) - - qux = @index["Foo::Qux::"] #: as !nil - .first #: as Entry::Class - assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names) - - constant_path_references = @index["ConstantPathReferences::"] #: as !nil - .first #: as Entry::Class - assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names) - end - - def test_tracking_singleton_classes - index(<<~RUBY) - class Foo; end - class Foo - # Some extra comments - class << self - end - end - RUBY - - foo = @index["Foo::"] #: as !nil - .first #: as Entry::SingletonClass - assert_equal(4, foo.location.start_line) - assert_equal("Some extra comments", foo.comments) - end - - def test_dynamic_singleton_class_blocks - index(<<~RUBY) - class Foo - # Some extra comments - class << bar - end - end - RUBY - - singleton = @index["Foo::"] #: as !nil - .first #: as Entry::SingletonClass - - # Even though this is not correct, we consider any dynamic singleton class block as a regular singleton class. - # That pattern cannot be properly analyzed statically and assuming that it's always a regular singleton simplifies - # the implementation considerably. - assert_equal(3, singleton.location.start_line) - assert_equal("Some extra comments", singleton.comments) - end - - def test_namespaces_inside_singleton_blocks - index(<<~RUBY) - class Foo - class << self - class Bar - end - end - end - RUBY - - assert_entry("Foo::::Bar", Entry::Class, "/fake/path/foo.rb:2-4:3-7") - end - - def test_name_location_points_to_constant_path_location - index(<<~RUBY) - class Foo - def foo; end - end - - module Bar - def bar; end - end - RUBY - - foo = @index["Foo"] #: as !nil - .first #: as Entry::Class - refute_equal(foo.location, foo.name_location) - - name_location = foo.name_location - assert_equal(1, name_location.start_line) - assert_equal(1, name_location.end_line) - assert_equal(6, name_location.start_column) - assert_equal(9, name_location.end_column) - - bar = @index["Bar"] #: as !nil - .first #: as Entry::Module - refute_equal(bar.location, bar.name_location) - - name_location = bar.name_location - assert_equal(5, name_location.start_line) - assert_equal(5, name_location.end_line) - assert_equal(7, name_location.start_column) - assert_equal(10, name_location.end_column) - end - - def test_indexing_namespaces_inside_top_level_references - index(<<~RUBY) - module ::Foo - class Bar - end - end - RUBY - - # We want to explicitly verify that we didn't introduce the leading `::` by accident, but `Index#[]` deletes the - # prefix when we use `refute_entry` - entries = @index.instance_variable_get(:@entries) - refute(entries.key?("::Foo")) - refute(entries.key?("::Foo::Bar")) - assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:3-3") - assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5") - end - - def test_indexing_singletons_inside_top_level_references - index(<<~RUBY) - module ::Foo - class Bar - class << self - end - end - end - RUBY - - # We want to explicitly verify that we didn't introduce the leading `::` by accident, but `Index#[]` deletes the - # prefix when we use `refute_entry` - entries = @index.instance_variable_get(:@entries) - refute(entries.key?("::Foo")) - refute(entries.key?("::Foo::Bar")) - refute(entries.key?("::Foo::Bar::")) - assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:5-3") - assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:4-5") - assert_entry("Foo::Bar::", Entry::SingletonClass, "/fake/path/foo.rb:2-4:3-7") - end - - def test_indexing_namespaces_inside_nested_top_level_references - index(<<~RUBY) - class Baz - module ::Foo - class Bar - end - - class ::Qux - end - end - end - RUBY - - refute_entry("Baz::Foo") - refute_entry("Baz::Foo::Bar") - assert_entry("Baz", Entry::Class, "/fake/path/foo.rb:0-0:8-3") - assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:1-2:7-5") - assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:2-4:3-7") - assert_entry("Qux", Entry::Class, "/fake/path/foo.rb:5-4:6-7") - end - - def test_lazy_comment_fetching_uses_correct_line_breaks_for_rendering - uri = URI::Generic.from_path( - load_path_entry: "#{Dir.pwd}/lib", - path: "#{Dir.pwd}/lib/ruby_lsp/node_context.rb", - ) - - @index.index_file(uri, collect_comments: false) - - entry = @index["RubyLsp::NodeContext"] #: as !nil - .first #: as !nil - - assert_equal(<<~COMMENTS.chomp, entry.comments) - This class allows listeners to access contextual information about a node in the AST, such as its parent, - its namespace nesting, and the surrounding CallNode (e.g. a method call). - COMMENTS - end - - def test_lazy_comment_fetching_does_not_fail_if_file_gets_deleted - uri = URI::Generic.from_path( - load_path_entry: "#{Dir.pwd}/lib", - path: "#{Dir.pwd}/lib/ruby_lsp/does_not_exist.rb", - ) - - @index.index_single(uri, <<~RUBY, collect_comments: false) - class Foo - end - RUBY - - entry = @index["Foo"]&.first #: as !nil - assert_empty(entry.comments) - end - - def test_singleton_inside_compact_namespace - index(<<~RUBY) - module Foo::Bar - class << self - def baz; end - end - end - RUBY - - # Verify we didn't index the incorrect name - assert_nil(@index["Foo::Bar::"]) - - # Verify we indexed the correct name - assert_entry("Foo::Bar::", Entry::SingletonClass, "/fake/path/foo.rb:1-2:3-5") - - method = @index["baz"]&.first #: as Entry::Method - assert_equal("Foo::Bar::", method.owner&.name) - end - - def test_lazy_comments_with_spaces_are_properly_attributed - path = File.join(Dir.pwd, "lib", "foo.rb") - source = <<~RUBY - require "whatever" - - # These comments belong to the declaration below - # They have to be associated with it - - class Foo - end - RUBY - File.write(path, source) - @index.index_single(URI::Generic.from_path(path: path), source, collect_comments: false) - - entry = @index["Foo"]&.first #: as !nil - - begin - assert_equal(<<~COMMENTS.chomp, entry.comments) - These comments belong to the declaration below - They have to be associated with it - COMMENTS - ensure - FileUtils.rm(path) - end - end - - def test_lazy_comments_with_no_spaces_are_properly_attributed - path = File.join(Dir.pwd, "lib", "foo.rb") - source = <<~RUBY - require "whatever" - - # These comments belong to the declaration below - # They have to be associated with it - class Foo - end - RUBY - File.write(path, source) - @index.index_single(URI::Generic.from_path(path: path), source, collect_comments: false) - - entry = @index["Foo"]&.first #: as !nil - - begin - assert_equal(<<~COMMENTS.chomp, entry.comments) - These comments belong to the declaration below - They have to be associated with it - COMMENTS - ensure - FileUtils.rm(path) - end - end - - def test_lazy_comments_with_two_extra_spaces_are_properly_ignored - path = File.join(Dir.pwd, "lib", "foo.rb") - source = <<~RUBY - require "whatever" - - # These comments don't belong to the declaration below - # They will not be associated with it - - - class Foo - end - RUBY - File.write(path, source) - @index.index_single(URI::Generic.from_path(path: path), source, collect_comments: false) - - entry = @index["Foo"]&.first #: as !nil - - begin - assert_empty(entry.comments) - ensure - FileUtils.rm(path) - end - end - - def test_lazy_comments_ignores_magic_comments - path = File.join(Dir.pwd, "lib", "foo.rb") - source = <<~RUBY - # frozen_string_literal: true - - class Foo - end - RUBY - File.write(path, source) - @index.index_single(URI::Generic.from_path(path: path), source, collect_comments: false) - - entry = @index["Foo"]&.first #: as !nil - - begin - assert_empty(entry.comments) - ensure - FileUtils.rm(path) - end - end - end -end diff --git a/lib/ruby_indexer/test/configuration_test.rb b/lib/ruby_indexer/test/configuration_test.rb deleted file mode 100644 index 862b10afb7..0000000000 --- a/lib/ruby_indexer/test/configuration_test.rb +++ /dev/null @@ -1,282 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "test_helper" - -module RubyIndexer - class ConfigurationTest < Minitest::Test - def setup - @config = Configuration.new - @workspace_path = File.expand_path(File.join("..", "..", ".."), __dir__) - @config.workspace_path = @workspace_path - end - - def test_load_configuration_executes_configure_block - @config.apply_config({ "excluded_patterns" => ["**/fixtures/**/*"] }) - uris = @config.indexable_uris - - bundle_path = Bundler.bundle_path.join("gems") - - assert(uris.none? { |uri| uri.full_path.include?("test/fixtures") }) - assert(uris.none? { |uri| uri.full_path.include?(bundle_path.join("minitest-reporters").to_s) }) - assert(uris.none? { |uri| uri.full_path.include?(bundle_path.join("ansi").to_s) }) - assert(uris.any? { |uri| uri.full_path.include?(bundle_path.join("prism").to_s) }) - assert(uris.none? { |uri| uri.full_path == __FILE__ }) - end - - def test_indexable_uris_have_expanded_full_paths - @config.apply_config({ "included_patterns" => ["**/*.rb"] }) - uris = @config.indexable_uris - - # All paths should be expanded - assert(uris.all? { |uri| File.absolute_path?(uri.full_path) }) - end - - def test_indexable_uris_only_includes_gem_require_paths - uris = @config.indexable_uris - - Bundler.locked_gems.specs.each do |lazy_spec| - next if lazy_spec.name == "ruby-lsp" - - spec = Gem::Specification.find_by_name(lazy_spec.name) - - test_uris = uris.select do |uri| - File.fnmatch?(File.join(spec.full_gem_path, "test/**/*"), uri.full_path, File::Constants::FNM_PATHNAME) - end - assert_empty(test_uris) - rescue Gem::MissingSpecError - # Transitive dependencies might be missing when running tests on Windows - end - end - - def test_indexable_uris_does_not_include_default_gem_path_when_in_bundle - uris = @config.indexable_uris - assert(uris.none? { |uri| uri.full_path.start_with?("#{RbConfig::CONFIG["rubylibdir"]}/psych") }) - end - - def test_indexable_uris_includes_default_gems - paths = @config.indexable_uris.map(&:full_path) - - assert_includes(paths, "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb") - assert_includes(paths, "#{RbConfig::CONFIG["rubylibdir"]}/ipaddr.rb") - end - - def test_indexable_uris_includes_project_files - paths = @config.indexable_uris.map(&:full_path) - - Dir.glob("#{Dir.pwd}/lib/**/*.rb").each do |path| - next if path.end_with?("_test.rb") - - assert_includes(paths, path) - end - end - - def test_indexable_uris_avoids_duplicates_if_bundle_path_is_inside_project - Bundler.settings.temporary(path: "vendor/bundle") do - config = Configuration.new - - assert_includes(config.instance_variable_get(:@excluded_patterns), "vendor/bundle/**/*.rb") - end - end - - def test_indexable_uris_does_not_include_gems_own_installed_files - uris = @config.indexable_uris - uris_inside_bundled_lsp = uris.select do |uri| - uri.full_path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s) - end - - assert_empty( - uris_inside_bundled_lsp, - "Indexable URIs should not include files from the gem currently being worked on. " \ - "Included: #{uris_inside_bundled_lsp.map(&:full_path)}", - ) - end - - def test_indexable_uris_does_not_include_non_ruby_files_inside_rubylibdir - Dir.mktmpdir do |dir| - original_rubylibdir = RbConfig::CONFIG["rubylibdir"] - RbConfig::CONFIG["rubylibdir"] = dir - - begin - path = Pathname.new(dir).join("extra_file.txt").to_s - FileUtils.touch(path) - - uris = @config.indexable_uris - assert(uris.none? { |uri| uri.full_path == path }) - ensure - RbConfig::CONFIG["rubylibdir"] = original_rubylibdir - end - end - end - - def test_paths_are_unique - uris = @config.indexable_uris - assert_equal(uris.uniq.length, uris.length) - end - - def test_configuration_raises_for_unknown_keys - assert_raises(ArgumentError) do - @config.apply_config({ "unknown_config" => 123 }) - end - end - - def test_magic_comments_regex - regex = @config.magic_comment_regex - - [ - "# frozen_string_literal:", - "# typed:", - "# compiled:", - "# encoding:", - "# shareable_constant_value:", - "# warn_indent:", - "# rubocop:", - "# nodoc:", - "# doc:", - "# coding:", - "# warn_past_scope:", - ].each do |comment| - assert_match(regex, comment) - end - end - - def test_indexable_uris_respect_given_workspace_path - Dir.mktmpdir do |dir| - FileUtils.mkdir(File.join(dir, "ignore")) - FileUtils.touch(File.join(dir, "ignore", "file0.rb")) - FileUtils.touch(File.join(dir, "file1.rb")) - FileUtils.touch(File.join(dir, "file2.rb")) - - @config.apply_config({ "excluded_patterns" => ["ignore/**/*.rb"] }) - @config.workspace_path = dir - - uris = @config.indexable_uris - assert(uris.none? { |uri| uri.full_path.start_with?(File.join(dir, "ignore")) }) - - # The regular default gem path is ~/.rubies/3.4.1/lib/ruby/3.4.0 - # The alternative default gem path is ~/.rubies/3.4.1/lib/ruby/gems/3.4.0 - # Here part_1 contains ~/.rubies/3.4.1/lib/ruby/ and part_2 contains 3.4.0, so that we can turn it into the - # alternative path - part_1, part_2 = Pathname.new(RbConfig::CONFIG["rubylibdir"]).split - other_default_gem_dir = part_1.join("gems").join(part_2).to_s - - # After switching the workspace path, all indexable URIs will be found in one of these places: - # - The new workspace path - # - The Ruby LSP's own code (because Bundler is requiring the dependency from source) - # - Bundled gems - # - Default gems - # - Other default gem directory - assert( - uris.all? do |u| - u.full_path.start_with?(dir) || - u.full_path.start_with?(File.join(Dir.pwd, "lib")) || - u.full_path.start_with?(Bundler.bundle_path.to_s) || - u.full_path.start_with?(RbConfig::CONFIG["rubylibdir"]) || - u.full_path.start_with?(other_default_gem_dir) - end, - ) - end - end - - def test_includes_top_level_files - Dir.mktmpdir do |dir| - FileUtils.touch(File.join(dir, "find_me.rb")) - @config.workspace_path = dir - - uris = @config.indexable_uris - assert(uris.find { |u| File.basename(u.full_path) == "find_me.rb" }) - end - end - - def test_transitive_dependencies_for_non_dev_gems_are_not_excluded - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - # Both IRB and debug depend on reline. Since IRB is in the default group, reline should not be excluded - File.write(File.join(dir, "Gemfile"), <<~RUBY) - source "https://rubygems.org" - gem "irb" - gem "ruby-lsp", path: "#{Bundler.root}" - - group :development do - gem "debug" - end - RUBY - - Bundler.with_unbundled_env do - capture_subprocess_io do - system("bundle install") - end - - stdout, _stderr = capture_subprocess_io do - script = [ - "require \"ruby_lsp/internal\"", - "print RubyIndexer::Configuration.new.instance_variable_get(:@excluded_gems).join(\",\")", - ].join(";") - system("bundle exec ruby -e '#{script}'") - end - - excluded_gems = stdout.split(",") - assert_includes(excluded_gems, "debug") - refute_includes(excluded_gems, "reline") - refute_includes(excluded_gems, "irb") - end - end - end - end - - def test_does_not_fail_if_there_are_missing_specs_due_to_platform_constraints - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - File.write(File.join(dir, "Gemfile"), <<~RUBY) - source "https://rubygems.org" - gem "ruby-lsp", path: "#{Bundler.root}" - - platforms :windows do - gem "tzinfo" - gem "tzinfo-data" - end - RUBY - - Bundler.with_unbundled_env do - capture_subprocess_io do - system("bundle install") - - script = [ - "require \"ruby_lsp/internal\"", - "RubyIndexer::Configuration.new.indexable_uris", - ].join(";") - - assert(system("bundle exec ruby -e '#{script}'")) - end - end - end - end - end - - def test_indexables_include_non_test_files_in_test_directories - # In order to linearize test parent classes and accurately detect the framework being used, then intermediate - # parent classes _must_ also be indexed. Otherwise, we have no way of linearizing the rest of the ancestors to - # determine what the test class ultimately inherits from. - # - # Therefore, we need to ensure that test files are excluded, but non test files inside test directories have to be - # indexed - FileUtils.touch("test/test_case.rb") - - uris = @config.indexable_uris - project_paths = uris.filter_map do |uri| - path = uri.full_path - next if path.start_with?(Bundler.bundle_path.to_s) || path.start_with?(RbConfig::CONFIG["rubylibdir"]) - - Pathname.new(path).relative_path_from(Dir.pwd).to_s - end - - begin - assert_includes(project_paths, "test/requests/support/expectations_test_runner.rb") - assert_includes(project_paths, "test/test_helper.rb") - assert_includes(project_paths, "test/test_case.rb") - ensure - FileUtils.rm("test/test_case.rb") - end - end - end -end diff --git a/lib/ruby_indexer/test/constant_test.rb b/lib/ruby_indexer/test/constant_test.rb deleted file mode 100644 index 7e70984b87..0000000000 --- a/lib/ruby_indexer/test/constant_test.rb +++ /dev/null @@ -1,402 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require_relative "test_case" - -module RubyIndexer - class ConstantTest < TestCase - def test_constant_writes - index(<<~RUBY) - FOO = 1 - - class ::Bar - FOO = 2 - end - - BAR = 3 if condition - RUBY - - assert_entry("FOO", Entry::Constant, "/fake/path/foo.rb:0-0:0-7") - assert_entry("Bar::FOO", Entry::Constant, "/fake/path/foo.rb:3-2:3-9") - assert_entry("BAR", Entry::Constant, "/fake/path/foo.rb:6-0:6-7") - end - - def test_constant_with_multibyte_characters - index(<<~RUBY) - CONST_💎 = "Ruby" - RUBY - - assert_entry("CONST_💎", Entry::Constant, "/fake/path/foo.rb:0-0:0-16") - end - - def test_constant_or_writes - index(<<~RUBY) - FOO ||= 1 - - class ::Bar - FOO ||= 2 - end - RUBY - - assert_entry("FOO", Entry::Constant, "/fake/path/foo.rb:0-0:0-9") - assert_entry("Bar::FOO", Entry::Constant, "/fake/path/foo.rb:3-2:3-11") - end - - def test_constant_path_writes - index(<<~RUBY) - class A - FOO = 1 - ::BAR = 1 - - module B - FOO = 1 - end - end - - A::BAZ = 1 - RUBY - - assert_entry("A::FOO", Entry::Constant, "/fake/path/foo.rb:1-2:1-9") - assert_entry("BAR", Entry::Constant, "/fake/path/foo.rb:2-2:2-11") - assert_entry("A::B::FOO", Entry::Constant, "/fake/path/foo.rb:5-4:5-11") - assert_entry("A::BAZ", Entry::Constant, "/fake/path/foo.rb:9-0:9-10") - end - - def test_constant_path_or_writes - index(<<~RUBY) - class A - FOO ||= 1 - ::BAR ||= 1 - end - - A::BAZ ||= 1 - RUBY - - assert_entry("A::FOO", Entry::Constant, "/fake/path/foo.rb:1-2:1-11") - assert_entry("BAR", Entry::Constant, "/fake/path/foo.rb:2-2:2-13") - assert_entry("A::BAZ", Entry::Constant, "/fake/path/foo.rb:5-0:5-12") - end - - def test_comments_for_constants - index(<<~RUBY) - # FOO comment - FOO = 1 - - class A - # A::FOO comment - FOO = 1 - - # ::BAR comment - ::BAR = 1 - end - - # A::BAZ comment - A::BAZ = 1 - RUBY - - foo = @index["FOO"]&.first #: as !nil - assert_equal("FOO comment", foo.comments) - - a_foo = @index["A::FOO"]&.first #: as !nil - assert_equal("A::FOO comment", a_foo.comments) - - bar = @index["BAR"]&.first #: as !nil - assert_equal("::BAR comment", bar.comments) - - a_baz = @index["A::BAZ"]&.first #: as !nil - assert_equal("A::BAZ comment", a_baz.comments) - end - - def test_variable_path_constants_are_ignored - index(<<~RUBY) - var::FOO = 1 - self.class::FOO = 1 - RUBY - - assert_no_indexed_entries - end - - def test_private_constant_indexing - index(<<~RUBY) - class A - B = 1 - private_constant(:B) - - C = 2 - private_constant("C") - - D = 1 - end - RUBY - - b_const = @index["A::B"]&.first #: as !nil - assert_predicate(b_const, :private?) - - c_const = @index["A::C"]&.first #: as !nil - assert_predicate(c_const, :private?) - - d_const = @index["A::D"]&.first #: as !nil - assert_predicate(d_const, :public?) - end - - def test_marking_constants_as_private_reopening_namespaces - index(<<~RUBY) - module A - module B - CONST_A = 1 - private_constant(:CONST_A) - - CONST_B = 2 - CONST_C = 3 - end - - module B - private_constant(:CONST_B) - end - end - - module A - module B - private_constant(:CONST_C) - end - end - RUBY - - a_const = @index["A::B::CONST_A"]&.first #: as !nil - assert_predicate(a_const, :private?) - - b_const = @index["A::B::CONST_B"]&.first #: as !nil - assert_predicate(b_const, :private?) - - c_const = @index["A::B::CONST_C"]&.first #: as !nil - assert_predicate(c_const, :private?) - end - - def test_marking_constants_as_private_with_receiver - index(<<~RUBY) - module A - module B - CONST_A = 1 - CONST_B = 2 - end - - B.private_constant(:CONST_A) - end - - A::B.private_constant(:CONST_B) - RUBY - - a_const = @index["A::B::CONST_A"]&.first #: as !nil - assert_predicate(a_const, :private?) - - b_const = @index["A::B::CONST_B"]&.first #: as !nil - assert_predicate(b_const, :private?) - end - - def test_indexing_constant_aliases - index(<<~RUBY) - module A - module B - module C - end - end - - FIRST = B::C - end - - SECOND = A::FIRST - RUBY - - unresolve_entry = @index["A::FIRST"]&.first #: as Entry::UnresolvedConstantAlias - assert_instance_of(Entry::UnresolvedConstantAlias, unresolve_entry) - assert_equal(["A"], unresolve_entry.nesting) - assert_equal("B::C", unresolve_entry.target) - - resolved_entry = @index.resolve("A::FIRST", [])&.first #: as Entry::ConstantAlias - assert_instance_of(Entry::ConstantAlias, resolved_entry) - assert_equal("A::B::C", resolved_entry.target) - end - - def test_aliasing_namespaces - index(<<~RUBY) - module A - module B - module C - end - end - - ALIAS = B - end - - module Other - ONE_MORE = A::ALIAS - end - RUBY - - unresolve_entry = @index["A::ALIAS"]&.first #: as Entry::UnresolvedConstantAlias - assert_instance_of(Entry::UnresolvedConstantAlias, unresolve_entry) - assert_equal(["A"], unresolve_entry.nesting) - assert_equal("B", unresolve_entry.target) - - resolved_entry = @index.resolve("ALIAS", ["A"])&.first #: as Entry::ConstantAlias - assert_instance_of(Entry::ConstantAlias, resolved_entry) - assert_equal("A::B", resolved_entry.target) - - resolved_entry = @index.resolve("ALIAS::C", ["A"])&.first #: as Entry::Module - assert_instance_of(Entry::Module, resolved_entry) - assert_equal("A::B::C", resolved_entry.name) - - unresolve_entry = @index["Other::ONE_MORE"]&.first #: as Entry::UnresolvedConstantAlias - assert_instance_of(Entry::UnresolvedConstantAlias, unresolve_entry) - assert_equal(["Other"], unresolve_entry.nesting) - assert_equal("A::ALIAS", unresolve_entry.target) - - resolved_entry = @index.resolve("Other::ONE_MORE::C", [])&.first - assert_instance_of(Entry::Module, resolved_entry) - end - - def test_indexing_same_line_constant_aliases - index(<<~RUBY) - module A - B = C = 1 - D = E ||= 1 - F = G::H &&= 1 - I::J = K::L = M = 1 - end - RUBY - - # B and C - unresolve_entry = @index["A::B"]&.first #: as Entry::UnresolvedConstantAlias - assert_instance_of(Entry::UnresolvedConstantAlias, unresolve_entry) - assert_equal(["A"], unresolve_entry.nesting) - assert_equal("C", unresolve_entry.target) - - resolved_entry = @index.resolve("A::B", [])&.first #: as Entry::ConstantAlias - assert_instance_of(Entry::ConstantAlias, resolved_entry) - assert_equal("A::C", resolved_entry.target) - - constant = @index["A::C"]&.first #: as Entry::Constant - assert_instance_of(Entry::Constant, constant) - - # D and E - unresolve_entry = @index["A::D"]&.first #: as Entry::UnresolvedConstantAlias - assert_instance_of(Entry::UnresolvedConstantAlias, unresolve_entry) - assert_equal(["A"], unresolve_entry.nesting) - assert_equal("E", unresolve_entry.target) - - resolved_entry = @index.resolve("A::D", [])&.first #: as Entry::ConstantAlias - assert_instance_of(Entry::ConstantAlias, resolved_entry) - assert_equal("A::E", resolved_entry.target) - - # F and G::H - unresolve_entry = @index["A::F"]&.first #: as Entry::UnresolvedConstantAlias - assert_instance_of(Entry::UnresolvedConstantAlias, unresolve_entry) - assert_equal(["A"], unresolve_entry.nesting) - assert_equal("G::H", unresolve_entry.target) - - resolved_entry = @index.resolve("A::F", [])&.first #: as Entry::ConstantAlias - assert_instance_of(Entry::ConstantAlias, resolved_entry) - assert_equal("A::G::H", resolved_entry.target) - - # I::J, K::L and M - unresolve_entry = @index["A::I::J"]&.first #: as Entry::UnresolvedConstantAlias - assert_instance_of(Entry::UnresolvedConstantAlias, unresolve_entry) - assert_equal(["A"], unresolve_entry.nesting) - assert_equal("K::L", unresolve_entry.target) - - resolved_entry = @index.resolve("A::I::J", [])&.first #: as Entry::ConstantAlias - assert_instance_of(Entry::ConstantAlias, resolved_entry) - assert_equal("A::K::L", resolved_entry.target) - - # When we are resolving A::I::J, we invoke `resolve("K::L", ["A"])`, which recursively resolves A::K::L too. - # Therefore, both A::I::J and A::K::L point to A::M by the end of the previous resolve invocation - resolved_entry = @index["A::K::L"]&.first #: as Entry::ConstantAlias - assert_instance_of(Entry::ConstantAlias, resolved_entry) - assert_equal("A::M", resolved_entry.target) - - constant = @index["A::M"]&.first - assert_instance_of(Entry::Constant, constant) - end - - def test_indexing_or_and_operator_nodes - index(<<~RUBY) - A ||= 1 - B &&= 2 - C &= 3 - D::E ||= 4 - F::G &&= 5 - H::I &= 6 - RUBY - - assert_entry("A", Entry::Constant, "/fake/path/foo.rb:0-0:0-7") - assert_entry("B", Entry::Constant, "/fake/path/foo.rb:1-0:1-7") - assert_entry("C", Entry::Constant, "/fake/path/foo.rb:2-0:2-6") - assert_entry("D::E", Entry::Constant, "/fake/path/foo.rb:3-0:3-10") - assert_entry("F::G", Entry::Constant, "/fake/path/foo.rb:4-0:4-10") - assert_entry("H::I", Entry::Constant, "/fake/path/foo.rb:5-0:5-9") - end - - def test_indexing_constant_targets - index(<<~RUBY) - module A - B, C = [1, Y] - D::E, F::G = [Z, 4] - H, I::J = [5, B] - K, L = C - end - - module Real - Z = 1 - Y = 2 - end - RUBY - - assert_entry("A::B", Entry::Constant, "/fake/path/foo.rb:1-2:1-3") - assert_entry("A::C", Entry::UnresolvedConstantAlias, "/fake/path/foo.rb:1-5:1-6") - assert_entry("A::D::E", Entry::UnresolvedConstantAlias, "/fake/path/foo.rb:2-2:2-6") - assert_entry("A::F::G", Entry::Constant, "/fake/path/foo.rb:2-8:2-12") - assert_entry("A::H", Entry::Constant, "/fake/path/foo.rb:3-2:3-3") - assert_entry("A::I::J", Entry::UnresolvedConstantAlias, "/fake/path/foo.rb:3-5:3-9") - assert_entry("A::K", Entry::Constant, "/fake/path/foo.rb:4-2:4-3") - assert_entry("A::L", Entry::Constant, "/fake/path/foo.rb:4-5:4-6") - end - - def test_indexing_constant_targets_with_splats - index(<<~RUBY) - A, *, B = baz - C, = bar - (D, E) = baz - F, G = *baz, qux - H, I = [baz, *qux] - J, L = [*something, String] - M = [String] - RUBY - - assert_entry("A", Entry::Constant, "/fake/path/foo.rb:0-0:0-1") - assert_entry("B", Entry::Constant, "/fake/path/foo.rb:0-6:0-7") - assert_entry("D", Entry::Constant, "/fake/path/foo.rb:2-1:2-2") - assert_entry("E", Entry::Constant, "/fake/path/foo.rb:2-4:2-5") - assert_entry("F", Entry::Constant, "/fake/path/foo.rb:3-0:3-1") - assert_entry("G", Entry::Constant, "/fake/path/foo.rb:3-3:3-4") - assert_entry("H", Entry::Constant, "/fake/path/foo.rb:4-0:4-1") - assert_entry("I", Entry::Constant, "/fake/path/foo.rb:4-3:4-4") - assert_entry("J", Entry::Constant, "/fake/path/foo.rb:5-0:5-1") - assert_entry("L", Entry::Constant, "/fake/path/foo.rb:5-3:5-4") - assert_entry("M", Entry::Constant, "/fake/path/foo.rb:6-0:6-12") - end - - def test_indexing_destructuring_an_array - index(<<~RUBY) - Baz = [1, 2] - Foo, Bar = Baz - This, That = foo, bar - RUBY - - assert_entry("Baz", Entry::Constant, "/fake/path/foo.rb:0-0:0-12") - assert_entry("Foo", Entry::Constant, "/fake/path/foo.rb:1-0:1-3") - assert_entry("Bar", Entry::Constant, "/fake/path/foo.rb:1-5:1-8") - assert_entry("This", Entry::Constant, "/fake/path/foo.rb:2-0:2-4") - assert_entry("That", Entry::Constant, "/fake/path/foo.rb:2-6:2-10") - end - end -end diff --git a/lib/ruby_indexer/test/enhancements_test.rb b/lib/ruby_indexer/test/enhancements_test.rb deleted file mode 100644 index 078606fa52..0000000000 --- a/lib/ruby_indexer/test/enhancements_test.rb +++ /dev/null @@ -1,325 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require_relative "test_case" - -module RubyIndexer - class EnhancementTest < TestCase - def teardown - super - Enhancement.clear - end - - def test_enhancing_indexing_included_hook - Class.new(Enhancement) do - def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod - owner = @listener.current_owner - return unless owner - return unless call_node.name == :extend - - arguments = call_node.arguments&.arguments - return unless arguments - - arguments.each do |node| - next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode) - - module_name = node.full_name - next unless module_name == "ActiveSupport::Concern" - - @listener.register_included_hook do |index, base| - class_methods_name = "#{owner.name}::ClassMethods" - - if index.indexed?(class_methods_name) - singleton = index.existing_or_new_singleton_class(base.name) - singleton.mixin_operations << Entry::Include.new(class_methods_name) - end - end - - @listener.add_method( - "new_method", - call_node.location, - [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])], - ) - rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError, - Prism::ConstantPathNode::MissingNodesInConstantPathError - # Do nothing - end - end - end - - index(<<~RUBY) - module ActiveSupport - module Concern - def self.extended(base) - base.class_eval("def new_method(a); end") - end - end - end - - module ActiveRecord - module Associations - extend ActiveSupport::Concern - - module ClassMethods - def belongs_to(something); end - end - end - - class Base - include Associations - end - end - - class User < ActiveRecord::Base - end - RUBY - - assert_equal( - [ - "User::", - "ActiveRecord::Base::", - "ActiveRecord::Associations::ClassMethods", - "Object::", - "BasicObject::", - "Class", - "Module", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("User::"), - ) - - assert_entry("new_method", Entry::Method, "/fake/path/foo.rb:10-4:10-33") - end - - def test_enhancing_indexing_configuration_dsl - Class.new(Enhancement) do - def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod - return unless @listener.current_owner - - name = node.name - return unless name == :has_many - - arguments = node.arguments&.arguments - return unless arguments - - association_name = arguments.first - return unless association_name.is_a?(Prism::SymbolNode) - - @listener.add_method( - association_name.value, #: as !nil - association_name.location, - [], - ) - end - end - - index(<<~RUBY) - module ActiveSupport - module Concern - def self.extended(base) - base.class_eval("def new_method(a); end") - end - end - end - - module ActiveRecord - module Associations - extend ActiveSupport::Concern - - module ClassMethods - def belongs_to(something); end - end - end - - class Base - include Associations - end - end - - class User < ActiveRecord::Base - has_many :posts - end - RUBY - - assert_entry("posts", Entry::Method, "/fake/path/foo.rb:23-11:23-17") - end - - def test_error_handling_in_on_call_node_enter_enhancement - Class.new(Enhancement) do - def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod - raise "Error" - end - - class << self - def name - "TestEnhancement" - end - end - end - - _stdout, stderr = capture_io do - index(<<~RUBY) - module ActiveSupport - module Concern - def self.extended(base) - base.class_eval("def new_method(a); end") - end - end - end - RUBY - end - - assert_match( - %r{Indexing error in file:///fake/path/foo\.rb with 'TestEnhancement' on call node enter enhancement}, - stderr, - ) - # The module should still be indexed - assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5") - end - - def test_error_handling_in_on_call_node_leave_enhancement - Class.new(Enhancement) do - def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod - raise "Error" - end - - class << self - def name - "TestEnhancement" - end - end - end - - _stdout, stderr = capture_io do - index(<<~RUBY) - module ActiveSupport - module Concern - def self.extended(base) - base.class_eval("def new_method(a); end") - end - end - end - RUBY - end - - assert_match( - %r{Indexing error in file:///fake/path/foo\.rb with 'TestEnhancement' on call node leave enhancement}, - stderr, - ) - # The module should still be indexed - assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5") - end - - def test_advancing_namespace_stack_from_enhancement - Class.new(Enhancement) do - def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod - owner = @listener.current_owner - return unless owner - - case call_node.name - when :class_methods - @listener.add_module("ClassMethods", call_node.location, call_node.location) - when :extend - arguments = call_node.arguments&.arguments - return unless arguments - - arguments.each do |node| - next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode) - - module_name = node.full_name - next unless module_name == "ActiveSupport::Concern" - - @listener.register_included_hook do |index, base| - class_methods_name = "#{owner.name}::ClassMethods" - - if index.indexed?(class_methods_name) - singleton = index.existing_or_new_singleton_class(base.name) - singleton.mixin_operations << Entry::Include.new(class_methods_name) - end - end - end - end - end - - def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod - return unless call_node.name == :class_methods - - @listener.pop_namespace_stack - end - end - - index(<<~RUBY) - module ActiveSupport - module Concern - end - end - - module MyConcern - extend ActiveSupport::Concern - - class_methods do - def foo; end - end - end - - class User - include MyConcern - end - RUBY - - assert_equal( - [ - "User::", - "MyConcern::ClassMethods", - "Object::", - "BasicObject::", - "Class", - "Module", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("User::"), - ) - - refute_nil(@index.resolve_method("foo", "User::")) - end - - def test_creating_anonymous_classes_from_enhancement - Class.new(Enhancement) do - def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod - case call_node.name - when :context - arguments = call_node.arguments&.arguments - first_argument = arguments&.first - return unless first_argument.is_a?(Prism::StringNode) - - @listener.add_class( - "", - call_node.location, - first_argument.location, - ) - when :subject - @listener.add_method("subject", call_node.location, []) - end - end - - def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod - return unless call_node.name == :context - - @listener.pop_namespace_stack - end - end - - index(<<~RUBY) - context "does something" do - subject { call_whatever } - end - RUBY - - refute_nil(@index.resolve_method("subject", "")) - end - end -end diff --git a/lib/ruby_indexer/test/global_variable_test.rb b/lib/ruby_indexer/test/global_variable_test.rb deleted file mode 100644 index a4fa5e9a8a..0000000000 --- a/lib/ruby_indexer/test/global_variable_test.rb +++ /dev/null @@ -1,49 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require_relative "test_case" - -module RubyIndexer - class GlobalVariableTest < TestCase - def test_global_variable_and_write - index(<<~RUBY) - $foo &&= 1 - RUBY - - assert_entry("$foo", Entry::GlobalVariable, "/fake/path/foo.rb:0-0:0-4") - end - - def test_global_variable_operator_write - index(<<~RUBY) - $foo += 1 - RUBY - - assert_entry("$foo", Entry::GlobalVariable, "/fake/path/foo.rb:0-0:0-4") - end - - def test_global_variable_or_write - index(<<~RUBY) - $foo ||= 1 - RUBY - - assert_entry("$foo", Entry::GlobalVariable, "/fake/path/foo.rb:0-0:0-4") - end - - def test_global_variable_target_node - index(<<~RUBY) - $foo, $bar = 1 - RUBY - - assert_entry("$foo", Entry::GlobalVariable, "/fake/path/foo.rb:0-0:0-4") - assert_entry("$bar", Entry::GlobalVariable, "/fake/path/foo.rb:0-6:0-10") - end - - def test_global_variable_write - index(<<~RUBY) - $foo = 1 - RUBY - - assert_entry("$foo", Entry::GlobalVariable, "/fake/path/foo.rb:0-0:0-4") - end - end -end diff --git a/lib/ruby_indexer/test/index_test.rb b/lib/ruby_indexer/test/index_test.rb deleted file mode 100644 index a38be36291..0000000000 --- a/lib/ruby_indexer/test/index_test.rb +++ /dev/null @@ -1,2273 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require_relative "test_case" - -module RubyIndexer - class IndexTest < TestCase - def test_deleting_one_entry_for_a_class - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - class Foo - end - RUBY - @index.index_single(URI::Generic.from_path(path: "/fake/path/other_foo.rb"), <<~RUBY) - class Foo - end - RUBY - - entries = @index["Foo"] #: as !nil - assert_equal(2, entries.length) - - @index.delete(URI::Generic.from_path(path: "/fake/path/other_foo.rb")) - entries = @index["Foo"] #: as !nil - assert_equal(1, entries.length) - end - - def test_deleting_all_entries_for_a_class - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - class Foo - end - RUBY - - entries = @index["Foo"] #: as !nil - assert_equal(1, entries.length) - - @index.delete(URI::Generic.from_path(path: "/fake/path/foo.rb")) - entries = @index["Foo"] - assert_nil(entries) - end - - def test_index_resolve - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - class Bar; end - - module Foo - class Bar - end - - class Baz - class Something - end - end - end - RUBY - - entries = @index.resolve("Something", ["Foo", "Baz"]) #: as !nil - refute_empty(entries) - assert_equal("Foo::Baz::Something", entries.first&.name) - - entries = @index.resolve("Bar", ["Foo"]) #: as !nil - refute_empty(entries) - assert_equal("Foo::Bar", entries.first&.name) - - entries = @index.resolve("Bar", ["Foo", "Baz"]) #: as !nil - refute_empty(entries) - assert_equal("Foo::Bar", entries.first&.name) - - entries = @index.resolve("Foo::Bar", ["Foo", "Baz"]) #: as !nil - refute_empty(entries) - assert_equal("Foo::Bar", entries.first&.name) - - assert_nil(@index.resolve("DoesNotExist", ["Foo"])) - end - - def test_accessing_with_colon_colon_prefix - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - class Bar; end - - module Foo - class Bar - end - - class Baz - class Something - end - end - end - RUBY - - entries = @index["::Foo::Baz::Something"] #: as !nil - refute_empty(entries) - assert_equal("Foo::Baz::Something", entries.first&.name) - end - - def test_fuzzy_search - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - class Zws; end - - module Qtl - class Zws - end - - class Zwo - class Something - end - end - end - RUBY - - result = @index.fuzzy_search("Zws") - assert_equal(["Zws", "Qtl::Zwo::Something"], result.map(&:name)) - - result = @index.fuzzy_search("qtlzwssomeking") - assert_equal(["Qtl::Zwo::Something", "Qtl::Zws", "Qtl::Zwo", "Qtl", "Zws", "blocking"], result.map(&:name)) - - result = @index.fuzzy_search("QltZwo") - assert_equal(["Qtl::Zwo", "Qtl::Zws", "Qtl::Zwo::Something", "Qtl"], result.map(&:name)) - end - - def test_index_single_ignores_directories - path = "#{Dir.pwd}/lib/this_is_a_dir.rb" - FileUtils.mkdir(path) - - begin - @index.index_file(URI::Generic.from_path(path: path)) - ensure - FileUtils.rm_r(path) - end - end - - def test_searching_for_require_paths - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb", load_path_entry: "/fake"), <<~RUBY) - class Foo - end - RUBY - @index.index_single(URI::Generic.from_path(path: "/fake/path/other_foo.rb", load_path_entry: "/fake"), <<~RUBY) - class Foo - end - RUBY - - assert_equal(["path/other_foo", "path/foo"], @index.search_require_paths("path").map(&:require_path)) - end - - def test_searching_for_entries_based_on_prefix - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb", load_path_entry: "/fake"), <<~RUBY) - class Foo::Bizw - end - RUBY - @index.index_single(URI::Generic.from_path(path: "/fake/path/other_foo.rb", load_path_entry: "/fake"), <<~RUBY) - class Foo::Bizw - end - - class Foo::Bizt - end - RUBY - - results = @index.prefix_search("Foo", []).map { |entries| entries.map(&:name) } - assert_equal([["Foo::Bizt"], ["Foo::Bizw", "Foo::Bizw"]], results) - - results = @index.prefix_search("Biz", ["Foo"]).map { |entries| entries.map(&:name) } - assert_equal([["Foo::Bizt"], ["Foo::Bizw", "Foo::Bizw"]], results) - end - - def test_resolve_normalizes_top_level_names - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb", load_path_entry: "/fake"), <<~RUBY) - class Bar; end - - module Foo - class Bar; end - end - RUBY - - entries = @index.resolve("::Foo::Bar", []) #: as !nil - refute_nil(entries) - - assert_equal("Foo::Bar", entries.first&.name) - - entries = @index.resolve("::Bar", ["Foo"]) #: as !nil - refute_nil(entries) - - assert_equal("Bar", entries.first&.name) - end - - def test_resolving_aliases_to_non_existing_constants_with_conflicting_names - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb", load_path_entry: "/fake"), <<~RUBY) - class Bar - end - - module Foo - class Bar < self - BAZ = ::Bar::BAZ - end - end - RUBY - - entry = @index.resolve("BAZ", ["Foo", "Bar"])&.first - refute_nil(entry) - - assert_instance_of(Entry::UnresolvedConstantAlias, entry) - end - - def test_visitor_does_not_visit_unnecessary_nodes - concats = (0...10_000).map do |i| - <<~STRING - "string#{i}" \\ - STRING - end.join - - index(<<~RUBY) - module Foo - local_var = #{concats} - "final" - @class_instance_var = #{concats} - "final" - @@class_var = #{concats} - "final" - $global_var = #{concats} - "final" - CONST = #{concats} - "final" - end - RUBY - end - - def test_resolve_method_with_known_receiver - index(<<~RUBY) - module Foo - module Bar - def baz; end - end - end - RUBY - - entries = @index.resolve_method("baz", "Foo::Bar") #: as !nil - assert_equal("baz", entries.first&.name) - owner = entries.first&.owner #: as !nil - assert_equal("Foo::Bar", owner.name) - end - - def test_resolve_method_with_class_name_conflict - index(<<~RUBY) - class Array - end - - class Foo - def Array(*args); end - end - RUBY - - entries = @index.resolve_method("Array", "Foo") #: as !nil - assert_equal("Array", entries.first&.name) - owner = entries.first&.owner #: as !nil - assert_equal("Foo", owner.name) - end - - def test_resolve_method_attribute - index(<<~RUBY) - class Foo - attr_reader :bar - end - RUBY - - entries = @index.resolve_method("bar", "Foo") #: as !nil - assert_equal("bar", entries.first&.name) - owner = entries.first&.owner #: as !nil - assert_equal("Foo", owner.name) - end - - def test_resolve_method_with_two_definitions - index(<<~RUBY) - class Foo - # Hello from first `bar` - def bar; end - end - - class Foo - # Hello from second `bar` - def bar; end - end - RUBY - - first_entry, second_entry = @index.resolve_method("bar", "Foo") #: as !nil - - assert_equal("bar", first_entry&.name) - owner = first_entry&.owner #: as !nil - assert_equal("Foo", owner.name) - assert_includes(first_entry&.comments, "Hello from first `bar`") - - assert_equal("bar", second_entry&.name) - owner = second_entry&.owner #: as !nil - assert_equal("Foo", owner.name) - assert_includes(second_entry&.comments, "Hello from second `bar`") - end - - def test_resolve_method_inherited_only - index(<<~RUBY) - class Bar - def baz; end - end - - class Foo < Bar - def baz; end - end - RUBY - - entry = @index.resolve_method("baz", "Foo", inherited_only: true)&.first #: as !nil - assert_equal("Bar", entry.owner&.name) - end - - def test_resolve_method_inherited_only_for_prepended_module - index(<<~RUBY) - module Bar - def baz - super - end - end - - class Foo - prepend Bar - - def baz; end - end - RUBY - - # This test is just to document the fact that we don't yet support resolving inherited methods for modules that - # are prepended. The only way to support this is to find all namespaces that have the module a subtype, so that we - # can show the results for everywhere the module has been prepended. - assert_nil(@index.resolve_method("baz", "Bar", inherited_only: true)) - end - - def test_prefix_search_for_methods - index(<<~RUBY) - module Foo - module Bar - def qzx; end - end - end - RUBY - - entries = @index.prefix_search("qz") - refute_empty(entries) - - entry = entries.first&.first #: as !nil - assert_equal("qzx", entry.name) - end - - def test_indexing_prism_fixtures_succeeds - unless Dir.exist?("test/fixtures/prism/test/prism/fixtures") - raise "Prism fixtures not found. Run `git submodule update --init` to fetch them." - end - - fixtures = Dir.glob("#{Dir.pwd}/test/fixtures/prism/test/prism/fixtures/**/*.txt") - - fixtures.each do |fixture| - uri = URI::Generic.from_path(path: fixture) - @index.index_file(uri) - end - - refute_empty(@index) - end - - def test_index_single_does_not_fail_for_non_existing_file - @index.index_file(URI::Generic.from_path(path: "/fake/path/foo.rb")) - entries_after_indexing = @index.names - assert_equal(@default_indexed_entries.keys, entries_after_indexing) - end - - def test_linearized_ancestors_basic_ordering - index(<<~RUBY) - module A; end - module B; end - - class Foo - prepend A - prepend B - end - - class Bar - include A - include B - end - RUBY - - assert_equal( - [ - "B", - "A", - "Foo", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Foo"), - ) - - assert_equal( - [ - "Bar", - "B", - "A", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Bar"), - ) - end - - def test_linearized_ancestors - index(<<~RUBY) - module A; end - module B; end - module C; end - - module D - include A - end - - module E - prepend B - end - - module F - include C - include A - end - - class Bar - prepend F - end - - class Foo < Bar - include E - prepend D - end - RUBY - - # Object, Kernel and BasicObject are intentionally commented out for now until we develop a strategy for indexing - # declarations made in C code - assert_equal( - [ - "D", - "A", - "Foo", - "B", - "E", - "F", - "A", - "C", - "Bar", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Foo"), - ) - end - - def test_linearized_ancestors_duplicates - index(<<~RUBY) - module A; end - module B - include A - end - - class Foo - include B - include A - end - - class Bar - prepend B - prepend A - end - RUBY - - assert_equal( - [ - "Foo", - "B", - "A", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Foo"), - ) - - assert_equal( - [ - "B", - "A", - "Bar", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Bar"), - ) - end - - def test_linearizing_ancestors_is_cached - index(<<~RUBY) - module C; end - module A; end - module B - include A - end - - class Foo - include B - include A - end - RUBY - - @index.linearized_ancestors_of("Foo") - ancestors = @index.instance_variable_get(:@ancestors) - assert(ancestors.key?("Foo")) - assert(ancestors.key?("A")) - assert(ancestors.key?("B")) - refute(ancestors.key?("C")) - end - - def test_duplicate_prepend_include - index(<<~RUBY) - module A; end - - class Foo - prepend A - include A - end - - class Bar - include A - prepend A - end - RUBY - - assert_equal( - [ - "A", - "Foo", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Foo"), - ) - - assert_equal( - [ - "A", - "Bar", - "A", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Bar"), - ) - end - - def test_linearizing_ancestors_handles_circular_parent_class - index(<<~RUBY) - class Foo < Foo - end - RUBY - - assert_equal(["Foo"], @index.linearized_ancestors_of("Foo")) - end - - def test_ancestors_linearization_complex_prepend_duplication - index(<<~RUBY) - module A; end - module B - prepend A - end - module C - prepend B - end - - class Foo - prepend A - prepend C - end - RUBY - - assert_equal( - [ - "A", - "B", - "C", - "Foo", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Foo"), - ) - end - - def test_ancestors_linearization_complex_include_duplication - index(<<~RUBY) - module A; end - module B - include A - end - module C - include B - end - - class Foo - include A - include C - end - RUBY - - assert_equal( - [ - "Foo", - "C", - "B", - "A", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Foo"), - ) - end - - def test_linearizing_ancestors_that_need_to_be_resolved - index(<<~RUBY) - module Foo - module Baz - end - module Qux - end - - class Something; end - - class Bar < Something - include Baz - prepend Qux - end - end - RUBY - - assert_equal( - [ - "Foo::Qux", - "Foo::Bar", - "Foo::Baz", - "Foo::Something", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Foo::Bar"), - ) - end - - def test_linearizing_ancestors_for_non_existing_namespaces - index(<<~RUBY) - def Bar(a); end - RUBY - - assert_raises(Index::NonExistingNamespaceError) do - @index.linearized_ancestors_of("Foo") - end - - assert_raises(Index::NonExistingNamespaceError) do - @index.linearized_ancestors_of("Bar") - end - end - - def test_linearizing_circular_ancestors - index(<<~RUBY) - module M1 - include M2 - end - - module M2 - include M1 - end - - module A1 - include A2 - end - - module A2 - include A3 - end - - module A3 - include A1 - end - - class Foo < Foo - include Foo - end - - module Bar - include Bar - end - RUBY - - assert_equal(["M2", "M1"], @index.linearized_ancestors_of("M2")) - assert_equal(["A3", "A1", "A2"], @index.linearized_ancestors_of("A3")) - assert_equal(["Foo"], @index.linearized_ancestors_of("Foo")) - assert_equal(["Bar"], @index.linearized_ancestors_of("Bar")) - end - - def test_linearizing_circular_aliased_dependency - index(<<~RUBY) - module A - end - - ALIAS = A - - module A - include ALIAS - end - RUBY - - assert_equal(["A", "ALIAS"], @index.linearized_ancestors_of("A")) - end - - def test_linearizing_ancestors_for_classes_with_overridden_parents - index(<<~RUBY) - # Find the re-open of a class first, without specifying a parent - class Child - end - - # Now, find the actual definition of the class, which includes a parent - class Parent; end - class Child < Parent - end - RUBY - - assert_equal( - [ - "Child", - "Parent", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Child"), - ) - end - - def test_resolving_an_inherited_method - index(<<~RUBY) - module Foo - def baz; end - end - - class Bar - def qux; end - end - - class Wow < Bar - include Foo - end - RUBY - - entry = @index.resolve_method("baz", "Wow")&.first #: as !nil - assert_equal("baz", entry.name) - assert_equal("Foo", entry.owner&.name) - - entry = @index.resolve_method("qux", "Wow")&.first #: as !nil - assert_equal("qux", entry.name) - assert_equal("Bar", entry.owner&.name) - end - - def test_resolving_an_inherited_method_lands_on_first_match - index(<<~RUBY) - module Foo - def qux; end - end - - class Bar - def qux; end - end - - class Wow < Bar - prepend Foo - - def qux; end - end - RUBY - - entries = @index.resolve_method("qux", "Wow") #: as !nil - assert_equal(1, entries.length) - - entry = entries.first #: as !nil - assert_equal("qux", entry.name) - assert_equal("Foo", entry.owner&.name) - end - - def test_handle_change_clears_ancestor_cache_if_tree_changed - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - # Write the original file - File.write(File.join(dir, "foo.rb"), <<~RUBY) - module Foo - end - - class Bar - include Foo - end - RUBY - - uri = URI::Generic.from_path(path: File.join(dir, "foo.rb")) - @index.index_file(uri) - - assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) - - # Remove include to invalidate the ancestor tree - File.write(File.join(dir, "foo.rb"), <<~RUBY) - module Foo - end - - class Bar - end - RUBY - - path = uri.full_path #: as !nil - @index.handle_change(uri, File.read(path)) - assert_empty(@index.instance_variable_get(:@ancestors)) - assert_equal(["Bar", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) - end - end - end - - def test_handle_change_does_not_clear_ancestor_cache_if_tree_not_changed - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - # Write the original file - File.write(File.join(dir, "foo.rb"), <<~RUBY) - module Foo - end - - class Bar - include Foo - end - RUBY - - uri = URI::Generic.from_path(path: File.join(dir, "foo.rb")) - @index.index_file(uri) - - assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) - - # Remove include to invalidate the ancestor tree - File.write(File.join(dir, "foo.rb"), <<~RUBY) - module Foo - end - - class Bar - include Foo - - def baz; end - end - RUBY - - path = uri.full_path #: as !nil - @index.handle_change(uri, File.read(path)) - refute_empty(@index.instance_variable_get(:@ancestors)) - assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) - end - end - end - - def test_handle_change_clears_ancestor_cache_if_parent_class_changed - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - # Write the original file - File.write(File.join(dir, "foo.rb"), <<~RUBY) - class Foo - end - - class Bar < Foo - end - RUBY - - uri = URI::Generic.from_path(path: File.join(dir, "foo.rb")) - @index.index_file(uri) - - assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) - - # Remove include to invalidate the ancestor tree - File.write(File.join(dir, "foo.rb"), <<~RUBY) - class Foo - end - - class Bar - end - RUBY - - path = uri.full_path #: as !nil - @index.handle_change(uri, File.read(path)) - assert_empty(@index.instance_variable_get(:@ancestors)) - assert_equal(["Bar", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) - end - end - end - - def test_resolving_inherited_constants - index(<<~RUBY) - module Foo - CONST = 1 - end - - module Baz - CONST = 2 - end - - module Qux - include Foo - end - - module Namespace - CONST = 3 - - include Baz - - class Bar - include Qux - end - end - - CONST = 4 - RUBY - - entry = @index.resolve("CONST", ["Namespace", "Bar"])&.first #: as !nil - assert_equal(14, entry.location.start_line) - end - - def test_resolving_inherited_aliased_namespace - index(<<~RUBY) - module Bar - TARGET = 123 - end - - module Foo - CONST = Bar - end - - module Namespace - class Bar - include Foo - end - end - RUBY - - entry = @index.resolve("Foo::CONST::TARGET", [])&.first #: as !nil - assert_equal(2, entry.location.start_line) - - entry = @index.resolve("Namespace::Bar::CONST::TARGET", [])&.first #: as !nil - assert_equal(2, entry.location.start_line) - end - - def test_resolving_same_constant_from_different_scopes - index(<<~RUBY) - module Namespace - CONST = 123 - - class Parent - CONST = 321 - end - - class Child < Parent - end - end - RUBY - - entry = @index.resolve("CONST", ["Namespace", "Child"])&.first #: as !nil - assert_equal(2, entry.location.start_line) - - entry = @index.resolve("Namespace::Child::CONST", [])&.first #: as !nil - assert_equal(5, entry.location.start_line) - end - - def test_resolving_prepended_constants - index(<<~RUBY) - module Included - CONST = 123 - end - - module Prepended - CONST = 321 - end - - class Foo - include Included - prepend Prepended - end - - class Bar - CONST = 456 - include Included - prepend Prepended - end - RUBY - - entry = @index.resolve("CONST", ["Foo"])&.first #: as !nil - assert_equal(6, entry.location.start_line) - - entry = @index.resolve("Foo::CONST", [])&.first #: as !nil - assert_equal(6, entry.location.start_line) - - entry = @index.resolve("Bar::CONST", [])&.first #: as !nil - assert_equal(15, entry.location.start_line) - end - - def test_resolving_constants_favors_ancestors_over_top_level - index(<<~RUBY) - module Value1 - CONST = 1 - end - - module Value2 - CONST = 2 - end - - CONST = 3 - module First - include Value1 - - module Second - include Value2 - end - end - RUBY - - entry = @index.resolve("CONST", ["First", "Second"])&.first #: as !nil - assert_equal(6, entry.location.start_line) - end - - def test_resolving_circular_alias - index(<<~RUBY) - module Namespace - FOO = BAR - BAR = FOO - end - RUBY - - foo_entry = @index.resolve("FOO", ["Namespace"])&.first #: as !nil - assert_equal(2, foo_entry.location.start_line) - assert_instance_of(Entry::ConstantAlias, foo_entry) - - bar_entry = @index.resolve("BAR", ["Namespace"])&.first #: as !nil - assert_equal(3, bar_entry.location.start_line) - assert_instance_of(Entry::ConstantAlias, bar_entry) - end - - def test_resolving_circular_alias_three_levels - index(<<~RUBY) - module Namespace - FOO = BAR - BAR = BAZ - BAZ = FOO - end - RUBY - - foo_entry = @index.resolve("FOO", ["Namespace"])&.first #: as !nil - assert_equal(2, foo_entry.location.start_line) - assert_instance_of(Entry::ConstantAlias, foo_entry) - - bar_entry = @index.resolve("BAR", ["Namespace"])&.first #: as !nil - assert_equal(3, bar_entry.location.start_line) - assert_instance_of(Entry::ConstantAlias, bar_entry) - - baz_entry = @index.resolve("BAZ", ["Namespace"])&.first #: as !nil - assert_equal(4, baz_entry.location.start_line) - assert_instance_of(Entry::ConstantAlias, baz_entry) - end - - def test_resolving_constants_in_aliased_namespace - index(<<~RUBY) - module Original - module Something - CONST = 123 - end - end - - module Other - ALIAS = Original::Something - end - - module Third - Other::ALIAS::CONST - end - RUBY - - entry = @index.resolve("Other::ALIAS::CONST", ["Third"])&.first #: as !nil - assert_kind_of(Entry::Constant, entry) - assert_equal("Original::Something::CONST", entry.name) - end - - def test_resolving_top_level_aliases - index(<<~RUBY) - class Foo - CONST = 123 - end - - FOO = Foo - FOO::CONST - RUBY - - entry = @index.resolve("FOO::CONST", [])&.first #: as !nil - assert_kind_of(Entry::Constant, entry) - assert_equal("Foo::CONST", entry.name) - end - - def test_resolving_top_level_compact_reference - index(<<~RUBY) - class Foo::Bar - end - RUBY - - foo_entry = @index.resolve("Foo::Bar", [])&.first #: as !nil - assert_equal(1, foo_entry.location.start_line) - assert_instance_of(Entry::Class, foo_entry) - end - - def test_resolving_references_with_redundant_namespaces - index(<<~RUBY) - module Bar - CONST = 1 - end - - module A - CONST = 2 - - module B - CONST = 3 - - class Foo - include Bar - end - - A::B::Foo::CONST - end - end - RUBY - - foo_entry = @index.resolve("A::B::Foo::CONST", ["A", "B"])&.first #: as !nil - assert_equal(2, foo_entry.location.start_line) - end - - def test_resolving_self_referential_constant_alias - index(<<~RUBY) - module A - module B - class C - end - end - end - - module A - module D - B = B::C - end - end - RUBY - - entry = @index.resolve("A::D::B", [])&.first #: as Entry::ConstantAlias - - assert_kind_of(RubyIndexer::Entry::ConstantAlias, entry) - assert_equal(10, entry.location.start_line) - assert_equal("A::B::C", entry.target) - end - - def test_resolving_non_existing_self_referential_constant_alias - index(<<~RUBY) - module Foo - SomeClass = ::SomeClass - UNRESOLVED = SomeClass::CONSTANT - end - RUBY - - entry = @index.resolve("Foo::UNRESOLVED", [])&.first #: as Entry::UnresolvedConstantAlias - assert_kind_of(Entry::UnresolvedConstantAlias, entry) - assert_equal(3, entry.location.start_line) - assert_equal("SomeClass::CONSTANT", entry.target) - - entry = @index.resolve("SomeClass::CONSTANT", ["Foo"]) - refute(entry) - end - - def test_resolving_qualified_references - index(<<~RUBY) - module Namespace - class Entry - CONST = 1 - end - end - - module Namespace - class Index - end - end - RUBY - - foo_entry = @index.resolve("Entry::CONST", ["Namespace", "Index"])&.first #: as !nil - assert_equal(3, foo_entry.location.start_line) - end - - def test_resolving_unqualified_references - index(<<~RUBY) - module Foo - CONST = 1 - end - - module Namespace - CONST = 2 - - class Index - include Foo - end - end - RUBY - - foo_entry = @index.resolve("CONST", ["Namespace", "Index"])&.first #: as !nil - assert_equal(6, foo_entry.location.start_line) - end - - def test_resolving_references_with_only_top_level_declaration - index(<<~RUBY) - CONST = 1 - - module Foo; end - - module Namespace - class Index - include Foo - end - end - RUBY - - foo_entry = @index.resolve("CONST", ["Namespace", "Index"])&.first #: as !nil - assert_equal(1, foo_entry.location.start_line) - end - - def test_instance_variables_completions_from_different_owners_with_conflicting_names - index(<<~RUBY) - class Foo - def initialize - @bar = 1 - end - end - - class Bar - def initialize - @bar = 2 - end - end - RUBY - - entry = @index.instance_variable_completion_candidates("@", "Bar").first #: as !nil - assert_equal("@bar", entry.name) - assert_equal("Bar", entry.owner&.name) - end - - def test_resolving_a_qualified_reference - index(<<~RUBY) - class Base - module Third - CONST = 1 - end - end - - class Foo - module Third - CONST = 2 - end - - class Second < Base - end - end - RUBY - - foo_entry = @index.resolve("Third::CONST", ["Foo"])&.first #: as !nil - assert_equal(9, foo_entry.location.start_line) - end - - def test_resolving_unindexed_constant_with_no_nesting - assert_nil(@index.resolve("RSpec", [])) - end - - def test_object_superclass_indexing_and_resolution_with_reopened_object_class - index(<<~RUBY) - class Object; end - RUBY - - entries = @index["Object"] #: as !nil - assert_equal(2, entries.length) - reopened_entry = entries.last #: as Entry::Class - assert_equal("::BasicObject", reopened_entry.parent_class) - assert_equal(["Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Object")) - end - - def test_object_superclass_indexing_and_resolution_with_reopened_basic_object_class - index(<<~RUBY) - class BasicObject; end - RUBY - - entries = @index["BasicObject"] #: as !nil - assert_equal(2, entries.length) - reopened_entry = entries.last #: as Entry::Class - assert_nil(reopened_entry.parent_class) - assert_equal(["BasicObject"], @index.linearized_ancestors_of("BasicObject")) - end - - def test_object_superclass_resolution - index(<<~RUBY) - module Foo - class Object; end - - class Bar; end - class Baz < Object; end - end - RUBY - - assert_equal(["Foo::Bar", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Foo::Bar")) - assert_equal( - ["Foo::Baz", "Foo::Object", "Object", "Kernel", "BasicObject"], - @index.linearized_ancestors_of("Foo::Baz"), - ) - end - - def test_basic_object_superclass_resolution - index(<<~RUBY) - module Foo - class BasicObject; end - - class Bar; end - class Baz < BasicObject; end - end - RUBY - - assert_equal(["Foo::Bar", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Foo::Bar")) - assert_equal( - ["Foo::Baz", "Foo::BasicObject", "Object", "Kernel", "BasicObject"], - @index.linearized_ancestors_of("Foo::Baz"), - ) - end - - def test_top_level_object_superclass_resolution - index(<<~RUBY) - module Foo - class Object; end - - class Bar < ::Object; end - end - RUBY - - assert_equal(["Foo::Bar", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Foo::Bar")) - end - - def test_top_level_basic_object_superclass_resolution - index(<<~RUBY) - module Foo - class BasicObject; end - - class Bar < ::BasicObject; end - end - RUBY - - assert_equal(["Foo::Bar", "BasicObject"], @index.linearized_ancestors_of("Foo::Bar")) - end - - def test_resolving_method_inside_singleton_context - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - module Foo - class Bar - class << self - class Baz - class << self - def found_me!; end - end - end - end - end - end - RUBY - - entry = @index.resolve_method("found_me!", "Foo::Bar::::Baz::")&.first #: as !nil - refute_nil(entry) - assert_equal("found_me!", entry.name) - end - - def test_resolving_constants_in_singleton_contexts - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - module Foo - class Bar - CONST = 3 - - class << self - CONST = 2 - - class Baz - CONST = 1 - - class << self - end - end - end - end - end - RUBY - - entry = @index.resolve("CONST", ["Foo", "Bar", "", "Baz", ""])&.first #: as !nil - refute_nil(entry) - assert_equal(9, entry.location.start_line) - end - - def test_resolving_instance_variables_in_singleton_contexts - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - module Foo - class Bar - @a = 123 - - class << self - def hello - @b = 123 - end - - @c = 123 - end - end - end - RUBY - - entry = @index.resolve_instance_variable("@a", "Foo::Bar::")&.first #: as !nil - refute_nil(entry) - assert_equal("@a", entry.name) - - entry = @index.resolve_instance_variable("@b", "Foo::Bar::")&.first #: as !nil - refute_nil(entry) - assert_equal("@b", entry.name) - - entry = @index.resolve_instance_variable("@c", "Foo::Bar::::<>")&.first #: as !nil - refute_nil(entry) - assert_equal("@c", entry.name) - end - - def test_instance_variable_completion_in_singleton_contexts - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - module Foo - class Bar - @a = 123 - - class << self - def hello - @b = 123 - end - - @c = 123 - end - end - end - RUBY - - entries = @index.instance_variable_completion_candidates("@", "Foo::Bar::").map(&:name) - assert_includes(entries, "@a") - assert_includes(entries, "@b") - end - - def test_singletons_are_excluded_from_prefix_search - index(<<~RUBY) - class Zwq - class << self - end - end - RUBY - - assert_empty(@index.prefix_search("Zwq::, c: )", entry.decorated_parameters) - end - - def test_decorated_parameters_when_method_has_no_parameters - index(<<~RUBY) - class Foo - def bar - end - end - RUBY - - methods = @index.resolve_method("bar", "Foo") #: as !nil - refute_nil(methods) - - entry = methods.first #: as Entry::Method - assert_equal("()", entry.decorated_parameters) - end - - def test_linearizing_singleton_ancestors_of_singleton_when_class_has_parent - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - class Foo; end - - class Bar < Foo - end - - class Baz < Bar - class << self - class << self - end - end - end - RUBY - - assert_equal( - [ - "Baz::::<>", - "Bar::::<>", - "Foo::::<>", - "Object::::<>", - "BasicObject::::<>", - "Class::", - "Module::", - "Object::", - "BasicObject::", - "Class", - "Module", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Baz::::<>"), - ) - end - - def test_linearizing_singleton_object - assert_equal( - [ - "Object::", - "BasicObject::", - "Class", - "Module", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Object::"), - ) - end - - def test_extend_self - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - module Foo - def bar - end - - extend self - - def baz - end - end - RUBY - - ["bar", "baz"].product(["Foo", "Foo::"]).each do |method, receiver| - entry = @index.resolve_method(method, receiver)&.first #: as !nil - refute_nil(entry) - assert_equal(method, entry.name) - end - - assert_equal( - [ - "Foo::", - "Foo", - "Module", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Foo::"), - ) - end - - def test_linearizing_singleton_ancestors - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - module First - end - - module Second - include First - end - - module Foo - class Bar - class << self - class Baz - extend Second - - class << self - include First - end - end - end - end - end - RUBY - - assert_equal( - [ - "Foo::Bar::::Baz::", - "Second", - "First", - "Object::", - "BasicObject::", - "Class", - "Module", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Foo::Bar::::Baz::"), - ) - end - - def test_linearizing_singleton_ancestors_when_class_has_parent - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - class Foo; end - - class Bar < Foo - end - - class Baz < Bar - class << self - end - end - RUBY - - assert_equal( - [ - "Baz::", - "Bar::", - "Foo::", - "Object::", - "BasicObject::", - "Class", - "Module", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Baz::"), - ) - end - - def test_linearizing_a_module_singleton_class - @index.index_single(URI::Generic.from_path(path: "/fake/path/foo.rb"), <<~RUBY) - module A; end - RUBY - - assert_equal( - [ - "A::", - "Module", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("A::"), - ) - end - - def test_linearizing_a_singleton_class_with_no_attached - assert_raises(Index::NonExistingNamespaceError) do - @index.linearized_ancestors_of("A::") - end - end - - def test_linearizing_singleton_parent_class_with_namespace - index(<<~RUBY) - class ActiveRecord::Base; end - - class User < ActiveRecord::Base - end - RUBY - - assert_equal( - [ - "User::", - "ActiveRecord::Base::", - "Object::", - "BasicObject::", - "Class", - "Module", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("User::"), - ) - end - - def test_singleton_nesting_is_correctly_split_during_linearization - index(<<~RUBY) - module Bar; end - - module Foo - class Namespace::Parent - extend Bar - end - end - - module Foo - class Child < Namespace::Parent - end - end - RUBY - - assert_equal( - [ - "Foo::Child::", - "Foo::Namespace::Parent::", - "Bar", - "Object::", - "BasicObject::", - "Class", - "Module", - "Object", - "Kernel", - "BasicObject", - ], - @index.linearized_ancestors_of("Foo::Child::"), - ) - end - - def test_resolving_circular_method_aliases_on_class_reopen - index(<<~RUBY) - class Foo - alias bar == - def ==(other) = true - end - - class Foo - alias == bar - end - RUBY - - method = @index.resolve_method("==", "Foo")&.first #: as Entry::Method - assert_kind_of(Entry::Method, method) - assert_equal("==", method.name) - - candidates = @index.method_completion_candidates("=", "Foo") - assert_equal(["==", "==="], candidates.map(&:name)) - end - - def test_entries_for - index(<<~RUBY) - class Foo; end - - module Bar - def my_def; end - def self.my_singleton_def; end - end - RUBY - - entries = @index.entries_for("file:///fake/path/foo.rb", Entry) #: as !nil - assert_equal(["Foo", "Bar", "my_def", "Bar::", "my_singleton_def"], entries.map(&:name)) - - entries = @index.entries_for("file:///fake/path/foo.rb", RubyIndexer::Entry::Namespace) #: as !nil - assert_equal(["Foo", "Bar", "Bar::"], entries.map(&:name)) - - entries = @index.entries_for("file:///fake/path/foo.rb") #: as !nil - assert_equal(["Foo", "Bar", "my_def", "Bar::", "my_singleton_def"], entries.map(&:name)) - end - - def test_entries_for_returns_nil_if_no_matches - assert_nil(@index.entries_for("non_existing_file.rb", Entry::Namespace)) - end - - def test_constant_completion_candidates_all_possible_constants - index(<<~RUBY) - XQRK = 3 - - module Bar - XQRK = 2 - end - - module Foo - XQRK = 1 - end - - module Namespace - XQRK = 0 - - class Baz - include Foo - include Bar - end - end - RUBY - - result = @index.constant_completion_candidates("X", ["Namespace", "Baz"]) - - result.each do |entries| - name = entries.first&.name - assert(entries.all? { |e| e.name == name }) - end - - assert_equal(["Namespace::XQRK", "Bar::XQRK", "XQRK"], result.map { |entries| entries.first&.name }) - - result = @index.constant_completion_candidates("::X", ["Namespace", "Baz"]) - assert_equal(["XQRK"], result.map { |entries| entries.first&.name }) - end - - def test_constant_completion_does_not_confuse_uppercase_methods - index(<<~RUBY) - class Foo - def Qux - end - end - RUBY - - candidates = @index.constant_completion_candidates("Q", []) - refute_includes(candidates.flat_map { |entries| entries.map(&:name) }, "Qux") - - candidates = @index.constant_completion_candidates("Qux", []) - assert_equal(0, candidates.length) - end - - def test_constant_completion_candidates_for_empty_name - index(<<~RUBY) - module Foo - Bar = 1 - end - - class Baz - include Foo - end - RUBY - - result = @index.constant_completion_candidates("Baz::", []) - assert_includes(result.map { |entries| entries.first&.name }, "Foo::Bar") - end - - def test_follow_alias_namespace - index(<<~RUBY) - module First - module Second - class Foo - end - end - end - - module Namespace - Second = First::Second - end - RUBY - - real_namespace = @index.follow_aliased_namespace("Namespace::Second") - assert_equal("First::Second", real_namespace) - end - - def test_resolving_alias_to_non_existing_namespace - index(<<~RUBY) - module Namespace - class Foo - module InnerNamespace - Constants = Namespace::Foo::Constants - end - end - end - RUBY - - entry = @index.resolve("Constants", ["Namespace", "Foo", "InnerNamespace"])&.first - assert_instance_of(Entry::UnresolvedConstantAlias, entry) - - entry = @index.resolve("Namespace::Foo::Constants", ["Namespace", "Foo", "InnerNamespace"])&.first - assert_nil(entry) - end - - def test_resolving_alias_to_existing_constant_from_inner_namespace - index(<<~RUBY) - module Parent - CONST = 123 - end - - module First - module Namespace - class Foo - include Parent - - module InnerNamespace - Constants = Namespace::Foo::CONST - end - end - end - end - RUBY - - entry = @index.resolve("Namespace::Foo::CONST", ["First", "Namespace", "Foo", "InnerNamespace"])&.first #: as !nil - assert_equal("Parent::CONST", entry.name) - assert_instance_of(Entry::Constant, entry) - end - - def test_build_non_redundant_name - assert_equal( - "Namespace::Foo::Constants", - @index.send( - :build_non_redundant_full_name, - "Namespace::Foo::Constants", - ["Namespace", "Foo", "InnerNamespace"], - ), - ) - - assert_equal( - "Namespace::Foo::Constants", - @index.send( - :build_non_redundant_full_name, - "Namespace::Foo::Constants", - ["Namespace", "Foo"], - ), - ) - - assert_equal( - "Namespace::Foo::Constants", - @index.send( - :build_non_redundant_full_name, - "Foo::Constants", - ["Namespace", "Foo"], - ), - ) - - assert_equal( - "Bar::Namespace::Foo::Constants", - @index.send( - :build_non_redundant_full_name, - "Namespace::Foo::Constants", - ["Bar"], - ), - ) - - assert_equal( - "First::Namespace::Foo::Constants", - @index.send( - :build_non_redundant_full_name, - "Namespace::Foo::Constants", - ["First", "Namespace", "Foo", "InnerNamespace"], - ), - ) - end - - def test_prevents_multiple_calls_to_index_all - @index.index_all - - assert_raises(Index::IndexNotEmptyError) do - @index.index_all - end - end - - def test_index_can_handle_entries_from_untitled_scheme - uri = URI("untitled:Untitled-1") - - index(<<~RUBY, uri: uri) - class Foo - end - RUBY - - entry = @index["Foo"]&.first #: as !nil - refute_nil(entry, "Expected indexer to be able to handle unsaved URIs") - assert_equal("untitled:Untitled-1", entry.uri.to_s) - assert_equal("Untitled-1", entry.file_name) - assert_nil(entry.file_path) - - @index.handle_change(uri, <<~RUBY) - # I added this comment! - class Foo - end - RUBY - - entry = @index["Foo"]&.first #: as !nil - refute_nil(entry, "Expected indexer to be able to handle unsaved URIs") - assert_equal("I added this comment!", entry.comments) - end - - def test_instance_variable_completion_returns_class_variables_too - index(<<~RUBY) - class Parent - @@abc = 123 - end - - class Child < Parent - @@adf = 123 - - def self.do - end - end - RUBY - - adf, abc = @index.instance_variable_completion_candidates("@", "Child::") - - refute_nil(abc) - refute_nil(adf) - - assert_equal("@@abc", abc&.name) - assert_equal("@@adf", adf&.name) - end - - def test_class_variable_completion_from_singleton_context - index(<<~RUBY) - class Foo - @@hello = 123 - - def self.do - end - end - RUBY - - candidates = @index.class_variable_completion_candidates("@@", "Foo::") - refute_empty(candidates) - - assert_equal("@@hello", candidates.first&.name) - end - - def test_resolve_class_variable_in_singleton_context - index(<<~RUBY) - class Foo - @@hello = 123 - end - RUBY - - candidates = @index.resolve_class_variable("@@hello", "Foo::") #: as !nil - refute_empty(candidates) - - assert_equal("@@hello", candidates.first&.name) - end - - def test_actual_nesting - assert_equal(["Foo"], Index.actual_nesting([], "Foo")) - assert_equal(["TopLevel", "Foo"], Index.actual_nesting(["First", "::TopLevel"], "Foo")) - assert_equal(["TopLevel", "Another", "Foo"], Index.actual_nesting(["::TopLevel", "Another"], "Foo")) - assert_equal(["TopLevel"], Index.actual_nesting(["First", "::TopLevel"], nil)) - end - - def test_constant_name - node = Prism.parse("class var::Foo; end").value.statements.body.first.constant_path - assert_nil(Index.constant_name(node)) - - node = Prism.parse("class ; end").value.statements.body.first.constant_path - assert_nil(Index.constant_name(node)) - - node = Prism.parse("class method_call; end").value.statements.body.first.constant_path - assert_nil(Index.constant_name(node)) - - node = Prism.parse("class Foo; end").value.statements.body.first.constant_path - assert_equal("Foo", Index.constant_name(node)) - - node = Prism.parse(<<~RUBY).value.statements.body.first.constant_path - class class Foo - end - end - RUBY - assert_nil(Index.constant_name(node)) - end - end -end diff --git a/lib/ruby_indexer/test/instance_variables_test.rb b/lib/ruby_indexer/test/instance_variables_test.rb deleted file mode 100644 index 53dc73c6ba..0000000000 --- a/lib/ruby_indexer/test/instance_variables_test.rb +++ /dev/null @@ -1,264 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require_relative "test_case" - -module RubyIndexer - class InstanceVariableTest < TestCase - def test_instance_variable_write - index(<<~RUBY) - module Foo - class Bar - def initialize - # Hello - @a = 1 - end - end - end - RUBY - - assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8") - - entry = @index["@a"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::Class, owner) - assert_equal("Foo::Bar", owner&.name) - end - - def test_instance_variable_with_multibyte_characters - index(<<~RUBY) - class Foo - def initialize - @あ = 1 - end - end - RUBY - - assert_entry("@あ", Entry::InstanceVariable, "/fake/path/foo.rb:2-4:2-6") - end - - def test_instance_variable_and_write - index(<<~RUBY) - module Foo - class Bar - def initialize - # Hello - @a &&= value - end - end - end - RUBY - - assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8") - - entry = @index["@a"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::Class, owner) - assert_equal("Foo::Bar", owner&.name) - end - - def test_instance_variable_operator_write - index(<<~RUBY) - module Foo - class Bar - def initialize - # Hello - @a += value - end - end - end - RUBY - - assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8") - - entry = @index["@a"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::Class, owner) - assert_equal("Foo::Bar", owner&.name) - end - - def test_instance_variable_or_write - index(<<~RUBY) - module Foo - class Bar - def initialize - # Hello - @a ||= value - end - end - end - RUBY - - assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8") - - entry = @index["@a"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::Class, owner) - assert_equal("Foo::Bar", owner&.name) - end - - def test_instance_variable_target - index(<<~RUBY) - module Foo - class Bar - def initialize - # Hello - @a, @b = [1, 2] - end - end - end - RUBY - - assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8") - assert_entry("@b", Entry::InstanceVariable, "/fake/path/foo.rb:4-10:4-12") - - entry = @index["@a"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::Class, owner) - assert_equal("Foo::Bar", owner&.name) - - entry = @index["@b"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::Class, owner) - assert_equal("Foo::Bar", owner&.name) - end - - def test_empty_name_instance_variables - index(<<~RUBY) - module Foo - class Bar - def initialize - @ = 123 - end - end - end - RUBY - - refute_entry("@") - end - - def test_class_instance_variables - index(<<~RUBY) - module Foo - class Bar - @a = 123 - - class << self - def hello - @b = 123 - end - - @c = 123 - end - end - end - RUBY - - assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:2-4:2-6") - - entry = @index["@a"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::SingletonClass, owner) - assert_equal("Foo::Bar::", owner&.name) - - assert_entry("@b", Entry::InstanceVariable, "/fake/path/foo.rb:6-8:6-10") - - entry = @index["@b"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::SingletonClass, owner) - assert_equal("Foo::Bar::", owner&.name) - - assert_entry("@c", Entry::InstanceVariable, "/fake/path/foo.rb:9-6:9-8") - - entry = @index["@c"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::SingletonClass, owner) - assert_equal("Foo::Bar::::<>", owner&.name) - end - - def test_top_level_instance_variables - index(<<~RUBY) - @a = 123 - RUBY - - entry = @index["@a"]&.first #: as Entry::InstanceVariable - assert_nil(entry.owner) - end - - def test_class_instance_variables_inside_self_method - index(<<~RUBY) - class Foo - def self.bar - @a = 123 - end - end - RUBY - - entry = @index["@a"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::SingletonClass, owner) - assert_equal("Foo::", owner&.name) - end - - def test_instance_variable_inside_dynamic_method_declaration - index(<<~RUBY) - class Foo - def something.bar - @a = 123 - end - end - RUBY - - # If the surrounding method is being defined on any dynamic value that isn't `self`, then we attribute the - # instance variable to the wrong owner since there's no way to understand that statically - entry = @index["@a"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::Class, owner) - assert_equal("Foo", owner&.name) - end - - def test_module_function_does_not_impact_instance_variables - # One possible way of implementing `module_function` would be to push a fake singleton class to the stack, so that - # methods are inserted into it. However, that would be incorrect because it would then bind instance variables to - # the wrong type. This test is here to prevent that from happening. - index(<<~RUBY) - module Foo - module_function - - def something; end - - @a = 123 - end - RUBY - - entry = @index["@a"]&.first #: as Entry::InstanceVariable - owner = entry.owner - assert_instance_of(Entry::SingletonClass, owner) - assert_equal("Foo::", owner&.name) - end - - def test_class_instance_variable_comments - index(<<~RUBY) - class Foo - # Documentation for @a - @a = "Hello" #: String - @b = "World" # trailing comment - @c = "!" - end - end - RUBY - - assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:2-4:2-6") - entry = @index["@a"]&.first #: as Entry::InstanceVariable - assert_equal("Documentation for @a", entry.comments) - - assert_entry("@b", Entry::InstanceVariable, "/fake/path/foo.rb:3-4:3-6") - entry = @index["@b"]&.first #: as Entry::InstanceVariable - assert_empty(entry.comments) - - assert_entry("@c", Entry::InstanceVariable, "/fake/path/foo.rb:4-4:4-6") - entry = @index["@c"]&.first #: as Entry::InstanceVariable - assert_empty(entry.comments) - end - end -end diff --git a/lib/ruby_indexer/test/method_test.rb b/lib/ruby_indexer/test/method_test.rb deleted file mode 100644 index c81f8a4bd6..0000000000 --- a/lib/ruby_indexer/test/method_test.rb +++ /dev/null @@ -1,990 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require_relative "test_case" - -module RubyIndexer - class MethodTest < TestCase - def test_method_with_no_parameters - index(<<~RUBY) - class Foo - def bar - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - end - - def test_conditional_method - index(<<~RUBY) - class Foo - def bar - end if condition - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - end - - def test_method_with_multibyte_characters - index(<<~RUBY) - class Foo - def こんにちは; end - end - RUBY - - assert_entry("こんにちは", Entry::Method, "/fake/path/foo.rb:1-2:1-16") - end - - def test_singleton_method_using_self_receiver - index(<<~RUBY) - class Foo - def self.bar - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - - entry = @index["bar"]&.first #: as Entry::Method - owner = entry.owner - assert_equal("Foo::", owner&.name) - assert_instance_of(Entry::SingletonClass, owner) - end - - def test_singleton_method_using_other_receiver_is_not_indexed - index(<<~RUBY) - class Foo - def String.bar - end - end - RUBY - - assert_no_entry("bar") - end - - def test_method_under_dynamic_class_or_module - index(<<~RUBY) - module Foo - class self::Bar - def bar - end - end - end - - module Bar - def bar - end - end - RUBY - - assert_equal(2, @index["bar"]&.length) - first_entry = @index["bar"]&.first #: as Entry::Method - assert_equal("Foo::self::Bar", first_entry.owner&.name) - second_entry = @index["bar"]&.last #: as Entry::Method - assert_equal("Bar", second_entry.owner&.name) - end - - def test_visibility_tracking - index(<<~RUBY) - class Foo - private def foo - end - - def bar; end - - protected - - def baz; end - end - RUBY - - assert_entry("foo", Entry::Method, "/fake/path/foo.rb:1-10:2-5", visibility: :private) - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:4-2:4-14", visibility: :public) - assert_entry("baz", Entry::Method, "/fake/path/foo.rb:8-2:8-14", visibility: :protected) - end - - def test_visibility_tracking_with_nested_class_or_modules - index(<<~RUBY) - class Foo - private - - def foo; end - - class Bar - def bar; end - end - - def baz; end - end - RUBY - - assert_entry("foo", Entry::Method, "/fake/path/foo.rb:3-2:3-14", visibility: :private) - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:6-4:6-16", visibility: :public) - assert_entry("baz", Entry::Method, "/fake/path/foo.rb:9-2:9-14", visibility: :private) - end - - def test_visibility_tracking_with_module_function - index(<<~RUBY) - module Test - def foo; end - def bar; end - module_function :foo, "bar" - end - RUBY - - ["foo", "bar"].each do |keyword| - entries = @index[keyword] #: as Array[Entry::Method] - # should receive two entries because module_function creates a singleton method - # for the Test module and a private method for classes include the Test module - assert_equal(entries.size, 2) - first_entry, second_entry = *entries - # The first entry points to the location of the module_function call - assert_equal("Test", first_entry&.owner&.name) - assert_instance_of(Entry::Module, first_entry&.owner) - assert_predicate(first_entry, :private?) - # The second entry points to the public singleton method - assert_equal("Test::", second_entry&.owner&.name) - assert_instance_of(Entry::SingletonClass, second_entry&.owner) - assert_equal(:public, second_entry&.visibility) - end - end - - def test_private_class_method_visibility_tracking_string_symbol_arguments - index(<<~RUBY) - class Test - def self.foo - end - - def self.bar - end - - private_class_method("foo", :bar) - - def self.baz - end - end - RUBY - - ["foo", "bar"].each do |keyword| - entries = @index[keyword] #: as Array[Entry::Method] - assert_equal(1, entries.size) - entry = entries.first - assert_predicate(entry, :private?) - end - - entries = @index["baz"] #: as Array[Entry::Method] - assert_equal(1, entries.size) - entry = entries.first - assert_predicate(entry, :public?) - end - - def test_private_class_method_visibility_tracking_array_argument - index(<<~RUBY) - class Test - def self.foo - end - - def self.bar - end - - private_class_method(["foo", :bar]) - - def self.baz - end - end - RUBY - - ["foo", "bar"].each do |keyword| - entries = @index[keyword] #: as Array[Entry::Method] - assert_equal(1, entries.size) - entry = entries.first - assert_predicate(entry, :private?) - end - - entries = @index["baz"] #: as Array[Entry::Method] - assert_equal(1, entries.size) - entry = entries.first - assert_predicate(entry, :public?) - end - - def test_private_class_method_visibility_tracking_method_argument - index(<<~RUBY) - class Test - private_class_method def self.foo - end - - def self.bar - end - end - RUBY - - entries = @index["foo"] #: as Array[Entry::Method] - assert_equal(1, entries.size) - entry = entries.first - assert_predicate(entry, :private?) - - entries = @index["bar"] #: as Array[Entry::Method] - assert_equal(1, entries.size) - entry = entries.first - assert_predicate(entry, :public?) - end - - def test_comments_documentation - index(<<~RUBY) - # Documentation for Foo - - class Foo - # #################### - # Documentation for bar - # #################### - # - def bar - end - - # test - - # Documentation for baz - def baz; end - def ban; end - end - RUBY - - foo = @index["Foo"]&.first #: as !nil - assert_equal("Documentation for Foo", foo.comments) - - bar = @index["bar"]&.first #: as !nil - assert_equal("####################\nDocumentation for bar\n####################\n", bar.comments) - - baz = @index["baz"]&.first #: as !nil - assert_equal("Documentation for baz", baz.comments) - - ban = @index["ban"]&.first #: as !nil - assert_empty(ban.comments) - end - - def test_method_with_parameters - index(<<~RUBY) - class Foo - def bar(a) - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - entry = @index["bar"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(1, parameters.length) - parameter = parameters.first - assert_equal(:a, parameter&.name) - assert_instance_of(Entry::RequiredParameter, parameter) - end - - def test_method_with_destructed_parameters - index(<<~RUBY) - class Foo - def bar((a, (b, ))) - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - entry = @index["bar"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(1, parameters.length) - parameter = parameters.first - assert_equal(:"(a, (b, ))", parameter&.name) - assert_instance_of(Entry::RequiredParameter, parameter) - end - - def test_method_with_optional_parameters - index(<<~RUBY) - class Foo - def bar(a = 123) - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - entry = @index["bar"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(1, parameters.length) - parameter = parameters.first - assert_equal(:a, parameter&.name) - assert_instance_of(Entry::OptionalParameter, parameter) - end - - def test_method_with_keyword_parameters - index(<<~RUBY) - class Foo - def bar(a:, b: 123) - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - entry = @index["bar"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(2, parameters.length) - a, b = parameters - - assert_equal(:a, a&.name) - assert_instance_of(Entry::KeywordParameter, a) - - assert_equal(:b, b&.name) - assert_instance_of(Entry::OptionalKeywordParameter, b) - end - - def test_method_with_rest_and_keyword_rest_parameters - index(<<~RUBY) - class Foo - def bar(*a, **b) - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - entry = @index["bar"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(2, parameters.length) - a, b = parameters - - assert_equal(:a, a&.name) - assert_instance_of(Entry::RestParameter, a) - - assert_equal(:b, b&.name) - assert_instance_of(Entry::KeywordRestParameter, b) - end - - def test_method_with_post_parameters - index(<<~RUBY) - class Foo - def bar(*a, b) - end - - def baz(**a, b) - end - - def qux(*a, (b, c)) - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - entry = @index["bar"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(2, parameters.length) - a, b = parameters - - assert_equal(:a, a&.name) - assert_instance_of(Entry::RestParameter, a) - - assert_equal(:b, b&.name) - assert_instance_of(Entry::RequiredParameter, b) - - entry = @index["baz"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(2, parameters.length) - a, b = parameters - - assert_equal(:a, a&.name) - assert_instance_of(Entry::KeywordRestParameter, a) - - assert_equal(:b, b&.name) - assert_instance_of(Entry::RequiredParameter, b) - - entry = @index["qux"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(2, parameters.length) - _a, second = parameters - - assert_equal(:"(b, c)", second&.name) - assert_instance_of(Entry::RequiredParameter, second) - end - - def test_method_with_destructured_rest_parameters - index(<<~RUBY) - class Foo - def bar((a, *b)) - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - entry = @index["bar"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(1, parameters.length) - param = parameters.first #: as Entry::Parameter - - assert_equal(:"(a, *b)", param.name) - assert_instance_of(Entry::RequiredParameter, param) - end - - def test_method_with_block_parameters - index(<<~RUBY) - class Foo - def bar(&block) - end - - def baz(&) - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - param = parameters.first #: as Entry::Parameter - assert_equal(:block, param.name) - assert_instance_of(Entry::BlockParameter, param) - - entry = @index["baz"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(1, parameters.length) - - param = parameters.first #: as Entry::Parameter - assert_equal(Entry::BlockParameter::DEFAULT_NAME, param.name) - assert_instance_of(Entry::BlockParameter, param) - end - - def test_method_with_anonymous_rest_parameters - index(<<~RUBY) - class Foo - def bar(*, **) - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - entry = @index["bar"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(2, parameters.length) - first, second = parameters - - assert_equal(Entry::RestParameter::DEFAULT_NAME, first&.name) - assert_instance_of(Entry::RestParameter, first) - - assert_equal(Entry::KeywordRestParameter::DEFAULT_NAME, second&.name) - assert_instance_of(Entry::KeywordRestParameter, second) - end - - def test_method_with_forbidden_keyword_splat_parameter - index(<<~RUBY) - class Foo - def bar(**nil) - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5") - entry = @index["bar"]&.first #: as Entry::Method - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_empty(parameters) - end - - def test_methods_with_argument_forwarding - index(<<~RUBY) - class Foo - def bar(...) - end - - def baz(a, ...) - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - assert_instance_of(Entry::Method, entry, "Expected `bar` to be indexed") - - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(1, parameters.length) - assert_instance_of(Entry::ForwardingParameter, parameters.first) - - entry = @index["baz"]&.first #: as Entry::Method - assert_instance_of(Entry::Method, entry, "Expected `baz` to be indexed") - - parameters = entry.signatures.first&.parameters #: as Array[Entry::Parameter] - assert_equal(2, parameters.length) - assert_instance_of(Entry::RequiredParameter, parameters[0]) - assert_instance_of(Entry::ForwardingParameter, parameters[1]) - end - - def test_keeps_track_of_method_owner - index(<<~RUBY) - class Foo - def bar - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - owner_name = entry.owner&.name - - assert_equal("Foo", owner_name) - end - - def test_keeps_track_of_attributes - index(<<~RUBY) - class Foo - # Hello there - attr_reader :bar, :other - attr_writer :baz - attr_accessor :qux - end - RUBY - - assert_entry("bar", Entry::Accessor, "/fake/path/foo.rb:2-15:2-18") - assert_equal("Hello there", @index["bar"]&.first&.comments) - assert_entry("other", Entry::Accessor, "/fake/path/foo.rb:2-21:2-26") - assert_equal("Hello there", @index["other"]&.first&.comments) - assert_entry("baz=", Entry::Accessor, "/fake/path/foo.rb:3-15:3-18") - assert_entry("qux", Entry::Accessor, "/fake/path/foo.rb:4-17:4-20") - assert_entry("qux=", Entry::Accessor, "/fake/path/foo.rb:4-17:4-20") - end - - def test_ignores_attributes_invoked_on_constant - index(<<~RUBY) - class Foo - end - - Foo.attr_reader :bar - RUBY - - assert_no_entry("bar") - end - - def test_properly_tracks_multiple_levels_of_nesting - index(<<~RUBY) - module Foo - def first_method; end - - module Bar - def second_method; end - end - - def third_method; end - end - RUBY - - entry = @index["first_method"]&.first #: as Entry::Method - assert_equal("Foo", entry.owner&.name) - - entry = @index["second_method"]&.first #: as Entry::Method - assert_equal("Foo::Bar", entry.owner&.name) - - entry = @index["third_method"]&.first #: as Entry::Method - assert_equal("Foo", entry.owner&.name) - end - - def test_keeps_track_of_aliases - index(<<~RUBY) - class Foo - alias whatever to_s - alias_method :foo, :to_a - alias_method "bar", "to_a" - - # These two are not indexed because they are dynamic or incomplete - alias_method baz, :to_a - alias_method :baz - end - RUBY - - assert_entry("whatever", Entry::UnresolvedMethodAlias, "/fake/path/foo.rb:1-8:1-16") - assert_entry("foo", Entry::UnresolvedMethodAlias, "/fake/path/foo.rb:2-15:2-19") - assert_entry("bar", Entry::UnresolvedMethodAlias, "/fake/path/foo.rb:3-15:3-20") - # Foo plus 3 valid aliases - assert_equal(4, @index.length - @default_indexed_entries.length) - end - - def test_singleton_methods - index(<<~RUBY) - class Foo - def self.bar; end - - class << self - def baz; end - end - end - RUBY - - assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:1-19") - assert_entry("baz", Entry::Method, "/fake/path/foo.rb:4-4:4-16") - - bar = @index["bar"]&.first #: as Entry::Method - baz = @index["baz"]&.first #: as Entry::Method - - assert_instance_of(Entry::SingletonClass, bar.owner) - assert_instance_of(Entry::SingletonClass, baz.owner) - - # Regardless of whether the method was added through `self.something` or `class << self`, the owner object must be - # the exact same - assert_same(bar.owner, baz.owner) - end - - def test_name_location_points_to_method_identifier_location - index(<<~RUBY) - class Foo - def bar - a = 123 - a + 456 - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - refute_equal(entry.location, entry.name_location) - - name_location = entry.name_location - assert_equal(2, name_location.start_line) - assert_equal(2, name_location.end_line) - assert_equal(6, name_location.start_column) - assert_equal(9, name_location.end_column) - end - - def test_signature_matches_for_a_method_with_positional_params - index(<<~RUBY) - class Foo - def bar(a, b = 123) - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - - # Matching calls - assert_signature_matches(entry, "bar()") - assert_signature_matches(entry, "bar(1)") - assert_signature_matches(entry, "bar(1, 2)") - assert_signature_matches(entry, "bar(...)") - assert_signature_matches(entry, "bar(1, ...)") - assert_signature_matches(entry, "bar(*a)") - assert_signature_matches(entry, "bar(1, *a)") - assert_signature_matches(entry, "bar(1, *a, 2)") - assert_signature_matches(entry, "bar(*a, 2)") - assert_signature_matches(entry, "bar(1, **a)") - assert_signature_matches(entry, "bar(1) {}") - # This call is impossible to analyze statically because it depends on whether there are elements inside `a` or - # not. If there's nothing, the call will fail. But if there's anything inside, the hash will become the first - # positional argument - assert_signature_matches(entry, "bar(**a)") - - # Non matching calls - - refute_signature_matches(entry, "bar(1, 2, 3)") - refute_signature_matches(entry, "bar(1, b: 2)") - refute_signature_matches(entry, "bar(1, 2, c: 3)") - end - - def test_signature_matches_for_a_method_with_argument_forwarding - index(<<~RUBY) - class Foo - def bar(...) - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - - # All calls match a forwarding parameter - assert_signature_matches(entry, "bar(1)") - assert_signature_matches(entry, "bar(1, 2)") - assert_signature_matches(entry, "bar(...)") - assert_signature_matches(entry, "bar(1, ...)") - assert_signature_matches(entry, "bar(*a)") - assert_signature_matches(entry, "bar(1, *a)") - assert_signature_matches(entry, "bar(1, *a, 2)") - assert_signature_matches(entry, "bar(*a, 2)") - assert_signature_matches(entry, "bar(1, **a)") - assert_signature_matches(entry, "bar(1) {}") - assert_signature_matches(entry, "bar()") - assert_signature_matches(entry, "bar(1, 2, 3)") - assert_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}") - end - - def test_signature_matches_for_post_forwarding_parameter - index(<<~RUBY) - class Foo - def bar(a, ...) - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - - # All calls with at least one positional argument match - assert_signature_matches(entry, "bar(1)") - assert_signature_matches(entry, "bar(1, 2)") - assert_signature_matches(entry, "bar(...)") - assert_signature_matches(entry, "bar(1, ...)") - assert_signature_matches(entry, "bar(*a)") - assert_signature_matches(entry, "bar(1, *a)") - assert_signature_matches(entry, "bar(1, *a, 2)") - assert_signature_matches(entry, "bar(*a, 2)") - assert_signature_matches(entry, "bar(1, **a)") - assert_signature_matches(entry, "bar(1) {}") - assert_signature_matches(entry, "bar(1, 2, 3)") - assert_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}") - assert_signature_matches(entry, "bar()") - end - - def test_signature_matches_for_destructured_parameters - index(<<~RUBY) - class Foo - def bar(a, (b, c)) - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - - # All calls with at least one positional argument match - assert_signature_matches(entry, "bar()") - assert_signature_matches(entry, "bar(1)") - assert_signature_matches(entry, "bar(1, 2)") - assert_signature_matches(entry, "bar(...)") - assert_signature_matches(entry, "bar(1, ...)") - assert_signature_matches(entry, "bar(*a)") - assert_signature_matches(entry, "bar(1, *a)") - assert_signature_matches(entry, "bar(*a, 2)") - # This matches because `bar(1, *[], 2)` would result in `bar(1, 2)`, which is a valid call - assert_signature_matches(entry, "bar(1, *a, 2)") - assert_signature_matches(entry, "bar(1, **a)") - assert_signature_matches(entry, "bar(1) {}") - - refute_signature_matches(entry, "bar(1, 2, 3)") - refute_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}") - end - - def test_signature_matches_for_post_parameters - index(<<~RUBY) - class Foo - def bar(*splat, a) - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - - # All calls with at least one positional argument match - assert_signature_matches(entry, "bar(1)") - assert_signature_matches(entry, "bar(1, 2)") - assert_signature_matches(entry, "bar(...)") - assert_signature_matches(entry, "bar(1, ...)") - assert_signature_matches(entry, "bar(*a)") - assert_signature_matches(entry, "bar(1, *a)") - assert_signature_matches(entry, "bar(*a, 2)") - assert_signature_matches(entry, "bar(1, *a, 2)") - assert_signature_matches(entry, "bar(1, **a)") - assert_signature_matches(entry, "bar(1, 2, 3)") - assert_signature_matches(entry, "bar(1) {}") - assert_signature_matches(entry, "bar()") - - refute_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}") - end - - def test_signature_matches_for_keyword_parameters - index(<<~RUBY) - class Foo - def bar(a:, b: 123) - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - - assert_signature_matches(entry, "bar(...)") - assert_signature_matches(entry, "bar()") - assert_signature_matches(entry, "bar(a: 1)") - assert_signature_matches(entry, "bar(a: 1, b: 32)") - - refute_signature_matches(entry, "bar(a: 1, c: 2)") - refute_signature_matches(entry, "bar(1, ...)") - refute_signature_matches(entry, "bar(1) {}") - refute_signature_matches(entry, "bar(1, *a)") - refute_signature_matches(entry, "bar(*a, 2)") - refute_signature_matches(entry, "bar(1, *a, 2)") - refute_signature_matches(entry, "bar(1, **a)") - refute_signature_matches(entry, "bar(*a)") - refute_signature_matches(entry, "bar(1)") - refute_signature_matches(entry, "bar(1, 2)") - refute_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}") - end - - def test_signature_matches_for_keyword_splats - index(<<~RUBY) - class Foo - def bar(a, b:, **kwargs) - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - - assert_signature_matches(entry, "bar(...)") - assert_signature_matches(entry, "bar()") - assert_signature_matches(entry, "bar(1)") - assert_signature_matches(entry, "bar(1, b: 2)") - assert_signature_matches(entry, "bar(1, b: 2, c: 3, d: 4)") - - refute_signature_matches(entry, "bar(1, 2, b: 2)") - end - - def test_partial_signature_matches - # It's important to match signatures partially, because we want to figure out which signature we should show while - # the user is in the middle of typing - index(<<~RUBY) - class Foo - def bar(a:, b:) - end - - def baz(a, b) - end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - assert_signature_matches(entry, "bar(a: 1)") - - entry = @index["baz"]&.first #: as Entry::Method - assert_signature_matches(entry, "baz(1)") - end - - def test_module_function_with_no_arguments - index(<<~RUBY) - module Foo - def bar; end - - module_function - - def baz; end - attr_reader :attribute - - public - - def qux; end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - assert_predicate(entry, :public?) - assert_equal("Foo", entry.owner&.name) - - instance_baz, singleton_baz = @index["baz"] #: as Array[Entry::Method] - assert_predicate(instance_baz, :private?) - assert_equal("Foo", instance_baz&.owner&.name) - - assert_predicate(singleton_baz, :public?) - assert_equal("Foo::", singleton_baz&.owner&.name) - - # After invoking `public`, the state of `module_function` is reset - instance_qux, singleton_qux = @index["qux"] #: as Array[Entry::Method] - assert_nil(singleton_qux) - assert_predicate(instance_qux, :public?) - assert_equal("Foo", instance_baz&.owner&.name) - - # Attributes are not turned into class methods, they do become private - instance_attribute, singleton_attribute = @index["attribute"] #: as Array[Entry::Method] - assert_nil(singleton_attribute) - assert_equal("Foo", instance_attribute&.owner&.name) - assert_predicate(instance_attribute, :private?) - end - - def test_module_function_does_nothing_in_classes - # Invoking `module_function` in a class raises an error. We simply ignore it - index(<<~RUBY) - class Foo - def bar; end - - module_function - - def baz; end - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - assert_predicate(entry, :public?) - assert_equal("Foo", entry.owner&.name) - - entry = @index["baz"]&.first #: as Entry::Method - assert_predicate(entry, :public?) - assert_equal("Foo", entry.owner&.name) - end - - def test_making_several_class_methods_private - index(<<~RUBY) - class Foo - def self.bar; end - def self.baz; end - def self.qux; end - - private_class_method :bar, :baz, :qux - - def initialize - end - end - RUBY - end - - def test_changing_visibility_post_definition - index(<<~RUBY) - class Foo - def bar; end - private :bar - - def baz; end - protected :baz - - private - def qux; end - - public :qux - end - RUBY - - entry = @index["bar"]&.first #: as Entry::Method - assert_predicate(entry, :private?) - - entry = @index["baz"]&.first #: as Entry::Method - assert_predicate(entry, :protected?) - - entry = @index["qux"]&.first #: as Entry::Method - assert_predicate(entry, :public?) - end - - def test_handling_attr - index(<<~RUBY) - class Foo - attr :bar - attr :baz, true - attr :qux, false - end - RUBY - - assert_entry("bar", Entry::Accessor, "/fake/path/foo.rb:1-8:1-11") - assert_no_entry("bar=") - assert_entry("baz", Entry::Accessor, "/fake/path/foo.rb:2-8:2-11") - assert_entry("baz=", Entry::Accessor, "/fake/path/foo.rb:2-8:2-11") - assert_entry("qux", Entry::Accessor, "/fake/path/foo.rb:3-8:3-11") - assert_no_entry("qux=") - end - - private - - #: (Entry::Method entry, String call_string) -> void - def assert_signature_matches(entry, call_string) - sig = entry.signatures.first #: as !nil - arguments = parse_prism_args(call_string) - assert(sig.matches?(arguments), "Expected #{call_string} to match #{entry.name}#{entry.decorated_parameters}") - end - - #: (Entry::Method entry, String call_string) -> void - def refute_signature_matches(entry, call_string) - sig = entry.signatures.first #: as !nil - arguments = parse_prism_args(call_string) - refute(sig.matches?(arguments), "Expected #{call_string} to not match #{entry.name}#{entry.decorated_parameters}") - end - - def parse_prism_args(s) - Array(Prism.parse(s).value.statements.body.first.arguments&.arguments) - end - end -end diff --git a/lib/ruby_indexer/test/prefix_tree_test.rb b/lib/ruby_indexer/test/prefix_tree_test.rb deleted file mode 100644 index 6e1cfa6211..0000000000 --- a/lib/ruby_indexer/test/prefix_tree_test.rb +++ /dev/null @@ -1,150 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "test_helper" - -module RubyIndexer - class PrefixTreeTest < Minitest::Test - def test_empty - tree = PrefixTree.new - - assert_empty(tree.search("")) - assert_empty(tree.search("foo")) - end - - def test_single_item - tree = PrefixTree.new - tree.insert("foo", "foo") - - assert_equal(["foo"], tree.search("")) - assert_equal(["foo"], tree.search("foo")) - assert_empty(tree.search("bar")) - end - - def test_multiple_items - tree = PrefixTree.new #: PrefixTree[String] - ["foo", "bar", "baz"].each { |item| tree.insert(item, item) } - - assert_equal(["baz", "bar", "foo"], tree.search("")) - assert_equal(["baz", "bar"], tree.search("b")) - assert_equal(["foo"], tree.search("fo")) - assert_equal(["baz", "bar"], tree.search("ba")) - assert_equal(["baz"], tree.search("baz")) - assert_empty(tree.search("qux")) - end - - def test_multiple_prefixes - tree = PrefixTree.new #: PrefixTree[String] - ["fo", "foo"].each { |item| tree.insert(item, item) } - - assert_equal(["fo", "foo"], tree.search("")) - assert_equal(["fo", "foo"], tree.search("f")) - assert_equal(["fo", "foo"], tree.search("fo")) - assert_equal(["foo"], tree.search("foo")) - assert_empty(tree.search("fooo")) - end - - def test_multiple_prefixes_with_shuffled_order - tree = PrefixTree.new #: PrefixTree[String] - [ - "foo/bar/base", - "foo/bar/on", - "foo/bar/support/selection", - "foo/bar/support/runner", - "foo/internal", - "foo/bar/document", - "foo/bar/code", - "foo/bar/support/rails", - "foo/bar/diagnostics", - "foo/bar/document2", - "foo/bar/support/runner2", - "foo/bar/support/diagnostic", - "foo/document", - "foo/bar/formatting", - "foo/bar/support/highlight", - "foo/bar/semantic", - "foo/bar/support/prefix", - "foo/bar/folding", - "foo/bar/selection", - "foo/bar/support/syntax", - "foo/bar/document3", - "foo/bar/hover", - "foo/bar/support/semantic", - "foo/bar/support/source", - "foo/bar/inlay", - "foo/requests", - "foo/bar/support/formatting", - "foo/bar/path", - "foo/executor", - ].each { |item| tree.insert(item, item) } - - assert_equal( - [ - "foo/bar/support/formatting", - "foo/bar/support/prefix", - "foo/bar/support/highlight", - "foo/bar/support/diagnostic", - "foo/bar/support/rails", - "foo/bar/support/runner", - "foo/bar/support/runner2", - "foo/bar/support/source", - "foo/bar/support/syntax", - "foo/bar/support/semantic", - "foo/bar/support/selection", - ], - tree.search("foo/bar/support"), - ) - end - - def test_deletion - tree = PrefixTree.new #: PrefixTree[String] - ["foo/bar", "foo/baz"].each { |item| tree.insert(item, item) } - assert_equal(["foo/baz", "foo/bar"], tree.search("foo")) - - tree.delete("foo/bar") - assert_empty(tree.search("foo/bar")) - assert_equal(["foo/baz"], tree.search("foo")) - end - - def test_delete_does_not_impact_other_keys_with_the_same_value - tree = PrefixTree.new #: PrefixTree[String] - tree.insert("key1", "value") - tree.insert("key2", "value") - assert_equal(["value", "value"], tree.search("key")) - - tree.delete("key2") - assert_empty(tree.search("key2")) - assert_equal(["value"], tree.search("key1")) - end - - def test_deleted_node_is_removed_from_the_tree - tree = PrefixTree.new #: PrefixTree[String] - tree.insert("foo/bar", "foo/bar") - assert_equal(["foo/bar"], tree.search("foo")) - - tree.delete("foo/bar") - root = tree.instance_variable_get(:@root) - assert_empty(root.children) - end - - def test_deleting_non_terminal_nodes - tree = PrefixTree.new #: PrefixTree[String] - tree.insert("abc", "value1") - tree.insert("abcdef", "value2") - - tree.delete("abcdef") - assert_empty(tree.search("abcdef")) - assert_equal(["value1"], tree.search("abc")) - end - - def test_overriding_values - tree = PrefixTree.new #: PrefixTree[Integer] - - tree.insert("foo/bar", 123) - assert_equal([123], tree.search("foo/bar")) - - tree.insert("foo/bar", 456) - assert_equal([456], tree.search("foo/bar")) - end - end -end diff --git a/lib/ruby_indexer/test/rbs_indexer_test.rb b/lib/ruby_indexer/test/rbs_indexer_test.rb deleted file mode 100644 index ea5a2c3b4a..0000000000 --- a/lib/ruby_indexer/test/rbs_indexer_test.rb +++ /dev/null @@ -1,381 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require_relative "test_case" - -module RubyIndexer - class RBSIndexerTest < TestCase - def test_index_core_classes - entries = @index["Array"] #: as !nil - refute_nil(entries) - # Array is a class but also an instance method on Kernel - assert_equal(2, entries.length) - entry = entries.find { |entry| entry.is_a?(Entry::Class) } #: as Entry::Class - assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path) - assert_equal("array.rbs", entry.file_name) - assert_equal("Object", entry.parent_class) - assert_equal(1, entry.mixin_operations.length) - enumerable_include = entry.mixin_operations.first #: as !nil - assert_equal("Enumerable", enumerable_include.module_name) - - # Using fixed positions would be fragile, so let's just check some basics. - assert_operator(entry.location.start_line, :>, 0) - assert_operator(entry.location.end_line, :>, entry.location.start_line) - assert_equal(0, entry.location.start_column) - assert_operator(entry.location.end_column, :>, 0) - end - - def test_index_core_modules - entries = @index["Kernel"] #: as !nil - refute_nil(entries) - assert_equal(2, entries.length) - entry = entries.first #: as Entry::Module - assert_match(%r{/gems/rbs-.*/core/kernel.rbs}, entry.file_path) - assert_equal("kernel.rbs", entry.file_name) - - # Using fixed positions would be fragile, so let's just check some basics. - assert_operator(entry.location.start_line, :>, 0) - assert_operator(entry.location.end_line, :>, entry.location.start_line) - assert_equal(0, entry.location.start_column) - assert_operator(entry.location.end_column, :>, 0) - end - - def test_index_core_constants - entries = @index["RUBY_VERSION"] #: as !nil - refute_nil(entries) - assert_equal(1, entries.length) - - entries = @index["Complex::I"] #: as !nil - refute_nil(entries) - assert_equal(1, entries.length) - - entries = @index["Encoding::US_ASCII"] #: as !nil - refute_nil(entries) - assert_equal(1, entries.length) - end - - def test_index_methods - entries = @index["initialize"] #: as Array[Entry::Method] - refute_nil(entries) - entry = entries.find { |entry| entry.owner&.name == "Array" } #: as Entry::Method - assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path) - assert_equal("array.rbs", entry.file_name) - assert_equal(:public, entry.visibility) - - # Using fixed positions would be fragile, so let's just check some basics. - assert_operator(entry.location.start_line, :>, 0) - assert_operator(entry.location.end_line, :>, entry.location.start_line) - assert_equal(2, entry.location.start_column) - assert_operator(entry.location.end_column, :>, 0) - end - - def test_index_global_declaration - entries = @index["$DEBUG"] #: as Array[Entry::GlobalVariable] - refute_nil(entries) - assert_equal(1, entries.length) - - entry = entries.first #: as Entry::GlobalVariable - - assert_instance_of(Entry::GlobalVariable, entry) - assert_equal("$DEBUG", entry.name) - assert_match(%r{/gems/rbs-.*/core/global_variables.rbs}, entry.file_path) - assert_operator(entry.location.start_column, :<, entry.location.end_column) - assert_equal(entry.location.start_line, entry.location.end_line) - end - - def test_attaches_correct_owner_to_singleton_methods - entries = @index["basename"] #: as Array[Entry::Method] - refute_nil(entries) - - owner = entries.first&.owner #: as Entry::SingletonClass - assert_instance_of(Entry::SingletonClass, owner) - assert_equal("File::", owner.name) - end - - def test_location_and_name_location_are_the_same - # NOTE: RBS does not store the name location for classes, modules or methods. This behavior is not exactly what - # we would like, but for now we assign the same location to both - - entries = @index["Array"] #: as Array[Entry::Class] - refute_nil(entries) - entry = entries.find { |entry| entry.is_a?(Entry::Class) } #: as Entry::Class - - assert_same(entry.location, entry.name_location) - end - - def test_rbs_method_with_required_positionals - entries = @index["crypt"] #: as Array[Entry::Method] - assert_equal(1, entries.length) - - entry = entries.first #: as Entry::Method - signatures = entry.signatures - assert_equal(1, signatures.length) - - first_signature = signatures.first #: as Entry::Signature - - # (::string salt_str) -> ::String - - assert_equal(1, first_signature.parameters.length) - assert_kind_of(Entry::RequiredParameter, first_signature.parameters[0]) - assert_equal(:salt_str, first_signature.parameters[0]&.name) - end - - def test_rbs_method_with_unnamed_required_positionals - entries = @index["try_convert"] #: as Array[Entry::Method] - entry = entries.find { |entry| entry.owner&.name == "Array::" } #: as Entry::Method - - parameters = entry.signatures[0]&.parameters #: as Array[Entry::Parameter] - - assert_equal([:arg0], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - end - - def test_rbs_method_with_optional_positionals - entries = @index["polar"] #: as Array[Entry::Method] - entry = entries.find { |entry| entry.owner&.name == "Complex::" } #: as Entry::Method - - # def self.polar: (Numeric, ?Numeric) -> Complex - - parameters = entry.signatures[0]&.parameters #: as Array[Entry::Parameter] - - assert_equal([:arg0, :arg1], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - assert_kind_of(Entry::OptionalParameter, parameters[1]) - end - - def test_rbs_method_with_optional_parameter - entries = @index["chomp"] #: as Array[Entry::Method] - assert_equal(1, entries.length) - - entry = entries.first #: as Entry::Method - signatures = entry.signatures - assert_equal(1, signatures.length) - - first_signature = signatures.first #: as Entry::Signature - - # (?::string? separator) -> ::String - - assert_equal(1, first_signature.parameters.length) - assert_kind_of(Entry::OptionalParameter, first_signature.parameters[0]) - assert_equal(:separator, first_signature.parameters[0]&.name) - end - - def test_rbs_method_with_required_and_optional_parameters - entries = @index["gsub"] #: as Array[Entry::Method] - assert_equal(1, entries.length) - - entry = entries.first #: as Entry::Method - - signatures = entry.signatures - assert_equal(3, signatures.length) - - # (::Regexp | ::string pattern, ::string | ::hash[::String, ::_ToS] replacement) -> ::String - # | (::Regexp | ::string pattern) -> ::Enumerator[::String, ::String] - # | (::Regexp | ::string pattern) { (::String match) -> ::_ToS } -> ::String - - parameters = signatures[0]&.parameters #: as !nil - assert_equal([:pattern, :replacement], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - assert_kind_of(Entry::RequiredParameter, parameters[1]) - - parameters = signatures[1]&.parameters #: as !nil - assert_equal([:pattern], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - - parameters = signatures[2]&.parameters #: as !nil - assert_equal([:pattern, :""], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - assert_kind_of(Entry::BlockParameter, parameters[1]) - end - - def test_rbs_anonymous_block_parameter - entries = @index["open"] #: as Array[Entry::Method] - entry = entries.find { |entry| entry.owner&.name == "File::" } #: as Entry::Method - - assert_equal(2, entry.signatures.length) - - # (::String name, ?::String mode, ?::Integer perm) -> ::IO? - # | [T] (::String name, ?::String mode, ?::Integer perm) { (::IO) -> T } -> T - - parameters = entry.signatures[0]&.parameters #: as !nil - assert_equal([:file_name, :mode, :perm], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - assert_kind_of(Entry::OptionalParameter, parameters[1]) - assert_kind_of(Entry::OptionalParameter, parameters[2]) - - parameters = entry.signatures[1]&.parameters #: as !nil - assert_equal([:file_name, :mode, :perm, :""], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - assert_kind_of(Entry::OptionalParameter, parameters[1]) - assert_kind_of(Entry::OptionalParameter, parameters[2]) - assert_kind_of(Entry::BlockParameter, parameters[3]) - end - - def test_rbs_method_with_rest_positionals - entries = @index["count"] #: as Array[Entry::Method] - entry = entries.find { |entry| entry.owner&.name == "String" } #: as Entry::Method - - parameters = entry.signatures.first&.parameters #: as !nil - assert_equal(1, entry.signatures.length) - - # (::String::selector selector_0, *::String::selector more_selectors) -> ::Integer - - assert_equal([:selector_0, :more_selectors], parameters.map(&:name)) - assert_kind_of(RubyIndexer::Entry::RequiredParameter, parameters[0]) - assert_kind_of(RubyIndexer::Entry::RestParameter, parameters[1]) - end - - def test_rbs_method_with_trailing_positionals - entries = @index["select"] #: as Array[Entry::Method] - entry = entries.find { |entry| entry.owner&.name == "IO::" } #: as !nil - - signatures = entry.signatures - assert_equal(2, signatures.length) - - # def self.select: [X, Y, Z] (::Array[X & io]? read_array, ?::Array[Y & io]? write_array, ?::Array[Z & io]? error_array) -> [ Array[X], Array[Y], Array[Z] ] - # | [X, Y, Z] (::Array[X & io]? read_array, ?::Array[Y & io]? write_array, ?::Array[Z & io]? error_array, Time::_Timeout? timeout) -> [ Array[X], Array[Y], Array[Z] ]? - - parameters = signatures[0]&.parameters #: as !nil - assert_equal([:read_array, :write_array, :error_array], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - assert_kind_of(Entry::OptionalParameter, parameters[1]) - assert_kind_of(Entry::OptionalParameter, parameters[2]) - - parameters = signatures[1]&.parameters #: as !nil - assert_equal([:read_array, :write_array, :error_array, :timeout], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - assert_kind_of(Entry::OptionalParameter, parameters[1]) - assert_kind_of(Entry::OptionalParameter, parameters[2]) - assert_kind_of(Entry::OptionalParameter, parameters[3]) - end - - def test_rbs_method_with_optional_keywords - entries = @index["step"] #: as Array[Entry::Method] - entry = entries.find { |entry| entry.owner&.name == "Numeric" } #: as !nil - - signatures = entry.signatures - assert_equal(4, signatures.length) - - # (?::Numeric limit, ?::Numeric step) { (::Numeric) -> void } -> self - # | (?::Numeric limit, ?::Numeric step) -> ::Enumerator[::Numeric, self] - # | (?by: ::Numeric, ?to: ::Numeric) { (::Numeric) -> void } -> self - # | (?by: ::Numeric, ?to: ::Numeric) -> ::Enumerator[::Numeric, self] - - parameters = signatures[0]&.parameters #: as !nil - assert_equal([:limit, :step, :""], parameters.map(&:name)) - assert_kind_of(Entry::OptionalParameter, parameters[0]) - assert_kind_of(Entry::OptionalParameter, parameters[1]) - assert_kind_of(Entry::BlockParameter, parameters[2]) - - parameters = signatures[1]&.parameters #: as !nil - assert_equal([:limit, :step], parameters.map(&:name)) - assert_kind_of(Entry::OptionalParameter, parameters[0]) - assert_kind_of(Entry::OptionalParameter, parameters[1]) - - parameters = signatures[2]&.parameters #: as !nil - assert_equal([:by, :to, :""], parameters.map(&:name)) - assert_kind_of(Entry::OptionalKeywordParameter, parameters[0]) - assert_kind_of(Entry::OptionalKeywordParameter, parameters[1]) - assert_kind_of(Entry::BlockParameter, parameters[2]) - - parameters = signatures[3]&.parameters #: as !nil - assert_equal([:by, :to], parameters.map(&:name)) - assert_kind_of(Entry::OptionalKeywordParameter, parameters[0]) - assert_kind_of(Entry::OptionalKeywordParameter, parameters[1]) - end - - def test_rbs_method_with_required_keywords - # There are no methods in Core that have required keyword arguments, - # so we test against RBS directly - - rbs = <<~RBS - class File - def foo: (a: ::Numeric sz, b: ::Numeric) -> void - end - RBS - signatures = parse_rbs_methods(rbs, "foo") - parameters = signatures[0].parameters - assert_equal([:a, :b], parameters.map(&:name)) - assert_kind_of(Entry::KeywordParameter, parameters[0]) - assert_kind_of(Entry::KeywordParameter, parameters[1]) - end - - def test_rbs_method_with_rest_keywords - entries = @index["method_missing"] #: as Array[Entry::Method] - entry = entries.find { |entry| entry.owner&.name == "BasicObject" } #: as !nil - signatures = entry.signatures - assert_equal(1, signatures.length) - - # (Symbol, *untyped, **untyped) ?{ (*untyped, **untyped) -> untyped } -> untyped - - parameters = signatures[0]&.parameters #: as !nil - assert_equal([:arg0, :"", :""], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - assert_kind_of(Entry::RestParameter, parameters[1]) - assert_kind_of(Entry::KeywordRestParameter, parameters[2]) - end - - def test_parse_simple_rbs - rbs = <<~RBS - class File - def self?.open: (String name, ?String mode, ?Integer perm) -> IO? - | [T] (String name, ?String mode, ?Integer perm) { (IO) -> T } -> T - end - RBS - signatures = parse_rbs_methods(rbs, "open") - assert_equal(2, signatures.length) - parameters = signatures[0].parameters - assert_equal([:name, :mode, :perm], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - assert_kind_of(Entry::OptionalParameter, parameters[1]) - assert_kind_of(Entry::OptionalParameter, parameters[2]) - - parameters = signatures[1].parameters - assert_equal([:name, :mode, :perm, :""], parameters.map(&:name)) - assert_kind_of(Entry::RequiredParameter, parameters[0]) - assert_kind_of(Entry::OptionalParameter, parameters[1]) - assert_kind_of(Entry::OptionalParameter, parameters[2]) - assert_kind_of(Entry::BlockParameter, parameters[3]) - end - - def test_signature_alias - # In RBS, an alias means that two methods have the same signature. - # It does not mean the same thing as a Ruby alias. - any_entries = @index["any?"] #: as Array[Entry::UnresolvedMethodAlias] - - assert_equal(["Array", "Enumerable", "Hash"], any_entries.map { _1.owner&.name }) - - entry = any_entries.find { |entry| entry.owner&.name == "Array" } #: as !nil - - assert_kind_of(RubyIndexer::Entry::UnresolvedMethodAlias, entry) - assert_equal("any?", entry.name) - assert_equal("all?", entry.old_name) - assert_equal("Array", entry.owner&.name) - assert(entry.file_path&.end_with?("core/array.rbs")) - refute_empty(entry.comments) - end - - def test_indexing_untyped_functions - entries = @index.resolve_method("call", "Method") #: as Array[Entry::Method] - - parameters = entries.first&.signatures&.first&.parameters #: as !nil - assert_equal(1, parameters.length) - assert_instance_of(Entry::ForwardingParameter, parameters.first) - end - - private - - def parse_rbs_methods(rbs, method_name) - buffer = RBS::Buffer.new(content: rbs, name: "") - _, _, declarations = RBS::Parser.parse_signature(buffer) - index = RubyIndexer::Index.new - indexer = RubyIndexer::RBSIndexer.new(index) - pathname = Pathname.new("/file.rbs") - indexer.process_signature(pathname, declarations) - entry = index[method_name] #: as !nil - .first #: as Entry::Method - - entry.signatures - end - end -end diff --git a/lib/ruby_indexer/test/test_case.rb b/lib/ruby_indexer/test/test_case.rb deleted file mode 100644 index e9e9d424a8..0000000000 --- a/lib/ruby_indexer/test/test_case.rb +++ /dev/null @@ -1,69 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "test_helper" - -module RubyIndexer - class TestCase < Minitest::Test - class << self - #: String? - attr_accessor :core_index_data - end - - def setup - self.class.core_index_data ||= begin - index = Index.new - RBSIndexer.new(index).index_ruby_core - Marshal.dump(index) - end - - core_data = self.class.core_index_data #: as !nil - loaded_index = Marshal.load(core_data) #: as Index - @index = loaded_index - @default_indexed_entries = @index.instance_variable_get(:@entries).dup - end - - def teardown - entries = @index.instance_variable_get(:@entries).values.flatten - entries.each do |entry| - assert_includes([:public, :private, :protected], entry.visibility) - end - end - - private - - def index(source, uri: URI::Generic.from_path(path: "/fake/path/foo.rb")) - @index.index_single(uri, source) - end - - def assert_entry(expected_name, type, expected_location, visibility: nil) - entries = @index[expected_name] #: as !nil - refute_nil(entries, "Expected #{expected_name} to be indexed") - refute_empty(entries, "Expected #{expected_name} to be indexed") - - entry = entries.first #: as !nil - assert_instance_of(type, entry, "Expected #{expected_name} to be a #{type}") - - location = entry.location - location_string = - "#{entry.file_path}:#{location.start_line - 1}-#{location.start_column}" \ - ":#{location.end_line - 1}-#{location.end_column}" - - assert_equal(expected_location, location_string) - assert_equal(visibility, entry.visibility) if visibility - end - - def refute_entry(expected_name) - entries = @index[expected_name] - assert_nil(entries, "Expected #{expected_name} to not be indexed") - end - - def assert_no_indexed_entries - assert_equal(@default_indexed_entries, @index.instance_variable_get(:@entries)) - end - - def assert_no_entry(entry) - refute(@index.indexed?(entry), "Expected '#{entry}' to not be indexed") - end - end -end diff --git a/lib/ruby_indexer/test/uri_test.rb b/lib/ruby_indexer/test/uri_test.rb deleted file mode 100644 index 8c064a1a77..0000000000 --- a/lib/ruby_indexer/test/uri_test.rb +++ /dev/null @@ -1,85 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "test_helper" - -module RubyIndexer - class URITest < Minitest::Test - def test_from_path_on_unix - uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb") - assert_equal("/some/unix/path/to/file.rb", uri.path) - end - - def test_from_path_on_windows - uri = URI::Generic.from_path(path: "C:/some/windows/path/to/file.rb") - assert_equal("/C%3A/some/windows/path/to/file.rb", uri.path) - end - - def test_from_path_on_windows_with_lowercase_drive - uri = URI::Generic.from_path(path: "c:/some/windows/path/to/file.rb") - assert_equal("/c%3A/some/windows/path/to/file.rb", uri.path) - end - - def test_to_standardized_path_on_unix - uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb") - assert_equal(uri.path, uri.to_standardized_path) - end - - def test_to_standardized_path_on_windows - uri = URI::Generic.from_path(path: "C:/some/windows/path/to/file.rb") - assert_equal("C:/some/windows/path/to/file.rb", uri.to_standardized_path) - end - - def test_to_standardized_path_on_windows_with_lowercase_drive - uri = URI::Generic.from_path(path: "c:/some/windows/path/to/file.rb") - assert_equal("c:/some/windows/path/to/file.rb", uri.to_standardized_path) - end - - def test_to_standardized_path_on_windows_with_received_uri - uri = URI("file:///c%3A/some/windows/path/to/file.rb") - assert_equal("c:/some/windows/path/to/file.rb", uri.to_standardized_path) - end - - def test_plus_signs_are_properly_unescaped - path = "/opt/rubies/3.3.0/lib/ruby/3.3.0+0/pathname.rb" - uri = URI::Generic.from_path(path: path) - assert_equal(path, uri.to_standardized_path) - end - - def test_from_path_with_fragment - uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb", fragment: "L1,3-2,9") - assert_equal("file:///some/unix/path/to/file.rb#L1,3-2,9", uri.to_s) - end - - def test_from_path_windows_long_file_paths - uri = URI::Generic.from_path(path: "//?/C:/hostedtoolcache/windows/Ruby/3.3.1/x64/lib/ruby/3.3.0/open-uri.rb") - assert_equal("C:/hostedtoolcache/windows/Ruby/3.3.1/x64/lib/ruby/3.3.0/open-uri.rb", uri.to_standardized_path) - end - - def test_from_path_computes_require_path_when_load_path_entry_is_given - uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb", load_path_entry: "/some/unix/path") - assert_equal("to/file", uri.require_path) - end - - def test_allows_adding_require_path_with_load_path_entry - uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb") - assert_nil(uri.require_path) - - uri.add_require_path_from_load_entry("/some/unix/path") - assert_equal("to/file", uri.require_path) - end - - def test_from_path_escapes_colon_characters - uri = URI::Generic.from_path(path: "c:/some/windows/path with/spaces/file.rb") - assert_equal("c:/some/windows/path with/spaces/file.rb", uri.to_standardized_path) - assert_equal("file:///c%3A/some/windows/path%20with/spaces/file.rb", uri.to_s) - end - - def test_from_path_with_unicode_characters - path = "/path/with/unicode/文件.rb" - uri = URI::Generic.from_path(path: path) - assert_equal(path, uri.to_standardized_path) - assert_equal("file:///path/with/unicode/%E6%96%87%E4%BB%B6.rb", uri.to_s) - end - end -end diff --git a/lib/ruby_lsp/global_state.rb b/lib/ruby_lsp/global_state.rb index 17f674c392..f6f936e32f 100644 --- a/lib/ruby_lsp/global_state.rb +++ b/lib/ruby_lsp/global_state.rb @@ -27,9 +27,6 @@ class GlobalState #: bool attr_reader :has_type_checker - #: RubyIndexer::Index - attr_reader :index - #: Rubydex::Graph attr_reader :graph @@ -60,7 +57,6 @@ def initialize @linters = [] #: Array[String] @test_library = "minitest" #: String @has_type_checker = true #: bool - @index = RubyIndexer::Index.new #: RubyIndexer::Index @graph = Rubydex::Graph.new #: Rubydex::Graph @supported_formatters = {} #: Hash[String, Requests::Support::Formatter] @type_inferrer = TypeInferrer.new(@graph) #: TypeInferrer @@ -206,7 +202,6 @@ def apply_options(options) @graph.encoding = "utf32" Encoding::UTF_32LE end - @index.configuration.encoding = @encoding @client_capabilities.apply_client_capabilities(options[:capabilities]) if options[:capabilities] diff --git a/lib/ruby_lsp/internal.rb b/lib/ruby_lsp/internal.rb index 54b3ffdc0f..08b1519e23 100644 --- a/lib/ruby_lsp/internal.rb +++ b/lib/ruby_lsp/internal.rb @@ -39,7 +39,7 @@ require "ruby-lsp" require "ruby_lsp/base_server" -require "ruby_indexer/ruby_indexer" +require "ruby_lsp/uri" require "ruby_lsp/utils" require "ruby_lsp/scope" require "ruby_lsp/client_capabilities" diff --git a/lib/ruby_lsp/listeners/code_lens.rb b/lib/ruby_lsp/listeners/code_lens.rb index 4582d60c32..d95e2892af 100644 --- a/lib/ruby_lsp/listeners/code_lens.rb +++ b/lib/ruby_lsp/listeners/code_lens.rb @@ -279,7 +279,7 @@ def add_spec_code_lens(node, kind:) when Prism::StringNode first_argument.content when Prism::ConstantReadNode, Prism::ConstantPathNode - RubyIndexer::Index.constant_name(first_argument) + constant_name(first_argument) end return unless name diff --git a/lib/ruby_lsp/listeners/definition.rb b/lib/ruby_lsp/listeners/definition.rb index 5e2c5fd6d9..58ad3ca8e4 100644 --- a/lib/ruby_lsp/listeners/definition.rb +++ b/lib/ruby_lsp/listeners/definition.rb @@ -106,7 +106,7 @@ def on_block_argument_node_enter(node) #: (Prism::ConstantPathNode node) -> void def on_constant_path_node_enter(node) - name = RubyIndexer::Index.constant_name(node) + name = constant_name(node) return if name.nil? handle_constant_definition(name) @@ -114,7 +114,7 @@ def on_constant_path_node_enter(node) #: (Prism::ConstantReadNode node) -> void def on_constant_read_node_enter(node) - name = RubyIndexer::Index.constant_name(node) + name = constant_name(node) return if name.nil? handle_constant_definition(name) diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index a757de5d2a..a4bde829a1 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -116,7 +116,7 @@ def on_interpolated_string_node_enter(node) def on_constant_read_node_enter(node) return unless @sorbet_level.ignore? - name = RubyIndexer::Index.constant_name(node) + name = constant_name(node) return if name.nil? generate_hover(name, node.location) @@ -133,7 +133,7 @@ def on_constant_write_node_enter(node) def on_constant_path_node_enter(node) return unless @sorbet_level.ignore? - name = RubyIndexer::Index.constant_name(node) + name = constant_name(node) return if name.nil? generate_hover(name, node.location) diff --git a/lib/ruby_lsp/listeners/test_discovery.rb b/lib/ruby_lsp/listeners/test_discovery.rb index e36d6b62a9..526cd00e84 100644 --- a/lib/ruby_lsp/listeners/test_discovery.rb +++ b/lib/ruby_lsp/listeners/test_discovery.rb @@ -56,7 +56,23 @@ def register_events(dispatcher, *events) #: (String? name) -> String def calc_fully_qualified_name(name) - RubyIndexer::Index.actual_nesting(@nesting, name).join("::") + parts = name ? @nesting + [name] : @nesting + return "" if parts.empty? + + last = parts.last #: as !nil + rest = parts[0...-1] #: as !nil + + resolved = @graph.resolve_constant(last, rest) + return resolved.name if resolved + + # Fallback for unresolved constants (e.g. dynamic references): preserve top-level reset semantics by + # truncating at the first `::`-prefixed part when scanning from the innermost out. + corrected = [] + parts.reverse_each do |part| + corrected.prepend(part.delete_prefix("::")) + break if part.start_with?("::") + end + corrected.join("::") end #: (Prism::ClassNode node, String fully_qualified_name) -> Array[String] diff --git a/lib/ruby_lsp/listeners/test_style.rb b/lib/ruby_lsp/listeners/test_style.rb index 7d2881bc48..3ac09f535e 100644 --- a/lib/ruby_lsp/listeners/test_style.rb +++ b/lib/ruby_lsp/listeners/test_style.rb @@ -220,7 +220,7 @@ def on_def_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMeth name = node.name.to_s return unless name.start_with?("test_") - current_group_name = RubyIndexer::Index.actual_nesting(@nesting, nil).join("::") + current_group_name = calc_fully_qualified_name(nil) parent = @parent_stack.last return unless parent.is_a?(Requests::Support::TestItem) diff --git a/lib/ruby_lsp/requests/rename.rb b/lib/ruby_lsp/requests/rename.rb index db7b2d78d0..548203d550 100644 --- a/lib/ruby_lsp/requests/rename.rb +++ b/lib/ruby_lsp/requests/rename.rb @@ -54,7 +54,7 @@ def perform target = target #: as Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantPathTargetNode - name = RubyIndexer::Index.constant_name(target) + name = constant_name(target) return unless name declaration = @graph.resolve_constant(name, node_context.nesting) diff --git a/lib/ruby_lsp/requests/support/common.rb b/lib/ruby_lsp/requests/support/common.rb index cbd7a65cf8..db04cada00 100644 --- a/lib/ruby_lsp/requests/support/common.rb +++ b/lib/ruby_lsp/requests/support/common.rb @@ -23,7 +23,7 @@ def range_from_node(node) ) end - #: ((Prism::Location | RubyIndexer::Location) location) -> Interface::Range + #: ((Prism::Location | Rubydex::Location) location) -> Interface::Range def range_from_location(location) Interface::Range.new( start: Interface::Position.new( @@ -98,7 +98,7 @@ def constant_reachable_from_call_site?(declaration, value, node_context) # `RubyIndexer::Entry::MethodAlias`. All respond to `public?`, `private?` and `owner` (an object with a # `name` attribute). # - #: ((Rubydex::Method | RubyIndexer::Entry::Member | RubyIndexer::Entry::MethodAlias) method_decl, TypeInferrer::Type? receiver_type, Rubydex::Graph graph, NodeContext node_context) -> bool + #: (Rubydex::Method method_decl, TypeInferrer::Type? receiver_type, Rubydex::Graph graph, NodeContext node_context) -> bool def method_reachable_from_call_site?(method_decl, receiver_type, graph, node_context) return true unless receiver_type @@ -108,12 +108,10 @@ def method_reachable_from_call_site?(method_decl, receiver_type, graph, node_con return true if method_decl.public? return false if method_decl.private? - owner_name = method_decl.owner&.name - return false unless owner_name - caller_declaration = graph[caller_namespace] return false unless caller_declaration.is_a?(Rubydex::Namespace) + owner_name = method_decl.owner.name caller_declaration.ancestors.any? { |ancestor| ancestor.name == owner_name } end @@ -162,55 +160,6 @@ def categorized_markdown_from_definitions(title, definitions, max_entries = nil) } end - #: (String title, (Array[RubyIndexer::Entry] | RubyIndexer::Entry) entries, ?Integer? max_entries) -> Hash[Symbol, String] - def categorized_markdown_from_index_entries(title, entries, max_entries = nil) - markdown_title = "```ruby\n#{title}\n```" - definitions = [] - content = +"" - entries = Array(entries) - entries_to_format = max_entries ? entries.take(max_entries) : entries - entries_to_format.each do |entry| - loc = entry.location - - # We always handle locations as zero based. However, for file links in Markdown we need them to be one - # based, which is why instead of the usual subtraction of 1 to line numbers, we are actually adding 1 to - # columns. The format for VS Code file URIs is - # `file:///path/to/file.rb#Lstart_line,start_column-end_line,end_column` - uri = "#{entry.uri}#L#{loc.start_line},#{loc.start_column + 1}-#{loc.end_line},#{loc.end_column + 1}" - definitions << "[#{entry.file_name}](#{uri})" - content << "\n\n#{entry.comments}" unless entry.comments.empty? - end - - additional_entries_text = if max_entries && entries.length > max_entries - additional = entries.length - max_entries - " | #{additional} other#{additional > 1 ? "s" : ""}" - else - "" - end - - { - title: markdown_title, - links: "**Definitions**: #{definitions.join(" | ")}#{additional_entries_text}", - documentation: content, - } - end - - #: (String title, (Array[RubyIndexer::Entry] | RubyIndexer::Entry) entries, ?Integer? max_entries, ?extra_links: String?) -> String - def markdown_from_index_entries(title, entries, max_entries = nil, extra_links: nil) - categorized_markdown = categorized_markdown_from_index_entries(title, entries, max_entries) - - markdown = +(categorized_markdown[:title] || "") - markdown << "\n\n#{extra_links}" if extra_links - - <<~MARKDOWN.chomp - #{markdown} - - #{categorized_markdown[:links]} - - #{categorized_markdown[:documentation]} - MARKDOWN - end - #: (String title, Enumerable[Rubydex::Definition] definitions, ?Integer? max_entries, ?extra_links: String?) -> String def markdown_from_definitions(title, definitions, max_entries = nil, extra_links: nil) categorized_markdown = categorized_markdown_from_definitions(title, definitions, max_entries) @@ -229,7 +178,20 @@ def markdown_from_definitions(title, definitions, max_entries = nil, extra_links #: ((Prism::ConstantPathNode | Prism::ConstantReadNode | Prism::ConstantPathTargetNode | Prism::CallNode | Prism::MissingNode) node) -> String? def constant_name(node) - RubyIndexer::Index.constant_name(node) + Common.constant_name(node) + end + + class << self + #: ((Prism::ConstantPathNode | Prism::ConstantReadNode | Prism::ConstantPathTargetNode | Prism::CallNode | Prism::MissingNode) node) -> String? + def constant_name(node) + case node + when Prism::ConstantPathNode, Prism::ConstantReadNode, Prism::ConstantPathTargetNode + node.full_name + end + rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError, + Prism::ConstantPathNode::MissingNodesInConstantPathError + nil + end end #: ((Prism::ModuleNode | Prism::ClassNode) node) -> String? @@ -253,28 +215,6 @@ def each_constant_path_part(node, &block) current = current.parent end end - - #: (RubyIndexer::Entry entry) -> Integer - def kind_for_entry(entry) - case entry - when RubyIndexer::Entry::Class - Constant::SymbolKind::CLASS - when RubyIndexer::Entry::Module - Constant::SymbolKind::NAMESPACE - when RubyIndexer::Entry::Constant, RubyIndexer::Entry::UnresolvedConstantAlias, RubyIndexer::Entry::ConstantAlias - Constant::SymbolKind::CONSTANT - when RubyIndexer::Entry::Method, RubyIndexer::Entry::UnresolvedMethodAlias, RubyIndexer::Entry::MethodAlias - entry.name == "initialize" ? Constant::SymbolKind::CONSTRUCTOR : Constant::SymbolKind::METHOD - when RubyIndexer::Entry::Accessor - Constant::SymbolKind::PROPERTY - when RubyIndexer::Entry::InstanceVariable, RubyIndexer::Entry::ClassVariable - Constant::SymbolKind::FIELD - when RubyIndexer::Entry::GlobalVariable - Constant::SymbolKind::VARIABLE - else - Constant::SymbolKind::NULL - end - end end end end diff --git a/lib/ruby_lsp/ruby_document.rb b/lib/ruby_lsp/ruby_document.rb index c60f7b494c..a1d421185a 100644 --- a/lib/ruby_lsp/ruby_document.rb +++ b/lib/ruby_lsp/ruby_document.rb @@ -4,22 +4,6 @@ module RubyLsp #: [ParseResultType = Prism::ParseLexResult] class RubyDocument < Document - METHODS_THAT_CHANGE_DECLARATIONS = [ - :private_constant, - :attr_reader, - :attr_writer, - :attr_accessor, - :alias_method, - :include, - :prepend, - :extend, - :public, - :protected, - :private, - :module_function, - :private_class_method, - ].freeze - class << self #: (Prism::Node node, Integer char_position, code_units_cache: (^(Integer arg0) -> Integer | Prism::CodeUnitsCache), ?node_types: Array[singleton(Prism::Node)]) -> NodeContext def locate(node, char_position, code_units_cache:, node_types: []) @@ -190,62 +174,5 @@ def locate_node(position, node_types: []) node_types: node_types, ) end - - #: -> bool - def should_index? - # This method controls when we should index documents. If there's no recent edit and the document has just been - # opened, we need to index it - return true unless @last_edit - - last_edit_may_change_declarations? - end - - private - - #: -> bool - def last_edit_may_change_declarations? - case @last_edit - when Delete - # Not optimized yet. It's not trivial to identify that a declaration has been removed since the source is no - # longer there and we don't remember the deleted text - true - when Insert, Replace - position_may_impact_declarations?(@last_edit.range[:start]) - else - false - end - end - - #: (Hash[Symbol, Integer] position) -> bool - def position_may_impact_declarations?(position) - node_context = locate_node(position) - node_at_edit = node_context.node - - # Adjust to the parent when editing the constant of a class/module declaration - if node_at_edit.is_a?(Prism::ConstantReadNode) || node_at_edit.is_a?(Prism::ConstantPathNode) - node_at_edit = node_context.parent - end - - case node_at_edit - when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode, - Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode, - Prism::ConstantPathAndWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantWriteNode, - Prism::ConstantAndWriteNode, Prism::ConstantOperatorWriteNode, Prism::GlobalVariableAndWriteNode, - Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableTargetNode, - Prism::GlobalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::InstanceVariableAndWriteNode, - Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, - Prism::InstanceVariableTargetNode, Prism::AliasMethodNode - true - when Prism::MultiWriteNode - [*node_at_edit.lefts, *node_at_edit.rest, *node_at_edit.rights].any? do |node| - node.is_a?(Prism::ConstantTargetNode) || node.is_a?(Prism::ConstantPathTargetNode) - end - when Prism::CallNode - receiver = node_at_edit.receiver - (!receiver || receiver.is_a?(Prism::SelfNode)) && METHODS_THAT_CHANGE_DECLARATIONS.include?(node_at_edit.name) - else - false - end - end end end diff --git a/lib/ruby_lsp/scripts/compose_bundle.rb b/lib/ruby_lsp/scripts/compose_bundle.rb index 00b4d31dfb..32ecbbe3fa 100644 --- a/lib/ruby_lsp/scripts/compose_bundle.rb +++ b/lib/ruby_lsp/scripts/compose_bundle.rb @@ -5,7 +5,7 @@ def compose(raw_initialize, **options) require_relative "../setup_bundler" require "json" require "uri" - require_relative "../../ruby_indexer/lib/ruby_indexer/uri" + require_relative "../uri" initialize_request = JSON.parse(raw_initialize, symbolize_names: true) workspace_uri = initialize_request.dig(:params, :workspaceFolders, 0, :uri) diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index 5ba7a41f3d..8c77906c50 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -325,8 +325,6 @@ def run_initialize(message) )) end - process_indexing_configuration(options.dig(:initializationOptions, :indexing)) - begin_progress("indexing-progress", "Ruby LSP: indexing files") global_state_notifications.each { |notification| send_message(notification) } @@ -499,39 +497,15 @@ def run_combined_requests(message) document, dispatcher, ) - - # The code lens listener requires the index to be populated, so the DeclarationListener must be inserted first in - # the dispatcher's state - code_lens = nil #: Requests::CodeLens? - - if document.is_a?(RubyDocument) && document.should_index? - # Re-index the file as it is modified. This mode of indexing updates entries only. Require path trees are only - # updated on save - @global_state.synchronize do - send_log_message("Determined that document should be indexed: #{uri}") - - @global_state.index.handle_change(uri) do |index| - index.delete(uri, skip_require_paths_tree: true) - RubyIndexer::DeclarationListener.new(index, dispatcher, parse_result, uri, collect_comments: true) - code_lens = Requests::CodeLens.new(@global_state, document, dispatcher) - dispatcher.dispatch(document.ast) - end - end - else - code_lens = Requests::CodeLens.new(@global_state, document, dispatcher) - dispatcher.dispatch(document.ast) - end + code_lens = Requests::CodeLens.new(@global_state, document, dispatcher) + dispatcher.dispatch(document.ast) # Store all responses retrieve in this round of visits in the cache and then return the response for the request # we actually received document.cache_set("textDocument/foldingRange", folding_range.perform) document.cache_set("textDocument/documentSymbol", document_symbol.perform) document.cache_set("textDocument/documentLink", document_link.perform) - document.cache_set( - "textDocument/codeLens", - code_lens #: as !nil - .perform, - ) + document.cache_set("textDocument/codeLens", code_lens.perform) document.cache_set("textDocument/inlayHint", inlay_hint.perform) send_message(Result.new(id: message[:id], response: document.cache_get(message[:method]))) @@ -1038,21 +1012,6 @@ def text_document_definition(message) #: (Hash[Symbol, untyped] message) -> void def workspace_did_change_watched_files(message) - # If indexing is not complete yet, delay processing did change watched file notifications. We need initial - # indexing to be in place so that we can handle file changes appropriately without risking duplicates. We also - # have to sleep before re-inserting the notification in the queue otherwise the worker can get stuck in its own - # loop of pushing and popping the same notification - unless @global_state.index.initial_indexing_completed - Thread.new do - sleep(2) - # We have to ensure that the queue is not closed yet, since nothing stops the user from saving a file and then - # immediately telling the LSP to shutdown - @incoming_queue << message unless @incoming_queue.closed? - end - - return - end - changes = message.dig(:params, :changes) # We allow add-ons to register for watching files and we have no restrictions for what they register for. If the # same pattern is registered more than once, the LSP will receive duplicate change notifications. Receiving them @@ -1076,18 +1035,12 @@ def workspace_did_change_watched_files(message) benchmark("index_all") { graph.index_all(additions_and_changes) } benchmark("incremental_resolve") { graph.resolve } - index = @global_state.index changes.each do |change| # File change events include folders, but we're only interested in files uri = URI(change[:uri]) file_path = uri.to_standardized_path next if file_path.nil? || File.directory?(file_path) - if file_path.end_with?(".rb") - handle_ruby_file_change(index, file_path, change[:type]) - next - end - file_name = File.basename(file_path) if file_name == ".rubocop.yml" || file_name == ".rubocop" || file_name == ".rubocop_todo.yml" @@ -1106,33 +1059,6 @@ def workspace_did_change_watched_files(message) end end - #: (RubyIndexer::Index index, String file_path, Integer change_type) -> void - def handle_ruby_file_change(index, file_path, change_type) - @global_state.synchronize do - load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) } - uri = URI::Generic.from_path(load_path_entry: load_path_entry, path: file_path) - - case change_type - when Constant::FileChangeType::CREATED - content = File.read(file_path) - # If we receive a late created notification for a file that has already been claimed by the client, we want to - # handle change for that URI so that the require path tree is updated - @store.key?(uri) ? index.handle_change(uri, content) : index.index_single(uri, content) - when Constant::FileChangeType::CHANGED - content = File.read(file_path) - # We only handle changes on file watched notifications if the client is not the one managing this URI. - # Otherwise, these changes are handled when running the combined requests - index.handle_change(uri, content) unless @store.key?(uri) - when Constant::FileChangeType::DELETED - index.delete(uri) - end - rescue Errno::ENOENT - # If a file is created and then delete immediately afterwards, we will process the created notification before - # we receive the deleted one, but the file no longer exists. This may happen when running a test suite that - # creates and deletes files automatically. - end - end - #: (URI::Generic uri) -> void def handle_rubocop_config_change(uri) return unless defined?(Requests::Support::RuboCopFormatter) @@ -1271,56 +1197,16 @@ def shutdown #: -> void def perform_initial_indexing + # Index progress("indexing-progress", message: "Indexing workspace...") benchmark("index_workspace") { @global_state.graph.index_workspace } + # Resolve progress("indexing-progress", message: "Resolving graph...") benchmark("full_resolve") { @global_state.graph.resolve } - # The begin progress invocation happens during `initialize`, so that the notification is sent before we are - # stuck indexing files - Thread.new do - begin - @global_state.index.index_all do |percentage| - progress("indexing-progress", percentage: percentage) - true - rescue ClosedQueueError - # Since we run indexing on a separate thread, it's possible to kill the server before indexing is complete. - # In those cases, the message queue will be closed and raise a ClosedQueueError. By returning `false`, we - # tell the index to stop working immediately - false - end - rescue StandardError => error - message = "Error while indexing (see [troubleshooting steps]" \ - "(https://shopify.github.io/ruby-lsp/troubleshooting#indexing)): #{error.message}" - send_message(Notification.window_show_message(message, type: Constant::MessageType::ERROR)) - end - - # Indexing produces a high number of short lived object allocations. That might lead to some fragmentation and - # an unnecessarily expanded heap. Compacting ensures that the heap is as small as possible and that future - # allocations and garbage collections are faster - GC.compact unless @test_mode - - @global_state.synchronize do - # If we linearize ancestors while the index is not fully populated, we may end up caching incorrect results - # that were missing namespaces. After indexing is complete, we need to clear the ancestors cache and start - # again - @global_state.index.clear_ancestors - - # The results for code lens depend on ancestor linearization, so we need to clear any previously computed - # responses - @store.each { |_uri, document| document.clear_cache("textDocument/codeLens") } - end - - # Always end the progress notification even if indexing failed or else it never goes away and the user has no - # way of dismissing it - end_progress("indexing-progress") - - # Request a code lens refresh if we populated them before all test parent classes were indexed - if @global_state.client_capabilities.supports_code_lens_refresh - send_message(Request.new(id: @current_request_id, method: "workspace/codeLens/refresh", params: nil)) - end - end + # End + end_progress("indexing-progress") end #: (String id, String title, ?percentage: Integer) -> void @@ -1373,47 +1259,6 @@ def check_formatter_is_available end end - #: (Hash[Symbol, untyped]? indexing_options) -> void - def process_indexing_configuration(indexing_options) - # Need to use the workspace URI, otherwise, this will fail for people working on a project that is a symlink. - index_path = File.join(@global_state.workspace_path, ".index.yml") - - if File.exist?(index_path) - begin - @global_state.index.configuration.apply_config(YAML.parse_file(index_path).to_ruby) - send_message( - Notification.new( - method: "window/showMessage", - params: Interface::ShowMessageParams.new( - type: Constant::MessageType::WARNING, - message: "The .index.yml configuration file is deprecated. " \ - "Please use editor settings to configure the index", - ), - ), - ) - rescue Psych::SyntaxError => e - message = "Syntax error while loading configuration: #{e.message}" - send_message( - Notification.new( - method: "window/showMessage", - params: Interface::ShowMessageParams.new( - type: Constant::MessageType::WARNING, - message: message, - ), - ), - ) - end - return - end - - configuration = @global_state.index.configuration - configuration.workspace_path = @global_state.workspace_path - return unless indexing_options - - # The index expects snake case configurations, but VS Code standardizes on camel case settings - configuration.apply_config(indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase }) - end - #: (Hash[Symbol, untyped] message) -> void def window_show_message_request(message) result = message[:result] diff --git a/lib/ruby_lsp/test_helper.rb b/lib/ruby_lsp/test_helper.rb index 31ee1fef1d..445e42e721 100644 --- a/lib/ruby_lsp/test_helper.rb +++ b/lib/ruby_lsp/test_helper.rb @@ -29,7 +29,6 @@ def with_server(source = nil, uri = Kernel.URI("file:///fake.rb"), stub_no_typec }, }) - server.global_state.index.index_single(uri, source) graph = server.global_state.graph graph.index_source(uri.to_s, source, "ruby") graph.resolve diff --git a/lib/ruby_lsp/test_reporters/lsp_reporter.rb b/lib/ruby_lsp/test_reporters/lsp_reporter.rb index 0c6573d556..f61cad00e4 100644 --- a/lib/ruby_lsp/test_reporters/lsp_reporter.rb +++ b/lib/ruby_lsp/test_reporters/lsp_reporter.rb @@ -5,7 +5,7 @@ require "json" require "socket" require "tmpdir" -require_relative "../../ruby_indexer/lib/ruby_indexer/uri" +require_relative "../uri" module RubyLsp class LspReporter diff --git a/lib/ruby_lsp/type_inferrer.rb b/lib/ruby_lsp/type_inferrer.rb index f87b1b70ee..ada28c74c2 100644 --- a/lib/ruby_lsp/type_inferrer.rb +++ b/lib/ruby_lsp/type_inferrer.rb @@ -78,7 +78,7 @@ def infer_receiver_for_call_node(node, node_context) # When the receiver is a constant reference, we have to try to resolve it to figure out the right # receiver. But since the invocation is directly on the constant, that's the singleton context of that # class/module - receiver_name = RubyIndexer::Index.constant_name(receiver) + receiver_name = Requests::Support::Common.constant_name(receiver) return unless receiver_name resolved_receiver = @graph.resolve_constant(receiver_name, node_context.nesting) @@ -124,7 +124,7 @@ def guess_type(raw_receiver, nesting) declaration = @graph.resolve_constant(guessed_name, nesting) declaration ||= @graph.search(guessed_name).first - return unless declaration + return unless declaration.is_a?(Rubydex::Namespace) GuessedType.new(declaration.name) end diff --git a/lib/ruby_indexer/lib/ruby_indexer/uri.rb b/lib/ruby_lsp/uri.rb similarity index 100% rename from lib/ruby_indexer/lib/ruby_indexer/uri.rb rename to lib/ruby_lsp/uri.rb diff --git a/rakelib/index.rake b/rakelib/index.rake deleted file mode 100644 index 41467c939d..0000000000 --- a/rakelib/index.rake +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -require "ruby_lsp/internal" - -# Based on https://github.com/ruby/prism/blob/main/rakelib/lex.rake - -module GemIndexing - class << self - # This method is responsible for iterating through a list of items and running - # each item in a separate thread. It will block until all items have been - # processed. This is particularly useful for tasks that are IO-bound like - # downloading files or reading files from disk. - def parallelize(items, &block) - Thread.abort_on_exception = true - - queue = Queue.new - items.each { |item| queue << item } - - workers = - ENV.fetch("WORKERS") { 16 }.to_i.times.map do - parallelize_thread(queue, &block) - end - - workers.map(&:join) - end - - private - - # Create a new thread with a minimal number of locals that it can access. - def parallelize_thread(queue, &block) - Thread.new { block.call(queue.shift) until queue.empty? } - end - end -end - -TOP_100_GEM_FILENAME = "rakelib/top_100_gems.yml" -TOP_100_GEMS_DIR = "tmp/top_100_gems" - -namespace :download do - directory TOP_100_GEMS_DIR - - desc "Download the top 100 rubygems under #{TOP_100_GEMS_DIR}/" - task topgems: TOP_100_GEMS_DIR do - $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) - require "net/http" - require "rubygems/package" - require "tmpdir" - - GemIndexing.parallelize(YAML.safe_load_file(TOP_100_GEM_FILENAME)) do |gem_name| - directory = File.expand_path("#{TOP_100_GEMS_DIR}/#{gem_name}") - next if File.directory?(directory) - - puts "Downloading #{gem_name}" - - uri = URI.parse("https://rubygems.org/gems/#{gem_name}.gem") - response = Net::HTTP.get_response(uri) - raise gem_name unless response.is_a?(Net::HTTPSuccess) - - Dir.mktmpdir do |tmpdir| - filepath = File.join(tmpdir, "#{gem_name}.gem") - File.write(filepath, response.body) - Gem::Package.new(filepath).extract_files(directory, "**/*.rb") - end - end - end -end - -# This task indexes against the top 100 gems, and will exit(1) if any fail. -desc "Index against the top 100 rubygems" -task "index:topgems": ["download:topgems"] do - $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) - require "net/http" - require "rubygems/package" - require "tmpdir" - - gem_names = YAML.safe_load_file(TOP_100_GEM_FILENAME) - - errors = [] - GemIndexing.parallelize(gem_names) do |gem_name| - directory = File.expand_path("#{TOP_100_GEMS_DIR}/#{gem_name}") - - index = RubyIndexer::Index.new - - errors = Dir[File.join(directory, "**", "*.rb")].filter_map do |filepath| - print(".") - index.index_file(URI::Generic.from_path(path: filepath)) - nil - rescue => e - errors << { message: e.message, file: filepath } - end - end - - puts "errors: #{errors}" if errors.any? -ensure - FileUtils.rm_rf(TOP_100_GEMS_DIR) -end diff --git a/rakelib/top_100_gems.yml b/rakelib/top_100_gems.yml deleted file mode 100644 index d05ddcb00d..0000000000 --- a/rakelib/top_100_gems.yml +++ /dev/null @@ -1,101 +0,0 @@ ---- -- actioncable-7.0.4.3 -- actionmailbox-7.0.4.3 -- actionmailer-7.0.4.3 -- actionpack-7.0.4.3 -- actiontext-7.0.4.3 -- actionview-7.0.4.3 -- activejob-7.0.4.3 -- activemodel-7.0.4.3 -- activerecord-7.0.4.3 -- activestorage-7.0.4.3 -- activesupport-7.0.4.3 -- addressable-2.8.4 -- autoprefixer-rails-10.4.13.0 -- aws-partitions-1.744.0 -- aws-sdk-cloudformation-1.77.0 -- aws-sdk-cloudfront-1.76.0 -- aws-sdk-cloudwatch-1.72.0 -- aws-sdk-core-3.171.0 -- aws-sdk-dynamodb-1.83.0 -- aws-sdk-ec2-1.375.0 -- aws-sdk-iam-1.77.0 -- aws-sdk-kinesis-1.45.0 -- aws-sdk-kms-1.63.0 -- aws-sdk-lambda-1.93.0 -- aws-sdk-rds-1.175.0 -- aws-sdk-resources-3.162.0 -- aws-sdk-s3-1.120.1 -- aws-sdk-secretsmanager-1.73.0 -- aws-sdk-sns-1.60.0 -- aws-sdk-ssm-1.150.0 -- backports-3.24.1 -- brakeman-5.4.1 -- bundler-2.4.11 -- capybara-3.39.0 -- concurrent-ruby-1.2.2 -- connection_pool-2.4.0 -- dalli-3.2.4 -- database_cleaner-2.0.2 -- devise-4.9.2 -- dry-types-1.7.1 -- elasticsearch-8.7.0 -- elasticsearch-api-8.7.0 -- excon-0.99.0 -- faker-3.1.1 -- faraday-retry-2.1.0 -- fastlane-2.212.1 -- fog-aws-3.18.0 -- git-1.18.0 -- google-cloud-errors-1.3.1 -- google-protobuf-3.22.2 -- googleauth-1.5.1 -- graphql-2.0.21 -- grpc-1.53.0 -- jwt-2.7.0 -- loofah-2.20.0 -- mail-2.8.1 -- mime-types-data-3.2023.0218.1 -- minitest-5.18.0 -- msgpack-1.7.0 -- net-http-persistent-4.0.2 -- net-ssh-7.1.0 -- newrelic_rpm-9.1.0 -- nio4r-2.5.9 -- nokogiri-1.14.3 -- octokit-6.1.1 -- oj-3.14.3 -- parser-3.2.2.0 -- pg-1.4.6 -- plist-3.7.0 -- puma-6.2.1 -- rack-3.0.7 -- rack-cors-2.0.1 -- rack-protection-3.0.6 -- rack-test-2.1.0 -- rails-7.0.4.3 -- railties-7.0.4.3 -- raindrops-0.20.1 -- redis-store-1.9.2 -- regexp_parser-2.7.0 -- responders-3.1.0 -- rouge-4.1.0 -- rspec-core-3.12.1 -- rspec-mocks-3.12.5 -- rubocop-1.50.0 -- rubocop-ast-1.28.0 -- rubocop-performance-1.17.1 -- rubocop-rails-2.19.0 -- rubocop-rspec-2.19.0 -- ruby-progressbar-1.13.0 -- ruby_parser-3.20.0 -- rubygems-update-3.4.11 -- selenium-webdriver-4.8.6 -- sidekiq-7.0.8 -- sinatra-3.0.6 -- slop-4.10.1 -- sqlite3-1.6.2 -- thin-1.8.2 -- tilt-2.1.0 -- yard-0.9.32 -- zeitwerk-2.6.7 diff --git a/ruby-lsp.gemspec b/ruby-lsp.gemspec index 5d425950d6..4adda3fb60 100644 --- a/ruby-lsp.gemspec +++ b/ruby-lsp.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| s.homepage = "https://github.com/Shopify/ruby-lsp" s.license = "MIT" - s.files = Dir.glob("lib/**/*.rb").grep_v(%r{^lib/ruby_indexer/test/}) + ["README.md", "VERSION", "LICENSE.txt"] + s.files = Dir.glob("lib/**/*.rb") + ["README.md", "VERSION", "LICENSE.txt"] s.bindir = "exe" s.executables = ["ruby-lsp", "ruby-lsp-check", "ruby-lsp-launcher", "ruby-lsp-test-exec"] s.require_paths = ["lib"] diff --git a/sorbet/rbi/shims/test_case.rbi b/sorbet/rbi/shims/test_case.rbi deleted file mode 100644 index 5eb67f30da..0000000000 --- a/sorbet/rbi/shims/test_case.rbi +++ /dev/null @@ -1,7 +0,0 @@ -# typed: true - -class RubyIndexer::TestCase < Minitest::Test - def initialize - @index = nil #: RubyIndexer::Index # rubocop:disable Layout/LeadingCommentSpace - end -end diff --git a/test/fixtures/minitest_example.rb b/test/fixtures/minitest_example.rb index 687e97f46b..8941580cb9 100644 --- a/test/fixtures/minitest_example.rb +++ b/test/fixtures/minitest_example.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "minitest/autorun" -require_relative "../../lib/ruby_indexer/lib/ruby_indexer/uri" +require_relative "../../lib/ruby_lsp/uri" # We are only testing the output of the runner, there's no need for to be random. Minitest::Test.i_suck_and_my_tests_are_order_dependent! diff --git a/test/integration_test.rb b/test/integration_test.rb index 66d8dc0494..5862985a0e 100644 --- a/test/integration_test.rb +++ b/test/integration_test.rb @@ -25,15 +25,6 @@ def test_ruby_lsp_invalid_option_rejected assert_match(/invalid option/, stderr) end - def test_ruby_lsp_doctor_works - skip("CI only") unless ENV["CI"] - - in_isolation do - system("bundle exec ruby-lsp --doctor") - assert_equal(0, $CHILD_STATUS) - end - end - def test_ruby_lsp_check_works skip("CI only") unless ENV["CI"] diff --git a/test/requests/code_lens_expectations_test.rb b/test/requests/code_lens_expectations_test.rb index bfdb74628e..18a2add7bd 100644 --- a/test/requests/code_lens_expectations_test.rb +++ b/test/requests/code_lens_expectations_test.rb @@ -233,9 +233,6 @@ class Test < Minitest::Test; end params: { textDocument: { uri: uri }, position: { line: 1, character: 2 } }, }) - # Pop the re-indexing notification - server.pop_response - result = server.pop_response assert_instance_of(RubyLsp::Result, result) diff --git a/test/requests/discover_tests_test.rb b/test/requests/discover_tests_test.rb index 1f2a04fab5..1f81bf4991 100644 --- a/test/requests/discover_tests_test.rb +++ b/test/requests/discover_tests_test.rb @@ -82,6 +82,25 @@ def test_something_else; end end end + def test_minitest_with_compact_path_resolving_to_outer_namespace + source = <<~RUBY + module Foo + end + + module Bar + class Foo::MyTest < Minitest::Test + def test_something; end + end + end + RUBY + + with_minitest_test(source) do |items| + assert_equal(["Foo::MyTest"], items.map { |i| i[:id] }) + assert_equal(["Foo::MyTest#test_something"], items[0][:children].map { |i| i[:id] }) + assert_all_items_tagged_with(items, :minitest) + end + end + def test_minitest_with_dynamic_constant_path source = File.read("test/fixtures/minitest_with_dynamic_constant_path.rb") @@ -144,13 +163,15 @@ def test_something_else; end RUBY with_server(source, URI::Generic.from_path(path: "/test/foo_test.rb")) do |server, uri| - server.global_state.index.index_single(URI("/other_file.rb"), <<~RUBY) + graph = server.global_state.graph + graph.index_source(URI("/other_file.rb").to_s, <<~RUBY, "ruby") module Test module Unit class TestCase; end end end RUBY + graph.resolve server.global_state.stubs(:enabled_feature?).returns(true) @@ -158,8 +179,6 @@ class TestCase; end textDocument: { uri: uri }, }) - # Discard the indexing log message - server.pop_response items = get_response(server) assert_equal(9, items.length) @@ -228,13 +247,15 @@ def test_something_else; end RUBY with_server(source, URI::Generic.from_path(path: "/test/foo_test.rb")) do |server, uri| - server.global_state.index.index_single(URI("/other_file.rb"), <<~RUBY) + graph = server.global_state.graph + graph.index_source(URI("/other_file.rb").to_s, <<~RUBY, "ruby") module Test module Unit class TestCase; end end end RUBY + graph.resolve state = server.global_state state.stubs(:enabled_feature?).returns(true) @@ -252,8 +273,6 @@ class TestCase; end textDocument: { uri: uri }, }) - # Discard the indexing log message - server.pop_response items = get_response(server) assert_empty(items) end @@ -271,13 +290,15 @@ def test_something_else; end RUBY with_server(source, URI::Generic.from_path(path: "/tests/something.rb")) do |server, uri| - server.global_state.index.index_single(URI("/other_file.rb"), <<~RUBY) + graph = server.global_state.graph + graph.index_source(URI("/other_file.rb").to_s, <<~RUBY, "ruby") module Test module Unit class TestCase; end end end RUBY + graph.resolve server.global_state.stubs(:enabled_feature?).returns(true) @@ -285,8 +306,6 @@ class TestCase; end textDocument: { uri: uri }, }) - # Discard the indexing log message - server.pop_response items = get_response(server) assert_empty(items) end diff --git a/test/requests/document_link_expectations_test.rb b/test/requests/document_link_expectations_test.rb index 6893421735..86af801a98 100644 --- a/test/requests/document_link_expectations_test.rb +++ b/test/requests/document_link_expectations_test.rb @@ -45,7 +45,6 @@ def bar params: { textDocument: { uri: uri } }, ) - server.pop_response assert_empty(server.pop_response.response) end end @@ -64,7 +63,6 @@ def bar params: { textDocument: { uri: uri } }, ) - server.pop_response assert_empty(server.pop_response.response) end end @@ -83,7 +81,6 @@ def bar params: { textDocument: { uri: uri } }, ) - server.pop_response assert_empty(server.pop_response.response) end end @@ -143,7 +140,6 @@ def bar params: { textDocument: { uri: uri } }, ) - server.pop_response assert_empty(server.pop_response.response) end end diff --git a/test/requests/document_symbol_expectations_test.rb b/test/requests/document_symbol_expectations_test.rb index ab5bedd978..9d21ad1186 100644 --- a/test/requests/document_symbol_expectations_test.rb +++ b/test/requests/document_symbol_expectations_test.rb @@ -90,9 +90,6 @@ class Foo params: { textDocument: { uri: uri } }, }) - # Pop the re-indexing notification - server.pop_response - result = server.pop_response assert_instance_of(RubyLsp::Result, result) diff --git a/test/requests/support/common_test.rb b/test/requests/support/common_test.rb deleted file mode 100644 index 00224bbbb9..0000000000 --- a/test/requests/support/common_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "test_helper" - -module RubyLsp - class CommonTest < Minitest::Test - include Requests::Support::Common - - def test_kinds_are_defined_for_every_entry - index = RubyIndexer::Index.new - index.index_all - - entries = index.instance_variable_get(:@entries).values.flatten - entries.each do |entry| - kind = kind_for_entry(entry) - refute_equal(kind, Constant::SymbolKind::NULL, "Kind not defined for entry: #{entry.inspect}") - end - end - end -end diff --git a/test/ruby_document_test.rb b/test/ruby_document_test.rb index 36b915f1fd..e0616f470f 100644 --- a/test/ruby_document_test.rb +++ b/test/ruby_document_test.rb @@ -1090,35 +1090,6 @@ class Foo RUBY end - def test_should_index_for_inserts - document = RubyLsp::RubyDocument.new(source: +<<~RUBY, version: 1, uri: @uri, global_state: @global_state) - class Foo - end - RUBY - assert_predicate(document, :should_index?) - - range = { start: { line: 0, character: 9 }, end: { line: 0, character: 9 } } - document.push_edits([{ range: range, text: "t" }], version: 2) - - assert_instance_of(RubyLsp::Document::Insert, document.last_edit) - assert_predicate(document, :should_index?) - end - - def test_should_index_for_replaces - document = RubyLsp::RubyDocument.new(source: +<<~RUBY, version: 1, uri: @uri, global_state: @global_state) - class Foo - end - RUBY - - assert_predicate(document, :should_index?) - - range = { start: { line: 0, character: 6 }, end: { line: 0, character: 9 } } - document.push_edits([{ range: range, text: "Bar" }], version: 2) - - assert_instance_of(RubyLsp::Document::Replace, document.last_edit) - assert_predicate(document, :should_index?) - end - private def assert_error_edit(actual, error_range) diff --git a/test/server_test.rb b/test/server_test.rb index a8bdd4ddf0..4651fd1574 100644 --- a/test/server_test.rb +++ b/test/server_test.rb @@ -188,22 +188,6 @@ def test_server_info_includes_formatter assert_equal("rubocop_internal", hash.dig("formatter")) end - def test_initialized_recovers_from_indexing_failures - @server.global_state.index.expects(:index_all).once.raises(StandardError, "boom!") - capture_subprocess_io do - @server.process_message({ method: "initialized" }) - end - - notification = find_message(RubyLsp::Notification, "window/showMessage") - expected_message = "Error while indexing (see [troubleshooting steps]" \ - "(https://shopify.github.io/ruby-lsp/troubleshooting#indexing)): boom!" - assert_equal( - expected_message, - notification.params #: as RubyLsp::Interface::ShowMessageParams - .message, - ) - end - def test_formatting_errors_push_window_notification @server.global_state.expects(:formatter).raises(StandardError, "boom").once @@ -226,21 +210,6 @@ def test_formatting_errors_push_window_notification ) end - def test_applies_workspace_uri_to_indexing_configs_even_if_no_configs_are_specified - @server.process_message({ - id: 1, - method: "initialize", - params: { - initializationOptions: {}, - capabilities: { general: { positionEncodings: ["utf-8"] } }, - workspaceFolders: [{ uri: URI::Generic.from_path(path: "/fake").to_s }], - }, - }) - - index = @server.instance_variable_get(:@global_state).index - assert_equal("/fake", index.configuration.instance_variable_get(:@workspace_path)) - end - def test_returns_nil_diagnostics_and_formatting_for_files_outside_workspace capture_subprocess_io do @server.process_message({ @@ -293,23 +262,6 @@ def test_did_close_clears_diagnostics ) end - def test_handles_invalid_configuration - File.write(".index.yml", "} invalid yaml") - - capture_subprocess_io do - @server.process_message(id: 1, method: "initialize", params: {}) - end - - notification = find_message(RubyLsp::Notification, "window/showMessage") - assert_match( - /Syntax error while loading configuration/, - notification.params #: as RubyLsp::Interface::ShowMessageParams - .message, - ) - ensure - FileUtils.rm(".index.yml") - end - def test_shows_error_if_formatter_set_to_rubocop_but_rubocop_not_available capture_subprocess_io do @server.process_message(id: 1, method: "initialize", params: { @@ -444,82 +396,6 @@ def test_send_log_message_passes_type_parameter assert_equal(RubyLsp::Constant::MessageType::ERROR, log.params.type) end - def test_changed_file_only_indexes_ruby - path = File.join(Dir.pwd, "lib", "foo.rb") - File.write(path, "class Foo\nend") - uri = URI::Generic.from_path(path: path) - - begin - @server.global_state.index.index_all(uris: []) - @server.global_state.index.expects(:index_single).once.with do |uri| - uri.full_path == path - end - - @server.process_message({ - method: "workspace/didChangeWatchedFiles", - params: { - changes: [ - { - uri: uri, - type: RubyLsp::Constant::FileChangeType::CREATED, - }, - { - uri: URI("file:///.rubocop.yml").to_s, - type: RubyLsp::Constant::FileChangeType::CREATED, - }, - ], - }, - }) - ensure - FileUtils.rm(path) - end - end - - def test_did_change_watched_files_does_not_fail_for_non_existing_files - @server.global_state.index.index_all(uris: []) - @server.process_message({ - method: "workspace/didChangeWatchedFiles", - params: { - changes: [ - { - uri: URI::Generic.from_path(path: File.join(Dir.pwd, "lib", "non_existing.rb")).to_s, - type: RubyLsp::Constant::FileChangeType::CREATED, - }, - ], - }, - }) - - assert_raises(Timeout::Error) do - Timeout.timeout(0.5) do - notification = find_message(RubyLsp::Notification, "window/logMessage") - flunk(notification.params.message) - end - end - end - - def test_did_change_watched_files_handles_deletions - path = File.join(Dir.pwd, "lib", "foo.rb") - - @server.global_state.index.expects(:delete).once.with do |uri| - uri.full_path == path - end - - uri = URI::Generic.from_path(path: path).to_s - - @server.global_state.index.index_all(uris: []) - @server.process_message({ - method: "workspace/didChangeWatchedFiles", - params: { - changes: [ - { - uri: uri, - type: RubyLsp::Constant::FileChangeType::DELETED, - }, - ], - }, - }) - end - def test_did_change_watched_files_reports_addon_errors Class.new(RubyLsp::Addon) do def activate(global_state, outgoing_queue); end @@ -562,7 +438,6 @@ def version bar.expects(:workspace_did_change_watched_files).once begin - @server.global_state.index.index_all(uris: []) @server.process_message({ method: "workspace/didChangeWatchedFiles", params: { @@ -584,7 +459,6 @@ def version end def test_did_change_watched_files_processes_unique_change_entries - @server.global_state.index.index_all(uris: []) @server.expects(:handle_rubocop_config_change).once @server.process_message({ method: "workspace/didChangeWatchedFiles", @@ -671,26 +545,6 @@ def test_dsl_error_setup_error_does_not_send_telemetry )) end - def test_handles_editor_indexing_settings - capture_io do - @server.process_message({ - id: 1, - method: "initialize", - params: { - initializationOptions: { - indexing: { - excludedGems: ["foo_gem"], - includedGems: ["bar_gem"], - }, - }, - }, - }) - end - - assert_includes(@server.global_state.index.configuration.instance_variable_get(:@excluded_gems), "foo_gem") - assert_includes(@server.global_state.index.configuration.instance_variable_get(:@included_gems), "bar_gem") - end - def test_closing_document_before_computing_features_does_not_error uri = URI("file:///foo.rb") @@ -982,225 +836,44 @@ def test_requests_cancelled_during_processing_are_deleted_from_cancelled_request assert_empty(@server.instance_variable_get(:@cancelled_requests)) end - def test_unsaved_changes_are_indexed_when_computing_automatic_features - uri = URI("file:///foo.rb") - index = @server.global_state.index - - # Simulate opening a file. First, send the notification to open the file with a class inside - @server.process_message({ - method: "textDocument/didOpen", - params: { - textDocument: { - uri: uri, - text: +"class Foo\nend", - version: 1, - languageId: "ruby", - }, - }, - }) - # Fire the automatic features requests to trigger indexing - @server.process_message({ - id: 1, - method: "textDocument/documentSymbol", - params: { textDocument: { uri: uri } }, - }) - - entries = index["Foo"] - assert_equal(1, entries.length) - - # Modify the file without saving - @server.process_message({ - method: "textDocument/didChange", - params: { - textDocument: { uri: uri, version: 2 }, - contentChanges: [ - { text: " def bar\n end\n", range: { start: { line: 1, character: 0 }, end: { line: 1, character: 0 } } }, - ], - }, - }) - - # Parse the document after it was modified. This occurs automatically when we receive a text document request, to - # avoid parsing the document multiple times, but that depends on request coming in through the STDIN pipe, which - # isn't reproduced here. Parsing manually matches what happens normally - store = @server.instance_variable_get(:@store) - store.get(uri).parse! - - # Trigger the automatic features again - @server.process_message({ - id: 2, - method: "textDocument/documentSymbol", - params: { textDocument: { uri: uri } }, - }) - - # There should still only be one entry for each declaration, but we should have picked up the new ones - entries = index["Foo"] - assert_equal(1, entries.length) - - entries = index["bar"] - assert_equal(1, entries.length) - end - - def test_ancestors_are_recomputed_even_on_unsaved_changes - uri = URI("file:///foo.rb") - index = @server.global_state.index - source = +<<~RUBY - module Bar; end - - class Foo - extend Bar - end - RUBY - - # Simulate opening a file. First, send the notification to open the file with a class inside - @server.process_message({ - method: "textDocument/didOpen", - params: { - textDocument: { - uri: uri, - text: source, - version: 1, - languageId: "ruby", - }, - }, - }) - # Fire the automatic features requests to trigger indexing - @server.process_message({ - id: 1, - method: "textDocument/documentSymbol", - params: { textDocument: { uri: uri } }, - }) - - assert_equal(["Foo::", "Bar"], index.linearized_ancestors_of("Foo::")) - - # Delete the extend - @server.process_message({ - method: "textDocument/didChange", - params: { - textDocument: { uri: uri, version: 2 }, - contentChanges: [ - { text: "", range: { start: { line: 3, character: 0 }, end: { line: 3, character: 12 } } }, - ], - }, - }) - - # Parse the document after it was modified. This occurs automatically when we receive a text document request, to - # avoid parsing the document multiple times, but that depends on request coming in through the STDIN pipe, which - # isn't reproduced here. Parsing manually matches what happens normally - store = @server.instance_variable_get(:@store) - document = store.get(uri) - - assert_equal(<<~RUBY, document.source) - module Bar; end - - class Foo - - end - RUBY - - document.parse! - - # Trigger the automatic features again - @server.process_message({ - id: 2, - method: "textDocument/documentSymbol", - params: { textDocument: { uri: uri } }, - }) - - result = find_message(RubyLsp::Result, id: 2) - refute_nil(result) - - assert_equal(["Foo::"], index.linearized_ancestors_of("Foo::")) - end - - def test_edits_outside_of_declarations_do_not_trigger_indexing + def test_server_indexes_upon_edit uri = URI("file:///foo.rb") - index = @server.global_state.index + graph = @server.global_state.graph + initial_source = +"class Bar; end\nclass Foo\nend" - # Simulate opening a file. First, send the notification to open the file with a class inside @server.process_message({ method: "textDocument/didOpen", params: { textDocument: { uri: uri, - text: +"class Foo\n\nend", + text: initial_source, version: 1, languageId: "ruby", }, }, }) - # Fire the automatic features requests to trigger indexing - @server.process_message({ - id: 1, - method: "textDocument/documentSymbol", - params: { textDocument: { uri: uri } }, - }) + graph.index_source(uri.to_s, initial_source, "ruby") + graph.resolve - entries = index["Foo"] - assert_equal(1, entries.length) + foo = graph["Foo"] #: as Rubydex::Class + refute_nil(foo) + refute_includes(foo.ancestors.map(&:name), "Bar") - # Modify the file without saving + # Modify the file without saving so that `Foo` inherits from `Bar`. The server reindexes the document on each + # change, so the graph should reflect the new ancestry without the file ever being saved to disk @server.process_message({ method: "textDocument/didChange", params: { textDocument: { uri: uri, version: 2 }, contentChanges: [ - { text: "d", range: { start: { line: 1, character: 0 }, end: { line: 1, character: 0 } } }, + { text: " < Bar", range: { start: { line: 1, character: 9 }, end: { line: 1, character: 9 } } }, ], }, }) - # Parse the document after it was modified. This occurs automatically when we receive a text document request, to - # avoid parsing the document multiple times, but that depends on request coming in through the STDIN pipe, which - # isn't reproduced here. Parsing manually matches what happens normally - store = @server.instance_variable_get(:@store) - store.get(uri).parse! - - # Trigger the automatic features again - index.expects(:delete).never - @server.process_message({ - id: 2, - method: "textDocument/documentSymbol", - params: { textDocument: { uri: uri } }, - }) - - entries = index["Foo"] - assert_equal(1, entries.length) - end - - def test_rubocop_config_changes_trigger_workspace_diagnostic_refresh - @server.process_message({ - id: 1, - method: "initialize", - params: { - initializationOptions: {}, - capabilities: { - general: { - positionEncodings: ["utf-8"], - }, - workspace: { diagnostics: { refreshSupport: true } }, - }, - }, - }) - @server.global_state.index.index_all(uris: []) - - [".rubocop.yml", ".rubocop", ".rubocop_todo.yml"].each do |config_file| - uri = URI::Generic.from_path(path: File.join(Dir.pwd, config_file)).to_s - - @server.process_message({ - method: "workspace/didChangeWatchedFiles", - params: { - changes: [ - { - uri: uri, - type: RubyLsp::Constant::FileChangeType::CHANGED, - }, - ], - }, - }) - - request = find_message(RubyLsp::Request) - assert_equal("workspace/diagnostic/refresh", request.method) - end + foo = graph["Foo"] #: as Rubydex::Class + refute_nil(foo) + assert_includes(foo.ancestors.map(&:name), "Bar") end def test_compose_bundle_creates_file_to_skip_next_compose @@ -1326,107 +999,6 @@ def test_compose_bundle_does_not_fail_if_restarting_on_lockfile_deletion end end - def test_does_not_index_on_did_change_watched_files_if_document_is_managed_by_client - path = File.join(Dir.pwd, "lib", "foo.rb") - source = <<~RUBY - class Foo - end - RUBY - File.write(path, source) - uri = URI::Generic.from_path(path: path).to_s - - begin - @server.process_message({ - method: "textDocument/didOpen", - params: { - textDocument: { - uri: uri, - text: source, - version: 1, - languageId: "ruby", - }, - }, - }) - - @server.global_state.index.index_all(uris: []) - @server.global_state.index.expects(:handle_change).never - @server.process_message({ - method: "workspace/didChangeWatchedFiles", - params: { - changes: [ - { - uri: uri, - type: RubyLsp::Constant::FileChangeType::CHANGED, - }, - ], - }, - }) - - @server.global_state.index.expects(:handle_change).once - @server.process_message({ - method: "textDocument/documentSymbol", - params: { - textDocument: { - uri: uri, - }, - }, - }) - ensure - FileUtils.rm(path) if File.exist?(path) - end - end - - def test_receiving_a_created_file_watch_notification_after_did_open_uses_handle_change - path = File.join(Dir.pwd, "lib", "foo.rb") - source = <<~RUBY - class Foo - end - RUBY - File.write(path, source) - uri = URI::Generic.from_path(path: path) - - begin - # Simulate the editor opening a document and then immediately firing a document symbol request - @server.process_message({ - method: "textDocument/didOpen", - params: { - textDocument: { - uri: uri, - text: source, - version: 1, - languageId: "ruby", - }, - }, - }) - @server.process_message({ - method: "textDocument/documentSymbol", - params: { textDocument: { uri: uri } }, - }) - - @server.global_state.index.index_all(uris: []) - # Then send a late did change watched files notification for the creation of the file - @server.process_message({ - method: "workspace/didChangeWatchedFiles", - params: { - changes: [ - { - uri: uri, - type: RubyLsp::Constant::FileChangeType::CREATED, - }, - ], - }, - }) - - entries = @server.global_state.index["Foo"] - assert_equal(1, entries&.length) - - uris = @server.global_state.index.search_require_paths("foo") - assert_equal(["foo"], uris.map(&:require_path)) - ensure - FileUtils.rm(path) if File.exist?(path) - end - end - def test_diagnose_state @server.process_message({ method: "textDocument/didOpen", @@ -1448,87 +1020,6 @@ def test_diagnose_state assert_equal(0, result.response[:incomingQueueSize]) end - def test_modifying_files_during_initial_indexing_does_not_duplicate_entries - path = File.join(Dir.pwd, "lib", "foo.rb") - uri = URI::Generic.from_path(path: path) - - begin - @server.process_message({ - id: 1, - method: "initialize", - params: { - initializationOptions: {}, - capabilities: { general: { positionEncodings: ["utf-8"] }, window: { workDoneProgress: true } }, - }, - }) - - # Start indexing - File.write(path, "class Foo\nend") - @server.process_message({ method: "initialized", params: {} }) - - # Then immediately notify that a file was modified before indexing is finished - File.write(path, "class Foo\n def bar\n end\nend") - @server.process_message({ - method: "workspace/didChangeWatchedFiles", - params: { - changes: [ - { - uri: uri.to_s, - type: RubyLsp::Constant::FileChangeType::CHANGED, - }, - ], - }, - }) - - wait_for_indexing - - # There should not be a duplicate declaration - index = @server.global_state.index - assert_equal(1, index["Foo"]&.length) - ensure - FileUtils.rm(path) - end - end - - def test_requests_code_lens_refresh_after_indexing - @server.process_message({ - id: 1, - method: "initialize", - params: { - initializationOptions: {}, - capabilities: { - general: { positionEncodings: ["utf-8"] }, - window: { workDoneProgress: true }, - workspace: { codeLens: { refreshSupport: true } }, - }, - }, - }) - - @server.process_message({ method: "initialized", params: {} }) - - wait_for_indexing - - request = find_message(RubyLsp::Request, "workspace/codeLens/refresh") - refute_nil(request) - end - - def test_busts_ancestor_cache_after_indexing - @server.process_message({ - id: 1, - method: "initialize", - params: { - initializationOptions: {}, - capabilities: { general: { positionEncodings: ["utf-8"] }, window: { workDoneProgress: true } }, - }, - }) - - @server.process_message({ method: "initialized", params: {} }) - - wait_for_indexing - - assert_empty(@server.global_state.index.instance_variable_get(:@ancestors)) - end - def test_code_lens_resolve_populates_run_test_command arguments = ["/workspace/test/foo_test.rb", "FooTest#test_something"] @server.process_message({ @@ -1659,7 +1150,6 @@ def version @server.load_addons begin - @server.global_state.index.index_all(uris: []) @server.process_message({ method: "workspace/didChangeWatchedFiles", params: { diff --git a/test/type_inferrer_test.rb b/test/type_inferrer_test.rb index 1d564a1e21..14138f8d88 100644 --- a/test/type_inferrer_test.rb +++ b/test/type_inferrer_test.rb @@ -321,6 +321,28 @@ class User assert_equal("User", @type_inferrer.infer_receiver_type(node_context).name) end + def test_infer_guessed_types_returns_nil_when_resolved_constant_is_not_a_namespace + node_context = index_and_locate(<<~RUBY, { line: 2, character: 4 }) + User = "guest" + + user.name + RUBY + + assert_nil(@type_inferrer.infer_receiver_type(node_context)) + end + + def test_infer_guessed_types_returns_nil_when_search_fallback_finds_non_namespace + node_context = index_and_locate(<<~RUBY, { line: 4, character: 9 }) + module Foo + SOMETHING = 1 + end + + something.bar + RUBY + + assert_nil(@type_inferrer.infer_receiver_type(node_context)) + end + def test_infer_guessed_types_inside_nesting node_context = index_and_locate(<<~RUBY, { line: 9, character: 9 }) module Blog