# typed: strict # frozen_string_literal: true begin require "rubocop" rescue LoadError return end begin gem("rubocop", ">= 1.4.0") rescue LoadError raise StandardError, "Incompatible RuboCop version. Ruby LSP requires >= 1.4.0" end if RuboCop.const_defined?(:LSP) # This condition will be removed when requiring RuboCop >= 1.61. RuboCop::LSP.enable end module RubyLsp module Requests module Support class InternalRuboCopError < StandardError extend T::Sig MESSAGE = <<~EOS An internal error occurred %s. Updating to a newer version of RuboCop may solve this. For more details, run RuboCop on the command line. EOS sig { params(rubocop_error: T.any(RuboCop::ErrorWithAnalyzedFileLocation, StandardError)).void } def initialize(rubocop_error) message = case rubocop_error when RuboCop::ErrorWithAnalyzedFileLocation format(MESSAGE, "for the #{rubocop_error.cop.name} cop") when StandardError format(MESSAGE, rubocop_error.message) end super(message) end end # :nodoc: class RuboCopRunner < RuboCop::Runner extend T::Sig class ConfigurationError < StandardError; end DEFAULT_ARGS = T.let( [ "--stderr", # Print any output to stderr so that our stdout does not get polluted "--force-exclusion", "--format", "RuboCop::Formatter::BaseFormatter", # Suppress any output by using the base formatter ], T::Array[String], ) sig { returns(T::Array[RuboCop::Cop::Offense]) } attr_reader :offenses sig { returns(::RuboCop::Config) } attr_reader :config_for_working_directory begin RuboCop::Options.new.parse(["--raise-cop-error"]) DEFAULT_ARGS << "--raise-cop-error" rescue OptionParser::InvalidOption # older versions of RuboCop don't support this flag end DEFAULT_ARGS.freeze sig { params(args: String).void } def initialize(*args) @options = T.let({}, T::Hash[Symbol, T.untyped]) @offenses = T.let([], T::Array[RuboCop::Cop::Offense]) @errors = T.let([], T::Array[String]) @warnings = T.let([], T::Array[String]) args += DEFAULT_ARGS rubocop_options = ::RuboCop::Options.new.parse(args).first config_store = ::RuboCop::ConfigStore.new config_store.options_config = rubocop_options[:config] if rubocop_options[:config] @config_for_working_directory = T.let(config_store.for_pwd, ::RuboCop::Config) super(rubocop_options, config_store) end sig { params(path: String, contents: String).void } def run(path, contents) # Clear Runner state between runs since we get a single instance of this class # on every use site. @errors = [] @warnings = [] @offenses = [] @options[:stdin] = contents super([path]) # RuboCop rescues interrupts and then sets the `@aborting` variable to true. We don't want them to be rescued, # so here we re-raise in case RuboCop received an interrupt. raise Interrupt if aborting? rescue RuboCop::Runner::InfiniteCorrectionLoop => error raise Formatting::Error, error.message rescue RuboCop::ValidationError => error raise ConfigurationError, error.message rescue StandardError => error raise InternalRuboCopError, error end sig { returns(String) } def formatted_source @options[:stdin] end class << self extend T::Sig sig { params(cop_name: String).returns(T.nilable(T.class_of(RuboCop::Cop::Base))) } def find_cop_by_name(cop_name) cop_registry[cop_name]&.first end private sig { returns(T::Hash[String, [T.class_of(RuboCop::Cop::Base)]]) } def cop_registry @cop_registry ||= T.let( RuboCop::Cop::Registry.global.to_h, T.nilable(T::Hash[String, [T.class_of(RuboCop::Cop::Base)]]), ) end end private sig { params(_file: String, offenses: T::Array[RuboCop::Cop::Offense]).void } def file_finished(_file, offenses) @offenses = offenses end end end end end