Steepコードリーディング(5日目)

# Steepコードリーディング(5日目)

前回、このままコードを追っていくのは難しそうという判断になったため、今回は少し別角度から追っておくことにする。ひとまず今後の短期ゴールを「実際に型検査しているところに辿り着く」こととする。

前回は`steep worker --typecheck ...`のコマンドが何してるかわからないという話になり、そのまま追うのは難しそうという結論になったが、このコマンド自体は型検査に関わっていそうなのは明確。ひとまずこのコマンドから追っていく。

## `Steep::CLI`

https://github.com/soutaro/steep/blob/290ab9a3d023f1ed8011fb720c420c611e5473b9/lib/steep/cli.rb

帰ってきました、`Steep::CLI`。

### `Steep::CLI#process_worker`

`Drivers::Worker`の`worker_type`に`:typecheck`を代入し、`#run`を実行している。

```rb
def process_worker
  Drivers::Worker.new(stdout: stdout, stderr: stderr, stdin: stdin).tap do |command|
    OptionParser.new do |opts|
      opts.banner = "Usage: steep worker [options] [dir]"
      handle_logging_options opts

      opts.on("--interaction") { command.worker_type = :interaction }
      opts.on("--typecheck") { command.worker_type = :typecheck }
      opts.on("--steepfile=PATH") {|path| command.steepfile = Pathname(path) }
      opts.on("--name=NAME") {|name| command.worker_name = name }
      opts.on("--delay-shutdown") { command.delay_shutdown = true }
      opts.on("--max-index=COUNT") {|count| command.max_index = Integer(count) }
      opts.on("--index=INDEX") {|index| command.index = Integer(index) }
    end.parse!(argv)

    # Disable any `ui_logger` output in workers
    Steep.ui_logger.level = :fatal

    command.commandline_args.push(*argv)
  end.run
end
```

## `Steep:Drivers::Worker`

https://github.com/soutaro/steep/blob/290ab9a3d023f1ed8011fb720c420c611e5473b9/lib/steep/drivers/worker.rb

### `Steep::Drivers::Worker#run`

`Server::TypeCheckWorker`インスタンスを生成して`#run`を実行している。

```rb
def run()
  Steep.logger.tagged("#{worker_type}:#{worker_name}") do
    project = load_config()

    reader = LanguageServer::Protocol::Transport::Io::Reader.new(stdin)
    writer = LanguageServer::Protocol::Transport::Io::Writer.new(stdout)

    worker = case worker_type
             when :typecheck
               assignment = Services::PathAssignment.new(max_index: max_index, index: index)
               Server::TypeCheckWorker.new(project: project,
                                           reader: reader,
                                           writer: writer,
                                           assignment: assignment,
                                           commandline_args: commandline_args)
             when :interaction
               Server::InteractionWorker.new(project: project, reader: reader, writer: writer)
             else
               raise "Unknown worker type: #{worker_type}"
             end

    unless delay_shutdown
      worker.skip_jobs_after_shutdown!
    end

    Steep.logger.info "Starting #{worker_type} worker..."

    worker.run()
  rescue Interrupt
    Steep.logger.info "Shutting down by interrupt..."
  end

  0
end
```

## `Steep::Server::TypeCheckWorker`

https://github.com/soutaro/steep/blob/290ab9a3d023f1ed8011fb720c420c611e5473b9/lib/steep/server/type_check_worker.rb

### `Steep::Server::TypeCheckWorker#run`

runメソッド自体はSuperクラスの`Steep::Server::BaseWorker`が持っている。

https://github.com/soutaro/steep/blob/290ab9a3d023f1ed8011fb720c420c611e5473b9/lib/steep/server/base_worker.rb

これ自体はQueueからenqueueして`handle_job`で処理しているだけなので、enqueueされるJobと`handle_job`を追うと良さそう。

```rb
def run
  tags = Steep.logger.formatter.current_tags.dup
  thread = Thread.new do
    Thread.current.abort_on_exception = true

    Steep.logger.formatter.push_tags(*tags)
    Steep.logger.tagged "background" do
      while job = queue.pop
        case job
        when ShutdownJob
          writer.write(id: job.id, result: nil)
        else
          if skip_job?
            Steep.logger.info "Skipping job..."
          else
            begin
              handle_job(job)
            rescue => exn
              Steep.log_error exn
              writer.write(
                {
                  method: "window/showMessage",
                  params: {
                    type: LSP::Constant::MessageType::ERROR,
                    message: "Unexpected error: #{exn.message} (#{exn.class})"
                  }
                }
              )
            end
          end
        end
      end
    end
  end

  Steep.logger.tagged "frontend" do
    begin
      reader.read do |request|
        Steep.logger.info "Received message from master: #{request[:method]}(#{request[:id]})"
        case request[:method]
        when "shutdown"
          queue << ShutdownJob.new(id: request[:id])
          @skip_job = skip_jobs_after_shutdown?
          queue.close
        when "exit"
          break
        else
          handle_request(request) unless @shutdown
        end
      end
    ensure
      thread.join
    end
  end
end
```

### `Steep::Server::TypeCheckWorker#handle_job`

Job毎のハンドリングがされていそう。今回はこの辺りでエネルギー切れ。

```rb
def handle_job(job)
  case job
  when StartTypeCheckJob
    Steep.logger.info { "Processing StartTypeCheckJob for guid=#{job.guid}" }
    service.update(changes: job.changes)

  when ValidateAppSignatureJob
    if job.guid == current_type_check_guid
      Steep.logger.info { "Processing ValidateAppSignature for guid=#{job.guid}, path=#{job.path}" }

      formatter = Diagnostic::LSPFormatter.new({}, **{})

      diagnostics = service.validate_signature(path: project.relative_path(job.path), target: job.target)

      typecheck_progress(
        path: job.path,
        guid: job.guid,
        target: job.target,
        diagnostics: diagnostics.filter_map { formatter.format(_1) }
      )
    end

  when ValidateLibrarySignatureJob
    if job.guid == current_type_check_guid
      Steep.logger.info { "Processing ValidateLibrarySignature for guid=#{job.guid}, path=#{job.path}" }

      formatter = Diagnostic::LSPFormatter.new({}, **{})
      diagnostics = service.validate_signature(path: job.path, target: job.target)

      typecheck_progress(path: job.path, guid: job.guid, target: job.target, diagnostics: diagnostics.filter_map { formatter.format(_1) })
    end

  when TypeCheckCodeJob
    if job.guid == current_type_check_guid
      Steep.logger.info { "Processing TypeCheckCodeJob for guid=#{job.guid}, path=#{job.path}, target=#{job.target.name}" }
      group_target = project.group_for_source_path(job.path) || job.target
      formatter = Diagnostic::LSPFormatter.new(group_target.code_diagnostics_config)
      relative_path = project.relative_path(job.path)
      diagnostics = service.typecheck_source(path: relative_path, target: job.target)
      typecheck_progress(path: job.path, guid: job.guid, target: job.target, diagnostics: diagnostics&.filter_map { formatter.format(_1) })
    end

  when WorkspaceSymbolJob
    writer.write(
      id: job.id,
      result: workspace_symbol_result(job.query)
    )
  when StatsJob
    writer.write(
      id: job.id,
      result: stats_result().map(&:as_json)
    )
  when GotoJob
    writer.write(
      id: job.id,
      result: goto(job)
    )
  end
end
```

次回はそれぞれのJobが何をしてるのかざっくりと追っていく。