lib/rubocop/lsp/diagnostic.rb



# frozen_string_literal: true

require_relative 'severity'

#
# This code is based on https://github.com/standardrb/standard.
#
# Copyright (c) 2023 Test Double, Inc.
#
# The MIT License (MIT)
#
# https://github.com/standardrb/standard/blob/main/LICENSE.txt
#
module RuboCop
  module LSP
    # Diagnostic for Language Server Protocol of RuboCop.
    # @api private
    class Diagnostic
      def initialize(document_encoding, offense, uri, cop_class)
        @document_encoding = document_encoding
        @offense = offense
        @uri = uri
        @cop_class = cop_class
      end

      def to_lsp_code_actions
        code_actions = []

        code_actions << autocorrect_action if correctable?
        code_actions << disable_line_action

        code_actions
      end

      # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
      def to_lsp_diagnostic(config)
        highlighted = @offense.highlighted_area

        LanguageServer::Protocol::Interface::Diagnostic.new(
          message: message,
          source: 'RuboCop',
          code: @offense.cop_name,
          code_description: code_description(config),
          severity: severity,
          range: LanguageServer::Protocol::Interface::Range.new(
            start: LanguageServer::Protocol::Interface::Position.new(
              line: @offense.line - 1,
              character: highlighted.begin_pos
            ),
            end: LanguageServer::Protocol::Interface::Position.new(
              line: @offense.line - 1,
              character: highlighted.end_pos
            )
          ),
          data: {
            correctable: correctable?,
            code_actions: to_lsp_code_actions
          }
        )
      end
      # rubocop:enable Metrics/AbcSize, Metrics/MethodLength

      private

      def message
        message = @offense.message
        message += "\n\nThis offense is not autocorrectable.\n" unless correctable?
        message
      end

      def severity
        Severity.find_by(@offense.severity.name)
      end

      def code_description(config)
        return unless @cop_class
        return unless (doc_url = @cop_class.documentation_url(config))

        LanguageServer::Protocol::Interface::CodeDescription.new(href: doc_url)
      end

      # rubocop:disable Layout/LineLength, Metrics/MethodLength
      def autocorrect_action
        LanguageServer::Protocol::Interface::CodeAction.new(
          title: "Autocorrect #{@offense.cop_name}",
          kind: LanguageServer::Protocol::Constant::CodeActionKind::QUICK_FIX,
          edit: LanguageServer::Protocol::Interface::WorkspaceEdit.new(
            document_changes: [
              LanguageServer::Protocol::Interface::TextDocumentEdit.new(
                text_document: LanguageServer::Protocol::Interface::OptionalVersionedTextDocumentIdentifier.new(
                  uri: ensure_uri_scheme(@uri.to_s).to_s,
                  version: nil
                ),
                edits: correctable? ? offense_replacements : []
              )
            ]
          ),
          is_preferred: true
        )
      end
      # rubocop:enable Layout/LineLength, Metrics/MethodLength

      # rubocop:disable Metrics/MethodLength
      def offense_replacements
        @offense.corrector.as_replacements.map do |range, replacement|
          LanguageServer::Protocol::Interface::TextEdit.new(
            range: LanguageServer::Protocol::Interface::Range.new(
              start: LanguageServer::Protocol::Interface::Position.new(
                line: range.line - 1,
                character: range.column
              ),
              end: LanguageServer::Protocol::Interface::Position.new(
                line: range.last_line - 1,
                character: range.last_column
              )
            ),
            new_text: replacement
          )
        end
      end
      # rubocop:enable Metrics/MethodLength

      # rubocop:disable Layout/LineLength, Metrics/MethodLength
      def disable_line_action
        LanguageServer::Protocol::Interface::CodeAction.new(
          title: "Disable #{@offense.cop_name} for this line",
          kind: LanguageServer::Protocol::Constant::CodeActionKind::QUICK_FIX,
          edit: LanguageServer::Protocol::Interface::WorkspaceEdit.new(
            document_changes: [
              LanguageServer::Protocol::Interface::TextDocumentEdit.new(
                text_document: LanguageServer::Protocol::Interface::OptionalVersionedTextDocumentIdentifier.new(
                  uri: ensure_uri_scheme(@uri.to_s).to_s,
                  version: nil
                ),
                edits: line_disable_comment
              )
            ]
          )
        )
      end
      # rubocop:enable Layout/LineLength, Metrics/MethodLength

      def line_disable_comment
        new_text = if @offense.source_line.include?(' # rubocop:disable ')
                     ",#{@offense.cop_name}"
                   else
                     " # rubocop:disable #{@offense.cop_name}"
                   end

        eol = LanguageServer::Protocol::Interface::Position.new(
          line: @offense.line - 1,
          character: length_of_line(@offense.source_line)
        )

        # TODO: fails for multiline strings - may be preferable to use block
        # comments to disable some offenses
        inline_comment = LanguageServer::Protocol::Interface::TextEdit.new(
          range: LanguageServer::Protocol::Interface::Range.new(start: eol, end: eol),
          new_text: new_text
        )

        [inline_comment]
      end

      def length_of_line(line)
        if @document_encoding == Encoding::UTF_16LE
          line_length = 0
          line.codepoints.each do |codepoint|
            line_length += 1
            line_length += 1 if codepoint > RubyLsp::Document::Scanner::SURROGATE_PAIR_START
          end
          line_length
        else
          line.length
        end
      end

      def correctable?
        !@offense.corrector.nil?
      end

      def ensure_uri_scheme(uri)
        uri = URI.parse(uri)
        uri.scheme = 'file' if uri.scheme.nil?
        uri
      end
    end
  end
end