Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/ruby_lsp/requests/support/rubocop_diagnostic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ class RuboCopDiagnostic
fatal: Constant::DiagnosticSeverity::ERROR,
}.freeze #: Hash[Symbol, Integer]

# Cops where adding a `rubocop:disable` inline comment would itself resolve the offense,
# causing Lint/RedundantCopDisableDirective to flag the disable as unnecessary.
SELF_RESOLVING_DISABLE_COPS = Set.new([
"Layout/EmptyComment",
]).freeze #: Set[String]

ENHANCED_DOC_URL = begin
gem("rubocop", ">= 1.64.0")
true
Expand All @@ -35,7 +41,7 @@ def to_lsp_code_actions
code_actions = []

code_actions << autocorrect_action if correctable?
code_actions << disable_line_action
code_actions << disable_line_action unless SELF_RESOLVING_DISABLE_COPS.include?(@offense.cop_name)

code_actions
end
Expand Down
85 changes: 57 additions & 28 deletions test/requests/code_actions_formatting_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ def test_disable_line__continuation
assert_disable_line("continuation", "Layout/SpaceAroundOperators")
end

def test_no_disable_line_for_self_resolving_cops
source = <<~RUBY
#
def foo; end
RUBY

assert_no_disable_action(source, "Layout/EmptyComment")
end

private

def assert_disable_line(fixture, cop_name)
Expand All @@ -49,6 +58,15 @@ def assert_disable_line(fixture, cop_name)
)
end

#: (String source, String diagnostic_code) -> void
def assert_no_disable_action(source, diagnostic_code)
_document, _diagnostic, result = setup_code_action_context(source, diagnostic_code)

disable_action = find_code_action_by_title(result, "Disable #{diagnostic_code} for this line")

assert_nil(disable_action, "Should not offer disable action for self-resolving cops")
end

def assert_fixtures_match(name, diagnostic_code, code_action_title)
actual, expected = load_expectation(name)
assert_corrects_to_expected(
Expand All @@ -59,8 +77,36 @@ def assert_fixtures_match(name, diagnostic_code, code_action_title)
)
end

#: (String name) -> [String, String]
def load_expectation(name)
source = File.read(File.join(TEST_FIXTURES_DIR, "#{name}.rb"))
expected = File.read(File.join(TEST_EXP_DIR, "#{name}.exp.rb"))
[source, expected]
end

#: (String diagnostic_code, String code_action_title, String source, String expected) -> untyped
def assert_corrects_to_expected(diagnostic_code, code_action_title, source, expected)
document, _diagnostic, result = setup_code_action_context(source, diagnostic_code)

selected_action = find_code_action_by_title(result, code_action_title) #: as !nil

assert(selected_action, "Expected to find code action with title '#{code_action_title}'")

# transform edits from lsp to the format RubyLsp::Document wants them
# this doesn't work with multiple edits if any edits add lines
edits = selected_action.dig(:edit, :documentChanges).flat_map do |doc_change|
doc_change[:edits].map do |edit|
{ range: edit[:range], text: edit[:newText] }
end
end

document.push_edits(edits, version: 2)

assert_equal(document.source, expected)
end

#: (String source, String diagnostic_code) -> [RubyLsp::RubyDocument, LanguageServer::Protocol::Interface::Diagnostic, Array[LanguageServer::Protocol::Interface::CodeAction]?]
def setup_code_action_context(source, diagnostic_code)
global_state = RubyLsp::GlobalState.new
global_state.apply_options({
initializationOptions: { linters: ["rubocop_internal"] },
Expand All @@ -78,43 +124,26 @@ def assert_corrects_to_expected(diagnostic_code, code_action_title, source, expe
)

diagnostics = RubyLsp::Requests::Diagnostics.new(global_state, document).perform
# The source of the returned attributes may be RuboCop or Prism. Prism diagnostics don't have a code.
rubocop_diagnostics = diagnostics&.select { _1.attributes[:source] == "RuboCop" }
diagnostic = rubocop_diagnostics&.find { |d| d.attributes[:code] && (d.code == diagnostic_code) } #: as !nil
diagnostic = rubocop_diagnostics&.find { |d| d.attributes[:code] == diagnostic_code } #: as !nil

assert(diagnostic, "Expected #{diagnostic_code} diagnostic to be present")

range = diagnostic.range.to_hash.transform_values(&:to_hash)
result = RubyLsp::Requests::CodeActions.new(document, range, {
diagnostics: [JSON.parse(diagnostic.to_json, symbolize_names: true)],
}).perform

[document, diagnostic, result]
end

#: (Array[LanguageServer::Protocol::Interface::CodeAction]?, String) -> Hash[Symbol, untyped]?
def find_code_action_by_title(result, title)
# CodeActions#run returns Array<CodeAction, Hash>. We're interested in the
# hashes here, so cast to untyped and only look at those.
untyped_result = result #: untyped
selected_action = untyped_result.find do |ca|
ca.respond_to?(:[]) && ca[:title] == code_action_title
end

# transform edits from lsp to the format RubyLsp::Document wants them
# this doesn't work with multiple edits if any edits add lines
edits = selected_action.dig(:edit, :documentChanges).flat_map do |doc_change|
doc_change[:edits].map do |edit|
{ range: edit[:range], text: edit[:newText] }
end
untyped_result.find do |ca|
ca.respond_to?(:[]) && ca[:title] == title
end

document.push_edits(edits, version: 2)
# if document.source != expected
# $stderr.puts("\n### #{@NAME.sub("test_", "")} ###")
# $stderr.puts("#### ACTUAL ####\n#{document.source}\n")
# $stderr.puts("#### EXPECTED ####\n#{expected}\n")
# end

assert_equal(document.source, expected)
end

#: (String name) -> [String, String]
def load_expectation(name)
source = File.read(File.join(TEST_FIXTURES_DIR, "#{name}.rb"))
expected = File.read(File.join(TEST_EXP_DIR, "#{name}.exp.rb"))
[source, expected]
end
end