lib/steep/drivers/checkfile.rb



module Steep
  module Drivers
    class Checkfile
      LSP = LanguageServer::Protocol

      attr_reader :stdout
      attr_reader :stderr
      attr_reader :command_line_args
      attr_accessor :all_ruby, :all_rbs
      attr_reader :stdin_input
      attr_reader :jobs_option

      include Utils::DriverHelper

      def initialize(stdout:, stderr:)
        @stdout = stdout
        @stderr = stderr
        @command_line_args = []

        @all_rbs = false
        @all_ruby = false
        @stdin_input = {}

        @jobs_option = Utils::JobsOption.new()
      end

      def run
        return 0 if command_line_args.empty? && !all_rbs && !all_ruby && stdin_input.empty?

        project = load_config()

        client_read, server_write = IO.pipe
        server_read, client_write = IO.pipe

        client_reader = LanguageServer::Protocol::Transport::Io::Reader.new(client_read)
        client_writer = LanguageServer::Protocol::Transport::Io::Writer.new(client_write)

        server_reader = LanguageServer::Protocol::Transport::Io::Reader.new(server_read)
        server_writer = LanguageServer::Protocol::Transport::Io::Writer.new(server_write)

        # @type var target_paths: Set[Pathname]
        target_paths = Set[]
        # @type var signature_paths: Set[Pathname]
        signature_paths = Set[]

        loader = Services::FileLoader.new(base_dir: project.base_dir)
        project.targets.each do |target|
          ruby_patterns =
            case
            when all_ruby
              []
            when command_line_args.empty?
              nil
            else
              command_line_args
            end #: Array[String]?

          if ruby_patterns
            loader.each_path_in_patterns(target.source_pattern, ruby_patterns) do |path|
              target_paths << path
            end
          end

          rbs_patterns =
            case
            when all_rbs
              []
            when command_line_args.empty?
              nil
            else
              command_line_args
            end #: Array[String]

          if rbs_patterns
            loader.each_path_in_patterns(target.signature_pattern, rbs_patterns) do |path|
              signature_paths << path
            end
          end
        end

        stdin_input.each_key do |path|
          case
          when project.target_for_signature_path(path)
            signature_paths << path
          when project.target_for_source_path(path)
            target_paths << path
          end
        end

        files = target_paths + signature_paths

        count =
          if jobs_option.jobs_count
            jobs_option.jobs_count
          else
            [
              files.size + 2,
              jobs_option.default_jobs_count
            ].min || raise
          end

        Steep.logger.info { "Starting #{count} workers for #{files.size} files..." }

        typecheck_workers = Server::WorkerProcess.start_typecheck_workers(
          steepfile: project.steepfile_path,
          args: [],
          delay_shutdown: true,
          steep_command: jobs_option.steep_command,
          count: count
        )

        master = Server::Master.new(
          project: project,
          reader: server_reader,
          writer: server_writer,
          interaction_worker: nil,
          typecheck_workers: typecheck_workers
        )
        master.typecheck_automatically = false

        main_thread = Thread.start do
          Thread.current.abort_on_exception = true
          master.start()
        end

        Steep.logger.info { "Initializing server" }
        initialize_id = request_id()
        client_writer.write({ method: :initialize, id: initialize_id, params: DEFAULT_CLI_LSP_INITIALIZE_PARAMS })
        wait_for_response_id(reader: client_reader, id: initialize_id)

        stdin_input.each do |path, content|
          uri = PathHelper.to_uri(project.absolute_path(path))

          master.broadcast_notification(
            {
              method: "textDocument/didChange",
              params: {
              textDocument: { uri: uri, version: 0 },
              contentChanges: [{ text: content }]
              }
            }
          )
          master.broadcast_notification(
            {
              method: "textDocument/didSave",
              params: {
                textDocument: { uri: uri }
              }
            }
          )
        end

        ping_guid = master.fresh_request_id()
        client_writer.write({ method: "$/ping", id: ping_guid, params: {}})
        wait_for_response_id(reader: client_reader, id: ping_guid)

        request_guid = master.fresh_request_id()
        progress = master.work_done_progress(request_guid)
        request = Server::TypeCheckController::Request.new(guid: request_guid, progress: progress)

        target_paths.each do |path|
          target = project.target_for_source_path(path) or next
          request.code_paths << [target.name, project.absolute_path(path)]
        end
        signature_paths.each do |path|
          target = project.target_for_signature_path(path) or next
          request.signature_paths << [target.name, project.absolute_path(path)]
        end

        request.needs_response = true
        master.start_type_check(request: request, last_request: nil, report_progress_threshold: 0)

        Steep.logger.info { "Starting type checking: #{request_guid}" }

        error_messages = [] #: Array[String]
        client_reader.read do |response|
          Steep.logger.info { response.inspect }
          case
          when response[:method] == "textDocument/publishDiagnostics"
            params = response[:params]

            if path = PathHelper.to_pathname(params[:uri])
              stdout.puts(
                {
                  path: project.relative_path(path).to_s,
                  diagnostics: params[:diagnostics]
                }.to_json
              )
            end
          when response[:method] == "window/showMessage"
            # Assuming ERROR message means unrecoverable error.
            message = response[:params]
            if message[:type] == LSP::Constant::MessageType::ERROR
              error_messages << message[:message]
            end
          when response[:id] == request_guid
            break
          end
        end

        Steep.logger.info { "Shutting down..." }

        shutdown_exit(reader: client_reader, writer: client_writer)
        main_thread.join()

        0
      end
    end
  end
end