lib/steep/drivers/stats.rb



require "csv"

module Steep
  module Drivers
    class Stats
      class CSVPrinter
        attr_reader :io

        def initialize(io:)
          @io = io
        end

        def print(stats_result)
          io.puts(
            CSV.generate do |csv|
              csv << ["Target", "File", "Status", "Typed calls", "Untyped calls", "All calls", "Typed %"]
              stats_result.each do |row|
                if row[:type] == "success"
                  # @type var row: Steep::Services::StatsCalculator::SuccessStats::json
                  csv << [
                    row[:target],
                    row[:path],
                    row[:type],
                    row[:typed_calls],
                    row[:untyped_calls],
                    row[:total_calls],
                    if row[:total_calls].nonzero?
                      (row[:typed_calls].to_f / row[:total_calls] * 100).to_i
                    else
                      100
                    end
                  ]
                else
                  # @type var row: Steep::Services::StatsCalculator::ErrorStats::json
                  csv << [
                    row[:target],
                    row[:path],
                    row[:type],
                    0,
                    0,
                    0,
                    0
                  ]
                end
              end
            end
          )
        end
      end

      class TablePrinter
        attr_reader :io

        def initialize(io:)
          @io = io
        end

        def print(stats_result)
          rows = [] #: Array[Array[untyped]]
          stats_result.sort_by {|row| row[:path] }.each do |row|
            if row[:type] == "success"
                # @type var row: Steep::Services::StatsCalculator::SuccessStats::json
                rows << [
                row[:target],
                row[:path] + "  ",
                row[:type],
                row[:typed_calls],
                row[:untyped_calls],
                row[:total_calls],
                if row[:total_calls].nonzero?
                  "#{(row[:typed_calls].to_f / row[:total_calls] * 100).to_i}%"
                else
                  "100%"
                end
              ]
            else
              # @type var row: Steep::Services::StatsCalculator::ErrorStats::json
              rows << [
                row[:target],
                row[:path],
                row[:type],
                0,
                0,
                0,
                "N/A"
              ]
            end
          end

          table = Terminal::Table.new( # steep:ignore UnknownConstant
            headings: ["Target", "File", "Status", "Typed calls", "Untyped calls", "All calls", "Typed %"],
            rows: rows
          )
          table.align_column(3, :right)
          table.align_column(4, :right)
          table.align_column(5, :right)
          table.align_column(6, :right)
          table.style = { border_top: false, border_bottom: false, border_y: "", border_i: "" }
          io.puts(table)
        end
      end

      attr_reader :stdout
      attr_reader :stderr
      attr_reader :command_line_patterns
      attr_accessor :format
      attr_reader :jobs_option

      include Utils::DriverHelper

      def initialize(stdout:, stderr:)
        @stdout = stdout
        @stderr = stderr
        @command_line_patterns = []
        @jobs_option = Utils::JobsOption.new()
      end

      def run
        project = load_config()

        stderr.puts Rainbow("# Calculating stats:").bold
        stderr.puts

        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)

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

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

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

        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)

        typecheck_guid = SecureRandom.uuid

        master.job_queue << -> do
          Steep.logger.info { "Type checking for stats..." }
          progress = master.work_done_progress(typecheck_guid)
          master.start_type_check(last_request: nil, progress: progress, include_unchanged: true, report_progress_threshold: 0)
        end
        wait_for_message(reader: client_reader) do |message|
          message[:id] == typecheck_guid
        end

        Steep.logger.info { "Finished type checking for stats" }

        stats_id = request_id()
        client_writer.write(Server::CustomMethods::Stats.request(stats_id))

        stats_response = wait_for_response_id(reader: client_reader, id: stats_id)
        stats_result = stats_response[:result] #: Server::CustomMethods::Stats::result

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

        printer = case format
                  when "csv"
                    CSVPrinter.new(io: stdout)
                  when "table"
                    TablePrinter.new(io: stdout)
                  when nil
                    if stdout.tty?
                      TablePrinter.new(io: stdout)
                    else
                      CSVPrinter.new(io: stdout)
                    end
                  else
                    raise ArgumentError.new("Invalid format: #{format}")
                  end

        printer.print(stats_result)

        0
      end
    end
  end
end